@optimystic/db-core 0.25.1 → 0.26.0

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.
Files changed (104) hide show
  1. package/dist/src/cohort-topic/membership/verifier.d.ts +8 -0
  2. package/dist/src/cohort-topic/membership/verifier.d.ts.map +1 -1
  3. package/dist/src/cohort-topic/membership/verifier.js +17 -0
  4. package/dist/src/cohort-topic/membership/verifier.js.map +1 -1
  5. package/dist/src/cohort-topic/promotion.d.ts +25 -0
  6. package/dist/src/cohort-topic/promotion.d.ts.map +1 -1
  7. package/dist/src/cohort-topic/promotion.js +58 -11
  8. package/dist/src/cohort-topic/promotion.js.map +1 -1
  9. package/dist/src/cohort-topic/wire/primitives.d.ts +6 -0
  10. package/dist/src/cohort-topic/wire/primitives.d.ts.map +1 -1
  11. package/dist/src/cohort-topic/wire/primitives.js +6 -0
  12. package/dist/src/cohort-topic/wire/primitives.js.map +1 -1
  13. package/dist/src/cohort-topic/wire/types.d.ts +5 -0
  14. package/dist/src/cohort-topic/wire/types.d.ts.map +1 -1
  15. package/dist/src/cohort-topic/wire/validate.d.ts.map +1 -1
  16. package/dist/src/cohort-topic/wire/validate.js +10 -19
  17. package/dist/src/cohort-topic/wire/validate.js.map +1 -1
  18. package/dist/src/collection/collection.d.ts +8 -1
  19. package/dist/src/collection/collection.d.ts.map +1 -1
  20. package/dist/src/collection/collection.js +19 -3
  21. package/dist/src/collection/collection.js.map +1 -1
  22. package/dist/src/matchmaking/wire.d.ts +2 -2
  23. package/dist/src/matchmaking/wire.d.ts.map +1 -1
  24. package/dist/src/matchmaking/wire.js +3 -3
  25. package/dist/src/matchmaking/wire.js.map +1 -1
  26. package/dist/src/network/struct.d.ts +5 -5
  27. package/dist/src/network/struct.d.ts.map +1 -1
  28. package/dist/src/testing/index.d.ts +1 -0
  29. package/dist/src/testing/index.d.ts.map +1 -1
  30. package/dist/src/testing/index.js +1 -0
  31. package/dist/src/testing/index.js.map +1 -1
  32. package/dist/src/testing/refresh-probe.d.ts +31 -0
  33. package/dist/src/testing/refresh-probe.d.ts.map +1 -0
  34. package/dist/src/testing/refresh-probe.js +39 -0
  35. package/dist/src/testing/refresh-probe.js.map +1 -0
  36. package/dist/src/testing/test-transactor.d.ts +58 -5
  37. package/dist/src/testing/test-transactor.d.ts.map +1 -1
  38. package/dist/src/testing/test-transactor.js +87 -4
  39. package/dist/src/testing/test-transactor.js.map +1 -1
  40. package/dist/src/transaction/coordinator.d.ts +90 -6
  41. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  42. package/dist/src/transaction/coordinator.js +311 -90
  43. package/dist/src/transaction/coordinator.js.map +1 -1
  44. package/dist/src/transaction/errors.d.ts +42 -0
  45. package/dist/src/transaction/errors.d.ts.map +1 -1
  46. package/dist/src/transaction/errors.js +50 -0
  47. package/dist/src/transaction/errors.js.map +1 -1
  48. package/dist/src/transaction/index.d.ts +1 -1
  49. package/dist/src/transaction/index.d.ts.map +1 -1
  50. package/dist/src/transaction/index.js +1 -1
  51. package/dist/src/transaction/index.js.map +1 -1
  52. package/dist/src/transaction/session.d.ts +7 -3
  53. package/dist/src/transaction/session.d.ts.map +1 -1
  54. package/dist/src/transaction/session.js +7 -3
  55. package/dist/src/transaction/session.js.map +1 -1
  56. package/dist/src/transaction/transaction.d.ts +12 -2
  57. package/dist/src/transaction/transaction.d.ts.map +1 -1
  58. package/dist/src/transaction/transaction.js.map +1 -1
  59. package/dist/src/transactor/network-transactor.d.ts +1 -1
  60. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  61. package/dist/src/transactor/network-transactor.js +6 -26
  62. package/dist/src/transactor/network-transactor.js.map +1 -1
  63. package/dist/src/transactor/transactor-source.d.ts +4 -6
  64. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  65. package/dist/src/transactor/transactor-source.js +4 -6
  66. package/dist/src/transactor/transactor-source.js.map +1 -1
  67. package/dist/src/transform/atomic.d.ts.map +1 -1
  68. package/dist/src/transform/atomic.js +7 -0
  69. package/dist/src/transform/atomic.js.map +1 -1
  70. package/dist/src/transform/base-pins.d.ts +48 -0
  71. package/dist/src/transform/base-pins.d.ts.map +1 -0
  72. package/dist/src/transform/base-pins.js +65 -0
  73. package/dist/src/transform/base-pins.js.map +1 -0
  74. package/dist/src/transform/digest.d.ts +4 -2
  75. package/dist/src/transform/digest.d.ts.map +1 -1
  76. package/dist/src/transform/digest.js +14 -19
  77. package/dist/src/transform/digest.js.map +1 -1
  78. package/dist/src/transform/tracker.d.ts +31 -3
  79. package/dist/src/transform/tracker.d.ts.map +1 -1
  80. package/dist/src/transform/tracker.js +105 -13
  81. package/dist/src/transform/tracker.js.map +1 -1
  82. package/package.json +1 -1
  83. package/src/cohort-topic/membership/verifier.ts +487 -461
  84. package/src/cohort-topic/promotion.ts +86 -14
  85. package/src/cohort-topic/wire/primitives.ts +195 -188
  86. package/src/cohort-topic/wire/types.ts +480 -475
  87. package/src/cohort-topic/wire/validate.ts +504 -512
  88. package/src/collection/collection.ts +19 -3
  89. package/src/matchmaking/wire.ts +605 -603
  90. package/src/network/struct.ts +5 -5
  91. package/src/testing/index.ts +1 -0
  92. package/src/testing/refresh-probe.ts +52 -0
  93. package/src/testing/test-transactor.ts +100 -4
  94. package/src/transaction/coordinator.ts +312 -99
  95. package/src/transaction/errors.ts +141 -91
  96. package/src/transaction/index.ts +1 -1
  97. package/src/transaction/session.ts +7 -3
  98. package/src/transaction/transaction.ts +12 -2
  99. package/src/transactor/network-transactor.ts +1025 -1045
  100. package/src/transactor/transactor-source.ts +170 -172
  101. package/src/transform/atomic.ts +6 -0
  102. package/src/transform/base-pins.ts +83 -0
  103. package/src/transform/digest.ts +14 -19
  104. package/src/transform/tracker.ts +108 -15
