Skip to content

Lifecycles

Four orthogonal lifecycles cooperate on every mount. Each has its own owner and none crosses into another’s job.

Unconnected → Connected → Adopted → Disconnected → GC collected. The library never fights the platform: an unconnected node cannot bubble protocol events, a reparenting move is confirmed in a microtask before anything is torn down, and an adoptNode migration re-registers the session on the new document’s observer.

Blueprint → Initializing → Mounted → Suspended (keyed; suspensionTtlMs, default 50ms) → Resuscitated → Disposed. Keyed sessions (mount(bp, { key }) / data-context-key) pause DOM projection while suspended — state, subscribers, and storage keep evolving — and re-project the full state onto the new host on resuscitation (no replay queue). Disposed sessions poison their store: late update() calls safely return false instead of throwing.

singleton lives in the process-wide registry, scoped (default) lives with its session, transient is never cached. Factories are lazy by default and run on first inject. In-flight async factories coalesce concurrent callers onto one promise and evict it on rejection so the next call retries cleanly.

Seed → hydration decision (storageFirst | domFirst | blueprintFirst | merge) → reactive flow → microtask bridge transaction → finalized. Bridge writes run under a transaction lock with snapshot diffing, so state-to-DOM and DOM-to-state updates never loop. Storage failures emit to readErrorStream and fall back to the seed.

A DOM disconnect never disposes a session synchronously. The observer waits a microtask, checks isConnected again, and only then suspends or disposes. Rendering moves, drag-and-drop, and HTMX swaps stay lossless by default.