@optimystic/db-core 0.22.0 → 0.24.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 (141) hide show
  1. package/README.md +336 -336
  2. package/dist/src/cluster/structs.d.ts +39 -1
  3. package/dist/src/cluster/structs.d.ts.map +1 -1
  4. package/dist/src/cluster/structs.js +24 -0
  5. package/dist/src/cluster/structs.js.map +1 -1
  6. package/dist/src/collection/collection.d.ts +17 -0
  7. package/dist/src/collection/collection.d.ts.map +1 -1
  8. package/dist/src/collection/collection.js +24 -2
  9. package/dist/src/collection/collection.js.map +1 -1
  10. package/dist/src/collections/tree/tree.d.ts +5 -0
  11. package/dist/src/collections/tree/tree.d.ts.map +1 -1
  12. package/dist/src/collections/tree/tree.js +7 -0
  13. package/dist/src/collections/tree/tree.js.map +1 -1
  14. package/dist/src/network/i-peer-network.d.ts +16 -0
  15. package/dist/src/network/i-peer-network.d.ts.map +1 -1
  16. package/dist/src/network/struct.d.ts +39 -2
  17. package/dist/src/network/struct.d.ts.map +1 -1
  18. package/dist/src/network/struct.js +18 -0
  19. package/dist/src/network/struct.js.map +1 -1
  20. package/dist/src/testing/test-transactor.d.ts +95 -8
  21. package/dist/src/testing/test-transactor.d.ts.map +1 -1
  22. package/dist/src/testing/test-transactor.js +121 -8
  23. package/dist/src/testing/test-transactor.js.map +1 -1
  24. package/dist/src/transaction/transaction.d.ts +1 -1
  25. package/dist/src/transaction/transaction.js +1 -1
  26. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  27. package/dist/src/transactor/network-transactor.js +48 -13
  28. package/dist/src/transactor/network-transactor.js.map +1 -1
  29. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  30. package/dist/src/transactor/transactor-source.js +25 -2
  31. package/dist/src/transactor/transactor-source.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/cluster/membership.ts +85 -85
  34. package/src/cluster/structs.ts +43 -4
  35. package/src/cohort-topic/addressing.ts +120 -120
  36. package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -253
  37. package/src/cohort-topic/antidos/bootstrap-evidence.ts +106 -106
  38. package/src/cohort-topic/antidos/index.ts +5 -5
  39. package/src/cohort-topic/antidos/rate-limiter.ts +210 -210
  40. package/src/cohort-topic/antidos/replay-guard.ts +146 -146
  41. package/src/cohort-topic/antidos/topic-budget.ts +160 -160
  42. package/src/cohort-topic/antiflood/index.ts +2 -2
  43. package/src/cohort-topic/antiflood/invariants.ts +108 -108
  44. package/src/cohort-topic/antiflood/jitter.ts +117 -117
  45. package/src/cohort-topic/coldstart.ts +237 -237
  46. package/src/cohort-topic/dmax.ts +88 -88
  47. package/src/cohort-topic/gossip/bus.ts +254 -254
  48. package/src/cohort-topic/gossip/index.ts +3 -3
  49. package/src/cohort-topic/gossip/records.ts +45 -45
  50. package/src/cohort-topic/gossip/view.ts +91 -91
  51. package/src/cohort-topic/index.ts +20 -20
  52. package/src/cohort-topic/load/barometer.ts +134 -134
  53. package/src/cohort-topic/load/index.ts +1 -1
  54. package/src/cohort-topic/member-engine.ts +430 -430
  55. package/src/cohort-topic/membership/index.ts +3 -3
  56. package/src/cohort-topic/membership/publisher.ts +163 -163
  57. package/src/cohort-topic/membership/source.ts +41 -41
  58. package/src/cohort-topic/membership/verifier.ts +461 -461
  59. package/src/cohort-topic/ports.ts +157 -157
  60. package/src/cohort-topic/promotion.ts +405 -405
  61. package/src/cohort-topic/registration/bytes.ts +37 -37
  62. package/src/cohort-topic/registration/handoff.ts +154 -154
  63. package/src/cohort-topic/registration/index.ts +6 -6
  64. package/src/cohort-topic/registration/renewal.ts +495 -495
  65. package/src/cohort-topic/registration/sharding.ts +61 -61
  66. package/src/cohort-topic/registration/store.ts +81 -81
  67. package/src/cohort-topic/registration/types.ts +91 -91
  68. package/src/cohort-topic/ring-hash.ts +50 -50
  69. package/src/cohort-topic/service.ts +416 -416
  70. package/src/cohort-topic/sig/index.ts +2 -2
  71. package/src/cohort-topic/sig/payloads.ts +59 -59
  72. package/src/cohort-topic/sig/threshold.ts +64 -64
  73. package/src/cohort-topic/tiers.ts +74 -74
  74. package/src/cohort-topic/traffic.ts +233 -233
  75. package/src/cohort-topic/walk.ts +326 -326
  76. package/src/cohort-topic/willingness.ts +237 -237
  77. package/src/cohort-topic/wire/codec.ts +216 -216
  78. package/src/cohort-topic/wire/index.ts +18 -18
  79. package/src/cohort-topic/wire/payloads.ts +126 -126
  80. package/src/cohort-topic/wire/primitives.ts +188 -188
  81. package/src/cohort-topic/wire/types.ts +475 -475
  82. package/src/cohort-topic/wire/validate.ts +512 -512
  83. package/src/collection/collection-type-registry.ts +37 -37
  84. package/src/collection/collection.ts +25 -2
  85. package/src/collections/diary/diary.ts +68 -68
  86. package/src/collections/tree/readme.md +4 -0
  87. package/src/collections/tree/tree.ts +320 -312
  88. package/src/matchmaking/capability-filter.ts +45 -45
  89. package/src/matchmaking/config.ts +98 -98
  90. package/src/matchmaking/index.ts +21 -21
  91. package/src/matchmaking/multi-cohort-seeker.ts +234 -234
  92. package/src/matchmaking/provider.ts +123 -123
  93. package/src/matchmaking/query-eval.ts +105 -105
  94. package/src/matchmaking/seeker-walk.ts +127 -127
  95. package/src/matchmaking/seeker.ts +86 -86
  96. package/src/matchmaking/topic-anchor.ts +90 -90
  97. package/src/matchmaking/voting-quorum.ts +394 -394
  98. package/src/matchmaking/wire.ts +603 -603
  99. package/src/network/i-peer-network.ts +17 -0
  100. package/src/network/stale-failure.ts +43 -43
  101. package/src/network/struct.ts +41 -2
  102. package/src/network/types.ts +37 -37
  103. package/src/reactivity/backfill.ts +220 -220
  104. package/src/reactivity/backpressure.ts +191 -191
  105. package/src/reactivity/checkpoint.ts +308 -308
  106. package/src/reactivity/config.ts +172 -172
  107. package/src/reactivity/dedupe.ts +132 -132
  108. package/src/reactivity/forwarder.ts +87 -87
  109. package/src/reactivity/index.ts +34 -34
  110. package/src/reactivity/notification.ts +123 -123
  111. package/src/reactivity/policy.ts +79 -79
  112. package/src/reactivity/push-state.ts +310 -310
  113. package/src/reactivity/recover.ts +153 -153
  114. package/src/reactivity/replay-buffer.ts +141 -141
  115. package/src/reactivity/resume.ts +549 -549
  116. package/src/reactivity/rotation.ts +415 -415
  117. package/src/reactivity/subscriber.ts +132 -132
  118. package/src/reactivity/subscription.ts +66 -66
  119. package/src/reactivity/topic-anchor.ts +71 -71
  120. package/src/reactivity/verify.ts +73 -73
  121. package/src/reactivity/wire-validate.ts +13 -13
  122. package/src/reactivity/wire.ts +224 -224
  123. package/src/testing/async-wait.ts +65 -65
  124. package/src/testing/index.ts +2 -2
  125. package/src/testing/test-transactor.ts +638 -502
  126. package/src/transaction/errors.ts +91 -91
  127. package/src/transaction/operations-hash.ts +196 -196
  128. package/src/transaction/read-dependency-collector.ts +78 -78
  129. package/src/transaction/transaction.ts +1 -1
  130. package/src/transactor/change-notifier.ts +80 -80
  131. package/src/transactor/index.ts +5 -5
  132. package/src/transactor/network-transactor.ts +49 -14
  133. package/src/transactor/transactor-source.ts +25 -2
  134. package/src/transform/atomic-proxy.ts +92 -92
  135. package/src/transform/helpers.ts +159 -159
  136. package/src/utility/backoff.ts +95 -95
  137. package/src/utility/batch-coordinator.ts +191 -191
  138. package/dist/src/transaction/context.d.ts +0 -60
  139. package/dist/src/transaction/context.d.ts.map +0 -1
  140. package/dist/src/transaction/context.js +0 -91
  141. package/dist/src/transaction/context.js.map +0 -1
