// renderers/_shared/mount.js — the {update, destroy} lifecycle skeleton // every renderer builds on (CONTRACT 3). Tracks disposables (event // listeners, observers, animation loops, WebGL renderers) so destroy() // reliably stops loops, disposes GL, and removes nodes — the lifecycle // IDIO_GRAPH_V2 lacked. // // Typical renderer usage: // import { createLifecycle } from '../_shared/mount.js'; // export function mount(el, payload) { // const lc = createLifecycle(el); // const { card, body } = createCard(el, payload.meta); // cell.js // ...build scene... // lc.trackThree(renderer); // WebGL cleanup // lc.raf((tMs) => { ...per-frame... }); // auto-cancelled loop // lc.observeResize(body, (w, h) => { ... }); // container-driven sizing // return lc.handle((fresh) => { ...re-render from fresh payload... }); // } /** * Create a lifecycle context bound to a mount host. * Everything registered through it is torn down (in reverse order) by * destroy(), which finally clears the host's children. */ export function createLifecycle(el) { const disposables = []; let destroyed = false; function runDisposable(d) { try { if (typeof d === 'function') d(); else if (d && typeof d.destroy === 'function') d.destroy(); else if (d && typeof d.dispose === 'function') d.dispose(); } catch (err) { console.error('[mount] disposable failed during destroy:', err); } } const lc = { el, get isDestroyed() { return destroyed; }, /** * Register anything needing teardown: a function, or an object with * .destroy() or .dispose() (three.js geometries/materials/textures, * observers, child mount handles). Returns the argument for chaining. */ track(d) { if (d != null) disposables.push(d); return d; }, /** addEventListener with automatic removal on destroy. */ listen(target, type, fn, opts) { target.addEventListener(type, fn, opts); lc.track(() => target.removeEventListener(type, fn, opts)); return fn; }, /** * Watch an element's content-box size (ResizeObserver), auto- * disconnected on destroy. fn(width, height, entry). No-ops (returns * null) where ResizeObserver is unavailable (jsdom smoke tests). */ observeResize(target, fn) { if (typeof ResizeObserver === 'undefined') return null; const ro = new ResizeObserver((entries) => { for (const entry of entries) { const box = entry.contentBoxSize?.[0]; const w = box ? box.inlineSize : entry.contentRect.width; const h = box ? box.blockSize : entry.contentRect.height; fn(w, h, entry); } }); ro.observe(target); lc.track(() => ro.disconnect()); return ro; }, /** * Start a requestAnimationFrame loop: fn(timestampMs) every frame * until stopped. Returns stop(); also auto-stopped on destroy. * (For three.js prefer renderer.setAnimationLoop + trackThree — * this is for canvas/SVG/DOM animation.) */ raf(fn) { let id = 0; let running = true; const loop = (t) => { if (!running) return; fn(t); id = requestAnimationFrame(loop); }; id = requestAnimationFrame(loop); const stop = () => { running = false; cancelAnimationFrame(id); }; lc.track(stop); return stop; }, /** setInterval with automatic clearInterval on destroy. Returns stop(). */ interval(fn, ms) { const id = setInterval(fn, ms); const stop = () => clearInterval(id); lc.track(stop); return stop; }, /** * Take ownership of a THREE.WebGLRenderer: on destroy the animation * loop is stopped, the GL context disposed and force-lost, and the * canvas removed from the DOM. */ trackThree(renderer) { lc.track(() => { try { renderer.setAnimationLoop?.(null); renderer.dispose(); renderer.forceContextLoss?.(); renderer.domElement?.remove(); } catch (err) { console.error('[mount] three renderer teardown failed:', err); } }); return renderer; }, /** Tear everything down (idempotent) and empty the mount host. */ destroy() { if (destroyed) return; destroyed = true; for (let i = disposables.length - 1; i >= 0; i -= 1) { runDisposable(disposables[i]); } disposables.length = 0; el.replaceChildren(); }, /** * Produce the CONTRACT-3 mount handle. `update` is the renderer's * re-render-from-fresh-payload function; it is guarded so calls after * destroy() are ignored. */ handle(update) { return { update(payload) { if (!destroyed && typeof update === 'function') update(payload); }, destroy() { lc.destroy(); }, }; }, }; return lc; }