@optimystic/db-core 0.25.0 → 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
@@ -1,7 +1,28 @@
1
1
  import type { IBlock, BlockId, BlockStore as IBlockStore, BlockHeader, BlockOperation, BlockType, BlockSource as IBlockSource, ReadPurpose } from "../index.js";
2
2
  import { applyOperation, applyOperations, applyTransform, emptyTransforms, blockIdsForTransforms, transformForBlockId } from "./helpers.js";
3
+ import { BasePins } from "./base-pins.js";
4
+ import type { PinnedBase } from "./base-pins.js";
3
5
  import { ensured } from "../utility/ensured.js";
4
6
 
7
+ /** The base a source can answer from memory, with the committed revision of that content — the
8
+ * shared probe behind both {@link Tracker.probeBase} (pin time) and {@link Tracker.peekMaterialized}
9
+ * (declare time), so the two can never disagree about what "locally answerable" means. Duck-typed,
10
+ * because Tracker layers over test doubles as well as CacheSource.
11
+ *
12
+ * Returns undefined unless BOTH probes answer: an LRU-evicted id can leave a stale cached revision
13
+ * behind (see the NOTE on CacheSource's `revisions` map), and `peek` returning undefined for that id
14
+ * is what keeps the stale revision from pairing with a block. */
15
+ function cachedBase(source: unknown, id: BlockId): { block: IBlock; rev: number } | undefined {
16
+ const src = source as {
17
+ peek?: (id: BlockId) => IBlock | undefined;
18
+ getCachedRevision?: (id: BlockId) => number | undefined;
19
+ };
20
+ if (typeof src.peek !== 'function' || typeof src.getCachedRevision !== 'function') return undefined;
21
+ const block = src.peek(id); // already a clone (peek contract)
22
+ const rev = src.getCachedRevision(id);
23
+ return block === undefined || rev === undefined ? undefined : { block, rev };
24
+ }
25
+
5
26
  /** A block store that collects transformations, without applying them to the underlying source.
6
27
  * Transformations are also applied to the retrieved blocks, making it seem like the source has been modified.
7
28
  */
@@ -19,7 +40,28 @@ export class Tracker<T extends IBlock> implements IBlockStore<T> {
19
40
  private readonly source: IBlockSource<T>,
20
41
  /** The collected set of transformations to be applied. Treat as immutable */
21
42
  public transforms = emptyTransforms(),
22
- ) { }
43
+ /** Committed bases pinned at the moment each update was staged, so the digest pass can
44
+ * describe every updated block even after the read cache evicts its base. Shared by
45
+ * reference across the trackers of one transaction (see {@link BasePins}); pass an
46
+ * existing store to join a transaction, omit for a private one. */
47
+ public readonly pins: BasePins = new BasePins(),
48
+ ) {
49
+ // A pin's `gen` and `rev` are counters PRIVATE to one base source, so a store may only be
50
+ // shared between trackers that bottom out at the same one. Binding here turns the otherwise
51
+ // silent failure (a pin from cache A passing the freshness check against cache B, which
52
+ // numbers generations from 0 independently, and declaring A's content for B's block) into a
53
+ // throw at the moment the stores are joined.
54
+ pins.bindAuthority(this.baseSource());
55
+ }
56
+
57
+ /** The non-Tracker source at the bottom of the tracker stack. Single authority for everything
58
+ * pin-related: an Atomic layers over a Collection's tracker, which layers over the read cache,
59
+ * and only that cache can report a base, its committed revision, and its drift generation. */
60
+ private baseSource(): unknown {
61
+ let src: unknown = this.source;
62
+ while (src instanceof Tracker) src = src.source;
63
+ return src;
64
+ }
23
65
 
24
66
  /** The source's generation for an id, or undefined if the source cannot report drift. */
