doxum 0.1.41 → 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.
package/README.md CHANGED
@@ -276,8 +276,8 @@ runtime.dispose();
276
276
 
277
277
  The stable vocabulary is `read` for a synchronous current value, `select` for a
278
278
  `Readable`, and `observe` for declaring a lazy source boundary. `ProjectionRuntime`
279
- exposes `read`, `select`, `items`, `update`, `batch`, `scope` and `dispose`.
280
- `ProjectionScope` mirrors `read`, `select`, `items`, `update`, `batch` and `dispose`, plus
279
+ exposes `read`, `select`, `items`, `update`, `batch`, `scope`, `onDispose` and `dispose`.
280
+ `ProjectionScope` mirrors `read`, `select`, `items`, `update`, `batch`, `onDispose` and `dispose`, plus
281
281
  `own(definitionOrTree)` for lifecycle ownership of lazy projection definitions.
282
282
 
283
283
  ### Commands inside a batch
@@ -638,8 +638,40 @@ values. Ordinary source resets preserve retained state when one exists. If a pro
638
638
  faults, the Runtime owns recovery, recreates declared state, and performs a reset
639
639
  evaluation. Stateless processors use the same recovery path without a state object.
640
640
 
641
+ ### Projection disposal
642
+
643
+ `runtime.dispose()`, `scope.dispose()` and Doxum unsubscribe functions are idempotent.
644
+ A parent Runtime ends its scopes, so later component cleanup can safely dispose them
645
+ again. Every subscription registration is independent, including registrations with
646
+ the same callback; an old unsubscribe cannot cancel a later subscription.
647
+
648
+ `runtime.onDispose(cleanup)` / `scope.onDispose(cleanup)` register synchronous,
649
+ cancellable callbacks. The returned function unregisters that callback without
650
+ executing it. Owner callbacks run in LIFO order; Runtime shutdown runs child scope
651
+ callbacks before its own while all projections remain readable. Cleanup may read
652
+ current values and unsubscribe, but cannot update, batch, subscribe or register new
653
+ resources. Perform domain writes before disposal. After callbacks, consumers and
654
+ producers are released, then source connections; failures do not prevent remaining
655
+ cleanup, and are thrown after disposal completes. Repeated disposal does not retry
656
+ failed callbacks. Reads, commands and new subscriptions still throw after disposal.
657
+
641
658
  ### React
642
659
 