@@ -125,12 +125,12 @@ export type BlockContentDigest = {
125
125
  export type BlockContentDigests = Record<BlockId, BlockContentDigest>;
126
126
 
127
127
  export type CommitRequest = ActionBlocks & {
128
- /** The header block of the collection, if this is a new collection. Nominally "commit first" —
129
- * but see the NOTE in `NetworkTransactor.commit`: the header-first step is unreachable from the
130
- * only production producer of this field, so today it only affects hand-built requests. */
128
+ /** The header block of the collection, present only when this commit creates the collection
129
+ * (the header is a fresh insert). Collection-identifying metadata, not an ordering signal —
130
+ * the header commits inside the ordinary sweep, after the tail, like any other touched block.
131
+ * Consumed by dispute reporting (`dispute-service.ts`) as a fallback to name the collection. */
131
132
  headerId?: BlockId;
132
- /** The tail block of the log. Nominally "commit next"; since the header-first step above never
133
- * fires in production, this is in practice the FIRST block committed, which is the ordering
133
+ /** The tail block of the log. Unconditionally the first block committed — the ordering
134
134
  * `Collection.bootstrapContext` relies on. */
135
135
  tailId: BlockId;
136
136
  /** The new revision for the committed action */
@@ -1,2 +1,3 @@
1
1
  export * from './test-transactor.js';
2
2
  export * from './async-wait.js';
3
+ export * from './refresh-probe.js';
@@ -0,0 +1,52 @@
1
+ import type { Collection } from '../collection/collection.js';
2
+
3
+ /** Yield the event loop through the MACROTASK queue `turns` times, so a refresh that is free to
4
+ * proceed gets every chance to run its (await-heavy) course before a gate opens. A microtask
5
+ * drain would not be enough: `Collection.update()` awaits real transactor reads.
6
+ *
7
+ * NOTE: the turn count is what gives {@link releaseRefresh}'s `blocked()` assertions their teeth
8
+ * — an unlatched `Collection.update()` has to be able to run to COMPLETION within these turns,
9
+ * or "still pending" stops distinguishing "blocked on the latch" from "merely slow". Ten is a
10
+ * ~10x margin today: with the coordinator's latch reverted locally, the refreshes in
11
+ * `coordinator-latch-interleaving.spec.ts` completed on macrotask turn 1 against
12
+ * `TestTransactor`. If `update()` ever grows a deeper await chain — or a caller moves onto a
13
+ * transactor with real I/O — raise this rather than letting the assertion quietly weaken into a
14
+ * tautology. */
15
+ export async function drainMacrotasks(turns = 10): Promise<void> {
16
+ for (let i = 0; i < turns; i++) {
17
+ await new Promise(r => setTimeout(r, 0));
18
+ }
19
+ }
20
+
21
+ /** The handle {@link releaseRefresh} returns: `blocked()` reports whether the refresh is still
22
+ * pending, `settle()` awaits it and rethrows whatever it rejected with. */
23
+ export type RefreshProbe = {
24
+ /** Await the refresh, rethrowing its failure (captured at start time — see the note there). */
25
+ settle: () => Promise<void>;
26
+ /** True while the refresh has neither resolved nor rejected. */
27
+ blocked: () => boolean;
28
+ };
29
+
30
+ /** Start a refresh on `collection` and report — via the returned `blocked()` — whether it is
31
+ * STILL pending.
32
+ *
33
+ * A final-state assertion alone would also pass if the refresh had simply run to completion
34
+ * harmlessly, so on its own it never proves the latch is what protected the state. This does:
35
+ * checked after {@link drainMacrotasks} but before a parked commit's gate opens, a still-pending
36
+ * refresh is a refresh queued behind the commit's held latch. */
37
+ export function releaseRefresh<TAction>(collection: Collection<TAction>): RefreshProbe {
38
+ let done = false;
39
+ let failure: unknown;
40
+ // Both handlers attach SYNCHRONOUSLY, and the rethrowing promise is built only when the caller
41
+ // asks for it. The refresh sits unawaited across drainMacrotasks, so a rejection reaching the
42
+ // end of a turn with no handler would be a fatal unhandled rejection — killing the process
43
+ // instead of failing the case. Captured here, rethrown at `settle()`.
44
+ const captured = collection.update().then(
45
+ () => { done = true; },
46
+ (e: unknown) => { done = true; failure = e; },
47
+ );
48
+ return {
49
+ settle: () => captured.then(() => { if (failure !== undefined) throw failure; }),
50
+ blocked: () => !done,
51
+ };
52
+ }
@@ -4,6 +4,7 @@ import { Latches } from "../utility/latches.js";
4
4
  import { applyTransform, blockIdsForTransforms, transformForBlockId, emptyTransforms, concatTransform, transformsFromTransform } from "../transform/index.js";
5
5
  import { Tree } from "../collections/tree/tree.js";
6
6
  import type { TreeReplaceAction } from "../collections/tree/struct.js";
7
+ import type { Collection } from "../collection/collection.js";
7
8
 
8
9
  type RevisionNumber = number;
9
10
 
@@ -496,10 +497,10 @@ export class FlakyCommitTransactor extends DelegatingTransactor {
496
497
 
497
498
  /**
498
499
  * Commits durably on the inner {@link TestTransactor}, then reports a stale failure anyway —
499
- * the exact observable shape of `NetworkTransactor.commit`'s TORN ACTION: the collection header
500
- * and log tail are committed BEFORE the sweep of the remaining blocks, so a later sweep block
501
- * coming back as a confirmed conflict returns a stale failure over an action whose log entry is
502
- * already durable.
500
+ * the exact observable shape of `NetworkTransactor.commit`'s TORN ACTION: the collection's log
501
+ * tail is committed BEFORE the sweep of the remaining blocks, so a later sweep block coming back
502
+ * as a confirmed conflict returns a stale failure over an action whose log entry is already
503
+ * durable.
503
504
  *
504
505
  * Safe against the inner transactor's bookkeeping because `TransactorSource.transact` cancels
505
506
  * the pend on the reported failure and {@link TestTransactor.cancel} only deletes PENDING records
@@ -617,6 +618,101 @@ export class CompetingWriterTransactor extends DelegatingTransactor {
617
618
  }
618
619
  }
619
620
 
621
+ type Deferred = { promise: Promise<void>; resolve: () => void };
622
+
623
+ function deferred(): Deferred {
624
+ let resolve!: () => void;
625
+ const promise = new Promise<void>(r => { resolve = r; });
626
+ return { promise, resolve };
627
+ }
628
+
629
+ /**
630
+ * Parks the commit AFTER the inner transactor has made it durable and BEFORE the result gets back
631
+ * to the coordinator — precisely the window in which storage already holds the new revision but
632
+ * the caller's local fold (`Collection.recordCommitted`) has not run.
633
+ *
634
+ * Everything else delegates, so the pend/commit round trip is otherwise the real one.
635
+ *
636
+ * Parks only the FIRST commit call and delegates every later one: a multi-collection commit issues
637
+ * one `commit` per participant, and parking all of them would mean the span never completes.
638
+ */
639
+ export class GatedCommitTransactor extends DelegatingTransactor {
640
+ /** The `rev` carried on each pend request seen, in call order. */
641
+ readonly pendRevs: (number | undefined)[] = [];
642
+ /** The `rev` carried on each commit request seen, in call order. */
643
+ readonly commitRevs: number[] = [];
644
+ /** Resolves once a commit has landed durably in the inner transactor and is parked. */
645
+ readonly commitParked: Promise<void>;
646
+ private readonly parked = deferred();
647
+ private readonly gate = deferred();
648
+ private parkedOnce = false;
649
+
650
+ constructor(inner: TestTransactor) {
651
+ super(inner);
652
+ this.commitParked = this.parked.promise;
653
+ }
654
+
655
+ override async pend(request: PendRequest): Promise<PendResult> {
656
+ this.pendRevs.push(request.rev);
657
+ return this.inner.pend(request);
658
+ }
659
+
660
+ override async commit(request: CommitRequest): Promise<CommitResult> {
661
+ this.commitRevs.push(request.rev);
662
+ const result = await this.inner.commit(request); // durable FIRST
663
+ if (!this.parkedOnce) {
664
+ this.parkedOnce = true;
665
+ this.parked.resolve();
666
+ await this.gate.promise;
667
+ }
668
+ return result;
669
+ }
670
+
671
+ openGate(): void { this.gate.resolve(); }
672
+ }
673
+
674
+ /**
675
+ * Parks the FIRST pend BEFORE it reaches the inner transactor, so a rival can durably take the
676
+ * revision while the coordinator is already committed to a set of transforms it captured at the
677
+ * log append.
678
+ *
679
+ * Snapshots the pair `(what was handed to the network, what the collection's tracker held)` at
680
+ * DELEGATION time — after the gate — for every pend, so a refresh that swapped the tracker's
681
+ * transforms object out from under the coordinator shows up as a divergent pair.
682
+ */
683
+ export class GatedPendTransactor<TAction> extends DelegatingTransactor {
684
+ /** One entry per delegated pend, in call order. */
685
+ readonly snapshots: { pended: Transforms; staged: Transforms }[] = [];
686
+ /** Resolves once a pend is parked and has NOT yet reached the inner transactor. */
687
+ readonly pendParked: Promise<void>;
688
+ /** The collection under test. Assigned after construction — the collection is built over this
689
+ * wrapper, so it cannot be a constructor argument. */
690
+ collection?: Collection<TAction>;
691
+ private readonly parked = deferred();
692
+ private readonly gate = deferred();
693
+ private parkedOnce = false;
694
+
695
+ constructor(inner: TestTransactor) {
696
+ super(inner);
697
+ this.pendParked = this.parked.promise;
698
+ }
699
+
700
+ override async pend(request: PendRequest): Promise<PendResult> {
701
+ if (!this.parkedOnce) {
702
+ this.parkedOnce = true;
703
+ this.parked.resolve();
704
+ await this.gate.promise;
705
+ }
706
+ this.snapshots.push({
707
+ pended: structuredClone(request.transforms),
708
+ staged: structuredClone(this.collection!.tracker.transforms),
709
+ });
710
+ return this.inner.pend(request);
711
+ }
712
+
713
+ openGate(): void { this.gate.resolve(); }
714
+ }
715
+
620
716
  /**
621
717
  * Durably commit a conflicting change to a tree collection: opens a SECOND {@link Tree} over the
622
718
  * same transactor + collection id and replaces `entries` through it, producing a real log entry