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.
@@ -1,4 +1,4 @@
1
- import { W as ObjectSchema, g as Readable, s as DocumentRuntime } from "./contract-COgxO5sv.js";
1
+ import { W as ObjectSchema, g as Readable, s as DocumentRuntime } from "./contract-PqdXNaP8.js";
2
2
 
3
3
  //#region core/src/local-sync/json.d.ts
4
4
  type JsonPrimitive = null | boolean | number | string;
@@ -1,5 +1,5 @@
1
1
  import { n as isPlainObject, r as isRecord } from "./record-B2yRhYsc.js";
2
- import { a as changeCount, n as installRuntimeWriteDriver, o as decodeChanges } from "./driver-B7RR-VfX.js";
2
+ import { a as changeCount, c as addListener, n as installRuntimeWriteDriver, o as decodeChanges } from "./driver-B7Hu6D0u.js";
3
3
  //#region core/src/local-sync/error.ts
4
4
  var LocalSyncError = class extends Error {
5
5
  code;
@@ -466,10 +466,7 @@ const attachLocalSync = async (input) => {
466
466
  revision: () => stateRevision,
467
467
  subscribe: (listener) => {
468
468
  if (disposed) throw new LocalSyncError("disposed", "Local sync has been disposed.");
469
- stateListeners.add(listener);
470
- return () => {
471
- stateListeners.delete(listener);
472
- };
469
+ return addListener(stateListeners, listener);
473
470
  }
474
471
  }),