@@ -1,312 +1,320 @@
1
- import { Collection, type CollectionInitOptions, type CollectionId, type CollectionSnapshot, type ReadViewOptions } from "../../collection/index.js";
2
- import type { ITransactor, BlockId, BlockStore, IBlock } from "../../index.js";
3
- import { BTree, type Path, type KeyRange } from "../../btree/index.js";
4
- import { CollectionTrunk } from "./collection-trunk.js";
5
- import { TreeHeaderBlockType, type TreeReplaceAction } from "./struct.js";
6
-
7
- /**
8
- * Read-only surface of a tree: every navigation/lookup method a reader needs, with
9
- * none of the mutation (stage/sync) or network-refresh (update) entry points. Both
10
- * the live {@link Tree} and the committed view returned by {@link Tree.readView}
11
- * structurally satisfy this, so a consumer can read through either uniformly.
12
- */
13
- export interface TreeReadView<TKey, TEntry> {
14
- first(): Promise<Path<TKey, TEntry>>;
15
- find(key: TKey): Promise<Path<TKey, TEntry>>;
16
- get(key: TKey): Promise<TEntry | undefined>;
17
- at(path: Path<TKey, TEntry>): TEntry | undefined;
18
- range(range: KeyRange<TKey>): AsyncIterableIterator<Path<TKey, TEntry>>;
19
- ascending(path: Path<TKey, TEntry>): AsyncIterableIterator<Path<TKey, TEntry>>;
20
- isValid(path: Path<TKey, TEntry>): boolean;
21
- }
22
-
23
- /** Carries the read {@link BTree} from wherever it gets built (the `createHeaderBlock`
24
- * callback on the create path, {@link Tree.attach} on the open path) to the `replace`
25
- * handler, which needs the live instance to invalidate outstanding paths. */
26
- interface BTreeHolder<TKey, TEntry> {
27
- btree?: BTree<TKey, TEntry>;
28
- }
29
-
30
- export class Tree<TKey, TEntry> implements TreeReadView<TKey, TEntry> {
31
-
32
- private constructor(
33
- private readonly collection: Collection<TreeReplaceAction<TKey, TEntry>>,
34
- private readonly btree: BTree<TKey, TEntry>,
35
- /** Captured so {@link readView} can rebuild a BTree over a committed tracker. */
36
- private readonly keyFromEntry: (entry: TEntry) => TKey,
37
- private readonly compare: (a: TKey, b: TKey) => number,
38
- ) {
39
- }
40
-
41
- /** Open an EXISTING tree, or resolve to `undefined` when no header block has ever been
42
- * committed under this id. Never brings a tree into existence — nothing is staged into the
43
- * collection's tracker on the absent path, so a caller that ignores the `undefined` cannot
44
- * later sync a phantom tree. Use on pure read paths; see {@link Collection.open}. */
45
- static async open<TKey, TEntry>(
46
- network: ITransactor,
47
- id: CollectionId,
48
- keyFromEntry = (entry: TEntry) => entry as unknown as TKey,
49
- compare = (a: TKey, b: TKey) => a < b ? -1 : a > b ? 1 : 0,
50
- /** See {@link Tree.createOrOpen}'s `nodeCapacity`. */
51
- nodeCapacity?: number,
52
- ): Promise<Tree<TKey, TEntry> | undefined> {
53
- const held: BTreeHolder<TKey, TEntry> = {};
54
- const init = Tree.buildInit(id, keyFromEntry, compare, nodeCapacity, held);
55
- const collection = await Collection.open<TreeReplaceAction<TKey, TEntry>>(network, id, init);
56
- return collection ? Tree.attach(collection, held, keyFromEntry, compare, nodeCapacity) : undefined;
57
- }
58
-
59
- static async createOrOpen<TKey, TEntry>(
60
- network: ITransactor,
61
- id: CollectionId,
62
- keyFromEntry = (entry: TEntry) => entry as unknown as TKey,
63
- compare = (a: TKey, b: TKey) => a < b ? -1 : a > b ? 1 : 0,
64
- /** B-tree node fan-out. Defaults to the BTree default (64). Exposed mainly so tests can
65
- * force a multi-level tree with few entries (a small capacity), which is what exercises the
66
- * interior-navigation read-exclusion path — with the default you would need thousands of
67
- * entries before a descent has any interior branch between root and leaf.
68
- *
69
- * NOTE: fan-out is NOT persisted in the collection header — it is a per-call construction
70
- * param. Creating a tree with a non-default capacity and later reopening it (a separate
71
- * createOrOpen call) without the SAME capacity silently uses 64, so subsequent writes split
72
- * at a fan-out the existing nodes were not built for. Fine today (only tests pass it, and
73
- * they pass it consistently); if a persisted tree ever needs a custom fan-out, persist it in
74
- * the header and read it back on reopen rather than trusting the caller to re-supply it. */
75
- nodeCapacity?: number,
76
- ): Promise<Tree<TKey, TEntry>> {
77
- const held: BTreeHolder<TKey, TEntry> = {};
78
- const init = Tree.buildInit(id, keyFromEntry, compare, nodeCapacity, held);
79
- const collection = await Collection.createOrOpen<TreeReplaceAction<TKey, TEntry>>(network, id, init);
80
- return Tree.attach(collection, held, keyFromEntry, compare, nodeCapacity);
81
- }
82
-
83
- /** The collection wiring both open paths share. `held` carries the read btree between the
84
- * `createHeaderBlock` callback (which must build it to obtain the root id) and {@link attach}. */
85
- private static buildInit<TKey, TEntry>(
86
- id: CollectionId,
87
- keyFromEntry: (entry: TEntry) => TKey,
88
- compare: (a: TKey, b: TKey) => number,
89
- nodeCapacity: number | undefined,
90
- held: BTreeHolder<TKey, TEntry>,
91
- ): CollectionInitOptions<TreeReplaceAction<TKey, TEntry>> {
92
- return {
93
- modules: {
94
- "replace": async ({ data: actions }, trx) => {
95
- // Write through the Atomic store the handler is handed (`trx`), NOT the captured
96
- // read btree, so `internalTransact`'s all-or-nothing wrapper actually governs this
97
- // action: if any entry throws, `atomic.commit()` is skipped and every staged node
98
- // write from this action is discarded (whole-action rollback) — identically for
99
- // freshly created and reopened trees. Binding a throwaway BTree to `trx` reuses the
100
- // public constructor; no btree API change needed.
101
- const actionTree = new BTree<TKey, TEntry>(
102
- trx,
103
- new CollectionTrunk(trx, id),
104
- keyFromEntry,
105
- compare,
106
- nodeCapacity, // keep the write btree's fan-out in lock-step with the read btree
107
- );
108
- for (const [key, entry] of actions) {
109
- if (entry) {
110
- await actionTree.upsert(entry);
111
- } else {
112
- await actionTree.deleteAt((await actionTree.find(key)));
113
- }
114
- }
115
- // Mutations landed in `trx`, not the read btree, so its version counter never moved.
116
- // Bump it to invalidate any Path a caller still holds — preserving the path-invalidation
117
- // the previous in-place handler gave for free.
118
- held.btree?.invalidatePaths();
119
- }
120
- },
121
- createHeaderBlock: (hid: BlockId, store: BlockStore<IBlock>) => { // Only called if the collection does not exist
122
- // Tricky bootstrapping here:
123
- // We need the root id to initialize the collection header, so we create the btree here.
124
- let rootId: BlockId;
125
- held.btree = BTree.create<TKey, TEntry>(store, (_s, r) => {
126
- rootId = r;
127
- return new CollectionTrunk(store, hid);
128
- }, keyFromEntry, compare, nodeCapacity);
129
- return {
130
- header: store.createBlockHeader(TreeHeaderBlockType, hid),
131
- rootId: rootId!,
132
- }
133
- }
134
- };
135
- }
136
-
137
- /** Bind an opened collection to its read btree. On the create path `createHeaderBlock` already
138
- * built one (it needed the root id for the header); on the open path it never ran, so build it
139
- * over the collection's existing tracker. Either way the result is written back into `held` so
140
- * the `replace` handler's path-invalidation targets the very btree reads go through. */
141
- private static attach<TKey, TEntry>(
142
- collection: Collection<TreeReplaceAction<TKey, TEntry>>,
143
- held: BTreeHolder<TKey, TEntry>,
144
- keyFromEntry: (entry: TEntry) => TKey,
145
- compare: (a: TKey, b: TKey) => number,
146
- nodeCapacity: number | undefined,
147
- ): Tree<TKey, TEntry> {
148
- held.btree = held.btree
149
- ?? new BTree<TKey, TEntry>(collection.tracker, new CollectionTrunk(collection.tracker, collection.id), keyFromEntry, compare, nodeCapacity);
150
- return new Tree<TKey, TEntry>(collection, held.btree, keyFromEntry, compare);
151
- }
152
-
153
- async replace(data: TreeReplaceAction<TKey, TEntry>): Promise<void> {
154
- await this.collection.act({ type: "replace", data });
155
- await this.collection.updateAndSync();
156
- }
157
-
158
- /** Stage a mutation into the collection's tracker WITHOUT flushing it to the
159
- * transactor. Reads through this same Tree instance see the staged change;
160
- * call {@link sync} to persist it, or {@link snapshot}/{@link restore} to drop it. This
161
- * is the deferred counterpart to {@link replace}, which stages and flushes in
162
- * one step — use {@link stage} when the persist/discard decision belongs to a
163
- * surrounding transaction's commit/rollback. */
164
- async stage(data: TreeReplaceAction<TKey, TEntry>): Promise<void> {
165
- await this.collection.act({ type: "replace", data });
166
- }
167
-
168
- /** Flush all staged (and any other pending) changes to the transactor.
169
- * Equivalent to the flush half of {@link replace}. */
170
- async sync(): Promise<void> {
171
- await this.collection.updateAndSync();
172
- }
173
-
174
- /** Capture the current staged state so it can be restored via {@link restore}.
175
- * Take this BEFORE staging a unit of work that may be rolled back. The snapshot
176
- * is opaque; pass the exact value back to {@link restore}. */
177
- snapshot(): CollectionSnapshot<TreeReplaceAction<TKey, TEntry>> {
178
- return this.collection.snapshotPending();
179
- }
180
-
181
- /** Restore the staged state captured by {@link snapshot}, discarding mutations
182
- * staged since. Counterpart to {@link stage} for transaction rollback —
183
- * preserves a never-synced collection's header/root rather than wiping it. */
184
- restore(snapshot: CollectionSnapshot<TreeReplaceAction<TKey, TEntry>>): void {
185
- this.collection.restorePending(snapshot);
186
- }
187
-
188
- /** Build a read-only view of this tree as captured by an earlier {@link snapshot}
189
- * typically the pre-transaction state recorded before any DML was staged. The
190
- * view reads through a FRESH tracker seeded with the snapshot's transforms over a
191
- * PRIVATE, revision-pinned read path (see {@link Collection.createReadTracker}),
192
- * so it observes exactly the snapshot's state from first read to last: it never
193
- * sees mutations staged into the live tree after the snapshot, it is untouched by
194
- * commits folding into (or clearing) the live tree's cache while it is walked, and
195
- * it does not disturb the live tree. This is how a `committed.*` scan reads the
196
- * pre-transaction snapshot while the live tree still holds this transaction's
197
- * in-flight inserts and keeps reading it even if the live tree commits mid-scan.
198
- *
199
- * By default the view records no read dependencies into the collection's conflict
200
- * set; pass `{ recordReads: true }` to opt in (see {@link ReadViewOptions}).
201
- *
202
- * The view pins to the snapshot's OWN committed boundary ({@link CollectionSnapshot.context}),
203
- * not the collection's current one so a snapshot captured before a commit yields a
204
- * coherent pre-commit view even when this tree has already flushed that commit (a
205
- * mid-sweep multi-tree commit). A snapshot with no recorded boundary (an invented
206
- * collection, or a hand-built snapshot) falls back to the current context, which is
207
- * the pre-boundary behaviour.
208
- *
209
- * `snapshot` is the opaque value returned by {@link snapshot}; pass it back
210
- * verbatim. */
211
- readView(
212
- snapshot: CollectionSnapshot<TreeReplaceAction<TKey, TEntry>>,
213
- options?: ReadViewOptions,
214
- ): TreeReadView<TKey, TEntry> {
215
- const effectiveOptions: ReadViewOptions = {
216
- ...options,
217
- pinContext: options?.pinContext ?? snapshot.context,
218
- };
219
- const tracker = this.collection.createReadTracker(snapshot.transforms, effectiveOptions);
220
- return new BTree<TKey, TEntry>(
221
- tracker,
222
- new CollectionTrunk(tracker, this.collection.id),
223
- this.keyFromEntry,
224
- this.compare,
225
- );
226
- }
227
-
228
- /** The underlying {@link Collection} this tree stages mutations into.
229
- *
230
- * Exposed (package-internal intent) so a transaction coordinator can register
231
- * and read the very tracker this tree mutates: session-mode commit reads
232
- * `collection.tracker.transforms` directly, so the coordinator's collection
233
- * map must hold the same instance the tree stages into. Prefer this accessor
234
- * over reaching through `tree['collection']`. */
235
- getCollection(): Collection<TreeReplaceAction<TKey, TEntry>> {
236
- return this.collection;
237
- }
238
-
239
- /** This tree's collection id, as a plain string. Used by consumers that flush
240
- * several trees together (e.g. the Quereus adapter's legacy commit sweep) to
241
- * name a specific tree in diagnostics when a partial flush leaves trees out of
242
- * sync. Structurally satisfies the adapter's `DirtyTree.describe()`. */
243
- describe(): string {
244
- return String(this.collection.id);
245
- }
246
-
247
- /**
248
- * Update the local state from the network.
249
- * Call this before reading to ensure you have the latest data.
250
- */
251
- async update(): Promise<void> {
252
- await this.collection.update();
253
- }
254
-
255
- // Read actions
256
-
257
- async first(): Promise<Path<TKey, TEntry>> {
258
- return await this.btree.first();
259
- }
260
-
261
- async last(): Promise<Path<TKey, TEntry>> {
262
- return await this.btree.last();
263
- }
264
-
265
- async find(key: TKey): Promise<Path<TKey, TEntry>> {
266
- return await this.btree.find(key);
267
- }
268
-
269
- async get(key: TKey): Promise<TEntry | undefined> {
270
- return await this.btree.get(key);
271
- }
272
-
273
- at(path: Path<TKey, TEntry>): TEntry | undefined {
274
- return this.btree.at(path);
275
- }
276
-
277
- range(range: KeyRange<TKey>): AsyncIterableIterator<Path<TKey, TEntry>> {
278
- return this.btree.range(range);
279
- }
280
-
281
- ascending(path: Path<TKey, TEntry>): AsyncIterableIterator<Path<TKey, TEntry>> {
282
- return this.btree.ascending(path);
283
- }
284
-
285
- descending(path: Path<TKey, TEntry>): AsyncIterableIterator<Path<TKey, TEntry>> {
286
- return this.btree.descending(path);
287
- }
288
-
289
- async getCount(from?: { path: Path<TKey, TEntry>, ascending?: boolean }): Promise<number> {
290
- return await this.btree.getCount(from);
291
- }
292
-
293
- async next(path: Path<TKey, TEntry>): Promise<Path<TKey, TEntry>> {
294
- return await this.btree.next(path);
295
- }
296
-
297
- async moveNext(path: Path<TKey, TEntry>): Promise<void> {
298
- await this.btree.moveNext(path);
299
- }
300
-
301
- async prior(path: Path<TKey, TEntry>): Promise<Path<TKey, TEntry>> {
302
- return await this.btree.prior(path);
303
- }
304
-
305
- async movePrior(path: Path<TKey, TEntry>): Promise<void> {
306
- await this.btree.movePrior(path);
307
- }
308
-
309
- isValid(path: Path<TKey, TEntry>): boolean {
310
- return this.btree.isValid(path);
311
- }
312
- }
1
+ import { Collection, type CollectionInitOptions, type CollectionId, type CollectionSnapshot, type ReadViewOptions } from "../../collection/index.js";
2
+ import type { ITransactor, BlockId, BlockStore, IBlock } from "../../index.js";
3
+ import { BTree, type Path, type KeyRange } from "../../btree/index.js";
4
+ import { CollectionTrunk } from "./collection-trunk.js";
5
+ import { TreeHeaderBlockType, type TreeReplaceAction } from "./struct.js";
6
+
7
+ /**
8
+ * Read-only surface of a tree: every navigation/lookup method a reader needs, with
9
+ * none of the mutation (stage/sync) or network-refresh (update) entry points. Both
10
+ * the live {@link Tree} and the committed view returned by {@link Tree.readView}
11
+ * structurally satisfy this, so a consumer can read through either uniformly.
12
+ */
13
+ export interface TreeReadView<TKey, TEntry> {
14
+ first(): Promise<Path<TKey, TEntry>>;
15
+ find(key: TKey): Promise<Path<TKey, TEntry>>;
16
+ get(key: TKey): Promise<TEntry | undefined>;
17
+ at(path: Path<TKey, TEntry>): TEntry | undefined;
18
+ range(range: KeyRange<TKey>): AsyncIterableIterator<Path<TKey, TEntry>>;
19
+ ascending(path: Path<TKey, TEntry>): AsyncIterableIterator<Path<TKey, TEntry>>;
20
+ isValid(path: Path<TKey, TEntry>): boolean;
21
+ }
22
+
23
+ /** Carries the read {@link BTree} from wherever it gets built (the `createHeaderBlock`
24
+ * callback on the create path, {@link Tree.attach} on the open path) to the `replace`
25
+ * handler, which needs the live instance to invalidate outstanding paths. */
26
+ interface BTreeHolder<TKey, TEntry> {
27
+ btree?: BTree<TKey, TEntry>;
28
+ }
29
+
30
+ export class Tree<TKey, TEntry> implements TreeReadView<TKey, TEntry> {
31
+
32
+ private constructor(
33
+ private readonly collection: Collection<TreeReplaceAction<TKey, TEntry>>,
34
+ private readonly btree: BTree<TKey, TEntry>,
35
+ /** Captured so {@link readView} can rebuild a BTree over a committed tracker. */
36
+ private readonly keyFromEntry: (entry: TEntry) => TKey,
37
+ private readonly compare: (a: TKey, b: TKey) => number,
38
+ ) {
39
+ }
40
+
41
+ /** Open an EXISTING tree, or resolve to `undefined` when no header block has ever been
42
+ * committed under this id. Never brings a tree into existence — nothing is staged into the
43
+ * collection's tracker on the absent path, so a caller that ignores the `undefined` cannot
44
+ * later sync a phantom tree. Use on pure read paths; see {@link Collection.open}. */
45
+ static async open<TKey, TEntry>(
46
+ network: ITransactor,
47
+ id: CollectionId,
48
+ keyFromEntry = (entry: TEntry) => entry as unknown as TKey,
49
+ compare = (a: TKey, b: TKey) => a < b ? -1 : a > b ? 1 : 0,
50
+ /** See {@link Tree.createOrOpen}'s `nodeCapacity`. */
51
+ nodeCapacity?: number,
52
+ ): Promise<Tree<TKey, TEntry> | undefined> {
53
+ const held: BTreeHolder<TKey, TEntry> = {};
54
+ const init = Tree.buildInit(id, keyFromEntry, compare, nodeCapacity, held);
55
+ const collection = await Collection.open<TreeReplaceAction<TKey, TEntry>>(network, id, init);
56
+ return collection ? Tree.attach(collection, held, keyFromEntry, compare, nodeCapacity) : undefined;
57
+ }
58
+
59
+ static async createOrOpen<TKey, TEntry>(
60
+ network: ITransactor,
61
+ id: CollectionId,
62
+ keyFromEntry = (entry: TEntry) => entry as unknown as TKey,
63
+ compare = (a: TKey, b: TKey) => a < b ? -1 : a > b ? 1 : 0,
64
+ /** B-tree node fan-out. Defaults to the BTree default (64). Exposed mainly so tests can
65
+ * force a multi-level tree with few entries (a small capacity), which is what exercises the
66
+ * interior-navigation read-exclusion path — with the default you would need thousands of
67
+ * entries before a descent has any interior branch between root and leaf.
68
+ *
69
+ * NOTE: fan-out is NOT persisted in the collection header — it is a per-call construction
70
+ * param. Creating a tree with a non-default capacity and later reopening it (a separate
71
+ * createOrOpen call) without the SAME capacity silently uses 64, so subsequent writes split
72
+ * at a fan-out the existing nodes were not built for. Fine today (only tests pass it, and
73
+ * they pass it consistently); if a persisted tree ever needs a custom fan-out, persist it in
74
+ * the header and read it back on reopen rather than trusting the caller to re-supply it. */
75
+ nodeCapacity?: number,
76
+ ): Promise<Tree<TKey, TEntry>> {
77
+ const held: BTreeHolder<TKey, TEntry> = {};
78
+ const init = Tree.buildInit(id, keyFromEntry, compare, nodeCapacity, held);
79
+ const collection = await Collection.createOrOpen<TreeReplaceAction<TKey, TEntry>>(network, id, init);
80
+ return Tree.attach(collection, held, keyFromEntry, compare, nodeCapacity);
81
+ }
82
+
83
+ /** The collection wiring both open paths share. `held` carries the read btree between the
84
+ * `createHeaderBlock` callback (which must build it to obtain the root id) and {@link attach}. */
85
+ private static buildInit<TKey, TEntry>(
86
+ id: CollectionId,
87
+ keyFromEntry: (entry: TEntry) => TKey,
88
+ compare: (a: TKey, b: TKey) => number,
89
+ nodeCapacity: number | undefined,
90
+ held: BTreeHolder<TKey, TEntry>,
91
+ ): CollectionInitOptions<TreeReplaceAction<TKey, TEntry>> {
92
+ return {
93
+ modules: {
94
+ "replace": async ({ data: actions }, trx) => {
95
+ // Write through the Atomic store the handler is handed (`trx`), NOT the captured
96
+ // read btree, so `internalTransact`'s all-or-nothing wrapper actually governs this
97
+ // action: if any entry throws, `atomic.commit()` is skipped and every staged node
98
+ // write from this action is discarded (whole-action rollback) — identically for
99
+ // freshly created and reopened trees. Binding a throwaway BTree to `trx` reuses the
100
+ // public constructor; no btree API change needed.
101
+ const actionTree = new BTree<TKey, TEntry>(
102
+ trx,
103
+ new CollectionTrunk(trx, id),
104
+ keyFromEntry,
105
+ compare,
106
+ nodeCapacity, // keep the write btree's fan-out in lock-step with the read btree
107
+ );
108
+ for (const [key, entry] of actions) {
109
+ if (entry) {
110
+ await actionTree.upsert(entry);
111
+ } else {
112
+ await actionTree.deleteAt((await actionTree.find(key)));
113
+ }
114
+ }
115
+ // Mutations landed in `trx`, not the read btree, so its version counter never moved.
116
+ // Bump it to invalidate any Path a caller still holds — preserving the path-invalidation
117
+ // the previous in-place handler gave for free.
118
+ held.btree?.invalidatePaths();
119
+ }
120
+ },
121
+ createHeaderBlock: (hid: BlockId, store: BlockStore<IBlock>) => { // Only called if the collection does not exist
122
+ // Tricky bootstrapping here:
123
+ // We need the root id to initialize the collection header, so we create the btree here.
124
+ let rootId: BlockId;
125
+ held.btree = BTree.create<TKey, TEntry>(store, (_s, r) => {
126
+ rootId = r;
127
+ return new CollectionTrunk(store, hid);
128
+ }, keyFromEntry, compare, nodeCapacity);
129
+ return {
130
+ header: store.createBlockHeader(TreeHeaderBlockType, hid),
131
+ rootId: rootId!,
132
+ }
133
+ }
134
+ };
135
+ }
136
+
137
+ /** Bind an opened collection to its read btree. On the create path `createHeaderBlock` already
138
+ * built one (it needed the root id for the header); on the open path it never ran, so build it
139
+ * over the collection's existing tracker. Either way the result is written back into `held` so
140
+ * the `replace` handler's path-invalidation targets the very btree reads go through. */
141
+ private static attach<TKey, TEntry>(
142
+ collection: Collection<TreeReplaceAction<TKey, TEntry>>,
143
+ held: BTreeHolder<TKey, TEntry>,
144
+ keyFromEntry: (entry: TEntry) => TKey,
145
+ compare: (a: TKey, b: TKey) => number,
146
+ nodeCapacity: number | undefined,
147
+ ): Tree<TKey, TEntry> {
148
+ held.btree = held.btree
149
+ ?? new BTree<TKey, TEntry>(collection.tracker, new CollectionTrunk(collection.tracker, collection.id), keyFromEntry, compare, nodeCapacity);
150
+ return new Tree<TKey, TEntry>(collection, held.btree, keyFromEntry, compare);
151
+ }
152
+
153
+ async replace(data: TreeReplaceAction<TKey, TEntry>): Promise<void> {
154
+ await this.collection.act({ type: "replace", data });
155
+ await this.collection.updateAndSync();
156
+ }
157
+
158
+ /** Stage a mutation into the collection's tracker WITHOUT flushing it to the
159
+ * transactor. Reads through this same Tree instance see the staged change;
160
+ * call {@link sync} to persist it, or {@link snapshot}/{@link restore} to drop it. This
161
+ * is the deferred counterpart to {@link replace}, which stages and flushes in
162
+ * one step — use {@link stage} when the persist/discard decision belongs to a
163
+ * surrounding transaction's commit/rollback. */
164
+ async stage(data: TreeReplaceAction<TKey, TEntry>): Promise<void> {
165
+ await this.collection.act({ type: "replace", data });
166
+ }
167
+
168
+ /** Flush all staged (and any other pending) changes to the transactor.
169
+ * Equivalent to the flush half of {@link replace}. */
170
+ async sync(): Promise<void> {
171
+ await this.collection.updateAndSync();
172
+ }
173
+
174
+ /** Whether {@link sync} has anything to push see
175
+ * {@link Collection.hasUnsyncedChanges}. `false` means the commit half of
176
+ * {@link sync} is a no-op, so a caller flushing several trees can skip the ones
177
+ * with nothing staged and avoid their (cache-bypassing) refresh. */
178
+ hasUnsyncedChanges(): boolean {
179
+ return this.collection.hasUnsyncedChanges();
180
+ }
181
+
182
+ /** Capture the current staged state so it can be restored via {@link restore}.
183
+ * Take this BEFORE staging a unit of work that may be rolled back. The snapshot
184
+ * is opaque; pass the exact value back to {@link restore}. */
185
+ snapshot(): CollectionSnapshot<TreeReplaceAction<TKey, TEntry>> {
186
+ return this.collection.snapshotPending();
187
+ }
188
+
189
+ /** Restore the staged state captured by {@link snapshot}, discarding mutations
190
+ * staged since. Counterpart to {@link stage} for transaction rollback
191
+ * preserves a never-synced collection's header/root rather than wiping it. */
192
+ restore(snapshot: CollectionSnapshot<TreeReplaceAction<TKey, TEntry>>): void {
193
+ this.collection.restorePending(snapshot);
194
+ }
195
+
196
+ /** Build a read-only view of this tree as captured by an earlier {@link snapshot}
197
+ * — typically the pre-transaction state recorded before any DML was staged. The
198
+ * view reads through a FRESH tracker seeded with the snapshot's transforms over a
199
+ * PRIVATE, revision-pinned read path (see {@link Collection.createReadTracker}),
200
+ * so it observes exactly the snapshot's state from first read to last: it never
201
+ * sees mutations staged into the live tree after the snapshot, it is untouched by
202
+ * commits folding into (or clearing) the live tree's cache while it is walked, and
203
+ * it does not disturb the live tree. This is how a `committed.*` scan reads the
204
+ * pre-transaction snapshot while the live tree still holds this transaction's
205
+ * in-flight inserts and keeps reading it even if the live tree commits mid-scan.
206
+ *
207
+ * By default the view records no read dependencies into the collection's conflict
208
+ * set; pass `{ recordReads: true }` to opt in (see {@link ReadViewOptions}).
209
+ *
210
+ * The view pins to the snapshot's OWN committed boundary ({@link CollectionSnapshot.context}),
211
+ * not the collection's current one — so a snapshot captured before a commit yields a
212
+ * coherent pre-commit view even when this tree has already flushed that commit (a
213
+ * mid-sweep multi-tree commit). A snapshot with no recorded boundary (an invented
214
+ * collection, or a hand-built snapshot) falls back to the current context, which is
215
+ * the pre-boundary behaviour.
216
+ *
217
+ * `snapshot` is the opaque value returned by {@link snapshot}; pass it back
218
+ * verbatim. */
219
+ readView(
220
+ snapshot: CollectionSnapshot<TreeReplaceAction<TKey, TEntry>>,
221
+ options?: ReadViewOptions,
222
+ ): TreeReadView<TKey, TEntry> {
223
+ const effectiveOptions: ReadViewOptions = {
224
+ ...options,
225
+ pinContext: options?.pinContext ?? snapshot.context,
226
+ };
227
+ const tracker = this.collection.createReadTracker(snapshot.transforms, effectiveOptions);
228
+ return new BTree<TKey, TEntry>(
229
+ tracker,
230
+ new CollectionTrunk(tracker, this.collection.id),
231
+ this.keyFromEntry,
232
+ this.compare,
233
+ );
234
+ }
235
+
236
+ /** The underlying {@link Collection} this tree stages mutations into.
237
+ *
238
+ * Exposed (package-internal intent) so a transaction coordinator can register
239
+ * and read the very tracker this tree mutates: session-mode commit reads
240
+ * `collection.tracker.transforms` directly, so the coordinator's collection
241
+ * map must hold the same instance the tree stages into. Prefer this accessor
242
+ * over reaching through `tree['collection']`. */
243
+ getCollection(): Collection<TreeReplaceAction<TKey, TEntry>> {
244
+ return this.collection;
245
+ }
246
+
247
+ /** This tree's collection id, as a plain string. Used by consumers that flush
248
+ * several trees together (e.g. the Quereus adapter's legacy commit sweep) to
249
+ * name a specific tree in diagnostics when a partial flush leaves trees out of
250
+ * sync. Structurally satisfies the adapter's `DirtyTree.describe()`. */
251
+ describe(): string {
252
+ return String(this.collection.id);
253
+ }
254
+
255
+ /**
256
+ * Update the local state from the network.
257
+ * Call this before reading to ensure you have the latest data.
258
+ */
259
+ async update(): Promise<void> {
260
+ await this.collection.update();
261
+ }
262
+
263
+ // Read actions
264
+
265
+ async first(): Promise<Path<TKey, TEntry>> {
266
+ return await this.btree.first();
267
+ }
268
+
269
+ async last(): Promise<Path<TKey, TEntry>> {
270
+ return await this.btree.last();
271
+ }
272
+
273
+ async find(key: TKey): Promise<Path<TKey, TEntry>> {
274
+ return await this.btree.find(key);
275
+ }
276
+
277
+ async get(key: TKey): Promise<TEntry | undefined> {
278
+ return await this.btree.get(key);
279
+ }
280
+
281
+ at(path: Path<TKey, TEntry>): TEntry | undefined {
282
+ return this.btree.at(path);
283
+ }
284
+
285
+ range(range: KeyRange<TKey>): AsyncIterableIterator<Path<TKey, TEntry>> {
286
+ return this.btree.range(range);
287
+ }
288
+
289
+ ascending(path: Path<TKey, TEntry>): AsyncIterableIterator<Path<TKey, TEntry>> {
290
+ return this.btree.ascending(path);
291
+ }
292
+
293
+ descending(path: Path<TKey, TEntry>): AsyncIterableIterator<Path<TKey, TEntry>> {
294
+ return this.btree.descending(path);
295
+ }
296
+
297
+ async getCount(from?: { path: Path<TKey, TEntry>, ascending?: boolean }): Promise<number> {
298
+ return await this.btree.getCount(from);
299
+ }
300
+
301
+ async next(path: Path<TKey, TEntry>): Promise<Path<TKey, TEntry>> {
302
+ return await this.btree.next(path);
303
+ }
304
+
305
+ async moveNext(path: Path<TKey, TEntry>): Promise<void> {
306
+ await this.btree.moveNext(path);
307
+ }
308
+
309
+ async prior(path: Path<TKey, TEntry>): Promise<Path<TKey, TEntry>> {
310
+ return await this.btree.prior(path);
311
+ }
312
+
313
+ async movePrior(path: Path<TKey, TEntry>): Promise<void> {
314
+ await this.btree.movePrior(path);
315
+ }
316
+
317
+ isValid(path: Path<TKey, TEntry>): boolean {
318
+ return this.btree.isValid(path);
319
+ }
320
+ }