660
+ ```tsx
661
+ // runtime is owned by the application/service; this Provider owns only its scope.
662
+ <ProjectionProvider runtime={runtime} fallback={<span>Loading…</span>}>
663
+ <TaskList />
664
+ </ProjectionProvider>
665
+ ```
666
+
667
+ Managed Providers acquire a scope after commit and release it on cleanup. StrictMode
668
+ setup replay creates a fresh scope; runtime replacement also remounts the scoped subtree.
669
+ `useProjectionScope()` returns that scope for `scope.own(...)` and `scope.onDispose(...)`.
670
+ The initial render and SSR render `fallback` (default `null`). For synchronous SSR or
671
+ an externally managed lifetime, use `<ProjectionProvider value={runtimeOrScope}>`;
672
+ this mode only injects the owner and never disposes it. The two modes are mutually
673
+ exclusive. Do not create disposable scopes in `useMemo` or revive a disposed scope.
674
+
643
675
  ```ts
644
676
  const task = useProjection(tasks, tasks => tasks.get(taskId), equality);
645
677
  const [mode, setMode] = useInput(filter);
@@ -648,8 +680,8 @@ const [selectedRows, updateSelectedRows] = useInput(selection);
648
680
 
649
681
  `useProjection(projection, selector, equality?)` uses the same keyed selector semantics
650
682
  as Core. `useInput` handles both scalar `Input<T>` and `CollectionInput<K,V>`; collection
651
- updates receive a keyed draft callback. `ProjectionProvider` accepts either a
652
- `ProjectionRuntime` or `ProjectionScope`.
683
+ updates receive a keyed draft callback. Reading a root projection through a managed
684
+ scope does not transfer its ownership; use `scope.own(...)` for local definitions.
653
685
 
654
686
  See [projection contracts](docs/projections.md).
655
687
 
package/dist/advanced.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_transform = require("./transform-D6njdP3k.cjs");
2
+ const require_transform = require("./transform-BPh55GWO.cjs");
3
3
  const require_record = require("./record-ChYUi8PP.cjs");
4
4
  //#region core/src/projection/advanced.ts
5
5
  const resetCollectionChange = Object.freeze({ kind: "reset" });
@@ -1,4 +1,4 @@
1
- import { c as ProjectionChange, d as CollectionRead, l as CollectionChange, n as KeyedDependencyValues, o as KeyedProjection, s as Projection, t as KeyedDependencies, u as CollectionDraft } from "./dependency-Cbf3C8eJ.cjs";
1
+ import { c as ProjectionChange, d as CollectionRead, l as CollectionChange, n as KeyedDependencyValues, o as KeyedProjection, s as Projection, t as KeyedDependencies, u as CollectionDraft } from "./dependency-D0bo_xe_.cjs";
2
2
 
3
3
  //#region core/src/projection/collection/change.d.ts
4
4
  declare const collectionChange: {
@@ -1,4 +1,4 @@
1
- import { c as ProjectionChange, d as CollectionRead, l as CollectionChange, n as KeyedDependencyValues, o as KeyedProjection, s as Projection, t as KeyedDependencies, u as CollectionDraft } from "./dependency-DzxXGYe1.js";
1
+ import { c as ProjectionChange, d as CollectionRead, l as CollectionChange, n as KeyedDependencyValues, o as KeyedProjection, s as Projection, t as KeyedDependencies, u as CollectionDraft } from "./dependency-BYiwTiBj.js";
2
2
 
3
3
  //#region core/src/projection/collection/change.d.ts
4
4
  declare const collectionChange: {
package/dist/advanced.js CHANGED
@@ -1,4 +1,4 @@
1
- import { D as defineProcessor, S as compileProjectionDependencies, _ as assertSynchronous, r as collectionChange, t as createKeyedTransform, w as collectionView } from "./transform-CKJ1ivhz.js";
1
+ import { D as defineProcessor, S as compileProjectionDependencies, _ as assertSynchronous, r as collectionChange, t as createKeyedTransform, w as collectionView } from "./transform-BvDYPUMm.js";
2
2
  import { n as isPlainObject } from "./record-B2yRhYsc.js";
3
3
  //#region core/src/projection/advanced.ts
4
4
  const resetCollectionChange = Object.freeze({ kind: "reset" });
@@ -509,10 +509,11 @@ type MutationIssue = {
509
509
  };
510
510
  //#endregion
511
511
  //#region core/src/readable.d.ts
512
+ /** Ends one registration at most once; safe after owner disposal. */
512
513
  type Unsubscribe = () => void;
513
514
  type Readable<TValue> = {
514
515
  current(): TValue;
515
- revision(): number;
516
+ revision(): number; /** Each call registers independently, even when the listener function is reused. */
516
517
  subscribe(listener: () => void): Unsubscribe;
517
518
  };
518
519
  //#endregion
@@ -509,10 +509,11 @@ type MutationIssue = {
509
509
  };
510
510
  //#endregion
511
511
  //#region core/src/readable.d.ts
512
+ /** Ends one registration at most once; safe after owner disposal. */
512
513
  type Unsubscribe = () => void;
513
514
  type Readable<TValue> = {
514
515
  current(): TValue;
515
- revision(): number;
516
+ revision(): number; /** Each call registers independently, even when the listener function is reused. */
516
517
  subscribe(listener: () => void): Unsubscribe;
517
518
  };
518
519
  //#endregion
@@ -1,4 +1,4 @@
1
- import { _ as Unsubscribe, b as CollectionImpact, p as Synchronous } from "./contract-COgxO5sv.js";
1
+ import { _ as Unsubscribe, b as CollectionImpact, p as Synchronous } from "./contract-PqdXNaP8.js";
2
2
 
3
3
  //#region core/src/projection/contract.d.ts
4
4
  /** The one processor-facing collection transition protocol. */
@@ -1,4 +1,4 @@
1
- import { _ as Unsubscribe, b as CollectionImpact, p as Synchronous } from "./contract-CNR6BS75.cjs";
1
+ import { _ as Unsubscribe, b as CollectionImpact, p as Synchronous } from "./contract-DovG1Yp-.cjs";
2
2
 
3
3
  //#region core/src/projection/contract.d.ts
4
4
  /** The one processor-facing collection transition protocol. */
@@ -208,6 +208,20 @@ var AddressIndex = class {
208
208
  }
209
209
  };
210
210
  //#endregion
211
+ //#region core/src/subscription.ts
212
+ /** Each registration owns its identity, independently of callback reuse. */
213
+ const addListener = (listeners, listener, removed) => {
214
+ const entry = (...args) => listener(...args);
215
+ listeners.add(entry);
216
+ let active = true;
217
+ return () => {
218
+ if (!active) return;
219
+ active = false;
220
+ listeners.delete(entry);
221
+ removed?.();
222
+ };
223
+ };
224
+ //#endregion
211
225
  //#region core/src/mutation/changes.ts
212
226
  const validated = /* @__PURE__ */ new WeakSet();
213
227
  const compareChanges = (a, b) => {
@@ -399,4 +413,4 @@ const installRuntimeWriteDriver = (runtime, driver) => {
399
413
  });
400
414
  };
401
415
  //#endregion
402
- export { changeCount as a, AddressIndex as c, invalidValue as d, children as f, validate as g, parent as h, contextOf as i, MutationRejected as l, is as m, installRuntimeWriteDriver as n, decodeChanges as o, contains as p, bindContext as r, sealChanges as s, assertRuntimeWritable as t, fail as u };
416
+ export { validate as _, changeCount as a, addListener as c, fail as d, invalidValue as f, parent as g, is as h, contextOf as i, AddressIndex as l, contains as m, installRuntimeWriteDriver as n, decodeChanges as o, children as p, bindContext as r, sealChanges as s, assertRuntimeWritable as t, MutationRejected as u };
@@ -208,6 +208,20 @@ var AddressIndex = class {
208
208
  }
209
209
  };
210
210
  //#endregion
211
+ //#region core/src/subscription.ts
212
+ /** Each registration owns its identity, independently of callback reuse. */
213
+ const addListener = (listeners, listener, removed) => {
214
+ const entry = (...args) => listener(...args);
215
+ listeners.add(entry);
216
+ let active = true;
217
+ return () => {
218
+ if (!active) return;
219
+ active = false;
220
+ listeners.delete(entry);
221
+ removed?.();
222
+ };
223
+ };
224
+ //#endregion
211
225
  //#region core/src/mutation/changes.ts
212
226
  const validated = /* @__PURE__ */ new WeakSet();
213
227
  const compareChanges = (a, b) => {
@@ -411,6 +425,12 @@ Object.defineProperty(exports, "MutationRejected", {
411
425
  return MutationRejected;
412
426
  }
413
427
  });
428
+ Object.defineProperty(exports, "addListener", {
429
+ enumerable: true,
430
+ get: function() {
431
+ return addListener;
432
+ }
433
+ });
414
434
  Object.defineProperty(exports, "assertRuntimeWritable", {
415
435
  enumerable: true,
416
436
  get: function() {
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_transform = require("./transform-D6njdP3k.cjs");
2
+ const require_transform = require("./transform-BPh55GWO.cjs");
3
3
  const require_record = require("./record-ChYUi8PP.cjs");
4
- const require_driver = require("./driver-CN3dGxQI.cjs");
4
+ const require_driver = require("./driver-i4G4iVX0.cjs");
5
5
  //#region core/src/schema/model.ts
6
6
  const schemaNodeOf = (schema) => schema;
7
7
  const collectionEntryNode = (node) => node.kind === "map" || node.kind === "table" || node.kind === "list" ? node.value : void 0;
@@ -1623,10 +1623,7 @@ const createHistory = (input) => {
1623
1623
  revision: () => revision,
1624
1624
  subscribe: (listener) => {
1625
1625
  if (disposed) throw new DocumentDisposedError();
1626
- listeners.add(listener);
1627
- return () => {
1628
- listeners.delete(listener);
1629
- };
1626
+ return require_driver.addListener(listeners, listener);
1630
1627
  },
1631
1628
  undo: () => {
1632
1629
  input.assertIdle();
@@ -2297,6 +2294,7 @@ const addRoot = (notification, listener) => {
2297
2294
  };
2298
2295
  notification.root.add(entry);
2299
2296
  return () => {
2297
+ if (!entry.active) return;
2300
2298
  entry.active = false;
2301
2299
  if (!notification.notifying) notification.root.delete(entry);
2302
2300
  else notification.cleanups.add(() => {
@@ -2670,7 +2668,9 @@ const select = (document, selector, equality = Object.is) => {
2670
2668
  };
2671
2669
  };
2672
2670
  const installSubscription = () => {
2673
- unsubscribeDocument?.();
2671
+ const stop = unsubscribeDocument;
2672
+ unsubscribeDocument = void 0;
2673
+ stop?.();
2674
2674
  unsubscribeDocument = dependencies.length > 0 ? context.notifications.subscribeTargets(dependencies, () => {
2675
2675
  const result = evaluate();
2676
2676
  if (result.rebound) installSubscription();
@@ -2690,15 +2690,22 @@ const select = (document, selector, equality = Object.is) => {
2690
2690
  },
2691
2691
  subscribe: (listener) => {
2692
2692
  ensureCurrent();
2693
- listeners.add(listener);
2694
- if (listeners.size === 1) installSubscription();
2695
- return () => {
2696
- listeners.delete(listener);
2693
+ const unsubscribe = require_driver.addListener(listeners, listener, () => {
2697
2694
  if (!listeners.size) {
2698
- unsubscribeDocument?.();
2695
+ const stop = unsubscribeDocument;
2699
2696
  unsubscribeDocument = void 0;
2697
+ stop?.();
2700
2698
  }
2701
- };
2699
+ });
2700
+ try {
2701
+ if (listeners.size === 1) installSubscription();
2702
+ } catch (error) {
2703
+ try {
2704
+ unsubscribe();
2705
+ } catch {}
2706
+ throw error;
2707
+ }
2708
+ return unsubscribe;
2702
2709
  }
2703
2710
  });
2704
2711
  };
@@ -3688,6 +3695,33 @@ function createValueDerive(dependencies, compute, equality = Object.is) {
3688
3695
  }
3689
3696
  const derive = Object.assign(createValueDerive, { keyed: keyedDerive });
3690
3697
  //#endregion
3698
+ //#region core/src/projection/cleanup.ts
3699
+ /** Owner-local, cancellable LIFO callbacks. Lifecycle admission stays with the owner. */
3700
+ const createCleanupStack = () => {
3701
+ const entries = /* @__PURE__ */ new Set();
3702
+ return {
3703
+ add(callback) {
3704
+ const entry = () => {
3705
+ require_transform.assertSynchronous(callback());
3706
+ };
3707
+ entries.add(entry);
3708
+ return () => {
3709
+ entries.delete(entry);
3710
+ };
3711
+ },
3712
+ drain(failures) {
3713
+ for (const entry of [...entries].reverse()) {
3714
+ if (!entries.delete(entry)) continue;
3715
+ try {
3716
+ entry();
3717
+ } catch (error) {
3718
+ failures.push(error);
3719
+ }
3720
+ }
3721
+ }
3722
+ };
3723
+ };
3724
+ //#endregion
3691
3725
  //#region core/src/projection/collection/changes.ts
3692
3726
  /** Net transitions for one consumer's unconsumed interval; bounded by touched keys. */
3693
3727
  const createCollectionChanges = () => {
@@ -4209,8 +4243,7 @@ const createCollectionOutput = (binding) => {
4209
4243
  },
4210
4244
  observe: (listener) => {
4211
4245
  binding.check();
4212
- observers.add(listener);
4213
- return () => observers.delete(listener);
4246
+ return require_driver.addListener(observers, listener);
4214
4247
  },
4215
4248
  hasConsumers: () => consumers.size > 0,
4216
4249
  forEachConsumer: (run) => consumers.forEach((_pending, consumer) => run(consumer)),
@@ -4242,8 +4275,7 @@ const createCollectionOutput = (binding) => {
4242
4275
  },
4243
4276
  subscribe: (listener) => {
4244
4277
  binding.check();
4245
- listeners.add(listener);
4246
- return () => listeners.delete(listener);
4278
+ return require_driver.addListener(listeners, listener);
4247
4279
  }
4248
4280
  };
4249
4281
  };
@@ -4358,8 +4390,7 @@ const createValueOutput = (binding) => {
4358
4390
  },
4359
4391
  observe: (listener) => {
4360
4392
  binding.check();
4361
- observers.add(listener);
4362
- return () => observers.delete(listener);
4393
+ return require_driver.addListener(observers, listener);
4363
4394
  },
4364
4395
  hasConsumers: () => consumers.size > 0,
4365
4396
  forEachConsumer: (run) => consumers.forEach((_revision, consumer) => run(consumer)),
@@ -4388,8 +4419,7 @@ const createValueOutput = (binding) => {
4388
4419
  },
4389
4420
  subscribe: (listener) => {
4390
4421
  binding.check();
4391
- listeners.add(listener);
4392
- return () => listeners.delete(listener);
4422
+ return require_driver.addListener(listeners, listener);
4393
4423
  }
4394
4424
  };
4395
4425
  };
@@ -4593,7 +4623,7 @@ const selectionAffects = (selection, change) => {
4593
4623
  const createDirectReadable = (source) => Object.freeze({
4594
4624
  current: source.current,
4595
4625
  revision: source.revision,
4596
- subscribe: (listener) => source.subscribe(() => listener())
4626
+ subscribe: source.subscribe
4597
4627
  });
4598
4628
  /** Consumer-side selector tracking; it never mutates processor dependencies. */
4599
4629
  const createSelectorReadable = (source, selector, equality = Object.is) => {
@@ -4697,15 +4727,22 @@ const createSelectorReadable = (source, selector, equality = Object.is) => {
4697
4727
  },
4698
4728
  subscribe: (listener) => {
4699
4729
  ensureCurrent();
4700
- listeners.add(listener);
4701
- installSource();
4702
- return () => {
4703
- listeners.delete(listener);
4704
- if (!listeners.size && unsubscribeSource) {
4705
- unsubscribeSource();
4730
+ const unsubscribe = require_driver.addListener(listeners, listener, () => {
4731
+ if (!listeners.size) {
4732
+ const stop = unsubscribeSource;
4706
4733
  unsubscribeSource = void 0;
4734
+ stop?.();
4707
4735
  }
4708
- };
4736
+ });
4737
+ try {
4738
+ installSource();
4739
+ } catch (error) {
4740
+ try {
4741
+ unsubscribe();
4742
+ } catch {}
4743
+ throw error;
4744
+ }
4745
+ return unsubscribe;
4709
4746
  }
4710
4747
  });
4711
4748
  };
@@ -6003,7 +6040,7 @@ const createSourceRegistry = (scheduler) => {
6003
6040
  //#endregion
6004
6041
  //#region core/src/projection/readable/keyed.ts
6005
6042
  /** Runtime-owned keyed consumer family over one materialized collection output. */
6006
- const createProjectionItems = (source, checkOwner) => {
6043
+ const createProjectionItems = (source, checkOwner, checkSubscription) => {
6007
6044
  if (source.kind !== "collection") throw new TypeError("Projection items require a keyed collection projection.");
6008
6045
  let disposed = false;
6009
6046
  const active = /* @__PURE__ */ new Map();
@@ -6132,12 +6169,12 @@ const createProjectionItems = (source, checkOwner) => {
6132
6169
  subscribe: (listener) => {
6133
6170
  check();
6134
6171
  ensureObserved();
6135
- state.listeners.add(listener);
6136
- if (!state.tracked && !state.ended) registerWaiting(state);
6137
- return () => {
6138
- state.listeners.delete(listener);
6172
+ checkSubscription();
6173
+ const unsubscribe = require_driver.addListener(state.listeners, listener, () => {
6139
6174
  if (!state.listeners.size && !state.tracked) unregisterWaiting(state);
6140
- };
6175
+ });
6176
+ if (!state.tracked && !state.ended) registerWaiting(state);
6177
+ return unsubscribe;
6141
6178
  }
6142
6179
  });
6143
6180
  return state;
@@ -6194,8 +6231,8 @@ const createProjectionItems = (source, checkOwner) => {
6194
6231
  },
6195
6232
  subscribe: (listener) => {
6196
6233
  check();
6197
- keyListeners.add(listener);
6198
- return () => keyListeners.delete(listener);
6234
+ checkSubscription();
6235
+ return require_driver.addListener(keyListeners, listener);
6199
6236
  }
6200
6237
  });
6201
6238
  return {
@@ -6203,6 +6240,7 @@ const createProjectionItems = (source, checkOwner) => {
6203
6240
  keys,
6204
6241
  get: (key) => {
6205
6242
  check();
6243
+ checkSubscription();
6206
6244
  const current = source.current();
6207
6245
  if (source.revision() !== settledRevision) pendingKeys.add(key);
6208
6246
  const canonical = active.get(key);
@@ -6247,6 +6285,7 @@ const createProjectionRuntime = (options) => {
6247
6285
  const materialized = /* @__PURE__ */ new WeakMap();
6248
6286
  const publicViews = /* @__PURE__ */ new WeakMap();
6249
6287
  const scopes = /* @__PURE__ */ new Set();
6288
+ const cleanups = createCleanupStack();
6250
6289
  const rootItems = /* @__PURE__ */ new Map();
6251
6290
  const disposeItemControllers = (registry, failures) => {
6252
6291
  const controllers = [...registry.values()];
@@ -6276,7 +6315,7 @@ const createProjectionRuntime = (options) => {
6276
6315
  const owner = definition.owner;
6277
6316
  if (owner === void 0) return;
6278
6317
  if (owner !== requester) throw new TypeError("Projection belongs to another scope.");
6279
- if (!requester?.active) throw new require_transform.ProjectionDisposedError();
6318
+ if (requester?.status === "disposed") throw new require_transform.ProjectionDisposedError();
6280
6319
  };
6281
6320
  const rememberScoped = (definition, requester) => {
6282
6321
  if (requester && definition.owner === requester) requester.materialized.push(definition);
@@ -6356,25 +6395,33 @@ const createProjectionRuntime = (options) => {
6356
6395
  assertOutput(output);
6357
6396
  return output.revision();
6358
6397
  },
6359
- subscribe: output.subscribe,
6360
- observe: output.observe
6398
+ subscribe: (listener) => {
6399
+ scheduler.assertCommand();
6400
+ return output.subscribe(listener);
6401
+ },
6402
+ observe: (listener) => {
6403
+ scheduler.assertCommand();
6404
+ return output.observe(listener);
6405
+ }
6361
6406
  });
6362
6407
  };
6363
6408
  const projectionItems = (projection, requester) => {
6409
+ scheduler.assertCommand();
6364
6410
  const output = resolveOutput(projection, requester);
6365
6411
  if (output.kind !== "collection") throw new TypeError("Projection items require a keyed collection projection.");
6366
6412
  const registry = requester?.items ?? rootItems;
6367
6413
  const existing = registry.get(output);
6368
6414
  if (existing) return existing.items;
6369
6415
  const controller = createProjectionItems(readableSource(projection, requester), () => {
6370
- if (requester && !requester.active) throw new require_transform.ProjectionDisposedError();
6416
+ if (requester?.status === "disposed") throw new require_transform.ProjectionDisposedError();
6371
6417
  scheduler.ensureCurrent(output);
6372
6418
  assertOutput(output);
6373
- });
6419
+ }, scheduler.assertCommand);
6374
6420
  registry.set(output, controller);
6375
6421
  return controller.items;
6376
6422
  };
6377
6423
  const makeReadable = (projection, selector, equality, requester) => {
6424
+ scheduler.assertCommand();
6378
6425
  const source = readableSource(projection, requester);
6379
6426
  return selector ? createSelectorReadable(source, selector, equality) : createDirectReadable(source);
6380
6427
  };
@@ -6382,8 +6429,8 @@ const createProjectionRuntime = (options) => {
6382
6429
  return makeReadable(projection, selector, equality);
6383
6430
  }
6384
6431
  const inputAccess = (target, requester) => {
6385
- scheduler.assertIdle();
6386
- if (requester && !requester.active) throw new require_transform.ProjectionDisposedError();
6432
+ scheduler.assertCommand();
6433
+ if (requester?.status === "disposed") throw new require_transform.ProjectionDisposedError();
6387
6434
  const ref = require_transform.projectionRef(target);
6388
6435
  if (ref.output !== 0 || ref.producer.kind !== "source") throw new TypeError("Projection is not an input.");
6389
6436
  if (ref.producer.source.kind !== "value-input" && ref.producer.source.kind !== "collection-input") throw new TypeError("Projection is not an input.");
@@ -6404,11 +6451,9 @@ const createProjectionRuntime = (options) => {
6404
6451
  updateInput(target, valueOrRun);
6405
6452
  }
6406
6453
  const batch = (run, options) => scheduler.batch(run, options);
6407
- const disposeScope = (state) => {
6408
- if (!state.active) return;
6409
- scheduler.assertIdle();
6410
- state.active = false;
6411
- const failures = [];
6454
+ const releaseScope = (state, failures) => {
6455
+ state.status = "disposed";
6456
+ scopes.delete(state);
6412
6457
  disposeScopeConsumers(state, failures);
6413
6458
  for (let index = state.materialized.length - 1; index >= 0; index--) {
6414
6459
  const definition = state.materialized[index];
@@ -6423,24 +6468,38 @@ const createProjectionRuntime = (options) => {
6423
6468
  }
6424
6469
  }
6425
6470
  state.materialized.length = 0;
6426
- scopes.delete(state);
6471
+ };
6472
+ const disposeScope = (state) => {
6473
+ if (state.status !== "active") return;
6474
+ scheduler.assertCommand();
6475
+ state.status = "closing";
6476
+ const failures = [];
6477
+ try {
6478
+ scheduler.cleanup(() => state.cleanups.drain(failures));
6479
+ } catch (error) {
6480
+ failures.push(error);
6481
+ } finally {
6482
+ scheduler.releaseOwned(() => releaseScope(state, failures));
6483
+ }
6427
6484
  throwCleanupFailures(failures, "Projection scope cleanup failed.");
6428
6485
  };
6429
6486
  const createScope = () => {
6430
- scheduler.assertIdle();
6487
+ scheduler.assertCommand();
6431
6488
  const state = {
6432
- active: true,
6489
+ status: "active",
6490
+ cleanups: createCleanupStack(),
6433
6491
  materialized: [],
6434
6492
  subscriptions: /* @__PURE__ */ new Set(),
6435
6493
  items: /* @__PURE__ */ new Map()
6436
6494
  };
6437
6495
  scopes.add(state);
6438
6496
  const assertActive = () => {
6439
- if (!state.active) throw new require_transform.ProjectionDisposedError();
6497
+ if (state.status === "disposed") throw new require_transform.ProjectionDisposedError();
6440
6498
  scheduler.assertActive();
6441
6499
  };
6442
6500
  function own(target) {
6443
6501
  assertActive();
6502
+ scheduler.assertCommand();
6444
6503
  const projections = [];
6445
6504
  const collect = (value) => {
6446
6505
  if (require_transform.isProjection(value)) {
@@ -6510,6 +6569,11 @@ const createProjectionRuntime = (options) => {
6510
6569
  },
6511
6570
  update: scopeUpdate,
6512
6571
  batch: scopeBatch,
6572
+ onDispose: (callback) => {
6573
+ assertActive();
6574
+ scheduler.assertCommand();
6575
+ return state.cleanups.add(callback);
6576
+ },
6513
6577
  dispose: () => disposeScope(state)
6514
6578
  });
6515
6579
  };
@@ -6520,24 +6584,33 @@ const createProjectionRuntime = (options) => {
6520
6584
  update,
6521
6585
  batch,
6522
6586
  scope: createScope,
6587
+ onDispose: (callback) => {
6588
+ scheduler.assertCommand();
6589
+ return cleanups.add(callback);
6590
+ },
6523
6591
  dispose: () => {
6524
- if (!scheduler.active) return;
6525
- scheduler.assertIdle();
6592
+ if (!scheduler.beginClose()) return;
6526
6593
  const failures = [];
6527
- for (const scope of scopes) {
6528
- scope.active = false;
6529
- disposeScopeConsumers(scope, failures);
6530
- scope.materialized.length = 0;
6531
- }
6532
- scopes.clear();
6533
- disposeItemControllers(rootItems, failures);
6594
+ const owned = [...scopes].reverse();
6595
+ for (const scope of owned) scope.status = "closing";
6534
6596
  try {
6535
- sources.dispose();
6597
+ scheduler.cleanup(() => {
6598
+ for (const scope of owned) scope.cleanups.drain(failures);
6599
+ cleanups.drain(failures);
6600
+ });
6536
6601
  } catch (error) {
6537
6602
  failures.push(error);
6538
6603
  }
6539
6604
  try {
6540
- scheduler.dispose();
6605
+ scheduler.dispose(() => {
6606
+ for (const scope of owned) {
6607
+ scope.status = "disposed";
6608
+ scope.materialized.length = 0;
6609
+ disposeScopeConsumers(scope, failures);
6610
+ }
6611
+ scopes.clear();
6612
+ disposeItemControllers(rootItems, failures);
6613
+ }, sources.dispose);
6541
6614
  } catch (error) {
6542
6615
  failures.push(error);
6543
6616
  }
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { $ as table, A as ParseError, B as DocumentListConfig, C as Read, D as ChangeSet, E as Change, F as CollectionPath, G as ReadonlyValue, H as DocumentTreeValue, I as PathValueOf, J as field, K as Schema, L as SchemaPath, M as parse, N as CollectionEntry, O as MemberChange, P as CollectionId, Q as optional, R as DocumentAddress, S as Draft, T as snapshot, U as Infer, V as DocumentTreeNode, W as ObjectSchema, X as map, Y as list, Z as object, _ as Unsubscribe, a as DocumentProblem, b as CollectionImpact, c as HistoryState, d as OperationResult, et as tree, f as ReadonlyDocument, g as Readable, h as TransactionResult, i as DocumentDisposedError, j as ParseIssue, k as ValueTransition, l as LocalHistory, m as TransactionRejected, n as DocumentCommit, o as DocumentReentrancyError, p as Synchronous, q as Validator, r as DocumentDiagnostic, s as DocumentRuntime, t as CommitSource, tt as variant, u as ObserverError, v as MutationIssue, w as replace, x as DocumentImpact, y as MutationIssueCode, z as DocumentAnchor } from "./contract-CNR6BS75.cjs";
2
- import { _ as ProjectionDisposedError, a as Input, f as ExternalCollectionEvent, g as ExternalValueSource, h as ExternalValueEvent, i as CollectionInputDraft, m as ExternalCollectionSource, n as KeyedDependencyValues, o as KeyedProjection, p as ExternalCollectionRead, r as CollectionInput, s as Projection, t as KeyedDependencies, v as ProjectionError } from "./dependency-Cbf3C8eJ.cjs";
1
+ import { $ as table, A as ParseError, B as DocumentListConfig, C as Read, D as ChangeSet, E as Change, F as CollectionPath, G as ReadonlyValue, H as DocumentTreeValue, I as PathValueOf, J as field, K as Schema, L as SchemaPath, M as parse, N as CollectionEntry, O as MemberChange, P as CollectionId, Q as optional, R as DocumentAddress, S as Draft, T as snapshot, U as Infer, V as DocumentTreeNode, W as ObjectSchema, X as map, Y as list, Z as object, _ as Unsubscribe, a as DocumentProblem, b as CollectionImpact, c as HistoryState, d as OperationResult, et as tree, f as ReadonlyDocument, g as Readable, h as TransactionResult, i as DocumentDisposedError, j as ParseIssue, k as ValueTransition, l as LocalHistory, m as TransactionRejected, n as DocumentCommit, o as DocumentReentrancyError, p as Synchronous, q as Validator, r as DocumentDiagnostic, s as DocumentRuntime, t as CommitSource, tt as variant, u as ObserverError, v as MutationIssue, w as replace, x as DocumentImpact, y as MutationIssueCode, z as DocumentAnchor } from "./contract-DovG1Yp-.cjs";
2
+ import { _ as ProjectionDisposedError, a as Input, f as ExternalCollectionEvent, g as ExternalValueSource, h as ExternalValueEvent, i as CollectionInputDraft, m as ExternalCollectionSource, n as KeyedDependencyValues, o as KeyedProjection, p as ExternalCollectionRead, r as CollectionInput, s as Projection, t as KeyedDependencies, v as ProjectionError } from "./dependency-D0bo_xe_.cjs";
3
3
 
4
4
  //#region core/src/runtime.d.ts
5
5
  declare const createDocument: <S extends ObjectSchema<object>>(input: {
@@ -119,6 +119,7 @@ type ProjectionRuntime = {
119
119
  readonly cause?: unknown;
120
120
  }): T;
121
121
  scope(): ProjectionScope;
122
+ onDispose<R>(cleanup: () => Synchronous<R>): Unsubscribe;
122
123
  dispose(): void;
123
124
  };
124
125
  type ProjectionOwnershipTree = {
@@ -136,6 +137,7 @@ type ProjectionScope = {
136
137
  batch<T>(run: () => Synchronous<T>, options?: {
137
138
  readonly cause?: unknown;
138
139
  }): T;
140
+ onDispose<R>(cleanup: () => Synchronous<R>): Unsubscribe;
139
141
  dispose(): void;
140
142
  };
141
143
  declare const createProjectionRuntime: (options?: {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { $ as table, A as ParseError, B as DocumentListConfig, C as Read, D as ChangeSet, E as Change, F as CollectionPath, G as ReadonlyValue, H as DocumentTreeValue, I as PathValueOf, J as field, K as Schema, L as SchemaPath, M as parse, N as CollectionEntry, O as MemberChange, P as CollectionId, Q as optional, R as DocumentAddress, S as Draft, T as snapshot, U as Infer, V as DocumentTreeNode, W as ObjectSchema, X as map, Y as list, Z as object, _ as Unsubscribe, a as DocumentProblem, b as CollectionImpact, c as HistoryState, d as OperationResult, et as tree, f as ReadonlyDocument, g as Readable, h as TransactionResult, i as DocumentDisposedError, j as ParseIssue, k as ValueTransition, l as LocalHistory, m as TransactionRejected, n as DocumentCommit, o as DocumentReentrancyError, p as Synchronous, q as Validator, r as DocumentDiagnostic, s as DocumentRuntime, t as CommitSource, tt as variant, u as ObserverError, v as MutationIssue, w as replace, x as DocumentImpact, y as MutationIssueCode, z as DocumentAnchor } from "./contract-COgxO5sv.js";
2
- import { _ as ProjectionDisposedError, a as Input, f as ExternalCollectionEvent, g as ExternalValueSource, h as ExternalValueEvent, i as CollectionInputDraft, m as ExternalCollectionSource, n as KeyedDependencyValues, o as KeyedProjection, p as ExternalCollectionRead, r as CollectionInput, s as Projection, t as KeyedDependencies, v as ProjectionError } from "./dependency-DzxXGYe1.js";
1
+ import { $ as table, A as ParseError, B as DocumentListConfig, C as Read, D as ChangeSet, E as Change, F as CollectionPath, G as ReadonlyValue, H as DocumentTreeValue, I as PathValueOf, J as field, K as Schema, L as SchemaPath, M as parse, N as CollectionEntry, O as MemberChange, P as CollectionId, Q as optional, R as DocumentAddress, S as Draft, T as snapshot, U as Infer, V as DocumentTreeNode, W as ObjectSchema, X as map, Y as list, Z as object, _ as Unsubscribe, a as DocumentProblem, b as CollectionImpact, c as HistoryState, d as OperationResult, et as tree, f as ReadonlyDocument, g as Readable, h as TransactionResult, i as DocumentDisposedError, j as ParseIssue, k as ValueTransition, l as LocalHistory, m as TransactionRejected, n as DocumentCommit, o as DocumentReentrancyError, p as Synchronous, q as Validator, r as DocumentDiagnostic, s as DocumentRuntime, t as CommitSource, tt as variant, u as ObserverError, v as MutationIssue, w as replace, x as DocumentImpact, y as MutationIssueCode, z as DocumentAnchor } from "./contract-PqdXNaP8.js";
2
+ import { _ as ProjectionDisposedError, a as Input, f as ExternalCollectionEvent, g as ExternalValueSource, h as ExternalValueEvent, i as CollectionInputDraft, m as ExternalCollectionSource, n as KeyedDependencyValues, o as KeyedProjection, p as ExternalCollectionRead, r as CollectionInput, s as Projection, t as KeyedDependencies, v as ProjectionError } from "./dependency-BYiwTiBj.js";
3
3
 
4
4
  //#region core/src/runtime.d.ts
5
5
  declare const createDocument: <S extends ObjectSchema<object>>(input: {
@@ -119,6 +119,7 @@ type ProjectionRuntime = {
119
119
  readonly cause?: unknown;
120
120
  }): T;
121
121
  scope(): ProjectionScope;
122
+ onDispose<R>(cleanup: () => Synchronous<R>): Unsubscribe;
122
123
  dispose(): void;
123
124
  };
124
125
  type ProjectionOwnershipTree = {
@@ -136,6 +137,7 @@ type ProjectionScope = {
136
137
  batch<T>(run: () => Synchronous<T>, options?: {
137
138
  readonly cause?: unknown;
138
139
  }): T;
140
+ onDispose<R>(cleanup: () => Synchronous<R>): Unsubscribe;
139
141
  dispose(): void;
140
142
  };
141
143
  declare const createProjectionRuntime: (options?: {