475
472
  flush: async () => {
package/dist/react.cjs CHANGED
@@ -1,9 +1,43 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- let doxum = require("doxum");
3
2
  let react = require("react");
4
- //#region react/src/hooks.ts
3
+ let doxum = require("doxum");
4
+ //#region react/src/projection.ts
5
5
  const ProjectionContext = (0, react.createContext)(void 0);
6
- const ProjectionProvider = ProjectionContext.Provider;
6
+ const ManagedProjectionProvider = ({ runtime, fallback, children }) => {
7
+ const generation = (0, react.useRef)(0);
8
+ const [owner, setOwner] = (0, react.useState)();
9
+ (0, react.useEffect)(() => {
10
+ const scope = runtime.scope();
11
+ setOwner({
12
+ runtime,
13
+ scope,
14
+ generation: ++generation.current
15
+ });
16
+ return () => scope.dispose();
17
+ }, [runtime]);
18
+ if (!owner || owner.runtime !== runtime) return fallback ?? null;
19
+ return (0, react.createElement)(ProjectionContext.Provider, {
20
+ value: owner.scope,
21
+ key: owner.generation
22
+ }, children);
23
+ };
24
+ /** runtime creates an owned scope after commit; value borrows an external owner. */
25
+ const ProjectionProvider = (props) => {
26
+ if (props.runtime !== void 0) {
27
+ if (props.value !== void 0) throw new TypeError("ProjectionProvider accepts runtime or value, not both.");
28
+ return (0, react.createElement)(ManagedProjectionProvider, props);
29
+ }
30
+ if (props.value === void 0) throw new TypeError("ProjectionProvider requires runtime or value.");
31
+ return (0, react.createElement)(ProjectionContext.Provider, { value: props.value }, props.children);
32
+ };
33
+ /** Reads a scope supplied by a managed Provider or an explicitly injected scope. */
34
+ const useProjectionScope = () => {
35
+ const owner = (0, react.useContext)(ProjectionContext);
36
+ if (!owner || !("own" in owner)) throw new Error("A ProjectionScope provider is required.");
37
+ return owner;
38
+ };
39
+ //#endregion
40
+ //#region react/src/hooks.ts
7
41
  function useProjection(projection, selector, equality = Object.is) {
8
42
  const owner = (0, react.useContext)(ProjectionContext);
9
43
  if (!owner) throw new Error("ProjectionRuntime is required.");
@@ -56,4 +90,5 @@ exports.useDocumentSelector = useDocumentSelector;
56
90
  exports.useHistory = useHistory;
57
91
  exports.useInput = useInput;
58
92
  exports.useProjection = useProjection;
93
+ exports.useProjectionScope = useProjectionScope;
59
94
  exports.useReadable = useReadable;
package/dist/react.d.cts CHANGED
@@ -1,8 +1,7 @@
1
- import * as _$react from "react";
2
1
  import { CollectionInput, CollectionInputDraft, DocumentSelector, HistoryState, Input, LocalHistory, ObjectSchema, OperationResult, Projection, ProjectionRuntime, ProjectionScope, Readable, ReadonlyDocument } from "doxum";
2
+ import { ReactElement, ReactNode } from "react";
3
3
 
4
4
  //#region react/src/hooks.d.ts
5
- declare const ProjectionProvider: _$react.Provider<ProjectionRuntime | ProjectionScope | undefined>;
6
5
  declare function useProjection<T>(projection: Projection<T>): T;
7
6
  declare function useProjection<T, R>(projection: Projection<T>, selector: (value: T) => R, equality?: (previous: R, next: R) => boolean): R;
8
7
  declare function useInput<T>(input: Input<T>): readonly [T, (value: T) => void];
@@ -14,4 +13,21 @@ declare function useHistory<TCommit>(history: LocalHistory<TCommit>): HistorySta
14
13
  redo(): OperationResult<TCommit>;
15
14
  };
16
15
  //#endregion
17
- export { ProjectionProvider, useDocumentSelector, useHistory, useInput, useProjection, useReadable };
16
+ //#region react/src/projection.d.ts
17
+ type ProjectionProviderProps = {
18
+ readonly children?: ReactNode;
19
+ } & ({
20
+ readonly runtime: ProjectionRuntime;
21
+ readonly fallback?: ReactNode;
22
+ readonly value?: never;
23
+ } | {
24
+ readonly value: ProjectionRuntime | ProjectionScope;
25
+ readonly runtime?: never;
26
+ readonly fallback?: never;
27
+ });
28
+ /** runtime creates an owned scope after commit; value borrows an external owner. */
29
+ declare const ProjectionProvider: (props: ProjectionProviderProps) => ReactElement;
30
+ /** Reads a scope supplied by a managed Provider or an explicitly injected scope. */
31
+ declare const useProjectionScope: () => ProjectionScope;
32
+ //#endregion
33
+ export { ProjectionProvider, type ProjectionProviderProps, useDocumentSelector, useHistory, useInput, useProjection, useProjectionScope, useReadable };
package/dist/react.d.ts CHANGED
@@ -1,8 +1,7 @@
1
+ import { ReactElement, ReactNode } from "react";
1
2
  import { CollectionInput, CollectionInputDraft, DocumentSelector, HistoryState, Input, LocalHistory, ObjectSchema, OperationResult, Projection, ProjectionRuntime, ProjectionScope, Readable, ReadonlyDocument } from "doxum";
2
- import * as _$react from "react";
3
3
 
4
4
  //#region react/src/hooks.d.ts
5
- declare const ProjectionProvider: _$react.Provider<ProjectionRuntime | ProjectionScope | undefined>;
6
5
  declare function useProjection<T>(projection: Projection<T>): T;
7
6
  declare function useProjection<T, R>(projection: Projection<T>, selector: (value: T) => R, equality?: (previous: R, next: R) => boolean): R;
8
7
  declare function useInput<T>(input: Input<T>): readonly [T, (value: T) => void];
@@ -14,4 +13,21 @@ declare function useHistory<TCommit>(history: LocalHistory<TCommit>): HistorySta
14
13
  redo(): OperationResult<TCommit>;
15
14
  };
16
15
  //#endregion
17
- export { ProjectionProvider, useDocumentSelector, useHistory, useInput, useProjection, useReadable };
16
+ //#region react/src/projection.d.ts
17
+ type ProjectionProviderProps = {
18
+ readonly children?: ReactNode;
19
+ } & ({
20
+ readonly runtime: ProjectionRuntime;
21
+ readonly fallback?: ReactNode;
22
+ readonly value?: never;
23
+ } | {
24
+ readonly value: ProjectionRuntime | ProjectionScope;
25
+ readonly runtime?: never;
26
+ readonly fallback?: never;
27
+ });
28
+ /** runtime creates an owned scope after commit; value borrows an external owner. */
29
+ declare const ProjectionProvider: (props: ProjectionProviderProps) => ReactElement;
30
+ /** Reads a scope supplied by a managed Provider or an explicitly injected scope. */
31
+ declare const useProjectionScope: () => ProjectionScope;
32
+ //#endregion
33
+ export { ProjectionProvider, type ProjectionProviderProps, useDocumentSelector, useHistory, useInput, useProjection, useProjectionScope, useReadable };
package/dist/react.js CHANGED
@@ -1,8 +1,42 @@
1
+ import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
1
2
  import { select } from "doxum";