25
67
  private sourceGeneration(id: BlockId): number | undefined {
@@ -27,6 +69,28 @@ export class Tracker<T extends IBlock> implements IBlockStore<T> {
27
69
  return typeof src.getGeneration === 'function' ? src.getGeneration(id) : undefined;
28
70
  }
29
71
 
72
+ /** The drift generation from the same authority {@link probeBase} pins from — the base source,
73
+ * so an Atomic validates its pins against the collection's read cache rather than against the
74
+ * drift-blind tracker in between. */
75
+ protected baseGeneration(id: BlockId): number | undefined {
76
+ const src = this.baseSource() as { getGeneration?: (id: BlockId) => number };
77
+ return typeof src.getGeneration === 'function' ? src.getGeneration(id) : undefined;
78
+ }
79
+
80
+ /** The committed base for `id` as the base source can report it, plus that source's revision and
81
+ * drift generation. Duck-typed (CacheSource supplies all three) and taken from
82
+ * {@link baseSource}, so an Atomic staged over a Collection's tracker pins from the collection's
83
+ * read cache instead of finding nothing. Returns undefined unless ALL THREE probes are
84
+ * available, which keeps drift-blind sources (test doubles) on exactly the pre-pin behaviour.
85
+ * Recency-neutral: uses CacheSource.peek. */
86
+ protected probeBase(id: BlockId): PinnedBase | undefined {
87
+ const src = this.baseSource() as { getGeneration?: (id: BlockId) => number };
88
+ // Without a drift signal a pin could never be proven fresh, so never take one.
89
+ if (typeof src.getGeneration !== 'function') return undefined;
90
+ const base = cachedBase(src, id);
91
+ return base && { ...base, gen: src.getGeneration(id) };
92
+ }
93
+
30
94
  async tryGet(id: BlockId, purpose: ReadPurpose = 'value'): Promise<T | undefined> {
31
95
  // NOTE: precedence here is insert > delete > source+updates. In a well-formed transform an id is
32
96
  // never in both `inserts` and `deletes` (insert/delete each clear the other), so order is moot. It
@@ -67,8 +131,9 @@ export class Tracker<T extends IBlock> implements IBlockStore<T> {
67
131
 
68
132
  /** The block `id` materializes to under the staged transforms, computed WITHOUT loading from the
69
133
  * source, plus the committed revision of the base used. `undefined` when not computable here —
70
- * nothing staged for the id, the result is a delete, or an update's base is not locally cached
71
- * (a commit must not pay a network round trip to describe itself).
134
+ * nothing staged for the id, the result is a delete, or an update's base is neither pinned
135
+ * (see {@link pins}) nor locally cached (a commit must not pay a network round trip to
136
+ * describe itself).
72
137
  *
73
138
  * Materializes with the canonical {@link applyTransform} — the exact function the member side
74
139
  * uses at commit — so client and member can never disagree on semantics (insert replaces the
@@ -92,18 +157,25 @@ export class Tracker<T extends IBlock> implements IBlockStore<T> {
92
157
  const block = applyTransform(undefined, transform);
93
158
  return block ? { block } : undefined;
94
159
  }
95
- const src = this.source as {
96
- peek?: (id: BlockId) => T | undefined;
97
- getCachedRevision?: (id: BlockId) => number | undefined;
98
- };
99
- if (typeof src.peek !== 'function' || typeof src.getCachedRevision !== 'function') return undefined;
100
- const base = src.peek(id);
101
- const baseRev = src.getCachedRevision(id);
102
- // Require BOTH: an LRU-evicted id can leave a stale cached revision behind (see the NOTE on
103
- // CacheSource's revisions map); peek returning undefined keeps it from pairing with a block.
104
- if (base === undefined || baseRev === undefined) return undefined;
105
- const block = applyTransform(base, transform); // base already a clone (peek contract)
106
- return block ? { block, baseRev } : undefined;
160
+ // Prefer the base pinned when the update was staged — it survives read-cache eviction, which
161
+ // is what keeps digest coverage a function of the transaction rather than of cache residency.
162
+ // The freshness check is correctness-critical, not an optimisation: a stale pin would declare
163
+ // a digest the member disagrees with, turning a blind-but-passing vote into a REJECT — an
164
+ // inaccurate declaration is strictly worse than no declaration, so a drifted pin falls through
165
+ // to the live peek below (which re-answers from the refreshed cache, or omits). The clone on
166
+ // use is also required, not defensive: applyTransform mutates, and syncAttempts re-runs the
167
+ // digest pass on every retry attempt against the same pin.
168
+ const pin = this.pins.get(id);
169
+ if (pin && pin.gen === this.baseGeneration(id)) {
170
+ const block = applyTransform(structuredClone(pin.block), transform);
171
+ return block ? { block, baseRev: pin.rev } : undefined;
172
+ }
173
+ // Unpinned fallback: the IMMEDIATE source, not {@link baseSource} — a drift-blind source that
174
+ // can still peek keeps exactly its pre-pin behaviour here.
175
+ const base = cachedBase(this.source, id);
176
+ if (!base) return undefined;
177
+ const block = applyTransform(base.block, transform); // base already a clone (peek contract)
178
+ return block ? { block, baseRev: base.rev } : undefined;
107
179
  }
108
180
 
109
181
  /** Forward a leaf-value upgrade down to the source's read collector (duck-typed: only the
@@ -127,6 +199,8 @@ export class Tracker<T extends IBlock> implements IBlockStore<T> {
127
199
  inserts[block.header.id] = structuredClone(block);
128
200
  // Served from `inserts` now, not source+updates — the materialized memo no longer applies.
129
201
  this.materialized.delete(block.header.id);
202
+ // An insert makes the materialized result base-independent, so any pinned base is moot.
203
+ this.pins.delete(block.header.id);
130
204
  const deletes = this.transforms.deletes;
131
205
  const deleteIndex = deletes?.indexOf(block.header.id) ?? -1;
132
206
  if (deleteIndex >= 0) {
@@ -149,6 +223,20 @@ export class Tracker<T extends IBlock> implements IBlockStore<T> {
149
223
  if (memo) {
150
224
  applyOperation(memo.block, op);
151
225
  }
226
+ // Pin the committed base NOW — the caller just read this block, so it is resident — and
227
+ // only when there is no still-fresh pin, so a block updated 50 times pays one base clone.
228
+ // A generation change (an external commit folded into the cache) re-pins against the new
229
+ // base, which is the base the member will apply the whole op list to.
230
+ // NOTE: read-far-then-update stays unpinned — a block read, then evicted by 128+ other
231
+ // reads, and only then updated, probes an already-evicted cache here and is omitted from
232
+ // the digest (pre-existing behaviour; digest.spec.ts pins it). If a workload ever reads a
233
+ // large batch before writing any of it, the closure is to pin on READ for ids that later
234
+ // get updated, at retention proportional to reads rather than to writes.
235
+ const existing = this.pins.get(blockId);
236
+ if (!existing || existing.gen !== this.baseGeneration(blockId)) {
237
+ const pin = this.probeBase(blockId);
238
+ if (pin) this.pins.set(blockId, pin);
239
+ }
152
240
  }
153
241
  }
154
242
 
@@ -156,6 +244,8 @@ export class Tracker<T extends IBlock> implements IBlockStore<T> {
156
244
  if (this.transforms.inserts) delete this.transforms.inserts[blockId];
157
245
  if (this.transforms.updates) delete this.transforms.updates[blockId];
158
246
  this.materialized.delete(blockId);
247
+ // A delete materializes to nothing (delete-last-wins), so the pinned base is moot.
248
+ this.pins.delete(blockId);
159
249
  const deletes = this.transforms.deletes ??= [];
160
250
  deletes.push(blockId);
161
251
  }
@@ -164,6 +254,9 @@ export class Tracker<T extends IBlock> implements IBlockStore<T> {
164
254
  const oldTransform = this.transforms;
165
255
  this.transforms = newTransform;
166
256
  this.materialized.clear();
257
+ // The single reclamation point for pins: a plain reset clears them (empty updates), a
258
+ // rollback-style reset(transforms) keeps exactly the pins for ids still staged as updates.
259
+ this.pins.retainOnly(Object.keys(newTransform.updates ?? {}));
167
260
  return oldTransform;
168
261
  }
169
262