doxum 0.1.40 → 0.1.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -88,16 +88,47 @@ React consumes Doxum through `doxum/react` only. Do not duplicate Runtime subscr
88
88
  ### Projection provider
89
89
 
90
90
  ```tsx
91
- const runtime = useMemo(() => createProjectionRuntime({ onError: report }), []);
91
+ // runtime is created and disposed by the application/service outside rendering.
92
+ function ApplicationView({ runtime }: { runtime: ProjectionRuntime }) {
93
+ return (
94
+ <ProjectionProvider runtime={runtime} fallback={null}>
95
+ <App />
96
+ </ProjectionProvider>
97
+ );
98
+ }
99
+ ```
100
+
101
+ `runtime` mode owns a child scope, not the supplied Runtime. Scope acquisition happens
102
+ after commit; first render and SSR show `fallback` (default `null`). Every Effect setup
103
+ uses a fresh scope, including StrictMode replay. A runtime change replaces the scope
104
+ and remounts the child subtree with fresh local definitions/state. Abandoned renders
105
+ acquire no scope. Do not create a disposable scope or Runtime in `useMemo` and rely on
106
+ an Effect to clean up that render-created resource.
92
107
 
93
- return (
94
- <ProjectionProvider value={runtime}>
95
- <App />
96
- </ProjectionProvider>
97
- );
108
+ For synchronous SSR or externally owned lifetimes, inject the owner explicitly:
109
+ `<ProjectionProvider value={runtimeOrScope}>`. This mode never disposes the supplied
110
+ owner and takes no fallback. Never combine `runtime` and `value`.
111
+
112
+ `useProjectionScope()` returns an existing context scope. It works inside a managed
113
+ Provider or `value={scope}`, and throws for a missing Provider or `value={runtime}`.
114
+ It does not accept a Runtime argument or allocate another scope. To declare local
115
+ state, memoize a definition owned by the current scope, with the scope as a dependency:
116
+
117
+ ```tsx
118
+ const scope = useProjectionScope();
119
+ const mode = useMemo(() => scope.own(input('all')), [scope]);
120
+ const [value, update] = useInput(mode);
98
121
  ```
99
122
 
100
- The provider accepts a root `ProjectionRuntime` or `ProjectionScope`. The component/service that creates it owns disposal. Do not create a new Runtime on every render.
123
+ A scope does not adopt root projections merely because a component reads them. Use
124
+ `scope.onDispose` for synchronous finalization that needs projection reads; do not rely
125
+ on React parent/child cleanup order. Cleanup may read and unsubscribe, but may not write,
126
+ batch, subscribe or register more resources. Perform domain commands before disposal.
127
+ Registration returns an idempotent cancellation function suitable for Effect cleanup.
128
+ If work must run on scope shutdown, leave its registration active until that shutdown;
129
+ cancelling it in an earlier component cleanup intentionally prevents it from running.
130
+ Parent Runtime disposal ends its scopes; subsequent React cleanup and unsubscribe are
131
+ safe no-ops. Ordinary reads after disposal still throw.
101
132
 
102
133
  ### Reading projections
103
134
 