2
- import { createContext, useCallback, useContext, useMemo, useSyncExternalStore } from "react";
3
- //#region react/src/hooks.ts
3
+ //#region react/src/projection.ts
4
4
  const ProjectionContext = createContext(void 0);
5
- const ProjectionProvider = ProjectionContext.Provider;
5
+ const ManagedProjectionProvider = ({ runtime, fallback, children }) => {
6
+ const generation = useRef(0);
7
+ const [owner, setOwner] = useState();
8
+ useEffect(() => {
9
+ const scope = runtime.scope();
10
+ setOwner({
11
+ runtime,
12
+ scope,
13
+ generation: ++generation.current
14
+ });
15
+ return () => scope.dispose();
16
+ }, [runtime]);
17
+ if (!owner || owner.runtime !== runtime) return fallback ?? null;
18
+ return createElement(ProjectionContext.Provider, {
19
+ value: owner.scope,
20
+ key: owner.generation
21
+ }, children);
22
+ };
23
+ /** runtime creates an owned scope after commit; value borrows an external owner. */
24
+ const ProjectionProvider = (props) => {
25
+ if (props.runtime !== void 0) {
26
+ if (props.value !== void 0) throw new TypeError("ProjectionProvider accepts runtime or value, not both.");
27
+ return createElement(ManagedProjectionProvider, props);
28
+ }
29
+ if (props.value === void 0) throw new TypeError("ProjectionProvider requires runtime or value.");
30
+ return createElement(ProjectionContext.Provider, { value: props.value }, props.children);
31
+ };
32
+ /** Reads a scope supplied by a managed Provider or an explicitly injected scope. */
33
+ const useProjectionScope = () => {
34
+ const owner = useContext(ProjectionContext);
35
+ if (!owner || !("own" in owner)) throw new Error("A ProjectionScope provider is required.");
36
+ return owner;
37
+ };
38
+ //#endregion
39
+ //#region react/src/hooks.ts
6
40
  function useProjection(projection, selector, equality = Object.is) {
7
41
  const owner = useContext(ProjectionContext);
8
42
  if (!owner) throw new Error("ProjectionRuntime is required.");
@@ -50,4 +84,4 @@ function useHistory(history) {
50
84
  ]);
51
85
  }
52
86
  //#endregion
53
- export { ProjectionProvider, useDocumentSelector, useHistory, useInput, useProjection, useReadable };
87
+ export { ProjectionProvider, useDocumentSelector, useHistory, useInput, useProjection, useProjectionScope, useReadable };
@@ -29,6 +29,8 @@ const profile = {
29
29
  segmentCompared: () => {}
30
30
  },
31
31
  collectionIndex: {
32
+ batch: (sortedKeys) => {},
33
+ visit: () => {},
32
34
  node: () => {},
33
35
  build: (items) => {}
34
36
  },
