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 +36 -4
- package/dist/advanced.cjs +1 -1
- package/dist/advanced.d.cts +1 -1
- package/dist/advanced.d.ts +1 -1
- package/dist/advanced.js +1 -1
- package/dist/{contract-CNR6BS75.d.cts → contract-DovG1Yp-.d.cts} +2 -1
- package/dist/{contract-COgxO5sv.d.ts → contract-PqdXNaP8.d.ts} +2 -1
- package/dist/{dependency-DzxXGYe1.d.ts → dependency-BYiwTiBj.d.ts} +1 -1
- package/dist/{dependency-Cbf3C8eJ.d.cts → dependency-D0bo_xe_.d.cts} +1 -1
- package/dist/{driver-B7RR-VfX.js → driver-B7Hu6D0u.js} +15 -1
- package/dist/{driver-CN3dGxQI.cjs → driver-i4G4iVX0.cjs} +20 -0
- package/dist/index.cjs +137 -64
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +137 -64
- package/dist/local-sync.cjs +2 -5
- package/dist/local-sync.d.cts +1 -1
- package/dist/local-sync.d.ts +1 -1
- package/dist/local-sync.js +2 -5
- package/dist/react.cjs +38 -3
- package/dist/react.d.cts +19 -3
- package/dist/react.d.ts +19 -3
- package/dist/react.js +38 -4
- package/dist/{transform-D6njdP3k.cjs → transform-BPh55GWO.cjs} +80 -48
- package/dist/{transform-CKJ1ivhz.js → transform-BvDYPUMm.js} +80 -48
- package/package.json +1 -1
- package/skills/doxum-runtime/references/integrations.md +38 -7
- package/skills/doxum-runtime/references/invariants.md +13 -0
- package/skills/doxum-runtime/references/projections.md +22 -0
- package/skills/doxum-runtime/references/public-api.md +27 -8
|
@@ -261,7 +261,8 @@ var ProjectionDisposedError = class extends Error {
|
|
|
261
261
|
//#endregion
|
|
262
262
|
//#region core/src/projection/graph/scheduler.ts
|
|
263
263
|
const createScheduler = (onError) => {
|
|
264
|
-
let
|
|
264
|
+
let lifecycle = "active";
|
|
265
|
+
let lockDepth = 0;
|
|
265
266
|
let depth = 0;
|
|
266
267
|
let batchSequence = 0;
|
|
267
268
|
let activeBatch;
|
|
@@ -279,13 +280,39 @@ const createScheduler = (onError) => {
|
|
|
279
280
|
let reportingFailures = [];
|
|
280
281
|
const guards = /* @__PURE__ */ new Set();
|
|
281
282
|
const assertActive = () => {
|
|
282
|
-
if (disposed) throw new ProjectionDisposedError();
|
|
283
|
+
if (lifecycle === "disposed" || phase === "release") throw new ProjectionDisposedError();
|
|
283
284
|
};
|
|
284
285
|
const assertIdle = () => {
|
|
285
286
|
assertActive();
|
|
286
|
-
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
|
+
}
|
|
287
315
|
};
|
|
288
|
-
const lock = (value) => guards.forEach((guard) => guard(value));
|
|
289
316
|
const markDirty = (producer) => {
|
|
290
317
|
const pending = [producer];
|
|
291
318
|
for (let index = 0; index < pending.length; index++) {
|
|
@@ -302,7 +329,7 @@ const createScheduler = (onError) => {
|
|
|
302
329
|
};
|
|
303
330
|
const errorFor = (producer, cause, kind = producer.kind === "source" ? "source" : "processor") => new ProjectionError(kind, cause);
|
|
304
331
|
const capture = (source) => {
|
|
305
|
-
if (disposed) return;
|
|
332
|
+
if (lifecycle === "disposed") return;
|
|
306
333
|
if (phase !== "idle") {
|
|
307
334
|
source.fault = errorFor(source, /* @__PURE__ */ new Error("Source changed while projection was running."), "source");
|
|
308
335
|
errors.push(source.fault);
|
|
@@ -422,18 +449,9 @@ const createScheduler = (onError) => {
|
|
|
422
449
|
}
|
|
423
450
|
}
|
|
424
451
|
};
|
|
425
|
-
const compute = (run) =>
|
|
426
|
-
phase = "compute";
|
|
427
|
-
lock(true);
|
|
428
|
-
try {
|
|
429
|
-
return run();
|
|
430
|
-
} finally {
|
|
431
|
-
lock(false);
|
|
432
|
-
phase = "idle";
|
|
433
|
-
}
|
|
434
|
-
};
|
|
452
|
+
const compute = (run) => withPhase("compute", run);
|
|
435
453
|
const settle = () => {
|
|
436
|
-
if (
|
|
454
|
+
if (lifecycle !== "active" || depth || phase !== "idle") return;
|
|
437
455
|
const cause = settleCause();
|
|
438
456
|
compute(() => {
|
|
439
457
|
for (const producer of dirty) advance(producer, cause);
|
|
@@ -449,7 +467,7 @@ const createScheduler = (onError) => {
|
|
|
449
467
|
}
|
|
450
468
|
};
|
|
451
469
|
const flush = () => {
|
|
452
|
-
if (
|
|
470
|
+
if (lifecycle !== "active" || depth || phase === "cleanup" || phase === "release") return [];
|
|
453
471
|
if (emissions.size || completed.size) profile.projection("flushes");
|
|
454
472
|
phase = "notify";
|
|
455
473
|
lock(true);
|
|
@@ -497,7 +515,7 @@ const createScheduler = (onError) => {
|
|
|
497
515
|
return result;
|
|
498
516
|
};
|
|
499
517
|
const batch = (callback, options) => {
|
|
500
|
-
|
|
518
|
+
assertCommand();
|
|
501
519
|
if (depth === 0) activeBatch = Object.freeze({
|
|
502
520
|
id: ++batchSequence,
|
|
503
521
|
...options?.cause === void 0 ? {} : { cause: options.cause }
|
|
@@ -524,7 +542,8 @@ const createScheduler = (onError) => {
|
|
|
524
542
|
}
|
|
525
543
|
};
|
|
526
544
|
const releaseProducer = (producer) => {
|
|
527
|
-
|
|
545
|
+
if (producer.disposed) return;
|
|
546
|
+
if (phase !== "release") assertIdle();
|
|
528
547
|
if (producer.outputs.some((output) => output.hasConsumers())) throw new Error("Projection producer is still used by another processor.");
|
|
529
548
|
producer.disposed = true;
|
|
530
549
|
dirty.delete(producer);
|
|
@@ -554,6 +573,16 @@ const createScheduler = (onError) => {
|
|
|
554
573
|
return {
|
|
555
574
|
assertActive,
|
|
556
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
|
+
},
|
|
557
586
|
capture,
|
|
558
587
|
settle,
|
|
559
588
|
ensureCurrent,
|
|
@@ -562,15 +591,17 @@ const createScheduler = (onError) => {
|
|
|
562
591
|
registerGuard(guard) {
|
|
563
592
|
assertActive();
|
|
564
593
|
guards.add(guard);
|
|
594
|
+
if (lockDepth) guard(true);
|
|
565
595
|
let registered = true;
|
|
566
596
|
return () => {
|
|
567
597
|
if (!registered) return;
|
|
568
598
|
registered = false;
|
|
569
599
|
guards.delete(guard);
|
|
600
|
+
if (lockDepth) guard(false);
|
|
570
601
|
};
|
|
571
602
|
},
|
|
572
603
|
get active() {
|
|
573
|
-
return
|
|
604
|
+
return lifecycle !== "disposed" && phase !== "release";
|
|
574
605
|
},
|
|
575
606
|
addSource(source) {
|
|
576
607
|
assertIdle();
|
|
@@ -584,7 +615,7 @@ const createScheduler = (onError) => {
|
|
|
584
615
|
releaseProducer,
|
|
585
616
|
nextId: () => sequence++,
|
|
586
617
|
acceptInput(callback) {
|
|
587
|
-
|
|
618
|
+
assertCommand();
|
|
588
619
|
phase = "input";
|
|
589
620
|
lock(true);
|
|
590
621
|
try {
|
|
@@ -600,38 +631,39 @@ const createScheduler = (onError) => {
|
|
|
600
631
|
},
|
|
601
632
|
batch,
|
|
602
633
|
batchContext: () => activeBatch,
|
|
603
|
-
dispose() {
|
|
604
|
-
if (disposed) return;
|
|
605
|
-
|
|
606
|
-
disposed = true;
|
|
634
|
+
dispose(releaseConsumers, releaseSources) {
|
|
635
|
+
if (lifecycle === "disposed") return;
|
|
636
|
+
if (lifecycle === "active") beginClose();
|
|
607
637
|
const failures = [];
|
|
608
|
-
|
|
609
|
-
processor.disposed = true;
|
|
610
|
-
try {
|
|
611
|
-
processor.release();
|
|
612
|
-
} catch (error) {
|
|
613
|
-
failures.push(error);
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
sources.forEach((source) => {
|
|
617
|
-
source.disposed = true;
|
|
638
|
+
const attempt = (run) => {
|
|
618
639
|
try {
|
|
619
|
-
|
|
640
|
+
run();
|
|
620
641
|
} catch (error) {
|
|
621
642
|
failures.push(error);
|
|
622
643
|
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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];
|
|
635
667
|
if (failures.length) throw new AggregateError(failures, "Projection cleanup failed.");
|
|
636
668
|
}
|
|
637
669
|
};
|
|
@@ -261,7 +261,8 @@ var ProjectionDisposedError = class extends Error {
|
|
|
261
261
|
//#endregion
|
|
262
262
|
//#region core/src/projection/graph/scheduler.ts
|
|
263
263
|
const createScheduler = (onError) => {
|
|
264
|
-
let
|
|
264
|
+
let lifecycle = "active";
|
|
265
|
+
let lockDepth = 0;
|
|
265
266
|
let depth = 0;
|
|
266
267
|
let batchSequence = 0;
|
|
267
268
|
let activeBatch;
|
|
@@ -279,13 +280,39 @@ const createScheduler = (onError) => {
|
|
|
279
280
|
let reportingFailures = [];
|
|
280
281
|
const guards = /* @__PURE__ */ new Set();
|
|
281
282
|
const assertActive = () => {
|
|
282
|
-
if (disposed) throw new ProjectionDisposedError();
|
|
283
|
+
if (lifecycle === "disposed" || phase === "release") throw new ProjectionDisposedError();
|
|
283
284
|
};
|
|
284
285
|
const assertIdle = () => {
|
|
285
286
|
assertActive();
|
|
286
|
-
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
|
+
}
|
|
287
315
|
};
|
|
288
|
-
const lock = (value) => guards.forEach((guard) => guard(value));
|
|
289
316
|
const markDirty = (producer) => {
|
|
290
317
|
const pending = [producer];
|
|
291
318
|
for (let index = 0; index < pending.length; index++) {
|
|
@@ -302,7 +329,7 @@ const createScheduler = (onError) => {
|
|
|
302
329
|
};
|
|
303
330
|
const errorFor = (producer, cause, kind = producer.kind === "source" ? "source" : "processor") => new ProjectionError(kind, cause);
|
|
304
331
|
const capture = (source) => {
|
|
305
|
-
if (disposed) return;
|
|
332
|
+
if (lifecycle === "disposed") return;
|
|
306
333
|
if (phase !== "idle") {
|
|
307
334
|
source.fault = errorFor(source, /* @__PURE__ */ new Error("Source changed while projection was running."), "source");
|
|
308
335
|
errors.push(source.fault);
|
|
@@ -422,18 +449,9 @@ const createScheduler = (onError) => {
|
|
|
422
449
|
}
|
|
423
450
|
}
|
|
424
451
|
};
|
|
425
|
-
const compute = (run) =>
|
|
426
|
-
phase = "compute";
|
|
427
|
-
lock(true);
|
|
428
|
-
try {
|
|
429
|
-
return run();
|
|
430
|
-
} finally {
|
|
431
|
-
lock(false);
|
|
432
|
-
phase = "idle";
|
|
433
|
-
}
|
|
434
|
-
};
|
|
452
|
+
const compute = (run) => withPhase("compute", run);
|
|
435
453
|
const settle = () => {
|
|
436
|
-
if (
|
|
454
|
+
if (lifecycle !== "active" || depth || phase !== "idle") return;
|
|
437
455
|
const cause = settleCause();
|
|
438
456
|
compute(() => {
|
|
439
457
|
for (const producer of dirty) advance(producer, cause);
|
|
@@ -449,7 +467,7 @@ const createScheduler = (onError) => {
|
|
|
449
467
|
}
|
|
450
468
|
};
|
|
451
469
|
const flush = () => {
|
|
452
|
-
if (
|
|
470
|
+
if (lifecycle !== "active" || depth || phase === "cleanup" || phase === "release") return [];
|
|
453
471
|
if (emissions.size || completed.size) profile.projection("flushes");
|
|
454
472
|
phase = "notify";
|
|
455
473
|
lock(true);
|
|
@@ -497,7 +515,7 @@ const createScheduler = (onError) => {
|
|
|
497
515
|
return result;
|
|
498
516
|
};
|
|
499
517
|
const batch = (callback, options) => {
|
|
500
|
-
|
|
518
|
+
assertCommand();
|
|
501
519
|
if (depth === 0) activeBatch = Object.freeze({
|
|
502
520
|
id: ++batchSequence,
|
|
503
521
|
...options?.cause === void 0 ? {} : { cause: options.cause }
|
|
@@ -524,7 +542,8 @@ const createScheduler = (onError) => {
|
|
|
524
542
|
}
|
|
525
543
|
};
|
|
526
544
|
const releaseProducer = (producer) => {
|
|
527
|
-
|
|
545
|
+
if (producer.disposed) return;
|
|
546
|
+
if (phase !== "release") assertIdle();
|
|
528
547
|
if (producer.outputs.some((output) => output.hasConsumers())) throw new Error("Projection producer is still used by another processor.");
|
|
529
548
|
producer.disposed = true;
|
|
530
549
|
dirty.delete(producer);
|
|
@@ -554,6 +573,16 @@ const createScheduler = (onError) => {
|
|
|
554
573
|
return {
|
|
555
574
|
assertActive,
|
|
556
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
|
+
},
|
|
557
586
|
capture,
|
|
558
587
|
settle,
|
|
559
588
|
ensureCurrent,
|
|
@@ -562,15 +591,17 @@ const createScheduler = (onError) => {
|
|
|
562
591
|
registerGuard(guard) {
|
|
563
592
|
assertActive();
|
|
564
593
|
guards.add(guard);
|
|
594
|
+
if (lockDepth) guard(true);
|
|
565
595
|
let registered = true;
|
|
566
596
|
return () => {
|
|
567
597
|
if (!registered) return;
|
|
568
598
|
registered = false;
|
|
569
599
|
guards.delete(guard);
|
|
600
|
+
if (lockDepth) guard(false);
|
|
570
601
|
};
|
|
571
602
|
},
|
|
572
603
|
get active() {
|
|
573
|
-
return
|
|
604
|
+
return lifecycle !== "disposed" && phase !== "release";
|
|
574
605
|
},
|
|
575
606
|
addSource(source) {
|
|
576
607
|
assertIdle();
|
|
@@ -584,7 +615,7 @@ const createScheduler = (onError) => {
|
|
|
584
615
|
releaseProducer,
|
|
585
616
|
nextId: () => sequence++,
|
|
586
617
|
acceptInput(callback) {
|
|
587
|
-
|
|
618
|
+
assertCommand();
|
|
588
619
|
phase = "input";
|
|
589
620
|
lock(true);
|
|
590
621
|
try {
|
|
@@ -600,38 +631,39 @@ const createScheduler = (onError) => {
|
|
|
600
631
|
},
|
|
601
632
|
batch,
|
|
602
633
|
batchContext: () => activeBatch,
|
|
603
|
-
dispose() {
|
|
604
|
-
if (disposed) return;
|
|
605
|
-
|
|
606
|
-
disposed = true;
|
|
634
|
+
dispose(releaseConsumers, releaseSources) {
|
|
635
|
+
if (lifecycle === "disposed") return;
|
|
636
|
+
if (lifecycle === "active") beginClose();
|
|
607
637
|
const failures = [];
|
|
608
|
-
|
|
609
|
-
processor.disposed = true;
|
|
610
|
-
try {
|
|
611
|
-
processor.release();
|
|
612
|
-
} catch (error) {
|
|
613
|
-
failures.push(error);
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
sources.forEach((source) => {
|
|
617
|
-
source.disposed = true;
|
|
638
|
+
const attempt = (run) => {
|
|
618
639
|
try {
|
|
619
|
-
|
|
640
|
+
run();
|
|
620
641
|
} catch (error) {
|
|
621
642
|
failures.push(error);
|
|
622
643
|
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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];
|
|
635
667
|
if (failures.length) throw new AggregateError(failures, "Projection cleanup failed.");
|
|
636
668
|
}
|
|
637
669
|
};
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -71,6 +71,19 @@ This file is maintainer-only. Application/library consumers should use [Public A
|
|
|
71
71
|
Sorting changed keys is internal lookup work, never formal order. Count node
|
|
72
72
|
allocations and sorting separately from selector calls; this is not an O(delta)
|
|
73
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.
|
|
74
87
|
10. Local sync uses Web Lock leadership and contiguous durable sequence. Writes precede
|
|
75
88
|
asynchronous persistence. Version 5 / format 3 rejects old databases without editing
|
|
76
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
|
|
610
|
-
|
|
|
611
|
-
| `ProjectionProvider`
|
|
612
|
-
| `
|
|
613
|
-
| `
|
|
614
|
-
| `
|
|
615
|
-
| `
|
|
616
|
-
| `
|
|
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
|