@@ -61,6 +61,29 @@ This file is maintainer-only. Application/library consumers should use [Public A
61
61
  initialization failure releases already-created projection resources and preserves the
62
62
  original error. Document dirty/pending state has one write owner; structural
63
63
  materialization only reads it.
64
+ Collection storage installs an accepted entry batch through one persistent index
65
+ application. The index owns AVL batch merge/balance; storage owns the current Map
66
+ and formal ids; output sealing owns equality and publication. Share untouched
67
+ subtrees and copy the affected path union once for pure value updates. Batch
68
+ structural changes require height-aware join/concat, not just single-edit repair.
69
+ Preserve lazy index creation, durable old snapshots, zero index work for pure
70
+ reorder, and per-install versions for demanded reads inside a Runtime batch.
71
+ Sorting changed keys is internal lookup work, never formal order. Count node
72
+ allocations and sorting separately from selector calls; this is not an O(delta)
73
+ guarantee for all collection maintenance.
74
+ Scheduler owns Runtime active/closing/disposed state and cleanup/read/command
75
+ admission; scope owns only its status and local resource membership. Cleanup stacks
76
+ are cancellable and LIFO, child callbacks precede parent callbacks, and graph teardown
77
+ starts only after all callbacks. Cleanup permits demanded/lazy reads but no new public
78
+ resources or writes. Restore the caller phase after nested computation and balance
79
+ document locks for connections registered/detached while a lock is held. Release
80
+ dependents before dependencies; close all resources even after failures. Disposed
81
+ reads still throw; only repeated dispose/unsubscribe/cancellation are no-ops.
82
+ Simple listener sets share subscription.ts registration identity/removal semantics;
83
+ notification snapshot order/error policy remains with each notification owner.
84
+ React managed scope acquisition happens after commit, never in render/useMemo;
85
+ borrowed owner injection never takes disposal ownership. Fresh Effect setups own
86
+ fresh scopes/subtrees. Keep the managed first-render/SSR fallback explicit.
64
87
  10. Local sync uses Web Lock leadership and contiguous durable sequence. Writes precede
65
88
  asynchronous persistence. Version 5 / format 3 rejects old databases without editing
66
89
  them. External replace and remote apply are prohibited while attached.
@@ -459,6 +459,28 @@ Rules:
459
459
  - the same definition cannot belong to two scopes;
460
460
  - `scope.items` has the same membership/readable semantics as `runtime.items` and ends with scope disposal.
461
461
 
462
+ `scope.onDispose(cleanup)` and `runtime.onDispose(cleanup)` register synchronous LIFO
463
+ callbacks; the returned function cancels that one registration without executing it.
464
+ Runtime shutdown runs scope callbacks in reverse creation order, then root callbacks,
465
+ while the graph remains readable. All callbacks finish before teardown. Scope-only
466
+ disposal leaves root materialization alive. Read/Readable.current retain normal demand,
467
+ lazy initialization, ownership and fault semantics during cleanup; commands, new
468
+ subscriptions/resources and cleanup registrations are rejected. Connected document
469
+ writes are locked. Cleanup does not flush projection notifications. Business writes
470
+ belong before disposal. Existing source unsubscribe/processor release are teardown,
471
+ not read-capable cleanup callbacks.
472
+
473
+ All callbacks/releases are attempted even on failure; errors escape after terminal
474
+ closure. `dispose` can be called repeatedly, after an earlier cleanup failure, or on a
475
+ scope already ended by its parent. Unsubscribe and cleanup-registration cancellation
476
+ are idempotent. Each subscription owns a distinct registration even if callbacks are
477
+ identical; old unsubscribe functions cannot detach a newer registration. Normal reads,
478
+ commands, resource creation and existing Readable access still throw after disposal.
479
+
480
+ For React, use the managed `ProjectionProvider runtime={runtime}`; acquire the current
481
+ scope with `useProjectionScope()`. Read [React ownership and SSR](integrations.md#projection-provider)
482
+ before choosing managed versus externally owned Provider mode.
483
+
462
484
  ## Batching
463
485
 
464
486
  ```ts
@@ -420,6 +420,7 @@ type ProjectionRuntime = {
420
420
  update<K,V,R>(input: CollectionInput<K,V>, run: (draft: CollectionInputDraft<K,V>) => Synchronous<R>): void;
421
421
  batch<T>(run: () => Synchronous<T>, options?: { cause?: unknown }): T;
422
422
  scope(): ProjectionScope;
423
+ onDispose<R>(cleanup: () => Synchronous<R>): Unsubscribe;
423
424
  dispose(): void;
424
425
  };
425
426
 
@@ -600,25 +601,36 @@ incremental.group(
600
601
 
601
602
  Every declared output must be returned from the static output tree exactly once.
602
603
 
604
+ Runtime/scope `onDispose` registers synchronous, cancellable LIFO cleanup. Child scope
605
+ callbacks precede parent Runtime callbacks; projections remain readable until all
606
+ callbacks finish. Cleanup rejects commands/new registrations. Failures do not interrupt
607
+ remaining teardown and are thrown only after closure. `dispose` and unsubscribe are
608
+ idempotent; all normal projection access still throws after disposal. See
609
+ [cleanup and scope ownership](projections.md#projectionscope).
610
+
603
611
  ## `doxum/react`
604
612
 
605
613
  ### Public symbol inventory
606
614
 
607
615
  <!-- exports:doxum/react:start -->
608
616
 
609
- | Public export | Role |
610
- | --------------------- | -------------------------------------------------- |
611
- | `ProjectionProvider` | Provide a `ProjectionRuntime` or `ProjectionScope` |
612
- | `useDocumentSelector` | React adapter for Core document `select` |
613
- | `useReadable` | Subscribe to any Doxum `Readable` |
614
- | `useHistory` | Read history state and expose undo/redo callbacks |
615
- | `useProjection` | Read projection or projection selector |
616
- | `useInput` | Read/update scalar or collection projection input |
617
+ | Public export | Role |
618
+ | ------------------------- | --------------------------------------------------------- |
619
+ | `ProjectionProvider` | Manage a scope from a Runtime or borrow an external owner |
620
+ | `ProjectionProviderProps` | Mutually exclusive managed/borrowed Provider props |
621
+ | `useProjectionScope` | Read the current scope from context; never allocate one |
622
+ | `useDocumentSelector` | React adapter for Core document `select` |
623
+ | `useReadable` | Subscribe to any Doxum `Readable` |
624
+ | `useHistory` | Read history state and expose undo/redo callbacks |
625
+ | `useProjection` | Read projection or projection selector |
626
+ | `useInput` | Read/update scalar or collection projection input |
617
627
 
618
628
  <!-- exports:doxum/react:end -->
619
629
 
620
630
  ```ts
631
+ <ProjectionProvider runtime={runtime} fallback={null}>...</ProjectionProvider>
621
632
  <ProjectionProvider value={runtimeOrScope}>...</ProjectionProvider>
633
+ useProjectionScope(): ProjectionScope
622
634
 
623
635
  useProjection(projection): T
624
636
  useProjection(projection, selector, equality?): R
@@ -629,6 +641,13 @@ useReadable(readable): T
629
641
  useHistory(history): HistoryState & { undo(): OperationResult<C>; redo(): OperationResult<C> }
630
642
  ```
631
643
 
644
+ Managed `runtime` mode owns only a child scope, acquired after commit. Initial render
645
+ and SSR use `fallback` (default `null`); StrictMode setup replay and runtime replacement
646
+ create a fresh scope generation and remount its subtree. Borrowed `value` mode injects
647
+ synchronously and never disposes the supplied owner. Use it for synchronous SSR with
648
+ request-owned resources. `useProjectionScope()` requires a scope in context, including
649
+ an explicitly injected scope; an injected root Runtime does not implicitly create one.
650
+
632
651
  ## `doxum/local-sync`
633
652
 
634
653
  ### Public symbol inventory