@@ -259,7 +261,8 @@ var ProjectionDisposedError = class extends Error {
259
261
  //#endregion
260
262
  //#region core/src/projection/graph/scheduler.ts
261
263
  const createScheduler = (onError) => {
262
- let disposed = false;
264
+ let lifecycle = "active";
265
+ let lockDepth = 0;
263
266
  let depth = 0;
264
267
  let batchSequence = 0;
265
268
  let activeBatch;
@@ -277,13 +280,39 @@ const createScheduler = (onError) => {
277
280
  let reportingFailures = [];
278
281
  const guards = /* @__PURE__ */ new Set();
279
282
  const assertActive = () => {
280
- if (disposed) throw new ProjectionDisposedError();
283
+ if (lifecycle === "disposed" || phase === "release") throw new ProjectionDisposedError();
281
284
  };
282
285
  const assertIdle = () => {
283
286
  assertActive();
284
- if (phase !== "idle") throw new Error("Projection cannot be re-entered during input callbacks, processing or notification.");
287
+ if (phase !== "idle" && phase !== "cleanup") throw new Error("Projection cannot be re-entered during input callbacks, processing or notification.");
288
+ };
289
+ const assertCommand = () => {
290
+ assertActive();
291
+ if (lifecycle === "closing" || phase === "cleanup") throw new Error("Projection cleanup only permits reads and unsubscribe.");
292
+ assertIdle();
293
+ };
294
+ const lock = (value) => {
295
+ const previous = lockDepth;
296
+ lockDepth += value ? 1 : -1;
297
+ if (!previous || !lockDepth) guards.forEach((guard) => guard(value));
298
+ };
299
+ const beginClose = () => {
300
+ if (lifecycle !== "active") return false;
301
+ assertCommand();
302
+ lifecycle = "closing";
303
+ return true;
304
+ };
305
+ const withPhase = (next, run) => {
306
+ const previous = phase;
307
+ phase = next;
308
+ lock(true);
309
+ try {
310
+ return run();
311
+ } finally {
312
+ lock(false);
313
+ phase = previous;
314
+ }
285
315
  };
286
- const lock = (value) => guards.forEach((guard) => guard(value));
287
316
  const markDirty = (producer) => {
288
317
  const pending = [producer];
289
318
  for (let index = 0; index < pending.length; index++) {
@@ -300,7 +329,7 @@ const createScheduler = (onError) => {
300
329
  };
301
330
  const errorFor = (producer, cause, kind = producer.kind === "source" ? "source" : "processor") => new ProjectionError(kind, cause);
302
331
  const capture = (source) => {
303
- if (disposed) return;
332
+ if (lifecycle === "disposed") return;
304
333
  if (phase !== "idle") {
305
334
  source.fault = errorFor(source, /* @__PURE__ */ new Error("Source changed while projection was running."), "source");
306
335
  errors.push(source.fault);
@@ -420,18 +449,9 @@ const createScheduler = (onError) => {
420
449
  }
421
450
  }
422
451
  };
423
- const compute = (run) => {
424
- phase = "compute";
425
- lock(true);
426
- try {
427
- return run();
428
- } finally {
429
- lock(false);
430
- phase = "idle";
431
- }
432
- };
452
+ const compute = (run) => withPhase("compute", run);
433
453
  const settle = () => {
434
- if (disposed || depth || phase !== "idle") return;
454
+ if (lifecycle !== "active" || depth || phase !== "idle") return;
435
455
  const cause = settleCause();
436
456
  compute(() => {
437
457
  for (const producer of dirty) advance(producer, cause);
@@ -447,7 +467,7 @@ const createScheduler = (onError) => {
447
467
  }
448
468
  };
449
469
  const flush = () => {
450
- if (disposed || depth) return [];
470
+ if (lifecycle !== "active" || depth || phase === "cleanup" || phase === "release") return [];
451
471
  if (emissions.size || completed.size) profile.projection("flushes");
452
472
  phase = "notify";
453
473
  lock(true);
@@ -495,7 +515,7 @@ const createScheduler = (onError) => {
495
515
  return result;
496
516
  };
497
517
  const batch = (callback, options) => {
498
- assertIdle();
518
+ assertCommand();
499
519
  if (depth === 0) activeBatch = Object.freeze({
500
520
  id: ++batchSequence,
501
521
  ...options?.cause === void 0 ? {} : { cause: options.cause }
@@ -522,7 +542,8 @@ const createScheduler = (onError) => {
522
542
  }
523
543
  };
524
544
  const releaseProducer = (producer) => {
525
- assertIdle();
545
+ if (producer.disposed) return;
546
+ if (phase !== "release") assertIdle();
526
547
  if (producer.outputs.some((output) => output.hasConsumers())) throw new Error("Projection producer is still used by another processor.");
527
548
  producer.disposed = true;
528
549
  dirty.delete(producer);
@@ -552,6 +573,16 @@ const createScheduler = (onError) => {
552
573
  return {
553
574
  assertActive,
554
575
  assertIdle,
576
+ assertCommand,
577
+ beginClose,
578
+ cleanup(run) {
579
+ assertIdle();
580
+ return withPhase("cleanup", run);
581
+ },
582
+ releaseOwned(run) {
583
+ assertIdle();
584
+ return withPhase("release", run);
585
+ },
555
586
  capture,
556
587
  settle,
557
588
  ensureCurrent,
@@ -560,15 +591,17 @@ const createScheduler = (onError) => {
560
591
  registerGuard(guard) {
561
592
  assertActive();
562
593
  guards.add(guard);
594
+ if (lockDepth) guard(true);
563
595
  let registered = true;
564
596
  return () => {
565
597
  if (!registered) return;
566
598
  registered = false;
567
599
  guards.delete(guard);
600
+ if (lockDepth) guard(false);
568
601
  };
569
602
  },
570
603
  get active() {
571
- return !disposed;
604
+ return lifecycle !== "disposed" && phase !== "release";
572
605
  },
573
606
  addSource(source) {
574
607
  assertIdle();
@@ -582,7 +615,7 @@ const createScheduler = (onError) => {
582
615
  releaseProducer,
583
616
  nextId: () => sequence++,
584
617
  acceptInput(callback) {
585
- assertIdle();
618
+ assertCommand();
586
619
  phase = "input";
587
620
  lock(true);
588
621
  try {
@@ -598,38 +631,39 @@ const createScheduler = (onError) => {
598
631
  },
599
632
  batch,
600
633
  batchContext: () => activeBatch,
601
- dispose() {
602
- if (disposed) return;
603
- assertIdle();
604
- disposed = true;
634
+ dispose(releaseConsumers, releaseSources) {
635
+ if (lifecycle === "disposed") return;
636
+ if (lifecycle === "active") beginClose();
605
637
  const failures = [];
606
- processors.forEach((processor) => {
607
- processor.disposed = true;
608
- try {
609
- processor.release();
610
- } catch (error) {
611
- failures.push(error);
612
- }
613
- });
614
- sources.forEach((source) => {
615
- source.disposed = true;
638
+ const attempt = (run) => {
616
639
  try {
617
- source.release();
640
+ run();
618
641
  } catch (error) {
619
642
  failures.push(error);
620
643
  }
621
- });
622
- processors.clear();
623
- sources.clear();
624
- pendingSources.clear();
625
- dirty.clear();
626
- queued.clear();
627
- completed.clear();
628
- emissions.clear();
629
- forced.clear();
630
- guards.clear();
631
- errors.length = 0;
632
- reportingFailures = [];
644
+ };
645
+ try {
646
+ withPhase("release", () => {
647
+ attempt(releaseConsumers);
648
+ for (const processor of [...processors].reverse()) attempt(() => releaseProducer(processor));
649
+ for (const source of [...sources].reverse()) attempt(() => releaseProducer(source));
650
+ attempt(releaseSources);
651
+ });
652
+ } finally {
653
+ lifecycle = "disposed";
654
+ processors.clear();
655
+ sources.clear();
656
+ pendingSources.clear();
657
+ dirty.clear();
658
+ queued.clear();
659
+ completed.clear();
660
+ emissions.clear();
661
+ forced.clear();
662
+ guards.clear();
663
+ errors.length = 0;
664
+ reportingFailures = [];
665
+ }
666
+ if (failures.length === 1) throw failures[0];
633
667
  if (failures.length) throw new AggregateError(failures, "Projection cleanup failed.");
634
668
  }
635
669
  };
@@ -29,6 +29,8 @@ const profile = {
29
29
  segmentCompared: () => {}
30
30
  },
31
31
  collectionIndex: {
32
+ batch: (sortedKeys) => {},
33
+ visit: () => {},
32
34
  node: () => {},
33
35
  build: (items) => {}
34
36
  },
@@ -259,7 +261,8 @@ var ProjectionDisposedError = class extends Error {
259
261
  //#endregion
260
262
  //#region core/src/projection/graph/scheduler.ts
261
263
  const createScheduler = (onError) => {
262
- let disposed = false;
264
+ let lifecycle = "active";
265
+ let lockDepth = 0;
263
266
  let depth = 0;
264
267
  let batchSequence = 0;
265
268
  let activeBatch;
@@ -277,13 +280,39 @@ const createScheduler = (onError) => {
277
280
  let reportingFailures = [];
278
281
  const guards = /* @__PURE__ */ new Set();
279
282
  const assertActive = () => {
280
- if (disposed) throw new ProjectionDisposedError();
283
+ if (lifecycle === "disposed" || phase === "release") throw new ProjectionDisposedError();
281
284
  };
282
285
  const assertIdle = () => {
283
286
  assertActive();
284
- if (phase !== "idle") throw new Error("Projection cannot be re-entered during input callbacks, processing or notification.");
287
+ if (phase !== "idle" && phase !== "cleanup") throw new Error("Projection cannot be re-entered during input callbacks, processing or notification.");
288
+ };
289
+ const assertCommand = () => {
290
+ assertActive();
291
+ if (lifecycle === "closing" || phase === "cleanup") throw new Error("Projection cleanup only permits reads and unsubscribe.");
292
+ assertIdle();
293
+ };
294
+ const lock = (value) => {
295
+ const previous = lockDepth;
296
+ lockDepth += value ? 1 : -1;
297
+ if (!previous || !lockDepth) guards.forEach((guard) => guard(value));
298
+ };
299
+ const beginClose = () => {
300
+ if (lifecycle !== "active") return false;
301
+ assertCommand();
302
+ lifecycle = "closing";
303
+ return true;
304
+ };
305
+ const withPhase = (next, run) => {
306
+ const previous = phase;
307
+ phase = next;
308
+ lock(true);
309
+ try {
310
+ return run();
311
+ } finally {
312
+ lock(false);
313
+ phase = previous;
314
+ }
285
315
  };
286
- const lock = (value) => guards.forEach((guard) => guard(value));
287
316
  const markDirty = (producer) => {
288
317
  const pending = [producer];
289
318
  for (let index = 0; index < pending.length; index++) {
@@ -300,7 +329,7 @@ const createScheduler = (onError) => {
300
329
  };
301
330
  const errorFor = (producer, cause, kind = producer.kind === "source" ? "source" : "processor") => new ProjectionError(kind, cause);
302
331
  const capture = (source) => {
303
- if (disposed) return;
332
+ if (lifecycle === "disposed") return;
304
333
  if (phase !== "idle") {
305
334
  source.fault = errorFor(source, /* @__PURE__ */ new Error("Source changed while projection was running."), "source");
306
335
  errors.push(source.fault);
@@ -420,18 +449,9 @@ const createScheduler = (onError) => {
420
449
  }
421
450
  }
422
451
  };
423
- const compute = (run) => {
424
- phase = "compute";
425
- lock(true);
426
- try {
427
- return run();
428
- } finally {
429
- lock(false);
430
- phase = "idle";
431
- }
432
- };
452
+ const compute = (run) => withPhase("compute", run);
433
453
  const settle = () => {
434
- if (disposed || depth || phase !== "idle") return;
454
+ if (lifecycle !== "active" || depth || phase !== "idle") return;
435
455
  const cause = settleCause();
436
456
  compute(() => {
437
457
  for (const producer of dirty) advance(producer, cause);
@@ -447,7 +467,7 @@ const createScheduler = (onError) => {
447
467
  }
448
468
  };
449
469
  const flush = () => {
450
- if (disposed || depth) return [];
470
+ if (lifecycle !== "active" || depth || phase === "cleanup" || phase === "release") return [];
451
471
  if (emissions.size || completed.size) profile.projection("flushes");
452
472
  phase = "notify";
453
473
  lock(true);
@@ -495,7 +515,7 @@ const createScheduler = (onError) => {
495
515
  return result;
496
516
  };
497
517
  const batch = (callback, options) => {
498
- assertIdle();
518
+ assertCommand();
499
519
  if (depth === 0) activeBatch = Object.freeze({
500
520
  id: ++batchSequence,
501
521
  ...options?.cause === void 0 ? {} : { cause: options.cause }
@@ -522,7 +542,8 @@ const createScheduler = (onError) => {
522
542
  }
523
543
  };
524
544
  const releaseProducer = (producer) => {
525
- assertIdle();
545
+ if (producer.disposed) return;
546
+ if (phase !== "release") assertIdle();
526
547
  if (producer.outputs.some((output) => output.hasConsumers())) throw new Error("Projection producer is still used by another processor.");
527
548
  producer.disposed = true;
528
549
  dirty.delete(producer);
@@ -552,6 +573,16 @@ const createScheduler = (onError) => {
552
573
  return {
553
574
  assertActive,
554
575
  assertIdle,
576
+ assertCommand,
577
+ beginClose,
578
+ cleanup(run) {
579
+ assertIdle();
580
+ return withPhase("cleanup", run);
581
+ },
582
+ releaseOwned(run) {
583
+ assertIdle();
584
+ return withPhase("release", run);
585
+ },
555
586
  capture,
556
587
  settle,
557
588
  ensureCurrent,
@@ -560,15 +591,17 @@ const createScheduler = (onError) => {
560
591
  registerGuard(guard) {
561
592
  assertActive();
562
593
  guards.add(guard);
594
+ if (lockDepth) guard(true);
563
595
  let registered = true;
564
596
  return () => {
565
597
  if (!registered) return;
566
598
  registered = false;
567
599
  guards.delete(guard);
600
+ if (lockDepth) guard(false);
568
601
  };
569
602
  },
570
603
  get active() {
571
- return !disposed;
604
+ return lifecycle !== "disposed" && phase !== "release";
572
605
  },
573
606
  addSource(source) {
574
607
  assertIdle();
@@ -582,7 +615,7 @@ const createScheduler = (onError) => {
582
615
  releaseProducer,
583
616
  nextId: () => sequence++,
584
617
  acceptInput(callback) {
585
- assertIdle();
618
+ assertCommand();
586
619
  phase = "input";
587
620
  lock(true);
588
621
  try {
@@ -598,38 +631,39 @@ const createScheduler = (onError) => {
598
631
  },
599
632
  batch,
600
633
  batchContext: () => activeBatch,
601
- dispose() {
602
- if (disposed) return;
603
- assertIdle();
604
- disposed = true;
634
+ dispose(releaseConsumers, releaseSources) {
635
+ if (lifecycle === "disposed") return;
636
+ if (lifecycle === "active") beginClose();
605
637
  const failures = [];
606
- processors.forEach((processor) => {
607
- processor.disposed = true;
608
- try {
609
- processor.release();
610
- } catch (error) {
611
- failures.push(error);
612
- }
613
- });
614
- sources.forEach((source) => {
615
- source.disposed = true;
638
+ const attempt = (run) => {
616
639
  try {
617
- source.release();
640
+ run();
618
641
  } catch (error) {
619
642
  failures.push(error);
620
643
  }
621
- });
622
- processors.clear();
623
- sources.clear();
624
- pendingSources.clear();
625
- dirty.clear();
626
- queued.clear();
627
- completed.clear();
628
- emissions.clear();
629
- forced.clear();
630
- guards.clear();
631
- errors.length = 0;
632
- reportingFailures = [];
644
+ };
645
+ try {
646
+ withPhase("release", () => {
647
+ attempt(releaseConsumers);
648
+ for (const processor of [...processors].reverse()) attempt(() => releaseProducer(processor));
649
+ for (const source of [...sources].reverse()) attempt(() => releaseProducer(source));
650
+ attempt(releaseSources);
651
+ });
652
+ } finally {
653
+ lifecycle = "disposed";
654
+ processors.clear();
655
+ sources.clear();
656
+ pendingSources.clear();
657
+ dirty.clear();
658
+ queued.clear();
659
+ completed.clear();
660
+ emissions.clear();
661
+ forced.clear();
662
+ guards.clear();
663
+ errors.length = 0;
664
+ reportingFailures = [];
665
+ }
666
+ if (failures.length === 1) throw failures[0];
633
667
  if (failures.length) throw new AggregateError(failures, "Projection cleanup failed.");
634
668
  }
635
669
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doxum",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Doxum is a typed runtime for complex mutable documents.",