Ref-counted, thread-aware projection registry.
Owns the spec.key → (store, runtime) mapping for one
StreamController. Lifecycle:
acquire(spec) → +1 ref, returns { store, release }. The
first acquire opens the projection's runtime; subsequent
acquires for the same key share both the store and the
runtime.release() → -1 ref. When the last consumer releases,
the entry is removed and its runtime disposed.bind(thread) → swap or detach the underlying thread; every
live entry's runtime is recreated against the new thread,
keeping the same store identity.dispose() → tear everything down (idempotent). Safe to
call multiple times.The registry is intentionally not generic over a state shape —
different consumers can hold projections producing different
snapshot types, so the registry keys everything as unknown and
lets acquire reapply the caller's T at the boundary.
class ChannelRegistryManage the thread state externally.
Acquire a ref-counted projection.
If no entry exists for spec.key, one is created (allocating a
StreamStore seeded with spec.initial) and — when a
thread is currently bound — its runtime is opened immediately.
If an entry already exists, its ref count is incremented and the
existing store is returned.
The returned release() is idempotent: calling it more than once
is a no-op. When the ref count drops to zero, the entry is removed
and its runtime disposed (best-effort, never throws into callers).
Safe to call from any framework lifecycle hook. Subsequent calls
for the same spec.key always return the same store reference
for the lifetime of the controller, so consumers can rely on store
identity.
Rebind every live entry to a new ThreadStream (or detach
when thread == null).
Each live entry has its current runtime disposed (best-effort)
and its store reset to entry.initial so consumers see a clean
slate during the swap. When thread != null, a fresh runtime is
opened against the new thread.
Critically the StreamStore instance is preserved across
the rebind: framework subscribers (e.g. React's
useSyncExternalStore) keep observing the same store reference,
so their subscriptions survive the swap.
No-op when called with the currently bound thread.
Tear everything down.
Detaches the bound thread (so no further bind() calls reopen
runtimes) and disposes every live runtime in parallel. Safe to
call multiple times — subsequent calls find an empty registry
and resolve immediately.