@minnowdb/core 0.6.9 → 0.7.1

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 (103) hide show
  1. package/dist/block-format/block.js +254 -325
  2. package/dist/block-format/checksum.js +30 -52
  3. package/dist/block-format/codecs.js +112 -125
  4. package/dist/block-format/column.js +187 -205
  5. package/dist/block-format/index.js +5 -1
  6. package/dist/block-format/physical.js +371 -387
  7. package/dist/block-format/types.js +4 -1
  8. package/dist/block-format/unicode.js +30 -39
  9. package/dist/date-value.d.ts +5 -0
  10. package/dist/date-value.js +71 -41
  11. package/dist/engine/artifact-cache.js +106 -116
  12. package/dist/engine/batch.js +36 -44
  13. package/dist/engine/buffered-writer.js +156 -158
  14. package/dist/engine/byte-estimates.js +20 -22
  15. package/dist/engine/cache-limits.js +24 -25
  16. package/dist/engine/cancellation.js +5 -3
  17. package/dist/engine/catalog.js +64 -73
  18. package/dist/engine/client.js +715 -780
  19. package/dist/engine/database.js +15952 -18465
  20. package/dist/engine/defaults.js +104 -111
  21. package/dist/engine/errors.js +113 -128
  22. package/dist/engine/fts.js +233 -280
  23. package/dist/engine/group-index.js +287 -321
  24. package/dist/engine/index.js +23 -12
  25. package/dist/engine/join-index.js +177 -186
  26. package/dist/engine/keyed-live.js +168 -173
  27. package/dist/engine/live-api.js +0 -1
  28. package/dist/engine/live-equal.js +32 -37
  29. package/dist/engine/live.js +603 -647
  30. package/dist/engine/memory.js +109 -111
  31. package/dist/engine/optimizer.d.ts +19 -1
  32. package/dist/engine/optimizer.js +2642 -2515
  33. package/dist/engine/point-read.d.ts +4 -2
  34. package/dist/engine/point-read.js +121 -171
  35. package/dist/engine/query-api.js +12 -3
  36. package/dist/engine/query-cache.js +61 -69
  37. package/dist/engine/query.d.ts +45 -0
  38. package/dist/engine/query.js +8506 -9375
  39. package/dist/engine/result-wire.js +221 -252
  40. package/dist/engine/schema-wire.js +97 -112
  41. package/dist/engine/schema.js +1099 -1254
  42. package/dist/engine/sort-keys.js +315 -389
  43. package/dist/engine/sql-domains.d.ts +7 -1
  44. package/dist/engine/sql-domains.js +648 -734
  45. package/dist/engine/sql-driver.js +0 -1
  46. package/dist/engine/sql-functions.d.ts +11 -0
  47. package/dist/engine/sql-functions.js +1092 -0
  48. package/dist/engine/sql-json.js +199 -218
  49. package/dist/engine/sql-semantics.d.ts +25 -4
  50. package/dist/engine/sql-semantics.js +579 -513
  51. package/dist/engine/typed-live.js +271 -293
  52. package/dist/engine/vector.d.ts +7 -1
  53. package/dist/engine/vector.js +4513 -4580
  54. package/dist/engine/worker-host.d.ts +8 -63
  55. package/dist/engine/worker-host.js +28 -1130
  56. package/dist/engine/worker-indexeddb.d.ts +1 -0
  57. package/dist/engine/worker-indexeddb.js +3 -0
  58. package/dist/engine/worker-memory.d.ts +1 -0
  59. package/dist/engine/worker-memory.js +3 -0
  60. package/dist/engine/worker-opfs.d.ts +1 -0
  61. package/dist/engine/worker-opfs.js +3 -0
  62. package/dist/engine/worker-server.d.ts +93 -0
  63. package/dist/engine/worker-server.js +999 -0
  64. package/dist/engine/worker-store-indexeddb.d.ts +2 -0
  65. package/dist/engine/worker-store-indexeddb.js +11 -0
  66. package/dist/engine/worker-store-memory.d.ts +2 -0
  67. package/dist/engine/worker-store-memory.js +6 -0
  68. package/dist/engine/worker-store-opfs.d.ts +2 -0
  69. package/dist/engine/worker-store-opfs.js +9 -0
  70. package/dist/engine/worker.js +0 -12
  71. package/dist/engine/write-block-planner.js +93 -100
  72. package/dist/plan/index.js +15 -16
  73. package/dist/plan/model.d.ts +9 -1
  74. package/dist/plan/model.js +20 -27
  75. package/dist/storage/index.js +0 -12
  76. package/dist/storage/indexeddb.js +11776 -13171
  77. package/dist/storage/memory.js +1198 -1241
  78. package/dist/storage/opfs/files.js +238 -295
  79. package/dist/storage/opfs/index.js +9 -3
  80. package/dist/storage/opfs/leader.js +4386 -4856
  81. package/dist/storage/opfs/rpc.js +223 -259
  82. package/dist/storage/opfs/snapshot-ledger.js +219 -260
  83. package/dist/storage/opfs/store.js +884 -1003
  84. package/dist/storage/persistence.js +59 -74
  85. package/dist/storage/snapshot-stream.js +788 -826
  86. package/dist/storage/snapshot.js +36 -37
  87. package/dist/storage/toolkit/extents.js +444 -520
  88. package/dist/storage/toolkit/index.js +28 -29
  89. package/dist/storage/toolkit/record-core.js +5792 -6377
  90. package/dist/storage/toolkit/sync-file.js +34 -42
  91. package/dist/storage/toolkit/wal.js +92 -127
  92. package/dist/storage/toolkit/wire.js +232 -256
  93. package/dist/storage/types.js +2952 -3228
  94. package/dist/testing/block-store-conformance.js +1556 -1629
  95. package/dist/testing/index.js +310 -291
  96. package/dist/testing/opfs-shim.js +280 -312
  97. package/dist/testing/simulator.js +496 -534
  98. package/dist/testing/sqllogictest.js +425 -365
  99. package/dist/transactions/index.js +1464 -1712
  100. package/dist/worker-protocol/index.js +70 -69
  101. package/package.json +20 -2
  102. package/postgres-feature-profile.json +15 -5
  103. package/sql-feature-matrix.json +252 -2
@@ -1,1764 +1,1516 @@
1
1
  import { dateIsoString, dateMilliseconds } from "../date-value.js";
2
- import { assertTransactionArtifactBatchLimits, assertTransactionArtifactJournalLimits, GarbageCollectionJobConflictError, LeaseConflictError, LeaseExpiredError, MAX_LEVEL_ZERO_SEGMENTS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_LEASE_TTL_MS, MAX_TRANSACTION_STAGE_BLOCKS, MAX_TRANSACTION_STAGE_BYTES, MAX_TRANSACTION_STAGE_SEGMENTS, MAX_TRANSACTION_COMMIT_DELTA_BYTES, MAX_TRANSACTION_COMMIT_DELTA_ENTRIES, transactionCommitDeltaRetainedBytes, SnapshotManifestMissingError, TransactionRecordConflictError, WriteConflictError, } from "../storage/types.js";
3
- export const DEFAULT_TRANSACTION_TTL_MS = 30_000;
4
- export { MAX_LEASE_TTL_MS };
5
- export class TransactionClosedError extends Error {
6
- transactionId;
7
- name = "TransactionClosedError";
8
- constructor(transactionId) {
9
- super(`Transaction is no longer active: ${transactionId}`);
10
- this.transactionId = transactionId;
11
- }
2
+ import { assertTransactionArtifactBatchLimits, assertTransactionArtifactJournalLimits, GarbageCollectionJobConflictError, LeaseConflictError, LeaseExpiredError, MAX_LEVEL_ZERO_SEGMENTS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_LEASE_TTL_MS, MAX_TRANSACTION_STAGE_BLOCKS, MAX_TRANSACTION_STAGE_BYTES, MAX_TRANSACTION_STAGE_SEGMENTS, MAX_TRANSACTION_COMMIT_DELTA_BYTES, MAX_TRANSACTION_COMMIT_DELTA_ENTRIES, transactionCommitDeltaRetainedBytes, SnapshotManifestMissingError, TransactionRecordConflictError, WriteConflictError } from "../storage/types.js";
3
+ const DEFAULT_TRANSACTION_TTL_MS = 3e4;
4
+ class TransactionClosedError extends Error {
5
+ transactionId;
6
+ name = "TransactionClosedError";
7
+ constructor(transactionId) {
8
+ super(`Transaction is no longer active: ${transactionId}`);
9
+ this.transactionId = transactionId;
10
+ }
12
11
  }
13
- export class Snapshot {
14
- store;
15
- version;
16
- constructor(store, version) {
17
- this.store = store;
18
- this.version = version;
19
- }
20
- async hasBlock(id) {
21
- return (await this.store.hasManifestBlocks(this.version, [id]))[0] === true;
22
- }
23
- async hasBlocks(ids) {
24
- const present = [];
25
- for (let start = 0; start < ids.length; start += MAX_MANIFEST_BLOCK_PRESENCE_IDS) {
26
- present.push(...(await this.store.hasManifestBlocks(this.version, ids.slice(start, start + MAX_MANIFEST_BLOCK_PRESENCE_IDS))));
27
- }
28
- return present;
29
- }
30
- async getBlock(id) {
31
- return this.store.readManifestBlock(this.version, id);
32
- }
12
+ class Snapshot {
13
+ store;
14
+ version;
15
+ constructor(store, version) {
16
+ this.store = store;
17
+ this.version = version;
18
+ }
19
+ async hasBlock(id) {
20
+ return (await this.store.hasManifestBlocks(this.version, [id]))[0] === true;
21
+ }
22
+ async hasBlocks(ids) {
23
+ const present = [];
24
+ for (let start = 0; start < ids.length; start += MAX_MANIFEST_BLOCK_PRESENCE_IDS) {
25
+ present.push(...await this.store.hasManifestBlocks(this.version, ids.slice(start, start + MAX_MANIFEST_BLOCK_PRESENCE_IDS)));
26
+ }
27
+ return present;
28
+ }
29
+ async getBlock(id) {
30
+ return this.store.readManifestBlock(this.version, id);
31
+ }
33
32
  }
34
- export class LeasedSnapshot extends Snapshot {
35
- now;
36
- #record;
37
- #released = false;
38
- /** The record's ISO expiry, parsed once: expiry is checked before every batched block read. */
39
- #expiresAtIso;
40
- #expiresAtMs = Number.NaN;
41
- constructor(store, version, record, now) {
42
- super(store, version);
43
- this.now = now;
44
- this.#record = structuredClone(record);
45
- this.#store = store;
46
- }
47
- #store;
48
- get leaseId() {
49
- return this.#record.id;
50
- }
51
- get expiresAt() {
52
- const iso = this.#record.expiresAt;
53
- if (iso !== this.#expiresAtIso) {
54
- this.#expiresAtIso = iso;
55
- this.#expiresAtMs = Date.parse(iso);
56
- }
57
- return new Date(this.#expiresAtMs);
58
- }
59
- async getBlock(id) {
60
- this.#assertOpen();
61
- return super.getBlock(id);
62
- }
63
- async renew(ttlMs) {
64
- this.#assertOpen();
65
- const now = this.now();
66
- const expiresAt = expiryAfter(now, ttlMs);
67
- this.#record = await this.#store.renewLease({
68
- id: this.#record.id,
69
- expectedRevision: this.#record.revision,
70
- expiresAtCutoff: dateIsoString(now),
71
- expiresAt,
72
- });
73
- return new Date(this.#record.expiresAt);
74
- }
75
- /**
76
- * Re-pins this lease to another version in one store step, handing the record over to the
77
- * returned snapshot; this one is closed on success (the lease is not released — it has
78
- * moved). Undefined when the store lacks the atomic move; the store's typed refusals
79
- * (`SnapshotManifestMissingError`, `LeaseConflictError`) leave this snapshot open and pass
80
- * through.
81
- */
82
- async moveTo(version, ttlMs) {
83
- this.#assertOpen();
84
- const move = this.#store.moveLease?.bind(this.#store);
85
- if (move === undefined)
86
- return undefined;
87
- const now = this.now();
88
- const expiresAt = expiryAfter(now, ttlMs);
89
- const record = await move({
90
- id: this.#record.id,
91
- expectedRevision: this.#record.revision,
92
- manifestVersion: version,
93
- expiresAtCutoff: dateIsoString(now),
94
- expiresAt,
95
- });
96
- this.#released = true;
97
- return new LeasedSnapshot(this.#store, version, record, this.now);
98
- }
99
- async release() {
100
- if (this.#released)
101
- return;
102
- await this.#store.removeLease({ id: this.#record.id, ownerId: this.#record.ownerId });
103
- this.#released = true;
104
- }
105
- /** Closes this snapshot without touching the store — for a lease the store no longer holds. */
106
- discard() {
107
- this.#released = true;
108
- }
109
- #assertOpen() {
110
- if (this.#released)
111
- throw new Error(`Snapshot lease has been released: ${this.#record.id}`);
112
- }
33
+ class LeasedSnapshot extends Snapshot {
34
+ now;
35
+ #record;
36
+ #released = false;
37
+ #expiresAtIso;
38
+ #expiresAtMs = Number.NaN;
39
+ constructor(store, version, record, now) {
40
+ super(store, version);
41
+ this.now = now;
42
+ this.#record = structuredClone(record);
43
+ this.#store = store;
44
+ }
45
+ #store;
46
+ get leaseId() {
47
+ return this.#record.id;
48
+ }
49
+ get expiresAt() {
50
+ const iso = this.#record.expiresAt;
51
+ if (iso !== this.#expiresAtIso) {
52
+ this.#expiresAtIso = iso;
53
+ this.#expiresAtMs = Date.parse(iso);
54
+ }
55
+ return new Date(this.#expiresAtMs);
56
+ }
57
+ async getBlock(id) {
58
+ this.#assertOpen();
59
+ return super.getBlock(id);
60
+ }
61
+ async renew(ttlMs) {
62
+ this.#assertOpen();
63
+ const now = this.now();
64
+ const expiresAt = expiryAfter(now, ttlMs);
65
+ this.#record = await this.#store.renewLease({
66
+ id: this.#record.id,
67
+ expectedRevision: this.#record.revision,
68
+ expiresAtCutoff: dateIsoString(now),
69
+ expiresAt
70
+ });
71
+ return new Date(this.#record.expiresAt);
72
+ }
73
+ async moveTo(version, ttlMs) {
74
+ this.#assertOpen();
75
+ const move = this.#store.moveLease?.bind(this.#store);
76
+ if (move === void 0)
77
+ return void 0;
78
+ const now = this.now();
79
+ const expiresAt = expiryAfter(now, ttlMs);
80
+ const record = await move({
81
+ id: this.#record.id,
82
+ expectedRevision: this.#record.revision,
83
+ manifestVersion: version,
84
+ expiresAtCutoff: dateIsoString(now),
85
+ expiresAt
86
+ });
87
+ this.#released = true;
88
+ return new LeasedSnapshot(this.#store, version, record, this.now);
89
+ }
90
+ async release() {
91
+ if (this.#released)
92
+ return;
93
+ await this.#store.removeLease({ id: this.#record.id, ownerId: this.#record.ownerId });
94
+ this.#released = true;
95
+ }
96
+ discard() {
97
+ this.#released = true;
98
+ }
99
+ #assertOpen() {
100
+ if (this.#released)
101
+ throw new Error(`Snapshot lease has been released: ${this.#record.id}`);
102
+ }
113
103
  }
114
- export class DatabaseTransaction {
115
- store;
116
- now;
117
- ttlMs;
118
- #record;
119
- #uniqueKeyChanges = [];
120
- #ftsChanges = new Map();
121
- #commitDeltaBytes = 0;
122
- #commitDeltaEntries = 0;
123
- #compactionJobId = null;
124
- #compactionSourceBlockIds = new Set();
125
- #changedTableIds = new Set();
126
- #levelZeroSegmentLimits = new Map();
127
- #logicallyUnchanged = false;
128
- /**
129
- * One storage-sized artifact batch may stay process-local until commit. This lets the common
130
- * one-statement transaction use the store's atomic writeTransaction operation instead of
131
- * staging a durable journal and committing it in a second adapter transaction. A second batch
132
- * flushes this one first, so retained bytes remain bounded by the storage batch limits.
133
- */
134
- #deferredBlocks = [];
135
- #deferredSegments = [];
136
- /** Segment metadata retained for local post-commit catalog-cache advancement. */
137
- #knownSegments = new Map();
138
- /**
139
- * Whether the record exists in the store. A deferred transaction (`beginDeferred`) starts
140
- * without one: it is written on the first two-step staging call, or folded into the
141
- * single-shot `stageArtifactsAndCommit`, or never — an abort of an unpersisted transaction
142
- * touches nothing.
143
- */
144
- #persisted;
145
- /** Durable pin for a deferred transaction that has no transaction record yet. */
146
- #snapshotLease;
147
- /** Serializes revisioned operations against the one lease record. */
148
- #snapshotLeaseOperation = Promise.resolve();
149
- #snapshotRenewal;
150
- #heartbeatTimer;
151
- #renewal;
152
- #initialCatalogProbe;
153
- constructor(store, record, now, ttlMs, createWeakRef, options = {}) {
154
- this.store = store;
155
- this.now = now;
156
- this.ttlMs = ttlMs;
157
- this.#record = structuredClone(record);
158
- this.#persisted = options.persisted ?? true;
159
- this.#initialCatalogProbe = options.initialCatalogProbe;
160
- this.#snapshotLease = options.snapshotLease;
161
- const reference = createWeakRef(this);
162
- const timer = setInterval(() => {
163
- const transaction = reference.deref();
164
- if (transaction === undefined) {
165
- clearInterval(timer);
166
- return;
167
- }
168
- void transaction.#renewOwnership(true).catch(() => undefined);
169
- }, Math.max(1, Math.floor(ttlMs / 3)));
170
- timer.unref?.();
171
- this.#heartbeatTimer = timer;
172
- }
173
- get id() {
174
- return this.#record.id;
175
- }
176
- get status() {
177
- return this.#record.status;
178
- }
179
- get snapshotVersion() {
180
- return this.#record.snapshotVersion;
181
- }
182
- get pendingBlockIds() {
183
- return [...this.#record.pendingBlockIds, ...this.#deferredBlocks.map((block) => block.id)];
184
- }
185
- get pendingSegmentIds() {
186
- return [
187
- ...this.#record.pendingSegmentIds,
188
- ...this.#deferredSegments.map((segment) => segment.id),
189
- ];
190
- }
191
- /** Process-local segments retained for a bounded single-shot commit. */
192
- get deferredSegments() {
193
- return structuredClone(this.#deferredSegments);
194
- }
195
- get pendingTableNextRowId() {
196
- return this.#record.pendingTableNextRowId;
197
- }
198
- get initialCatalogProbe() {
199
- return this.#initialCatalogProbe === undefined ? undefined : { ...this.#initialCatalogProbe };
200
- }
201
- /** Exact local commit contribution; undefined until this transaction commits. */
202
- get committedCatalogContribution() {
203
- if (this.#record.status !== "committed")
204
- return undefined;
205
- return {
206
- transaction: structuredClone(this.#record),
207
- segments: structuredClone([...this.#knownSegments.values()]),
208
- ...(this.#initialCatalogProbe === undefined
209
- ? {}
210
- : { initialCatalogProbe: { ...this.#initialCatalogProbe } }),
211
- };
212
- }
213
- get compactionSourceBlockIds() {
214
- return [...this.#compactionSourceBlockIds].sort();
215
- }
216
- /** Extends durable ownership; scope owners call this while user code is legitimately waiting. */
217
- async renew() {
218
- this.#assertActive();
219
- await this.#renewOwnership(true);
220
- }
221
- /**
222
- * Counts every registration this transaction carries: staged blocks and segments, unique-key
223
- * entries, full-text entries, and supersessions. A caller that fails part-way through a
224
- * multi-step stage compares this before and after to tell "nothing was registered" (the
225
- * statement is cleanly undone) from "partial work landed" (the transaction can only abort),
226
- * since there is no statement-level rollback inside a transaction.
227
- */
228
- get stagedWorkCount() {
229
- return (this.pendingBlockIds.length +
230
- this.pendingSegmentIds.length +
231
- this.#uniqueKeyChanges.length +
232
- this.#ftsChanges.size +
233
- this.#compactionSourceBlockIds.size);
234
- }
235
- /** Captures all publish-relevant state; row-id/autoincrement reservations deliberately burn. */
236
- checkpoint() {
237
- this.#assertActive();
238
- return {
239
- transactionId: this.id,
240
- pendingBlockIds: this.pendingBlockIds,
241
- pendingSegmentIds: this.pendingSegmentIds,
242
- uniqueKeyChanges: structuredClone(this.#uniqueKeyChanges),
243
- ftsChanges: structuredClone(this.#materializedFtsChanges()),
244
- compactionJobId: this.#compactionJobId,
245
- compactionSourceBlockIds: [...this.#compactionSourceBlockIds],
246
- levelZeroSegmentLimits: [...this.#levelZeroSegmentLimits].map(([tableId, limit]) => ({
247
- tableId,
248
- limit,
249
- })),
250
- changedTableIds: [...this.#changedTableIds],
251
- logicallyUnchanged: this.#logicallyUnchanged,
252
- };
253
- }
254
- /** Conservative retained-size estimate used to refuse savepoint clones before allocating. */
255
- checkpointRetainedBytes() {
256
- this.#assertActive();
257
- let bytes = 256;
258
- const addString = (value) => {
259
- bytes = checkpointByteSum(bytes, 16 + value.length * 2);
260
- };
261
- addString(this.id);
262
- for (const id of this.pendingBlockIds)
263
- addString(id);
264
- for (const id of this.pendingSegmentIds)
265
- addString(id);
266
- for (const change of this.#uniqueKeyChanges) {
267
- addString(change.tableId);
268
- for (const token of change.keyTokens)
269
- addString(token);
270
- bytes = checkpointByteSum(bytes, 32);
271
- }
272
- for (const change of this.#materializedFtsChanges()) {
273
- addString(change.tableId);
274
- for (const column of change.columns) {
275
- addString(column.columnId);
276
- bytes = checkpointByteSum(bytes, 48);
277
- for (const posting of column.postings) {
278
- addString(posting.term);
279
- bytes = checkpointByteSum(bytes, posting.rowIds.length * 8 + posting.tf.length * 8 + 32);
280
- }
281
- }
282
- }
283
- if (this.#compactionJobId !== null)
284
- addString(this.#compactionJobId);
285
- for (const id of this.#compactionSourceBlockIds)
286
- addString(id);
287
- for (const [tableId] of this.#levelZeroSegmentLimits)
288
- addString(tableId);
289
- for (const id of this.#changedTableIds)
290
- addString(id);
291
- return bytes;
292
- }
293
- /** Rewinds staged artifacts and every in-memory commit delta to one earlier checkpoint. */
294
- async rollbackTo(checkpoint) {
295
- this.#assertActive();
296
- if (checkpoint.transactionId !== this.id) {
297
- throw new TypeError("A transaction checkpoint belongs to another transaction");
298
- }
299
- const blockPrefix = checkpoint.pendingBlockIds;
300
- const segmentPrefix = checkpoint.pendingSegmentIds;
301
- const retainedBlocks = new Set(blockPrefix);
302
- const retainedSegments = new Set(segmentPrefix);
303
- const currentBlockIds = this.pendingBlockIds;
304
- const currentSegmentIds = this.pendingSegmentIds;
305
- if (blockPrefix.some((id) => !currentBlockIds.includes(id)) ||
306
- segmentPrefix.some((id) => !currentSegmentIds.includes(id))) {
307
- throw new TypeError("A transaction checkpoint is no longer reachable");
308
- }
309
- const removedBlockIds = currentBlockIds.filter((id) => !retainedBlocks.has(id));
310
- const removedSegmentIds = currentSegmentIds.filter((id) => !retainedSegments.has(id));
311
- if (this.#persisted && (removedBlockIds.length > 0 || removedSegmentIds.length > 0)) {
312
- await this.#renewOwnership();
313
- this.#record = await this.store.rollbackTransactionArtifacts({
314
- transactionId: this.id,
315
- expectedRevision: this.#record.revision,
316
- pendingBlockIds: blockPrefix,
317
- pendingSegmentIds: segmentPrefix,
318
- removeBlockIds: removedBlockIds,
319
- removeSegmentIds: removedSegmentIds,
320
- updatedAt: dateIsoString(this.now()),
321
- });
322
- }
323
- else if (!this.#persisted) {
324
- for (let index = this.#deferredBlocks.length - 1; index >= 0; index -= 1) {
325
- if (!retainedBlocks.has(this.#deferredBlocks[index]?.id ?? "")) {
326
- this.#deferredBlocks.splice(index, 1);
327
- }
328
- }
329
- for (let index = this.#deferredSegments.length - 1; index >= 0; index -= 1) {
330
- if (!retainedSegments.has(this.#deferredSegments[index]?.id ?? "")) {
331
- this.#deferredSegments.splice(index, 1);
332
- }
333
- }
334
- }
335
- for (const id of removedSegmentIds)
336
- this.#knownSegments.delete(id);
337
- this.#uniqueKeyChanges.splice(0);
338
- this.#ftsChanges.clear();
339
- this.#commitDeltaBytes = 0;
340
- this.#commitDeltaEntries = 0;
341
- for (const changes of checkpoint.uniqueKeyChanges)
342
- this.setUniqueKeyChanges(changes);
343
- for (const changes of checkpoint.ftsChanges)
344
- this.setFtsChanges(changes);
345
- this.#compactionJobId = checkpoint.compactionJobId;
346
- this.#compactionSourceBlockIds.clear();
347
- for (const id of checkpoint.compactionSourceBlockIds)
348
- this.#compactionSourceBlockIds.add(id);
349
- this.#levelZeroSegmentLimits.clear();
350
- for (const { tableId, limit } of checkpoint.levelZeroSegmentLimits) {
351
- this.#levelZeroSegmentLimits.set(tableId, limit);
352
- }
353
- this.#changedTableIds.clear();
354
- for (const id of checkpoint.changedTableIds)
355
- this.#changedTableIds.add(id);
356
- this.#logicallyUnchanged = checkpoint.logicallyUnchanged;
357
- }
358
- async snapshot() {
359
- return loadSnapshot(this.store, this.#record.snapshotVersion);
360
- }
361
- async getBlock(id) {
362
- this.#assertActive();
363
- const deferred = this.#deferredBlocks.find((block) => block.id === id);
364
- if (deferred !== undefined)
365
- return new Uint8Array(deferred.bytes);
366
- if (this.#record.pendingBlockIds.includes(id))
367
- return this.store.getBlock(id);
368
- return (await this.snapshot()).getBlock(id);
369
- }
370
- #assertProspectiveArtifactJournal(blocks, segments) {
371
- const blockIds = new Set(this.pendingBlockIds);
372
- const segmentIds = new Set(this.pendingSegmentIds);
373
- for (const block of blocks) {
374
- if (block.id.length === 0)
375
- throw new TypeError("Block ID cannot be empty");
376
- if (blockIds.has(block.id))
377
- throw new Error(`Block already exists: ${block.id}`);
378
- blockIds.add(block.id);
379
- }
380
- for (const segment of segments) {
381
- if (segment.id.length === 0)
382
- throw new TypeError("Segment ID cannot be empty");
383
- if (segmentIds.has(segment.id))
384
- throw new Error(`Segment already exists: ${segment.id}`);
385
- segmentIds.add(segment.id);
386
- }
387
- assertTransactionArtifactJournalLimits([...blockIds], [...segmentIds]);
388
- }
389
- async #stageArtifactBatch(blocks, segments) {
390
- if (blocks.length === 0 && segments.length === 0)
391
- return;
392
- assertTransactionArtifactBatchLimits(blocks, segments);
393
- await this.#ensurePersisted();
394
- await this.#renewOwnership();
395
- try {
396
- this.#record = await this.store.stageTransactionArtifacts({
397
- transactionId: this.id,
398
- expectedRevision: this.#record.revision,
399
- blocks,
400
- segments,
401
- updatedAt: dateIsoString(this.now()),
402
- });
403
- }
404
- catch (error) {
405
- await this.#recoverStagedAcknowledgement(error, blocks, segments);
406
- }
407
- }
408
- async stageBlock(id, bytes) {
409
- return this.stageBlocks([{ id, bytes }]);
410
- }
411
- async stageBlocks(blocks) {
412
- this.#assertActive();
413
- if (blocks.length === 0)
414
- return;
415
- await this.stageArtifacts(blocks, []);
416
- }
417
- /**
418
- * Stages blocks and segments in one journal step. When the store implements the atomic
419
- * combined operation this is a single storage transaction instead of the four the sequential
420
- * shape costs (block writes, journal update, segment writes, journal update); either way the
421
- * observable journal invariant is identical — a journaled artifact always exists.
422
- */
423
- async stageArtifacts(blocks, segments) {
424
- this.#assertActive();
425
- for (const segment of segments) {
426
- if (segment.transactionId !== this.id) {
427
- throw new Error(`Segment ${segment.id} belongs to another transaction`);
428
- }
429
- }
430
- // The journal is an append-only ordered sequence. Stamp the same position on each segment:
431
- // reads and recovery use the redundant pair to reject reordering or ambiguous same-commit
432
- // folds before publication.
433
- const ordinalBase = this.pendingSegmentIds.length;
434
- const ordered = segments.map((segment, index) => ({
435
- ...segment,
436
- commitOrdinal: ordinalBase + index,
437
- }));
438
- this.#assertProspectiveArtifactJournal(blocks, ordered);
439
- this.#registerLevelZeroSegments(ordered);
440
- if (blocks.length === 0 && ordered.length === 0)
441
- return;
442
- const batches = transactionArtifactBatches(blocks, ordered);
443
- if (!this.#persisted &&
444
- this.#deferredBlocks.length === 0 &&
445
- this.#deferredSegments.length === 0 &&
446
- batches.length === 1) {
447
- assertTransactionArtifactBatchLimits(blocks, ordered);
448
- this.#deferredBlocks.push(...blocks.map((block) => ({ id: block.id, bytes: new Uint8Array(block.bytes) })));
449
- this.#deferredSegments.push(...structuredClone(ordered));
450
- for (const segment of ordered)
451
- this.#knownSegments.set(segment.id, structuredClone(segment));
452
- for (const segment of ordered)
453
- this.#changedTableIds.add(segment.tableId);
454
- return;
455
- }
456
- await this.#persistDeferredArtifacts();
457
- for (const batch of batches) {
458
- await this.#stageArtifactBatch(batch.blocks, batch.segments);
459
- }
460
- for (const segment of ordered)
461
- this.#knownSegments.set(segment.id, structuredClone(segment));
462
- for (const segment of ordered)
463
- this.#changedTableIds.add(segment.tableId);
464
- }
465
- /**
466
- * Stages blocks and segments and commits, in one step. On a store with the single-shot
467
- * `writeTransaction` that is one storage transaction — and for a deferred transaction the
468
- * only one, the record begun, journaled, and committed together. Elsewhere it is
469
- * `stageArtifacts` followed by `commit`. The outcome and its typed refusals match the
470
- * two-step shape exactly, and a retry after `WriteConflictError` + `rebase()` is safe:
471
- * artifacts a failed attempt left journaled are committed rather than staged twice, and a
472
- * single-shot refusal left nothing to repeat.
473
- */
474
- async stageArtifactsAndCommit(blocks, segments) {
475
- this.#assertActive();
476
- for (const segment of segments) {
477
- if (segment.transactionId !== this.id) {
478
- throw new Error(`Segment ${segment.id} belongs to another transaction`);
479
- }
480
- }
481
- const pendingBlockIds = new Set(this.#record.pendingBlockIds);
482
- const pendingSegmentIds = new Set(this.#record.pendingSegmentIds);
483
- await this.#assertRetainedArtifactsUnchanged(blocks.filter((block) => pendingBlockIds.has(block.id)), segments.filter((segment) => pendingSegmentIds.has(segment.id)));
484
- const unstagedBlocks = blocks.filter((block) => !pendingBlockIds.has(block.id));
485
- const unstagedSegments = segments.filter((segment) => !pendingSegmentIds.has(segment.id));
486
- if (blocks.length + segments.length > 0 &&
487
- unstagedBlocks.length === 0 &&
488
- unstagedSegments.length === 0) {
489
- return this.commit();
490
- }
491
- this.#assertProspectiveArtifactJournal(unstagedBlocks, unstagedSegments);
492
- const single = this.store.writeTransaction?.bind(this.store);
493
- const batches = transactionArtifactBatches(unstagedBlocks, unstagedSegments);
494
- if (single === undefined || batches.length !== 1) {
495
- await this.stageArtifacts(unstagedBlocks, unstagedSegments);
496
- return this.commit();
497
- }
498
- await this.#renewOwnership();
499
- const ordinalBase = this.#record.pendingSegmentIds.length;
500
- const ordered = unstagedSegments.map((segment, index) => ({
501
- ...segment,
502
- commitOrdinal: ordinalBase + index,
503
- }));
504
- assertTransactionArtifactBatchLimits(unstagedBlocks, ordered);
505
- this.#registerLevelZeroSegments(ordered);
506
- const changedTableIds = new Set(this.#changedTableIds);
507
- for (const segment of ordered)
508
- changedTableIds.add(segment.tableId);
509
- const { snapshotVersion: _pinned, ...fresh } = this.#record;
510
- void _pinned;
511
- const committedAt = dateIsoString(this.now());
512
- const committedRevision = advanceRevision(this.#record.revision, 2);
513
- const compactionJobId = this.#compactionJobId;
514
- const ftsChanges = this.#materializedFtsChanges();
515
- // Keep the exact staged metadata before issuing the atomic write. A store is allowed to
516
- // commit and then lose the acknowledgement; #recoverCommitted turns that into success, so
517
- // recording this only after the await would make the caller's local catalog advancement
518
- // omit segments that are already durable.
519
- for (const segment of ordered)
520
- this.#knownSegments.set(segment.id, structuredClone(segment));
521
- for (const segment of ordered)
522
- this.#changedTableIds.add(segment.tableId);
523
- try {
524
- const manifest = await single({
525
- transaction: this.#persisted
526
- ? { id: this.id, expectedRevision: this.#record.revision }
527
- : { record: fresh },
528
- blocks: unstagedBlocks,
529
- segments: ordered,
530
- expectedManifestVersion: this.#record.snapshotVersion,
531
- changedTableIds: this.#logicallyUnchanged ? [] : [...changedTableIds],
532
- ...(this.#compactionSourceBlockIds.size === 0
533
- ? {}
534
- : {
535
- compactionJobId: requiredCompactionJobId(compactionJobId),
536
- removedBlockIds: [...this.#compactionSourceBlockIds],
537
- }),
538
- ...(this.#uniqueKeyChanges.length === 0
539
- ? {}
540
- : { uniqueKeyChanges: this.#uniqueKeyChanges }),
541
- ...(ftsChanges.length === 0 ? {} : { ftsChanges }),
542
- ...(this.#levelZeroSegmentLimits.size === 0
543
- ? {}
544
- : {
545
- levelZeroSegmentLimits: [...this.#levelZeroSegmentLimits].map(([tableId, limit]) => ({
546
- tableId,
547
- limit,
548
- })),
549
- }),
550
- committedAt,
551
- });
552
- this.#record = {
553
- ...this.#record,
554
- pendingBlockIds: [...pendingBlockIds, ...unstagedBlocks.map((block) => block.id)],
555
- pendingSegmentIds: [...pendingSegmentIds, ...ordered.map((segment) => segment.id)],
556
- status: "committed",
557
- committedVersion: manifest.version,
558
- // One journal step and one commit, as the two-step shape would have left it.
559
- revision: committedRevision,
560
- updatedAt: committedAt,
561
- };
562
- this.#persisted = true;
563
- this.#stopHeartbeat();
564
- await this.#releaseSnapshotLease().catch(() => undefined);
565
- return manifest;
566
- }
567
- catch (error) {
568
- return this.#recoverCommitted(error);
569
- }
570
- }
571
- /**
572
- * Adds already-persisted immutable blocks to this transaction's journal.
573
- *
574
- * Resumable background jobs use this to reconcile the narrow crash window
575
- * between an immutable block write and the transaction-record update. The
576
- * bytes must already exist, and already-journaled IDs are ignored.
577
- */
578
- async stageExistingBlocks(blockIds) {
579
- this.#assertActive();
580
- await this.#persistDeferredArtifacts();
581
- const pending = new Set(this.#record.pendingBlockIds);
582
- const additions = [...new Set(blockIds)].filter((id) => !pending.has(id));
583
- if (additions.length === 0)
584
- return;
585
- if (additions.some((id) => id.length === 0)) {
586
- throw new TypeError("Block ID cannot be empty");
587
- }
588
- assertTransactionArtifactJournalLimits([...this.#record.pendingBlockIds, ...additions], this.#record.pendingSegmentIds);
589
- for (const id of additions) {
590
- if ((await this.store.getBlock(id)) === undefined) {
591
- throw new Error(`Cannot stage a missing existing block: ${id}`);
104
+ class DatabaseTransaction {
105
+ store;
106
+ now;
107
+ ttlMs;
108
+ #record;
109
+ #uniqueKeyChanges = [];
110
+ #ftsChanges = /* @__PURE__ */ new Map();
111
+ #commitDeltaBytes = 0;
112
+ #commitDeltaEntries = 0;
113
+ #compactionJobId = null;
114
+ #compactionSourceBlockIds = /* @__PURE__ */ new Set();
115
+ #changedTableIds = /* @__PURE__ */ new Set();
116
+ #levelZeroSegmentLimits = /* @__PURE__ */ new Map();
117
+ #logicallyUnchanged = false;
118
+ #deferredBlocks = [];
119
+ #deferredSegments = [];
120
+ #knownSegments = /* @__PURE__ */ new Map();
121
+ #persisted;
122
+ #snapshotLease;
123
+ #snapshotLeaseOperation = Promise.resolve();
124
+ #snapshotRenewal;
125
+ #heartbeatTimer;
126
+ #renewal;
127
+ #initialCatalogProbe;
128
+ constructor(store, record, now, ttlMs, createWeakRef, options = {}) {
129
+ this.store = store;
130
+ this.now = now;
131
+ this.ttlMs = ttlMs;
132
+ this.#record = structuredClone(record);
133
+ this.#persisted = options.persisted ?? true;
134
+ this.#initialCatalogProbe = options.initialCatalogProbe;
135
+ this.#snapshotLease = options.snapshotLease;
136
+ const reference = createWeakRef(this);
137
+ const timer = setInterval(() => {
138
+ const transaction = reference.deref();
139
+ if (transaction === void 0) {
140
+ clearInterval(timer);
141
+ return;
142
+ }
143
+ void transaction.#renewOwnership(true).catch(() => void 0);
144
+ }, Math.max(1, Math.floor(ttlMs / 3)));
145
+ timer.unref?.();
146
+ this.#heartbeatTimer = timer;
147
+ }
148
+ get id() {
149
+ return this.#record.id;
150
+ }
151
+ get status() {
152
+ return this.#record.status;
153
+ }
154
+ get snapshotVersion() {
155
+ return this.#record.snapshotVersion;
156
+ }
157
+ get pendingBlockIds() {
158
+ return [...this.#record.pendingBlockIds, ...this.#deferredBlocks.map((block) => block.id)];
159
+ }
160
+ get pendingSegmentIds() {
161
+ return [
162
+ ...this.#record.pendingSegmentIds,
163
+ ...this.#deferredSegments.map((segment) => segment.id)
164
+ ];
165
+ }
166
+ get deferredSegments() {
167
+ return structuredClone(this.#deferredSegments);
168
+ }
169
+ get pendingTableNextRowId() {
170
+ return this.#record.pendingTableNextRowId;
171
+ }
172
+ get initialCatalogProbe() {
173
+ return this.#initialCatalogProbe === void 0 ? void 0 : { ...this.#initialCatalogProbe };
174
+ }
175
+ get committedCatalogContribution() {
176
+ if (this.#record.status !== "committed")
177
+ return void 0;
178
+ return {
179
+ transaction: structuredClone(this.#record),
180
+ segments: structuredClone([...this.#knownSegments.values()]),
181
+ ...this.#initialCatalogProbe === void 0 ? {} : { initialCatalogProbe: { ...this.#initialCatalogProbe } }
182
+ };
183
+ }
184
+ get compactionSourceBlockIds() {
185
+ return [...this.#compactionSourceBlockIds].sort();
186
+ }
187
+ async renew() {
188
+ this.#assertActive();
189
+ await this.#renewOwnership(true);
190
+ }
191
+ get stagedWorkCount() {
192
+ return this.pendingBlockIds.length + this.pendingSegmentIds.length + this.#uniqueKeyChanges.length + this.#ftsChanges.size + this.#compactionSourceBlockIds.size;
193
+ }
194
+ checkpoint() {
195
+ this.#assertActive();
196
+ return {
197
+ transactionId: this.id,
198
+ pendingBlockIds: this.pendingBlockIds,
199
+ pendingSegmentIds: this.pendingSegmentIds,
200
+ uniqueKeyChanges: structuredClone(this.#uniqueKeyChanges),
201
+ ftsChanges: structuredClone(this.#materializedFtsChanges()),
202
+ compactionJobId: this.#compactionJobId,
203
+ compactionSourceBlockIds: [...this.#compactionSourceBlockIds],
204
+ levelZeroSegmentLimits: [...this.#levelZeroSegmentLimits].map(([tableId, limit]) => ({
205
+ tableId,
206
+ limit
207
+ })),
208
+ changedTableIds: [...this.#changedTableIds],
209
+ logicallyUnchanged: this.#logicallyUnchanged
210
+ };
211
+ }
212
+ checkpointRetainedBytes() {
213
+ this.#assertActive();
214
+ let bytes = 256;
215
+ const addString = (value) => {
216
+ bytes = checkpointByteSum(bytes, 16 + value.length * 2);
217
+ };
218
+ addString(this.id);
219
+ for (const id of this.pendingBlockIds)
220
+ addString(id);
221
+ for (const id of this.pendingSegmentIds)
222
+ addString(id);
223
+ for (const change of this.#uniqueKeyChanges) {
224
+ addString(change.tableId);
225
+ for (const token of change.keyTokens)
226
+ addString(token);
227
+ bytes = checkpointByteSum(bytes, 32);
228
+ }
229
+ for (const change of this.#materializedFtsChanges()) {
230
+ addString(change.tableId);
231
+ for (const column of change.columns) {
232
+ addString(column.columnId);
233
+ bytes = checkpointByteSum(bytes, 48);
234
+ for (const posting of column.postings) {
235
+ addString(posting.term);
236
+ bytes = checkpointByteSum(bytes, posting.rowIds.length * 8 + posting.tf.length * 8 + 32);
237
+ }
238
+ }
239
+ }
240
+ if (this.#compactionJobId !== null)
241
+ addString(this.#compactionJobId);
242
+ for (const id of this.#compactionSourceBlockIds)
243
+ addString(id);
244
+ for (const [tableId] of this.#levelZeroSegmentLimits)
245
+ addString(tableId);
246
+ for (const id of this.#changedTableIds)
247
+ addString(id);
248
+ return bytes;
249
+ }
250
+ async rollbackTo(checkpoint) {
251
+ this.#assertActive();
252
+ if (checkpoint.transactionId !== this.id) {
253
+ throw new TypeError("A transaction checkpoint belongs to another transaction");
254
+ }
255
+ const blockPrefix = checkpoint.pendingBlockIds;
256
+ const segmentPrefix = checkpoint.pendingSegmentIds;
257
+ const retainedBlocks = new Set(blockPrefix);
258
+ const retainedSegments = new Set(segmentPrefix);
259
+ const currentBlockIds = this.pendingBlockIds;
260
+ const currentSegmentIds = this.pendingSegmentIds;
261
+ if (blockPrefix.some((id) => !currentBlockIds.includes(id)) || segmentPrefix.some((id) => !currentSegmentIds.includes(id))) {
262
+ throw new TypeError("A transaction checkpoint is no longer reachable");
263
+ }
264
+ const removedBlockIds = currentBlockIds.filter((id) => !retainedBlocks.has(id));
265
+ const removedSegmentIds = currentSegmentIds.filter((id) => !retainedSegments.has(id));
266
+ if (this.#persisted && (removedBlockIds.length > 0 || removedSegmentIds.length > 0)) {
267
+ await this.#renewOwnership();
268
+ this.#record = await this.store.rollbackTransactionArtifacts({
269
+ transactionId: this.id,
270
+ expectedRevision: this.#record.revision,
271
+ pendingBlockIds: blockPrefix,
272
+ pendingSegmentIds: segmentPrefix,
273
+ removeBlockIds: removedBlockIds,
274
+ removeSegmentIds: removedSegmentIds,
275
+ updatedAt: dateIsoString(this.now())
276
+ });
277
+ } else if (!this.#persisted) {
278
+ for (let index = this.#deferredBlocks.length - 1; index >= 0; index -= 1) {
279
+ if (!retainedBlocks.has(this.#deferredBlocks[index]?.id ?? "")) {
280
+ this.#deferredBlocks.splice(index, 1);
281
+ }
282
+ }
283
+ for (let index = this.#deferredSegments.length - 1; index >= 0; index -= 1) {
284
+ if (!retainedSegments.has(this.#deferredSegments[index]?.id ?? "")) {
285
+ this.#deferredSegments.splice(index, 1);
286
+ }
287
+ }
288
+ }
289
+ for (const id of removedSegmentIds)
290
+ this.#knownSegments.delete(id);
291
+ this.#uniqueKeyChanges.splice(0);
292
+ this.#ftsChanges.clear();
293
+ this.#commitDeltaBytes = 0;
294
+ this.#commitDeltaEntries = 0;
295
+ for (const changes of checkpoint.uniqueKeyChanges)
296
+ this.setUniqueKeyChanges(changes);
297
+ for (const changes of checkpoint.ftsChanges)
298
+ this.setFtsChanges(changes);
299
+ this.#compactionJobId = checkpoint.compactionJobId;
300
+ this.#compactionSourceBlockIds.clear();
301
+ for (const id of checkpoint.compactionSourceBlockIds)
302
+ this.#compactionSourceBlockIds.add(id);
303
+ this.#levelZeroSegmentLimits.clear();
304
+ for (const { tableId, limit } of checkpoint.levelZeroSegmentLimits) {
305
+ this.#levelZeroSegmentLimits.set(tableId, limit);
306
+ }
307
+ this.#changedTableIds.clear();
308
+ for (const id of checkpoint.changedTableIds)
309
+ this.#changedTableIds.add(id);
310
+ this.#logicallyUnchanged = checkpoint.logicallyUnchanged;
311
+ }
312
+ async snapshot() {
313
+ return loadSnapshot(this.store, this.#record.snapshotVersion);
314
+ }
315
+ async getBlock(id) {
316
+ this.#assertActive();
317
+ const deferred = this.#deferredBlocks.find((block) => block.id === id);
318
+ if (deferred !== void 0)
319
+ return new Uint8Array(deferred.bytes);
320
+ if (this.#record.pendingBlockIds.includes(id))
321
+ return this.store.getBlock(id);
322
+ return (await this.snapshot()).getBlock(id);
323
+ }
324
+ #assertProspectiveArtifactJournal(blocks, segments) {
325
+ const blockIds = new Set(this.pendingBlockIds);
326
+ const segmentIds = new Set(this.pendingSegmentIds);
327
+ for (const block of blocks) {
328
+ if (block.id.length === 0)
329
+ throw new TypeError("Block ID cannot be empty");
330
+ if (blockIds.has(block.id))
331
+ throw new Error(`Block already exists: ${block.id}`);
332
+ blockIds.add(block.id);
333
+ }
334
+ for (const segment of segments) {
335
+ if (segment.id.length === 0)
336
+ throw new TypeError("Segment ID cannot be empty");
337
+ if (segmentIds.has(segment.id))
338
+ throw new Error(`Segment already exists: ${segment.id}`);
339
+ segmentIds.add(segment.id);
340
+ }
341
+ assertTransactionArtifactJournalLimits([...blockIds], [...segmentIds]);
342
+ }
343
+ async #stageArtifactBatch(blocks, segments) {
344
+ if (blocks.length === 0 && segments.length === 0)
345
+ return;
346
+ assertTransactionArtifactBatchLimits(blocks, segments);
347
+ await this.#ensurePersisted();
348
+ await this.#renewOwnership();
349
+ try {
350
+ this.#record = await this.store.stageTransactionArtifacts({
351
+ transactionId: this.id,
352
+ expectedRevision: this.#record.revision,
353
+ blocks,
354
+ segments,
355
+ updatedAt: dateIsoString(this.now())
356
+ });
357
+ } catch (error) {
358
+ await this.#recoverStagedAcknowledgement(error, blocks, segments);
359
+ }
360
+ }
361
+ async stageBlock(id, bytes) {
362
+ return this.stageBlocks([{ id, bytes }]);
363
+ }
364
+ async stageBlocks(blocks) {
365
+ this.#assertActive();
366
+ if (blocks.length === 0)
367
+ return;
368
+ await this.stageArtifacts(blocks, []);
369
+ }
370
+ async stageArtifacts(blocks, segments) {
371
+ this.#assertActive();
372
+ for (const segment of segments) {
373
+ if (segment.transactionId !== this.id) {
374
+ throw new Error(`Segment ${segment.id} belongs to another transaction`);
375
+ }
376
+ }
377
+ const ordinalBase = this.pendingSegmentIds.length;
378
+ const ordered = segments.map((segment, index) => ({
379
+ ...segment,
380
+ commitOrdinal: ordinalBase + index
381
+ }));
382
+ this.#assertProspectiveArtifactJournal(blocks, ordered);
383
+ this.#registerLevelZeroSegments(ordered);
384
+ if (blocks.length === 0 && ordered.length === 0)
385
+ return;
386
+ const batches = transactionArtifactBatches(blocks, ordered);
387
+ if (!this.#persisted && this.#deferredBlocks.length === 0 && this.#deferredSegments.length === 0 && batches.length === 1) {
388
+ assertTransactionArtifactBatchLimits(blocks, ordered);
389
+ this.#deferredBlocks.push(...blocks.map((block) => ({ id: block.id, bytes: new Uint8Array(block.bytes) })));
390
+ this.#deferredSegments.push(...structuredClone(ordered));
391
+ for (const segment of ordered)
392
+ this.#knownSegments.set(segment.id, structuredClone(segment));
393
+ for (const segment of ordered)
394
+ this.#changedTableIds.add(segment.tableId);
395
+ return;
396
+ }
397
+ await this.#persistDeferredArtifacts();
398
+ for (const batch of batches) {
399
+ await this.#stageArtifactBatch(batch.blocks, batch.segments);
400
+ }
401
+ for (const segment of ordered)
402
+ this.#knownSegments.set(segment.id, structuredClone(segment));
403
+ for (const segment of ordered)
404
+ this.#changedTableIds.add(segment.tableId);
405
+ }
406
+ async stageArtifactsAndCommit(blocks, segments) {
407
+ this.#assertActive();
408
+ for (const segment of segments) {
409
+ if (segment.transactionId !== this.id) {
410
+ throw new Error(`Segment ${segment.id} belongs to another transaction`);
411
+ }
412
+ }
413
+ const pendingBlockIds = new Set(this.#record.pendingBlockIds);
414
+ const pendingSegmentIds = new Set(this.#record.pendingSegmentIds);
415
+ await this.#assertRetainedArtifactsUnchanged(blocks.filter((block) => pendingBlockIds.has(block.id)), segments.filter((segment) => pendingSegmentIds.has(segment.id)));
416
+ const unstagedBlocks = blocks.filter((block) => !pendingBlockIds.has(block.id));
417
+ const unstagedSegments = segments.filter((segment) => !pendingSegmentIds.has(segment.id));
418
+ if (blocks.length + segments.length > 0 && unstagedBlocks.length === 0 && unstagedSegments.length === 0) {
419
+ return this.commit();
420
+ }
421
+ this.#assertProspectiveArtifactJournal(unstagedBlocks, unstagedSegments);
422
+ const single = this.store.writeTransaction?.bind(this.store);
423
+ const batches = transactionArtifactBatches(unstagedBlocks, unstagedSegments);
424
+ if (single === void 0 || batches.length !== 1) {
425
+ await this.stageArtifacts(unstagedBlocks, unstagedSegments);
426
+ return this.commit();
427
+ }
428
+ await this.#renewOwnership();
429
+ const ordinalBase = this.#record.pendingSegmentIds.length;
430
+ const ordered = unstagedSegments.map((segment, index) => ({
431
+ ...segment,
432
+ commitOrdinal: ordinalBase + index
433
+ }));
434
+ assertTransactionArtifactBatchLimits(unstagedBlocks, ordered);
435
+ this.#registerLevelZeroSegments(ordered);
436
+ const changedTableIds = new Set(this.#changedTableIds);
437
+ for (const segment of ordered)
438
+ changedTableIds.add(segment.tableId);
439
+ const { snapshotVersion: _pinned, ...fresh } = this.#record;
440
+ void _pinned;
441
+ const committedAt = dateIsoString(this.now());
442
+ const committedRevision = advanceRevision(this.#record.revision, 2);
443
+ const compactionJobId = this.#compactionJobId;
444
+ const ftsChanges = this.#materializedFtsChanges();
445
+ for (const segment of ordered)
446
+ this.#knownSegments.set(segment.id, structuredClone(segment));
447
+ for (const segment of ordered)
448
+ this.#changedTableIds.add(segment.tableId);
449
+ try {
450
+ const manifest = await single({
451
+ transaction: this.#persisted ? { id: this.id, expectedRevision: this.#record.revision } : { record: fresh },
452
+ blocks: unstagedBlocks,
453
+ segments: ordered,
454
+ expectedManifestVersion: this.#record.snapshotVersion,
455
+ changedTableIds: this.#logicallyUnchanged ? [] : [...changedTableIds],
456
+ ...this.#compactionSourceBlockIds.size === 0 ? {} : {
457
+ compactionJobId: requiredCompactionJobId(compactionJobId),
458
+ removedBlockIds: [...this.#compactionSourceBlockIds]
459
+ },
460
+ ...this.#uniqueKeyChanges.length === 0 ? {} : { uniqueKeyChanges: this.#uniqueKeyChanges },
461
+ ...ftsChanges.length === 0 ? {} : { ftsChanges },
462
+ ...this.#levelZeroSegmentLimits.size === 0 ? {} : {
463
+ levelZeroSegmentLimits: [...this.#levelZeroSegmentLimits].map(([tableId, limit]) => ({
464
+ tableId,
465
+ limit
466
+ }))
467
+ },
468
+ committedAt
469
+ });
470
+ this.#record = {
471
+ ...this.#record,
472
+ pendingBlockIds: [...pendingBlockIds, ...unstagedBlocks.map((block) => block.id)],
473
+ pendingSegmentIds: [...pendingSegmentIds, ...ordered.map((segment) => segment.id)],
474
+ status: "committed",
475
+ committedVersion: manifest.version,
476
+ revision: committedRevision,
477
+ updatedAt: committedAt
478
+ };
479
+ this.#persisted = true;
480
+ this.#stopHeartbeat();
481
+ await this.#releaseSnapshotLease().catch(() => void 0);
482
+ return manifest;
483
+ } catch (error) {
484
+ return this.#recoverCommitted(error);
485
+ }
486
+ }
487
+ async stageExistingBlocks(blockIds) {
488
+ this.#assertActive();
489
+ await this.#persistDeferredArtifacts();
490
+ const pending = new Set(this.#record.pendingBlockIds);
491
+ const additions = [...new Set(blockIds)].filter((id) => !pending.has(id));
492
+ if (additions.length === 0)
493
+ return;
494
+ if (additions.some((id) => id.length === 0)) {
495
+ throw new TypeError("Block ID cannot be empty");
496
+ }
497
+ assertTransactionArtifactJournalLimits([...this.#record.pendingBlockIds, ...additions], this.#record.pendingSegmentIds);
498
+ for (const id of additions) {
499
+ if (await this.store.getBlock(id) === void 0) {
500
+ throw new Error(`Cannot stage a missing existing block: ${id}`);
501
+ }
502
+ }
503
+ await this.#ensurePersisted();
504
+ await this.#renewOwnership();
505
+ this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
506
+ pendingBlockIds: [...this.#record.pendingBlockIds, ...additions],
507
+ updatedAt: dateIsoString(this.now())
508
+ });
509
+ }
510
+ async stageSegment(record) {
511
+ this.#assertActive();
512
+ if (record.transactionId !== this.id) {
513
+ throw new Error(`Segment ${record.id} belongs to another transaction`);
514
+ }
515
+ await this.stageArtifacts([], [record]);
516
+ }
517
+ async stageExistingSegment(segmentId) {
518
+ this.#assertActive();
519
+ await this.#persistDeferredArtifacts();
520
+ if (this.#record.pendingSegmentIds.includes(segmentId))
521
+ return;
522
+ const record = await this.store.getSegment(segmentId);
523
+ if (record === void 0)
524
+ throw new Error(`Cannot stage a missing existing segment: ${segmentId}`);
525
+ if (record.transactionId !== this.id) {
526
+ throw new Error(`Segment ${segmentId} belongs to another transaction`);
527
+ }
528
+ if (record.commitOrdinal !== this.#record.pendingSegmentIds.length) {
529
+ throw new Error(`Existing segment ${segmentId} is not the next journal ordinal`);
530
+ }
531
+ assertTransactionArtifactJournalLimits(this.#record.pendingBlockIds, [
532
+ ...this.#record.pendingSegmentIds,
533
+ segmentId
534
+ ]);
535
+ this.#registerLevelZeroSegments([record]);
536
+ this.#changedTableIds.add(record.tableId);
537
+ await this.#ensurePersisted();
538
+ await this.#renewOwnership();
539
+ this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
540
+ pendingSegmentIds: [...this.#record.pendingSegmentIds, segmentId],
541
+ updatedAt: dateIsoString(this.now())
542
+ });
543
+ }
544
+ async adoptAbortedCompactionSegment(record, abortedOwner) {
545
+ this.#assertActive();
546
+ await this.#persistDeferredArtifacts();
547
+ if (record.transactionId !== this.id) {
548
+ throw new Error(`Segment ${record.id} belongs to another transaction`);
549
+ }
550
+ if (abortedOwner.status !== "aborted") {
551
+ throw new Error(`Segment ${record.id} owner is not aborted`);
552
+ }
553
+ const compactionJobId = requiredCompactionJobId(this.#compactionJobId);
554
+ this.#assertProspectiveArtifactJournal([], [record]);
555
+ this.#registerLevelZeroSegments([record]);
556
+ this.#changedTableIds.add(record.tableId);
557
+ await this.#ensurePersisted();
558
+ await this.#renewOwnership();
559
+ try {
560
+ this.#record = await this.store.adoptAbortedSegment({
561
+ segment: record,
562
+ expectedAbortedTransactionId: abortedOwner.id,
563
+ expectedAbortedTransactionRevision: abortedOwner.revision,
564
+ replacementTransactionId: this.id,
565
+ expectedReplacementTransactionRevision: this.#record.revision,
566
+ compactionJobId,
567
+ updatedAt: dateIsoString(this.now())
568
+ });
569
+ } catch (error) {
570
+ const [current, segment] = await Promise.all([
571
+ this.store.getTransaction(this.id),
572
+ this.store.getSegment(record.id)
573
+ ]);
574
+ if (current?.status !== "active" || !current.pendingSegmentIds.includes(record.id) || segment?.transactionId !== this.id || !sameSegmentArtifacts(segment, record)) {
575
+ throw error;
576
+ }
577
+ this.#record = current;
578
+ }
579
+ }
580
+ markTableChanged(tableId) {
581
+ this.#assertActive();
582
+ this.#changedTableIds.add(tableId);
583
+ }
584
+ markLogicallyUnchanged() {
585
+ this.#assertActive();
586
+ this.#logicallyUnchanged = true;
587
+ }
588
+ get accumulatedUniqueKeyChanges() {
589
+ return this.#uniqueKeyChanges;
590
+ }
591
+ setUniqueKeyChanges(changes) {
592
+ this.#assertActive();
593
+ if (changes.keyTokens.length > MAX_TRANSACTION_COMMIT_DELTA_ENTRIES) {
594
+ throw new RangeError(`Transaction commit deltas exceed ${String(MAX_TRANSACTION_COMMIT_DELTA_ENTRIES)} entries`);
595
+ }
596
+ transactionCommitDeltaRetainedBytes([changes], []);
597
+ const normalized = {
598
+ tableId: changes.tableId,
599
+ keyTokens: [...new Set(changes.keyTokens)],
600
+ requireAbsent: changes.requireAbsent,
601
+ ...changes.remove === void 0 ? {} : { remove: changes.remove }
602
+ };
603
+ this.#reserveCommitDelta([normalized], []);
604
+ this.#uniqueKeyChanges.push(normalized);
605
+ }
606
+ setFtsChanges(changes) {
607
+ this.#assertActive();
608
+ const existingColumns = this.#ftsChanges.get(changes.tableId);
609
+ const tokenTotals = /* @__PURE__ */ new Map();
610
+ for (const column of changes.columns) {
611
+ const prior = tokenTotals.get(column.columnId) ?? existingColumns?.get(column.columnId)?.totalTokens ?? 0;
612
+ tokenTotals.set(column.columnId, safeWholeNumberSum([prior, column.totalTokens], "Full-text transaction token count"));
613
+ }
614
+ this.#reserveCommitDelta([], [changes]);
615
+ let columns = this.#ftsChanges.get(changes.tableId);
616
+ if (columns === void 0) {
617
+ columns = /* @__PURE__ */ new Map();
618
+ this.#ftsChanges.set(changes.tableId, columns);
619
+ }
620
+ for (const column of changes.columns) {
621
+ const present = columns.get(column.columnId);
622
+ if (present === void 0) {
623
+ columns.set(column.columnId, {
624
+ postings: new Map(column.postings.map((posting) => [
625
+ posting.term,
626
+ {
627
+ term: posting.term,
628
+ rowIds: [...posting.rowIds],
629
+ tf: [...posting.tf]
592
630
  }
593
- }
594
- await this.#ensurePersisted();
595
- await this.#renewOwnership();
596
- this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
597
- pendingBlockIds: [...this.#record.pendingBlockIds, ...additions],
598
- updatedAt: dateIsoString(this.now()),
599
- });
600
- }
601
- async stageSegment(record) {
602
- this.#assertActive();
603
- if (record.transactionId !== this.id) {
604
- throw new Error(`Segment ${record.id} belongs to another transaction`);
605
- }
606
- await this.stageArtifacts([], [record]);
607
- }
608
- /** Reconciles a persisted segment whose journal update was interrupted. */
609
- async stageExistingSegment(segmentId) {
610
- this.#assertActive();
611
- await this.#persistDeferredArtifacts();
612
- if (this.#record.pendingSegmentIds.includes(segmentId))
613
- return;
614
- const record = await this.store.getSegment(segmentId);
615
- if (record === undefined)
616
- throw new Error(`Cannot stage a missing existing segment: ${segmentId}`);
617
- if (record.transactionId !== this.id) {
618
- throw new Error(`Segment ${segmentId} belongs to another transaction`);
619
- }
620
- if (record.commitOrdinal !== this.#record.pendingSegmentIds.length) {
621
- throw new Error(`Existing segment ${segmentId} is not the next journal ordinal`);
622
- }
623
- assertTransactionArtifactJournalLimits(this.#record.pendingBlockIds, [
624
- ...this.#record.pendingSegmentIds,
625
- segmentId,
626
- ]);
627
- this.#registerLevelZeroSegments([record]);
628
- this.#changedTableIds.add(record.tableId);
629
- await this.#ensurePersisted();
630
- await this.#renewOwnership();
631
- this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
632
- pendingSegmentIds: [...this.#record.pendingSegmentIds, segmentId],
633
- updatedAt: dateIsoString(this.now()),
631
+ ])),
632
+ totalTokens: column.totalTokens
634
633
  });
635
- }
636
- /** Atomically adopts an exact unpublished output left by an aborted attempt of this job. */
637
- async adoptAbortedCompactionSegment(record, abortedOwner) {
638
- this.#assertActive();
634
+ continue;
635
+ }
636
+ let duplicateTokens = 0;
637
+ for (const posting of column.postings) {
638
+ const held = present.postings.get(posting.term);
639
+ if (held === void 0) {
640
+ present.postings.set(posting.term, {
641
+ term: posting.term,
642
+ rowIds: [...posting.rowIds],
643
+ tf: [...posting.tf]
644
+ });
645
+ } else {
646
+ const merged = mergeFtsPostingRows(held, posting);
647
+ duplicateTokens = safeWholeNumberSum([
648
+ duplicateTokens,
649
+ postingTokenCount(held) + postingTokenCount(posting) - postingTokenCount(merged)
650
+ ], "Duplicate posting token count");
651
+ present.postings.set(posting.term, merged);
652
+ }
653
+ }
654
+ const totalTokens = tokenTotals.get(column.columnId) ?? present.totalTokens;
655
+ if (duplicateTokens > totalTokens)
656
+ throw new Error("Posting token merge is inconsistent");
657
+ present.totalTokens = totalTokens - duplicateTokens;
658
+ }
659
+ }
660
+ #materializedFtsChanges() {
661
+ return [...this.#ftsChanges].map(([tableId, columns]) => ({
662
+ tableId,
663
+ columns: [...columns].map(([columnId, change]) => ({
664
+ columnId,
665
+ postings: [...change.postings.values()].sort((left, right) => left.term < right.term ? -1 : left.term > right.term ? 1 : 0),
666
+ totalTokens: change.totalTokens
667
+ }))
668
+ }));
669
+ }
670
+ #reserveCommitDelta(uniqueKeyChanges, ftsChanges) {
671
+ const added = transactionCommitDeltaRetainedBytes(uniqueKeyChanges, ftsChanges);
672
+ const bytes = this.#commitDeltaBytes + added.bytes;
673
+ const entries = this.#commitDeltaEntries + added.entries;
674
+ if (!Number.isSafeInteger(bytes) || bytes > MAX_TRANSACTION_COMMIT_DELTA_BYTES) {
675
+ throw new RangeError(`Transaction commit deltas exceed ${String(MAX_TRANSACTION_COMMIT_DELTA_BYTES)} retained bytes`);
676
+ }
677
+ if (!Number.isSafeInteger(entries) || entries > MAX_TRANSACTION_COMMIT_DELTA_ENTRIES) {
678
+ throw new RangeError(`Transaction commit deltas exceed ${String(MAX_TRANSACTION_COMMIT_DELTA_ENTRIES)} entries`);
679
+ }
680
+ this.#commitDeltaBytes = bytes;
681
+ this.#commitDeltaEntries = entries;
682
+ }
683
+ setCompactionIntent(compactionJobId, sourceBlockIds) {
684
+ this.#assertActive();
685
+ if (compactionJobId.length === 0)
686
+ throw new TypeError("Compaction job ID cannot be empty");
687
+ if (sourceBlockIds.length === 0) {
688
+ throw new TypeError("A compaction retirement must name at least one source block");
689
+ }
690
+ const next = new Set(sourceBlockIds);
691
+ if (next.size !== sourceBlockIds.length) {
692
+ throw new TypeError("Compaction source block IDs must be unique");
693
+ }
694
+ if (this.#compactionJobId !== null) {
695
+ if (this.#compactionJobId !== compactionJobId || next.size !== this.#compactionSourceBlockIds.size || [...next].some((id) => !this.#compactionSourceBlockIds.has(id))) {
696
+ throw new Error("A transaction cannot change its compaction retirement intent");
697
+ }
698
+ return;
699
+ }
700
+ for (const id of next) {
701
+ if (id.length === 0)
702
+ throw new TypeError("Compaction source block ID cannot be empty");
703
+ if (this.#record.pendingBlockIds.includes(id)) {
704
+ throw new Error(`A compaction cannot retire its own pending block: ${id}`);
705
+ }
706
+ }
707
+ this.#compactionJobId = compactionJobId;
708
+ for (const id of next)
709
+ this.#compactionSourceBlockIds.add(id);
710
+ }
711
+ limitLevelZeroSegments(tableId, limit) {
712
+ this.#assertActive();
713
+ if (tableId.length === 0)
714
+ throw new TypeError("Table ID cannot be empty");
715
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
716
+ throw new RangeError("Level-zero segment limit must be a positive safe integer");
717
+ }
718
+ const existing = this.#levelZeroSegmentLimits.get(tableId);
719
+ this.#levelZeroSegmentLimits.set(tableId, existing === void 0 ? limit : Math.min(existing, limit));
720
+ }
721
+ #registerLevelZeroSegments(segments) {
722
+ for (const segment of segments) {
723
+ if (segment.level !== 0)
724
+ continue;
725
+ const existing = this.#levelZeroSegmentLimits.get(segment.tableId);
726
+ if (existing === void 0) {
727
+ this.#levelZeroSegmentLimits.set(segment.tableId, MAX_LEVEL_ZERO_SEGMENTS);
728
+ }
729
+ }
730
+ }
731
+ async commit() {
732
+ this.#assertActive();
733
+ if (this.#deferredBlocks.length > 0 || this.#deferredSegments.length > 0) {
734
+ if (this.store.writeTransaction === void 0) {
639
735
  await this.#persistDeferredArtifacts();
640
- if (record.transactionId !== this.id) {
641
- throw new Error(`Segment ${record.id} belongs to another transaction`);
642
- }
643
- if (abortedOwner.status !== "aborted") {
644
- throw new Error(`Segment ${record.id} owner is not aborted`);
645
- }
646
- const compactionJobId = requiredCompactionJobId(this.#compactionJobId);
647
- this.#assertProspectiveArtifactJournal([], [record]);
648
- this.#registerLevelZeroSegments([record]);
649
- this.#changedTableIds.add(record.tableId);
650
- await this.#ensurePersisted();
651
- await this.#renewOwnership();
736
+ } else {
737
+ const blocks = this.#deferredBlocks.splice(0);
738
+ const segments = this.#deferredSegments.splice(0);
652
739
  try {
653
- this.#record = await this.store.adoptAbortedSegment({
654
- segment: record,
655
- expectedAbortedTransactionId: abortedOwner.id,
656
- expectedAbortedTransactionRevision: abortedOwner.revision,
657
- replacementTransactionId: this.id,
658
- expectedReplacementTransactionRevision: this.#record.revision,
659
- compactionJobId,
660
- updatedAt: dateIsoString(this.now()),
661
- });
662
- }
663
- catch (error) {
664
- // A lost acknowledgement is safe to reconcile: the exact segment and journal move are one
665
- // adapter transaction. Any partial or foreign state fails the strict equality check.
666
- const [current, segment] = await Promise.all([
667
- this.store.getTransaction(this.id),
668
- this.store.getSegment(record.id),
669
- ]);
670
- if (current?.status !== "active" ||
671
- !current.pendingSegmentIds.includes(record.id) ||
672
- segment?.transactionId !== this.id ||
673
- !sameSegmentArtifacts(segment, record)) {
674
- throw error;
675
- }
676
- this.#record = current;
677
- }
678
- }
679
- /**
680
- * Records that this commit changes a table's logical content without staging a segment for
681
- * it — dropping the table, for instance, whose only publish is the retirement of its blocks.
682
- * Change-driven readers such as live queries need to see it move.
683
- */
684
- markTableChanged(tableId) {
685
- this.#assertActive();
686
- this.#changedTableIds.add(tableId);
687
- }
688
- /**
689
- * Marks this commit as logically content-preserving (for example a compaction rewrite), so
690
- * change-driven readers such as live queries skip it even though it stages segments.
691
- */
692
- markLogicallyUnchanged() {
693
- this.#assertActive();
694
- this.#logicallyUnchanged = true;
695
- }
696
- /** Accumulated key changes in operation order, for in-scope membership overlays. */
697
- get accumulatedUniqueKeyChanges() {
698
- return this.#uniqueKeyChanges;
699
- }
700
- /** Appends one operation's key changes; entries commit in operation order. */
701
- setUniqueKeyChanges(changes) {
702
- this.#assertActive();
703
- if (changes.keyTokens.length > MAX_TRANSACTION_COMMIT_DELTA_ENTRIES) {
704
- throw new RangeError(`Transaction commit deltas exceed ${String(MAX_TRANSACTION_COMMIT_DELTA_ENTRIES)} entries`);
705
- }
706
- // Walk the caller-owned array before allocating a Set or clone. The aggregate tracker below
707
- // charges the deduplicated retained form, while this preflight prevents one hostile call from
708
- // creating a database-sized temporary allocation first.
709
- transactionCommitDeltaRetainedBytes([changes], []);
710
- const normalized = {
711
- tableId: changes.tableId,
712
- // Deduplicated, not sorted: membership is all a store reads from these, and sorting fifty
713
- // thousand tokens cost a bulk delete a fifth of its time.
714
- keyTokens: [...new Set(changes.keyTokens)],
715
- requireAbsent: changes.requireAbsent,
716
- ...(changes.remove === undefined ? {} : { remove: changes.remove }),
717
- };
718
- this.#reserveCommitDelta([normalized], []);
719
- this.#uniqueKeyChanges.push(normalized);
720
- }
721
- /**
722
- * Attaches one batch's full-text deltas; applied atomically with the publish. A second
723
- * batch for the same table merges per column — postings stay sorted by term and row ID,
724
- * duplicate row locators collapse to their greatest term frequency, and token totals are
725
- * summed — so a scope can mutate one indexed table any number of times. Secondary indexes
726
- * use stable hashed key locators rather than reserved row IDs, so operation order does not
727
- * imply locator order.
728
- */
729
- setFtsChanges(changes) {
730
- this.#assertActive();
731
- const existingColumns = this.#ftsChanges.get(changes.tableId);
732
- const tokenTotals = new Map();
733
- for (const column of changes.columns) {
734
- const prior = tokenTotals.get(column.columnId) ?? existingColumns?.get(column.columnId)?.totalTokens ?? 0;
735
- tokenTotals.set(column.columnId, safeWholeNumberSum([prior, column.totalTokens], "Full-text transaction token count"));
736
- }
737
- this.#reserveCommitDelta([], [changes]);
738
- let columns = this.#ftsChanges.get(changes.tableId);
739
- if (columns === undefined) {
740
- columns = new Map();
741
- this.#ftsChanges.set(changes.tableId, columns);
742
- }
743
- for (const column of changes.columns) {
744
- const present = columns.get(column.columnId);
745
- if (present === undefined) {
746
- columns.set(column.columnId, {
747
- postings: new Map(column.postings.map((posting) => [
748
- posting.term,
749
- {
750
- term: posting.term,
751
- rowIds: [...posting.rowIds],
752
- tf: [...posting.tf],
753
- },
754
- ])),
755
- totalTokens: column.totalTokens,
756
- });
757
- continue;
758
- }
759
- let duplicateTokens = 0;
760
- for (const posting of column.postings) {
761
- const held = present.postings.get(posting.term);
762
- if (held === undefined) {
763
- present.postings.set(posting.term, {
764
- term: posting.term,
765
- rowIds: [...posting.rowIds],
766
- tf: [...posting.tf],
767
- });
768
- }
769
- else {
770
- const merged = mergeFtsPostingRows(held, posting);
771
- duplicateTokens = safeWholeNumberSum([
772
- duplicateTokens,
773
- postingTokenCount(held) + postingTokenCount(posting) - postingTokenCount(merged),
774
- ], "Duplicate posting token count");
775
- present.postings.set(posting.term, merged);
776
- }
777
- }
778
- const totalTokens = tokenTotals.get(column.columnId) ?? present.totalTokens;
779
- if (duplicateTokens > totalTokens)
780
- throw new Error("Posting token merge is inconsistent");
781
- present.totalTokens = totalTokens - duplicateTokens;
782
- }
783
- }
784
- #materializedFtsChanges() {
785
- return [...this.#ftsChanges].map(([tableId, columns]) => ({
740
+ return await this.stageArtifactsAndCommit(blocks, segments);
741
+ } catch (error) {
742
+ this.#deferredBlocks.push(...blocks);
743
+ this.#deferredSegments.push(...segments);
744
+ throw error;
745
+ }
746
+ }
747
+ }
748
+ await this.#ensurePersisted();
749
+ await this.#renewOwnership();
750
+ const committedAt = dateIsoString(this.now());
751
+ const committedRevision = advanceRevision(this.#record.revision, 1);
752
+ const compactionJobId = this.#compactionJobId;
753
+ const ftsChanges = this.#materializedFtsChanges();
754
+ try {
755
+ const manifest = await this.store.commitTransaction({
756
+ transactionId: this.id,
757
+ expectedTransactionRevision: this.#record.revision,
758
+ expectedManifestVersion: this.#record.snapshotVersion,
759
+ changedTableIds: this.#logicallyUnchanged ? [] : [...this.#changedTableIds],
760
+ ...this.#compactionSourceBlockIds.size === 0 ? {} : {
761
+ compactionJobId: requiredCompactionJobId(compactionJobId),
762
+ removedBlockIds: [...this.#compactionSourceBlockIds]
763
+ },
764
+ ...this.#uniqueKeyChanges.length === 0 ? {} : { uniqueKeyChanges: this.#uniqueKeyChanges },
765
+ ...ftsChanges.length === 0 ? {} : { ftsChanges },
766
+ ...this.#levelZeroSegmentLimits.size === 0 ? {} : {
767
+ levelZeroSegmentLimits: [...this.#levelZeroSegmentLimits].map(([tableId, limit]) => ({
786
768
  tableId,
787
- columns: [...columns].map(([columnId, change]) => ({
788
- columnId,
789
- postings: [...change.postings.values()].sort((left, right) => left.term < right.term ? -1 : left.term > right.term ? 1 : 0),
790
- totalTokens: change.totalTokens,
791
- })),
792
- }));
793
- }
794
- #reserveCommitDelta(uniqueKeyChanges, ftsChanges) {
795
- const added = transactionCommitDeltaRetainedBytes(uniqueKeyChanges, ftsChanges);
796
- const bytes = this.#commitDeltaBytes + added.bytes;
797
- const entries = this.#commitDeltaEntries + added.entries;
798
- if (!Number.isSafeInteger(bytes) || bytes > MAX_TRANSACTION_COMMIT_DELTA_BYTES) {
799
- throw new RangeError(`Transaction commit deltas exceed ${String(MAX_TRANSACTION_COMMIT_DELTA_BYTES)} retained bytes`);
800
- }
801
- if (!Number.isSafeInteger(entries) || entries > MAX_TRANSACTION_COMMIT_DELTA_ENTRIES) {
802
- throw new RangeError(`Transaction commit deltas exceed ${String(MAX_TRANSACTION_COMMIT_DELTA_ENTRIES)} entries`);
803
- }
804
- this.#commitDeltaBytes = bytes;
805
- this.#commitDeltaEntries = entries;
806
- }
807
- /**
808
- * Binds a physical retirement to one persisted compaction job. Stores revalidate the job,
809
- * source segments, current manifest, and aliases atomically with publication; this in-memory
810
- * declaration is intent, never authority to hide arbitrary blocks.
811
- */
812
- setCompactionIntent(compactionJobId, sourceBlockIds) {
813
- this.#assertActive();
814
- if (compactionJobId.length === 0)
815
- throw new TypeError("Compaction job ID cannot be empty");
816
- if (sourceBlockIds.length === 0) {
817
- throw new TypeError("A compaction retirement must name at least one source block");
818
- }
819
- const next = new Set(sourceBlockIds);
820
- if (next.size !== sourceBlockIds.length) {
821
- throw new TypeError("Compaction source block IDs must be unique");
822
- }
823
- if (this.#compactionJobId !== null) {
824
- if (this.#compactionJobId !== compactionJobId ||
825
- next.size !== this.#compactionSourceBlockIds.size ||
826
- [...next].some((id) => !this.#compactionSourceBlockIds.has(id))) {
827
- throw new Error("A transaction cannot change its compaction retirement intent");
828
- }
829
- return;
830
- }
831
- for (const id of next) {
832
- if (id.length === 0)
833
- throw new TypeError("Compaction source block ID cannot be empty");
834
- if (this.#record.pendingBlockIds.includes(id)) {
835
- throw new Error(`A compaction cannot retire its own pending block: ${id}`);
836
- }
837
- }
838
- this.#compactionJobId = compactionJobId;
839
- for (const id of next)
840
- this.#compactionSourceBlockIds.add(id);
841
- }
842
- /** Adds an adapter-enforced post-commit level-zero ceiling for one table. */
843
- limitLevelZeroSegments(tableId, limit) {
844
- this.#assertActive();
845
- if (tableId.length === 0)
846
- throw new TypeError("Table ID cannot be empty");
847
- if (!Number.isSafeInteger(limit) || limit <= 0) {
848
- throw new RangeError("Level-zero segment limit must be a positive safe integer");
849
- }
850
- const existing = this.#levelZeroSegmentLimits.get(tableId);
851
- this.#levelZeroSegmentLimits.set(tableId, existing === undefined ? limit : Math.min(existing, limit));
852
- }
853
- #registerLevelZeroSegments(segments) {
854
- for (const segment of segments) {
855
- if (segment.level !== 0)
856
- continue;
857
- const existing = this.#levelZeroSegmentLimits.get(segment.tableId);
858
- if (existing === undefined) {
859
- this.#levelZeroSegmentLimits.set(segment.tableId, MAX_LEVEL_ZERO_SEGMENTS);
860
- }
861
- }
862
- }
863
- async commit() {
864
- this.#assertActive();
865
- if (this.#deferredBlocks.length > 0 || this.#deferredSegments.length > 0) {
866
- if (this.store.writeTransaction === undefined) {
867
- await this.#persistDeferredArtifacts();
868
- }
869
- else {
870
- const blocks = this.#deferredBlocks.splice(0);
871
- const segments = this.#deferredSegments.splice(0);
872
- try {
873
- return await this.stageArtifactsAndCommit(blocks, segments);
874
- }
875
- catch (error) {
876
- // A single-shot refusal mutates nothing. Keep the exact immutable artifacts available
877
- // for the caller's ordinary rebase-and-retry path.
878
- this.#deferredBlocks.push(...blocks);
879
- this.#deferredSegments.push(...segments);
880
- throw error;
881
- }
882
- }
883
- }
884
- await this.#ensurePersisted();
885
- await this.#renewOwnership();
886
- // The store derives the published manifest from its stored base plus this delta, so the
887
- // commit neither loads nor rebuilds the full block list.
888
- const committedAt = dateIsoString(this.now());
889
- const committedRevision = advanceRevision(this.#record.revision, 1);
890
- const compactionJobId = this.#compactionJobId;
891
- const ftsChanges = this.#materializedFtsChanges();
892
- try {
893
- const manifest = await this.store.commitTransaction({
894
- transactionId: this.id,
895
- expectedTransactionRevision: this.#record.revision,
896
- expectedManifestVersion: this.#record.snapshotVersion,
897
- changedTableIds: this.#logicallyUnchanged ? [] : [...this.#changedTableIds],
898
- ...(this.#compactionSourceBlockIds.size === 0
899
- ? {}
900
- : {
901
- compactionJobId: requiredCompactionJobId(compactionJobId),
902
- removedBlockIds: [...this.#compactionSourceBlockIds],
903
- }),
904
- ...(this.#uniqueKeyChanges.length === 0
905
- ? {}
906
- : { uniqueKeyChanges: this.#uniqueKeyChanges }),
907
- ...(ftsChanges.length === 0 ? {} : { ftsChanges }),
908
- ...(this.#levelZeroSegmentLimits.size === 0
909
- ? {}
910
- : {
911
- levelZeroSegmentLimits: [...this.#levelZeroSegmentLimits].map(([tableId, limit]) => ({
912
- tableId,
913
- limit,
914
- })),
915
- }),
916
- committedAt,
917
- });
918
- this.#record = {
919
- ...this.#record,
920
- status: "committed",
921
- committedVersion: manifest.version,
922
- revision: committedRevision,
923
- updatedAt: committedAt,
924
- };
925
- this.#stopHeartbeat();
926
- await this.#releaseSnapshotLease().catch(() => undefined);
927
- return manifest;
928
- }
929
- catch (error) {
930
- return this.#recoverCommitted(error);
931
- }
932
- }
933
- async rebase() {
934
- this.#assertActive();
935
- for (;;) {
936
- const currentVersion = await this.store.getCurrentManifestVersion();
937
- if (!this.#persisted) {
938
- // Nothing is in the store yet, so move the temporary durable pin before changing the
939
- // local snapshot. The old version remains protected until the replacement is admitted.
940
- await this.#moveSnapshotLease(currentVersion);
941
- this.#record = {
942
- ...this.#record,
943
- snapshotVersion: currentVersion,
944
- updatedAt: dateIsoString(this.now()),
945
- };
946
- return new Snapshot(this.store, currentVersion);
947
- }
948
- try {
949
- await this.#renewOwnership();
950
- this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
951
- snapshotVersion: currentVersion,
952
- updatedAt: dateIsoString(this.now()),
953
- });
954
- return new Snapshot(this.store, currentVersion);
955
- }
956
- catch (error) {
957
- if (error instanceof SnapshotManifestMissingError) {
958
- continue;
959
- }
960
- throw error;
961
- }
962
- }
963
- }
964
- async abort() {
965
- this.#assertActive();
966
- if (!this.#persisted) {
967
- // Never written, so there is nothing to mark: the transaction simply ends here.
968
- this.#deferredBlocks.length = 0;
969
- this.#deferredSegments.length = 0;
970
- this.#knownSegments.clear();
971
- this.#record = { ...this.#record, status: "aborted", updatedAt: dateIsoString(this.now()) };
972
- this.#stopHeartbeat();
973
- await this.#releaseSnapshotLease().catch(() => undefined);
974
- return;
975
- }
769
+ limit
770
+ }))
771
+ },
772
+ committedAt
773
+ });
774
+ this.#record = {
775
+ ...this.#record,
776
+ status: "committed",
777
+ committedVersion: manifest.version,
778
+ revision: committedRevision,
779
+ updatedAt: committedAt
780
+ };
781
+ this.#stopHeartbeat();
782
+ await this.#releaseSnapshotLease().catch(() => void 0);
783
+ return manifest;
784
+ } catch (error) {
785
+ return this.#recoverCommitted(error);
786
+ }
787
+ }
788
+ async rebase() {
789
+ this.#assertActive();
790
+ for (; ; ) {
791
+ const currentVersion = await this.store.getCurrentManifestVersion();
792
+ if (!this.#persisted) {
793
+ await this.#moveSnapshotLease(currentVersion);
794
+ this.#record = {
795
+ ...this.#record,
796
+ snapshotVersion: currentVersion,
797
+ updatedAt: dateIsoString(this.now())
798
+ };
799
+ return new Snapshot(this.store, currentVersion);
800
+ }
801
+ try {
976
802
  await this.#renewOwnership();
977
803
  this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
978
- status: "aborted",
979
- updatedAt: dateIsoString(this.now()),
804
+ snapshotVersion: currentVersion,
805
+ updatedAt: dateIsoString(this.now())
980
806
  });
981
- this.#stopHeartbeat();
982
- await this.#releaseSnapshotLease().catch(() => undefined);
983
- }
984
- #assertActive() {
985
- if (this.#record.status !== "active")
986
- throw new TransactionClosedError(this.id);
987
- }
988
- /**
989
- * Writes a deferred transaction's record before the first two-step staging call. The pinned
990
- * version can only be unavailable if a commit moved the manifest on and collection pruned it
991
- * meanwhile, so that surfaces as the `WriteConflictError` the rebase loops already handle.
992
- */
993
- async #ensurePersisted() {
994
- if (this.#persisted)
995
- return;
996
- this.#record = {
997
- ...this.#record,
998
- expiresAt: this.#nextExpiry(),
999
- updatedAt: dateIsoString(this.now()),
1000
- };
1001
- try {
1002
- await this.store.createTransaction(this.#record);
1003
- }
1004
- catch (error) {
1005
- if (error instanceof SnapshotManifestMissingError) {
1006
- throw new WriteConflictError(this.#record.snapshotVersion, await this.store.getCurrentManifestVersion());
1007
- }
1008
- throw error;
1009
- }
1010
- this.#persisted = true;
1011
- // The transaction record now owns the same durable pin and renews on this heartbeat. Keep
1012
- // only one record rooted; a failed cleanup remains bounded by the lease expiry.
1013
- void this.#releaseSnapshotLease().catch(() => undefined);
1014
- }
1015
- /** Flushes the bounded process-local batch before a second durable staging operation. */
1016
- async #persistDeferredArtifacts() {
1017
- if (this.#deferredBlocks.length === 0 && this.#deferredSegments.length === 0)
1018
- return;
1019
- const blocks = [...this.#deferredBlocks];
1020
- const segments = [...this.#deferredSegments];
1021
- await this.#stageArtifactBatch(blocks, segments);
1022
- this.#deferredBlocks.length = 0;
1023
- this.#deferredSegments.length = 0;
1024
- }
1025
- /** A commit whose acknowledgement was lost still committed: answer from the persisted record. */
1026
- async #recoverCommitted(error) {
1027
- const persisted = await this.store.getTransaction(this.id);
1028
- if (persisted?.status === "committed" && persisted.committedVersion !== null) {
1029
- const manifest = await this.store.getManifest(persisted.committedVersion);
1030
- if (manifest !== undefined) {
1031
- this.#record = persisted;
1032
- this.#persisted = true;
1033
- this.#stopHeartbeat();
1034
- await this.#releaseSnapshotLease().catch(() => undefined);
1035
- return manifest;
1036
- }
807
+ return new Snapshot(this.store, currentVersion);
808
+ } catch (error) {
809
+ if (error instanceof SnapshotManifestMissingError) {
810
+ continue;
1037
811
  }
1038
812
  throw error;
1039
- }
1040
- async #renewOwnership(force = false) {
1041
- await this.#renewSnapshotLease(force);
1042
- if (!this.#persisted || this.#record.status !== "active")
1043
- return;
1044
- const now = dateMilliseconds(this.now());
1045
- if (!force && Date.parse(this.#record.expiresAt) - now > this.ttlMs / 3)
1046
- return;
1047
- const expiresAt = expiryAfter(new Date(now), this.ttlMs);
1048
- const renewal = this.#renewal ??
1049
- this.store
1050
- .renewTransaction({
1051
- transactionId: this.id,
1052
- ownerId: this.#record.ownerId,
1053
- expiresAtCutoff: dateIsoString(new Date(now)),
1054
- expiresAt,
1055
- })
1056
- .then((owned) => ({ owned, expiresAt }));
1057
- this.#renewal = renewal;
1058
- let result;
1059
- try {
1060
- result = await renewal;
1061
- }
1062
- finally {
1063
- if (this.#renewal === renewal)
1064
- this.#renewal = undefined;
1065
- }
1066
- if (result.owned) {
1067
- this.#record = { ...this.#record, expiresAt: result.expiresAt };
1068
- return;
1069
- }
1070
- const persisted = await this.store.getTransaction(this.id);
1071
- if (persisted !== undefined)
1072
- this.#record = persisted;
813
+ }
814
+ }
815
+ }
816
+ async abort() {
817
+ this.#assertActive();
818
+ if (!this.#persisted) {
819
+ this.#deferredBlocks.length = 0;
820
+ this.#deferredSegments.length = 0;
821
+ this.#knownSegments.clear();
822
+ this.#record = { ...this.#record, status: "aborted", updatedAt: dateIsoString(this.now()) };
823
+ this.#stopHeartbeat();
824
+ await this.#releaseSnapshotLease().catch(() => void 0);
825
+ return;
826
+ }
827
+ await this.#renewOwnership();
828
+ this.#record = await this.store.updateTransaction(this.id, this.#record.revision, {
829
+ status: "aborted",
830
+ updatedAt: dateIsoString(this.now())
831
+ });
832
+ this.#stopHeartbeat();
833
+ await this.#releaseSnapshotLease().catch(() => void 0);
834
+ }
835
+ #assertActive() {
836
+ if (this.#record.status !== "active")
837
+ throw new TransactionClosedError(this.id);
838
+ }
839
+ async #ensurePersisted() {
840
+ if (this.#persisted)
841
+ return;
842
+ this.#record = {
843
+ ...this.#record,
844
+ expiresAt: this.#nextExpiry(),
845
+ updatedAt: dateIsoString(this.now())
846
+ };
847
+ try {
848
+ await this.store.createTransaction(this.#record);
849
+ } catch (error) {
850
+ if (error instanceof SnapshotManifestMissingError) {
851
+ throw new WriteConflictError(this.#record.snapshotVersion, await this.store.getCurrentManifestVersion());
852
+ }
853
+ throw error;
854
+ }
855
+ this.#persisted = true;
856
+ void this.#releaseSnapshotLease().catch(() => void 0);
857
+ }
858
+ async #persistDeferredArtifacts() {
859
+ if (this.#deferredBlocks.length === 0 && this.#deferredSegments.length === 0)
860
+ return;
861
+ const blocks = [...this.#deferredBlocks];
862
+ const segments = [...this.#deferredSegments];
863
+ await this.#stageArtifactBatch(blocks, segments);
864
+ this.#deferredBlocks.length = 0;
865
+ this.#deferredSegments.length = 0;
866
+ }
867
+ async #recoverCommitted(error) {
868
+ const persisted = await this.store.getTransaction(this.id);
869
+ if (persisted?.status === "committed" && persisted.committedVersion !== null) {
870
+ const manifest = await this.store.getManifest(persisted.committedVersion);
871
+ if (manifest !== void 0) {
872
+ this.#record = persisted;
873
+ this.#persisted = true;
1073
874
  this.#stopHeartbeat();
1074
- await this.#releaseSnapshotLease().catch(() => undefined);
1075
- throw new TransactionClosedError(this.id);
1076
- }
1077
- async #renewSnapshotLease(force) {
1078
- const lease = this.#snapshotLease;
1079
- if (lease === undefined || this.#record.status !== "active")
1080
- return;
1081
- if (!force &&
1082
- dateMilliseconds(lease.expiresAt) - dateMilliseconds(this.now()) > this.ttlMs / 3) {
1083
- return;
1084
- }
1085
- const renewal = this.#snapshotRenewal ??
1086
- this.#serializeSnapshotLeaseOperation(async () => {
1087
- const current = this.#snapshotLease;
1088
- if (current === undefined || this.#record.status !== "active")
1089
- return;
1090
- if (!force &&
1091
- dateMilliseconds(current.expiresAt) - dateMilliseconds(this.now()) > this.ttlMs / 3) {
1092
- return;
1093
- }
1094
- await current.renew(this.ttlMs);
1095
- });
1096
- this.#snapshotRenewal = renewal;
1097
- try {
1098
- await renewal;
1099
- }
1100
- finally {
1101
- if (this.#snapshotRenewal === renewal)
1102
- this.#snapshotRenewal = undefined;
1103
- }
1104
- }
1105
- async #moveSnapshotLease(version) {
1106
- await this.#serializeSnapshotLeaseOperation(async () => {
1107
- const current = this.#snapshotLease;
1108
- if (current === undefined || current.version === version)
1109
- return;
1110
- const moved = await current.moveTo(version, this.ttlMs);
1111
- if (moved !== undefined) {
1112
- if (this.#snapshotLease === current) {
1113
- this.#snapshotLease = moved;
1114
- }
1115
- else {
1116
- // A terminal path detached the old lease while the adapter move was in flight. Do not
1117
- // resurrect it in memory; retire the moved durable record as part of this operation.
1118
- await moved.release().catch(() => undefined);
1119
- }
1120
- return;
1121
- }
1122
- // Custom stores may omit atomic moveLease. Admit a replacement before releasing the old
1123
- // pin, so there is never a reclamation window between versions.
1124
- const createdAt = dateIsoString(this.now());
1125
- const record = {
1126
- id: crypto.randomUUID(),
1127
- kind: "reader",
1128
- manifestVersion: version,
1129
- ownerId: this.#record.ownerId,
1130
- createdAt,
1131
- expiresAt: expiryAfter(new Date(createdAt), this.ttlMs),
1132
- revision: 0,
1133
- };
1134
- await this.store.createLease(record);
1135
- const replacement = new LeasedSnapshot(this.store, version, record, this.now);
1136
- if (this.#snapshotLease === current) {
1137
- this.#snapshotLease = replacement;
1138
- }
1139
- else {
1140
- await replacement.release().catch(() => undefined);
1141
- }
1142
- await current.release().catch(() => undefined);
1143
- });
875
+ await this.#releaseSnapshotLease().catch(() => void 0);
876
+ return manifest;
877
+ }
878
+ }
879
+ throw error;
880
+ }
881
+ async #renewOwnership(force = false) {
882
+ await this.#renewSnapshotLease(force);
883
+ if (!this.#persisted || this.#record.status !== "active")
884
+ return;
885
+ const now = dateMilliseconds(this.now());
886
+ if (!force && Date.parse(this.#record.expiresAt) - now > this.ttlMs / 3)
887
+ return;
888
+ const expiresAt = expiryAfter(new Date(now), this.ttlMs);
889
+ const renewal = this.#renewal ?? this.store.renewTransaction({
890
+ transactionId: this.id,
891
+ ownerId: this.#record.ownerId,
892
+ expiresAtCutoff: dateIsoString(new Date(now)),
893
+ expiresAt
894
+ }).then((owned) => ({ owned, expiresAt }));
895
+ this.#renewal = renewal;
896
+ let result;
897
+ try {
898
+ result = await renewal;
899
+ } finally {
900
+ if (this.#renewal === renewal)
901
+ this.#renewal = void 0;
902
+ }
903
+ if (result.owned) {
904
+ this.#record = { ...this.#record, expiresAt: result.expiresAt };
905
+ return;
906
+ }
907
+ const persisted = await this.store.getTransaction(this.id);
908
+ if (persisted !== void 0)
909
+ this.#record = persisted;
910
+ this.#stopHeartbeat();
911
+ await this.#releaseSnapshotLease().catch(() => void 0);
912
+ throw new TransactionClosedError(this.id);
913
+ }
914
+ async #renewSnapshotLease(force) {
915
+ const lease = this.#snapshotLease;
916
+ if (lease === void 0 || this.#record.status !== "active")
917
+ return;
918
+ if (!force && dateMilliseconds(lease.expiresAt) - dateMilliseconds(this.now()) > this.ttlMs / 3) {
919
+ return;
920
+ }
921
+ const renewal = this.#snapshotRenewal ?? this.#serializeSnapshotLeaseOperation(async () => {
922
+ const current = this.#snapshotLease;
923
+ if (current === void 0 || this.#record.status !== "active")
924
+ return;
925
+ if (!force && dateMilliseconds(current.expiresAt) - dateMilliseconds(this.now()) > this.ttlMs / 3) {
926
+ return;
927
+ }
928
+ await current.renew(this.ttlMs);
929
+ });
930
+ this.#snapshotRenewal = renewal;
931
+ try {
932
+ await renewal;
933
+ } finally {
934
+ if (this.#snapshotRenewal === renewal)
935
+ this.#snapshotRenewal = void 0;
936
+ }
937
+ }
938
+ async #moveSnapshotLease(version) {
939
+ await this.#serializeSnapshotLeaseOperation(async () => {
940
+ const current = this.#snapshotLease;
941
+ if (current === void 0 || current.version === version)
942
+ return;
943
+ const moved = await current.moveTo(version, this.ttlMs);
944
+ if (moved !== void 0) {
945
+ if (this.#snapshotLease === current) {
946
+ this.#snapshotLease = moved;
947
+ } else {
948
+ await moved.release().catch(() => void 0);
949
+ }
950
+ return;
951
+ }
952
+ const createdAt = dateIsoString(this.now());
953
+ const record = {
954
+ id: crypto.randomUUID(),
955
+ kind: "reader",
956
+ manifestVersion: version,
957
+ ownerId: this.#record.ownerId,
958
+ createdAt,
959
+ expiresAt: expiryAfter(new Date(createdAt), this.ttlMs),
960
+ revision: 0
961
+ };
962
+ await this.store.createLease(record);
963
+ const replacement = new LeasedSnapshot(this.store, version, record, this.now);
964
+ if (this.#snapshotLease === current) {
965
+ this.#snapshotLease = replacement;
966
+ } else {
967
+ await replacement.release().catch(() => void 0);
968
+ }
969
+ await current.release().catch(() => void 0);
970
+ });
971
+ }
972
+ async #releaseSnapshotLease() {
973
+ const lease = this.#snapshotLease;
974
+ if (lease === void 0)
975
+ return;
976
+ this.#snapshotLease = void 0;
977
+ await this.#serializeSnapshotLeaseOperation(() => lease.release());
978
+ }
979
+ #serializeSnapshotLeaseOperation(operation) {
980
+ const run = this.#snapshotLeaseOperation.then(operation);
981
+ this.#snapshotLeaseOperation = run.then(() => void 0, () => void 0);
982
+ return run;
983
+ }
984
+ #nextExpiry() {
985
+ return expiryAfter(this.now(), this.ttlMs);
986
+ }
987
+ #stopHeartbeat() {
988
+ if (this.#heartbeatTimer !== void 0)
989
+ clearInterval(this.#heartbeatTimer);
990
+ this.#heartbeatTimer = void 0;
991
+ }
992
+ async #recoverStagedAcknowledgement(error, blocks, segments) {
993
+ try {
994
+ const persisted = await this.store.getTransaction(this.id);
995
+ const expectedBlocks = [...this.#record.pendingBlockIds, ...blocks.map((block) => block.id)];
996
+ const expectedSegments = [
997
+ ...this.#record.pendingSegmentIds,
998
+ ...segments.map((segment) => segment.id)
999
+ ];
1000
+ if (persisted?.status === "active" && persisted.revision === advanceRevision(this.#record.revision, 1) && sameStrings(persisted.pendingBlockIds, expectedBlocks) && sameStrings(persisted.pendingSegmentIds, expectedSegments)) {
1001
+ const [storedBlocks, storedSegments] = await Promise.all([
1002
+ Promise.all(blocks.map((block) => this.store.getBlock(block.id))),
1003
+ Promise.all(segments.map((segment) => this.store.getSegment(segment.id)))
1004
+ ]);
1005
+ if (blocks.every((block, index) => sameBytes(block.bytes, storedBlocks[index])) && segments.every((segment, index) => {
1006
+ const stored = storedSegments[index];
1007
+ return stored !== void 0 && sameSegmentArtifacts(segment, stored);
1008
+ })) {
1009
+ this.#record = persisted;
1010
+ this.#persisted = true;
1011
+ }
1012
+ }
1013
+ } catch {
1014
+ }
1015
+ throw error;
1016
+ }
1017
+ async #assertRetainedArtifactsUnchanged(blocks, segments) {
1018
+ for (const block of blocks) {
1019
+ if (!sameBytes(block.bytes, await this.store.getBlock(block.id))) {
1020
+ throw new Error(`Retried block differs from the staged artifact: ${block.id}`);
1021
+ }
1144
1022
  }
1145
- async #releaseSnapshotLease() {
1146
- const lease = this.#snapshotLease;
1147
- if (lease === undefined)
1148
- return;
1149
- // Detach before the adapter call. Once a transaction record is persisted it is the durable
1150
- // owner; a slow lease removal must not let the next ownership check renew the retiring pin
1151
- // concurrently and turn a healthy handoff into a LeaseConflictError.
1152
- this.#snapshotLease = undefined;
1153
- await this.#serializeSnapshotLeaseOperation(() => lease.release());
1154
- }
1155
- /**
1156
- * Lease revisions are compare-and-swap tokens, so renew, move, and release must observe one
1157
- * another's returned record before issuing the next adapter operation. Keep the tail fulfilled
1158
- * after a refusal: the caller still receives its error, while a terminal cleanup can proceed.
1159
- */
1160
- #serializeSnapshotLeaseOperation(operation) {
1161
- const run = this.#snapshotLeaseOperation.then(operation);
1162
- this.#snapshotLeaseOperation = run.then(() => undefined, () => undefined);
1163
- return run;
1164
- }
1165
- #nextExpiry() {
1166
- return expiryAfter(this.now(), this.ttlMs);
1167
- }
1168
- #stopHeartbeat() {
1169
- if (this.#heartbeatTimer !== undefined)
1170
- clearInterval(this.#heartbeatTimer);
1171
- this.#heartbeatTimer = undefined;
1172
- }
1173
- /**
1174
- * An adapter can durably stage the artifacts and lose the acknowledgement afterwards. Refresh
1175
- * the local revision only when the persisted journal is exactly the one this call would have
1176
- * produced, so the caller's abort/rollback can clean it up without masking the original error.
1177
- */
1178
- async #recoverStagedAcknowledgement(error, blocks, segments) {
1179
- try {
1180
- const persisted = await this.store.getTransaction(this.id);
1181
- const expectedBlocks = [...this.#record.pendingBlockIds, ...blocks.map((block) => block.id)];
1182
- const expectedSegments = [
1183
- ...this.#record.pendingSegmentIds,
1184
- ...segments.map((segment) => segment.id),
1185
- ];
1186
- if (persisted?.status === "active" &&
1187
- persisted.revision === advanceRevision(this.#record.revision, 1) &&
1188
- sameStrings(persisted.pendingBlockIds, expectedBlocks) &&
1189
- sameStrings(persisted.pendingSegmentIds, expectedSegments)) {
1190
- const [storedBlocks, storedSegments] = await Promise.all([
1191
- Promise.all(blocks.map((block) => this.store.getBlock(block.id))),
1192
- Promise.all(segments.map((segment) => this.store.getSegment(segment.id))),
1193
- ]);
1194
- if (blocks.every((block, index) => sameBytes(block.bytes, storedBlocks[index])) &&
1195
- segments.every((segment, index) => {
1196
- const stored = storedSegments[index];
1197
- return stored !== undefined && sameSegmentArtifacts(segment, stored);
1198
- })) {
1199
- this.#record = persisted;
1200
- this.#persisted = true;
1201
- }
1202
- }
1203
- }
1204
- catch {
1205
- // Preserve the operation error. A failed diagnostic read cannot prove a different outcome.
1206
- }
1207
- throw error;
1208
- }
1209
- async #assertRetainedArtifactsUnchanged(blocks, segments) {
1210
- for (const block of blocks) {
1211
- if (!sameBytes(block.bytes, await this.store.getBlock(block.id))) {
1212
- throw new Error(`Retried block differs from the staged artifact: ${block.id}`);
1213
- }
1214
- }
1215
- for (const segment of segments) {
1216
- const stored = await this.store.getSegment(segment.id);
1217
- if (stored === undefined ||
1218
- !sameSegmentArtifacts(stored, { ...segment, commitOrdinal: stored.commitOrdinal })) {
1219
- throw new Error(`Retried segment differs from the staged artifact: ${segment.id}`);
1220
- }
1221
- }
1023
+ for (const segment of segments) {
1024
+ const stored = await this.store.getSegment(segment.id);
1025
+ if (stored === void 0 || !sameSegmentArtifacts(stored, { ...segment, commitOrdinal: stored.commitOrdinal })) {
1026
+ throw new Error(`Retried segment differs from the staged artifact: ${segment.id}`);
1027
+ }
1222
1028
  }
1029
+ }
1223
1030
  }
1224
1031
  function sameStrings(left, right) {
1225
- return left.length === right.length && left.every((value, index) => value === right[index]);
1032
+ return left.length === right.length && left.every((value, index) => value === right[index]);
1226
1033
  }
1227
1034
  function sameBytes(left, right) {
1228
- return (left.byteLength === right?.byteLength && left.every((byte, index) => byte === right[index]));
1035
+ return left.byteLength === right?.byteLength && left.every((byte, index) => byte === right[index]);
1229
1036
  }
1230
- export class TransactionManager {
1231
- store;
1232
- #now;
1233
- #createId;
1234
- #transactionTtlMs;
1235
- #leaseCleanupCursor = null;
1236
- #createWeakRef;
1237
- constructor(store, options = {}) {
1238
- this.store = store;
1239
- this.#now = options.now ?? (() => new Date());
1240
- this.#createId = options.createId ?? (() => crypto.randomUUID());
1241
- this.#transactionTtlMs = options.transactionTtlMs ?? DEFAULT_TRANSACTION_TTL_MS;
1242
- validateTtl(this.#transactionTtlMs);
1243
- this.#createWeakRef = options.createWeakRef ?? ((transaction) => new WeakRef(transaction));
1244
- }
1245
- async begin() {
1246
- return (await this.beginWithReservation()).transaction;
1247
- }
1248
- /** Atomically reserves an invisible catalog table for a create-and-populate transaction. */
1249
- async beginWithPendingTable(record, expectedCatalogEpoch) {
1250
- const id = this.#createId();
1251
- const ownerId = `${id}/owner`;
1252
- const timestamp = dateIsoString(this.#now());
1253
- let begun;
1254
- for (;;) {
1255
- try {
1256
- begun = await this.store.beginTransaction({
1257
- record: {
1258
- id,
1259
- ownerId,
1260
- expiresAt: this.#transactionExpiry(),
1261
- pendingBlockIds: [],
1262
- pendingSegmentIds: [],
1263
- status: "active",
1264
- revision: 0,
1265
- startedAt: timestamp,
1266
- updatedAt: timestamp,
1267
- committedVersion: null,
1268
- },
1269
- pendingTable: { record, nextRowId: 1n, expectedCatalogEpoch },
1270
- });
1271
- break;
1272
- }
1273
- catch (error) {
1274
- // Atomic begin writes nothing when its current-manifest pin loses a pruning race.
1275
- if (error instanceof SnapshotManifestMissingError)
1276
- continue;
1277
- throw error;
1278
- }
1279
- }
1280
- return new DatabaseTransaction(this.store, begun.record, this.#now, this.#transactionTtlMs, this.#createWeakRef);
1281
- }
1282
- /**
1283
- * Begins without a transaction record. By default a renewable reader lease durably pins the
1284
- * probed manifest until the first two-step stage hands ownership to a transaction record. The
1285
- * record and lease are both omitted only for internal, uninterrupted single-shot writes that
1286
- * opt out and publish through `writeTransaction`. For writes whose artifacts need no prior
1287
- * reservation; `beginWithReservation` is the shape for the rest.
1288
- */
1289
- async beginDeferred(options = {}) {
1290
- const durableSnapshot = options.durableSnapshot ?? true;
1291
- for (;;) {
1292
- const probe = await this.store.getCatalogProbe();
1293
- const timestamp = dateIsoString(this.#now());
1294
- const id = this.#createId();
1295
- const ownerId = `${id}/owner`;
1296
- let snapshotLease;
1297
- if (durableSnapshot) {
1298
- try {
1299
- snapshotLease = await this.openLeasedSnapshot({
1300
- id: `${id}/snapshot`,
1301
- ownerId,
1302
- ttlMs: this.#transactionTtlMs,
1303
- version: probe.manifestVersion,
1304
- });
1305
- }
1306
- catch (error) {
1307
- // The version was current at the probe but collection won before the durable pin.
1308
- if (error instanceof SnapshotManifestMissingError)
1309
- continue;
1310
- throw error;
1311
- }
1312
- }
1313
- return new DatabaseTransaction(this.store, {
1314
- id,
1315
- ownerId,
1316
- expiresAt: this.#transactionExpiry(),
1317
- snapshotVersion: probe.manifestVersion,
1318
- schemaEpochGuard: probe.schemaEpoch,
1319
- pendingBlockIds: [],
1320
- pendingSegmentIds: [],
1321
- status: "active",
1322
- revision: 0,
1323
- startedAt: timestamp,
1324
- updatedAt: timestamp,
1325
- committedVersion: null,
1326
- }, this.#now, this.#transactionTtlMs, this.#createWeakRef, {
1327
- persisted: false,
1328
- initialCatalogProbe: probe,
1329
- ...(snapshotLease === undefined ? {} : { snapshotLease }),
1330
- });
1331
- }
1332
- }
1333
- /**
1334
- * Begins a transaction at the current manifest version and optionally reserves row ids and
1335
- * auto-increment values in the same step. Stores implementing the atomic `beginTransaction`
1336
- * pay one storage round trip instead of the version-read/create/reserve sequence; the
1337
- * read-then-create fallback retries when the version it read is pruned before the record lands.
1338
- */
1339
- async beginWithReservation(reserveRowIds, reserveAutoIncrement) {
1340
- const id = this.#createId();
1341
- const ownerId = `${id}/owner`;
1342
- const timestamp = dateIsoString(this.#now());
1343
- let begun;
1344
- for (;;) {
1345
- try {
1346
- begun = await this.store.beginTransaction({
1347
- record: {
1348
- id,
1349
- ownerId,
1350
- expiresAt: this.#transactionExpiry(),
1351
- pendingBlockIds: [],
1352
- pendingSegmentIds: [],
1353
- status: "active",
1354
- revision: 0,
1355
- startedAt: timestamp,
1356
- updatedAt: timestamp,
1357
- committedVersion: null,
1358
- },
1359
- ...(reserveRowIds === undefined ? {} : { reserveRowIds }),
1360
- ...(reserveAutoIncrement === undefined ? {} : { reserveAutoIncrement }),
1361
- });
1362
- break;
1363
- }
1364
- catch (error) {
1365
- // The store's atomic begin aborted in full, including any counter reservation.
1366
- if (error instanceof SnapshotManifestMissingError)
1367
- continue;
1368
- throw error;
1369
- }
1370
- }
1371
- return {
1372
- transaction: new DatabaseTransaction(this.store, begun.record, this.#now, this.#transactionTtlMs, this.#createWeakRef),
1373
- ...(begun.rowIds === undefined ? {} : { rowIds: begun.rowIds }),
1374
- ...(begun.autoIncrementValues === undefined
1375
- ? {}
1376
- : { autoIncrementValues: begun.autoIncrementValues }),
1377
- };
1037
+ class TransactionManager {
1038
+ store;
1039
+ #now;
1040
+ #createId;
1041
+ #transactionTtlMs;
1042
+ #leaseCleanupCursor = null;
1043
+ #createWeakRef;
1044
+ constructor(store, options = {}) {
1045
+ this.store = store;
1046
+ this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
1047
+ this.#createId = options.createId ?? (() => crypto.randomUUID());
1048
+ this.#transactionTtlMs = options.transactionTtlMs ?? DEFAULT_TRANSACTION_TTL_MS;
1049
+ validateTtl(this.#transactionTtlMs);
1050
+ this.#createWeakRef = options.createWeakRef ?? ((transaction) => new WeakRef(transaction));
1051
+ }
1052
+ async begin() {
1053
+ return (await this.beginWithReservation()).transaction;
1054
+ }
1055
+ async beginWithPendingTable(record, expectedCatalogEpoch) {
1056
+ const id = this.#createId();
1057
+ const ownerId = `${id}/owner`;
1058
+ const timestamp = dateIsoString(this.#now());
1059
+ let begun;
1060
+ for (; ; ) {
1061
+ try {
1062
+ begun = await this.store.beginTransaction({
1063
+ record: {
1064
+ id,
1065
+ ownerId,
1066
+ expiresAt: this.#transactionExpiry(),
1067
+ pendingBlockIds: [],
1068
+ pendingSegmentIds: [],
1069
+ status: "active",
1070
+ revision: 0,
1071
+ startedAt: timestamp,
1072
+ updatedAt: timestamp,
1073
+ committedVersion: null
1074
+ },
1075
+ pendingTable: { record, nextRowId: 1n, expectedCatalogEpoch }
1076
+ });
1077
+ break;
1078
+ } catch (error) {
1079
+ if (error instanceof SnapshotManifestMissingError)
1080
+ continue;
1081
+ throw error;
1082
+ }
1083
+ }
1084
+ return new DatabaseTransaction(this.store, begun.record, this.#now, this.#transactionTtlMs, this.#createWeakRef);
1085
+ }
1086
+ async beginDeferred(options = {}) {
1087
+ const durableSnapshot = options.durableSnapshot ?? true;
1088
+ for (; ; ) {
1089
+ const probe = await this.store.getCatalogProbe();
1090
+ const timestamp = dateIsoString(this.#now());
1091
+ const id = this.#createId();
1092
+ const ownerId = `${id}/owner`;
1093
+ let snapshotLease;
1094
+ if (durableSnapshot) {
1095
+ try {
1096
+ snapshotLease = await this.openLeasedSnapshot({
1097
+ id: `${id}/snapshot`,
1098
+ ownerId,
1099
+ ttlMs: this.#transactionTtlMs,
1100
+ version: probe.manifestVersion
1101
+ });
1102
+ } catch (error) {
1103
+ if (error instanceof SnapshotManifestMissingError)
1104
+ continue;
1105
+ throw error;
1106
+ }
1107
+ }
1108
+ return new DatabaseTransaction(this.store, {
1109
+ id,
1110
+ ownerId,
1111
+ expiresAt: this.#transactionExpiry(),
1112
+ snapshotVersion: probe.manifestVersion,
1113
+ schemaEpochGuard: probe.schemaEpoch,
1114
+ pendingBlockIds: [],
1115
+ pendingSegmentIds: [],
1116
+ status: "active",
1117
+ revision: 0,
1118
+ startedAt: timestamp,
1119
+ updatedAt: timestamp,
1120
+ committedVersion: null
1121
+ }, this.#now, this.#transactionTtlMs, this.#createWeakRef, {
1122
+ persisted: false,
1123
+ initialCatalogProbe: probe,
1124
+ ...snapshotLease === void 0 ? {} : { snapshotLease }
1125
+ });
1126
+ }
1127
+ }
1128
+ async beginWithReservation(reserveRowIds, reserveAutoIncrement) {
1129
+ const id = this.#createId();
1130
+ const ownerId = `${id}/owner`;
1131
+ const timestamp = dateIsoString(this.#now());
1132
+ let begun;
1133
+ for (; ; ) {
1134
+ try {
1135
+ begun = await this.store.beginTransaction({
1136
+ record: {
1137
+ id,
1138
+ ownerId,
1139
+ expiresAt: this.#transactionExpiry(),
1140
+ pendingBlockIds: [],
1141
+ pendingSegmentIds: [],
1142
+ status: "active",
1143
+ revision: 0,
1144
+ startedAt: timestamp,
1145
+ updatedAt: timestamp,
1146
+ committedVersion: null
1147
+ },
1148
+ ...reserveRowIds === void 0 ? {} : { reserveRowIds },
1149
+ ...reserveAutoIncrement === void 0 ? {} : { reserveAutoIncrement }
1150
+ });
1151
+ break;
1152
+ } catch (error) {
1153
+ if (error instanceof SnapshotManifestMissingError)
1154
+ continue;
1155
+ throw error;
1156
+ }
1378
1157
  }
1379
- async resume(transactionId) {
1380
- const record = await this.store.getTransaction(transactionId);
1381
- if (record === undefined)
1382
- throw new Error(`Transaction not found: ${transactionId}`);
1383
- if (record.status !== "active")
1384
- throw new TransactionClosedError(transactionId);
1385
- const transaction = new DatabaseTransaction(this.store, record, this.#now, this.#transactionTtlMs, this.#createWeakRef);
1386
- for (const segmentId of record.pendingSegmentIds) {
1387
- const segment = await this.store.getSegment(segmentId);
1388
- if (segment?.level === 0) {
1389
- transaction.limitLevelZeroSegments(segment.tableId, MAX_LEVEL_ZERO_SEGMENTS);
1390
- }
1391
- }
1392
- await transaction.renew();
1393
- return transaction;
1394
- }
1395
- #transactionExpiry() {
1396
- return expiryAfter(this.#now(), this.#transactionTtlMs);
1397
- }
1398
- async openSnapshot(version) {
1399
- if (version === null)
1400
- return new Snapshot(this.store, null);
1401
- if (version !== undefined)
1402
- return loadSnapshot(this.store, version);
1403
- return new Snapshot(this.store, await this.store.getCurrentManifestVersion());
1404
- }
1405
- async openLeasedSnapshot(options) {
1406
- validateTtl(options.ttlMs);
1407
- if (options.ownerId.trim().length === 0)
1408
- throw new TypeError("Lease owner cannot be empty");
1409
- if (options.id?.trim().length === 0) {
1410
- throw new TypeError("Lease ID cannot be empty");
1411
- }
1412
- const id = options.id ?? this.#createId();
1413
- for (;;) {
1414
- const snapshot = new Snapshot(this.store, options.version === undefined
1415
- ? await this.store.getCurrentManifestVersion()
1416
- : options.version);
1417
- const createdAt = dateIsoString(this.#now());
1418
- const record = {
1419
- id,
1420
- kind: options.kind ?? "reader",
1421
- manifestVersion: snapshot.version,
1422
- ownerId: options.ownerId,
1423
- createdAt,
1424
- expiresAt: expiryAfter(new Date(createdAt), options.ttlMs),
1425
- revision: 0,
1426
- };
1427
- try {
1428
- await this.store.createLease(record);
1429
- return new LeasedSnapshot(this.store, snapshot.version, record, this.#now);
1430
- }
1431
- catch (error) {
1432
- if (options.version === undefined && error instanceof SnapshotManifestMissingError) {
1433
- continue;
1434
- }
1435
- throw error;
1436
- }
1158
+ return {
1159
+ transaction: new DatabaseTransaction(this.store, begun.record, this.#now, this.#transactionTtlMs, this.#createWeakRef),
1160
+ ...begun.rowIds === void 0 ? {} : { rowIds: begun.rowIds },
1161
+ ...begun.autoIncrementValues === void 0 ? {} : { autoIncrementValues: begun.autoIncrementValues }
1162
+ };
1163
+ }
1164
+ async resume(transactionId) {
1165
+ const record = await this.store.getTransaction(transactionId);
1166
+ if (record === void 0)
1167
+ throw new Error(`Transaction not found: ${transactionId}`);
1168
+ if (record.status !== "active")
1169
+ throw new TransactionClosedError(transactionId);
1170
+ const transaction = new DatabaseTransaction(this.store, record, this.#now, this.#transactionTtlMs, this.#createWeakRef);
1171
+ for (const segmentId of record.pendingSegmentIds) {
1172
+ const segment = await this.store.getSegment(segmentId);
1173
+ if (segment?.level === 0) {
1174
+ transaction.limitLevelZeroSegments(segment.tableId, MAX_LEVEL_ZERO_SEGMENTS);
1175
+ }
1176
+ }
1177
+ await transaction.renew();
1178
+ return transaction;
1179
+ }
1180
+ #transactionExpiry() {
1181
+ return expiryAfter(this.#now(), this.#transactionTtlMs);
1182
+ }
1183
+ async openSnapshot(version) {
1184
+ if (version === null)
1185
+ return new Snapshot(this.store, null);
1186
+ if (version !== void 0)
1187
+ return loadSnapshot(this.store, version);
1188
+ return new Snapshot(this.store, await this.store.getCurrentManifestVersion());
1189
+ }
1190
+ async openLeasedSnapshot(options) {
1191
+ validateTtl(options.ttlMs);
1192
+ if (options.ownerId.trim().length === 0)
1193
+ throw new TypeError("Lease owner cannot be empty");
1194
+ if (options.id?.trim().length === 0) {
1195
+ throw new TypeError("Lease ID cannot be empty");
1196
+ }
1197
+ const id = options.id ?? this.#createId();
1198
+ for (; ; ) {
1199
+ const snapshot = new Snapshot(this.store, options.version === void 0 ? await this.store.getCurrentManifestVersion() : options.version);
1200
+ const createdAt = dateIsoString(this.#now());
1201
+ const record = {
1202
+ id,
1203
+ kind: options.kind ?? "reader",
1204
+ manifestVersion: snapshot.version,
1205
+ ownerId: options.ownerId,
1206
+ createdAt,
1207
+ expiresAt: expiryAfter(new Date(createdAt), options.ttlMs),
1208
+ revision: 0
1209
+ };
1210
+ try {
1211
+ await this.store.createLease(record);
1212
+ return new LeasedSnapshot(this.store, snapshot.version, record, this.#now);
1213
+ } catch (error) {
1214
+ if (options.version === void 0 && error instanceof SnapshotManifestMissingError) {
1215
+ continue;
1437
1216
  }
1438
- }
1439
- /**
1440
- * Re-pins an open leased snapshot to another version — one reader moving on to a newer
1441
- * commit. With the store's atomic `moveLease` the record stays and moves in one round trip;
1442
- * otherwise a fresh lease is created at the target (`options.id` names it) and the old one
1443
- * released. A lease the store no longer holds — expired and swept — is simply replaced. The
1444
- * given snapshot is closed on return either way; a missing target manifest throws
1445
- * `SnapshotManifestMissingError` and leaves it open.
1446
- */
1447
- async moveLeasedSnapshot(snapshot, options) {
1448
- validateTtl(options.ttlMs);
1217
+ throw error;
1218
+ }
1219
+ }
1220
+ }
1221
+ async moveLeasedSnapshot(snapshot, options) {
1222
+ validateTtl(options.ttlMs);
1223
+ try {
1224
+ const moved = await snapshot.moveTo(options.version, options.ttlMs);
1225
+ if (moved !== void 0)
1226
+ return moved;
1227
+ } catch (error) {
1228
+ if (!(error instanceof LeaseConflictError) && !(error instanceof LeaseExpiredError)) {
1229
+ throw error;
1230
+ }
1231
+ const replacement2 = await this.openLeasedSnapshot(options);
1232
+ snapshot.discard();
1233
+ return replacement2;
1234
+ }
1235
+ const replacement = await this.openLeasedSnapshot(options);
1236
+ await snapshot.release();
1237
+ return replacement;
1238
+ }
1239
+ async removeExpiredLeases(at = this.#now()) {
1240
+ const timestamp = dateMilliseconds(at);
1241
+ if (!Number.isFinite(timestamp))
1242
+ throw new TypeError("Lease cutoff must be a valid date");
1243
+ const removed = [];
1244
+ const cutoff = dateIsoString(at);
1245
+ const page = await this.store.listExpiredLeasePage(cutoff, this.#leaseCleanupCursor, 64);
1246
+ this.#leaseCleanupCursor = page.nextCursor;
1247
+ for (const lease of page.records) {
1248
+ try {
1249
+ if (await this.store.removeLeaseIfExpired(lease.id, lease.revision, cutoff)) {
1250
+ removed.push(lease.id);
1251
+ }
1252
+ } catch (error) {
1253
+ if (error instanceof LeaseConflictError)
1254
+ continue;
1255
+ throw error;
1256
+ }
1257
+ }
1258
+ return removed;
1259
+ }
1260
+ async recover(options) {
1261
+ const staleBefore = dateMilliseconds(options.staleBefore);
1262
+ if (!Number.isFinite(staleBefore))
1263
+ throw new TypeError("Recovery cutoff must be a valid date");
1264
+ const abortedTransactionIds = [];
1265
+ const skippedTransactionIds = [];
1266
+ const candidates = /* @__PURE__ */ new Set();
1267
+ const segmentCandidates = /* @__PURE__ */ new Set();
1268
+ let transactionCursor = null;
1269
+ do {
1270
+ const page = await this.store.listTransactionPage(transactionCursor, 64);
1271
+ for (const record of page.records) {
1272
+ if (record.status !== "active" || Date.parse(record.expiresAt) > staleBefore)
1273
+ continue;
1449
1274
  try {
1450
- const moved = await snapshot.moveTo(options.version, options.ttlMs);
1451
- if (moved !== undefined)
1452
- return moved;
1453
- }
1454
- catch (error) {
1455
- if (!(error instanceof LeaseConflictError) && !(error instanceof LeaseExpiredError)) {
1456
- throw error;
1457
- }
1458
- // Not ours to move any more — swept after expiring, most likely. A fresh lease takes its
1459
- // place; whatever record remains is left to expire rather than deleted from under anyone.
1460
- const replacement = await this.openLeasedSnapshot(options);
1461
- snapshot.discard();
1462
- return replacement;
1463
- }
1464
- const replacement = await this.openLeasedSnapshot(options);
1465
- await snapshot.release();
1466
- return replacement;
1467
- }
1468
- async removeExpiredLeases(at = this.#now()) {
1469
- const timestamp = dateMilliseconds(at);
1470
- if (!Number.isFinite(timestamp))
1471
- throw new TypeError("Lease cutoff must be a valid date");
1472
- const removed = [];
1473
- const cutoff = dateIsoString(at);
1474
- const page = await this.store.listExpiredLeasePage(cutoff, this.#leaseCleanupCursor, 64);
1475
- this.#leaseCleanupCursor = page.nextCursor;
1476
- for (const lease of page.records) {
1477
- try {
1478
- if (await this.store.removeLeaseIfExpired(lease.id, lease.revision, cutoff)) {
1479
- removed.push(lease.id);
1480
- }
1481
- }
1482
- catch (error) {
1483
- if (error instanceof LeaseConflictError)
1484
- continue;
1485
- throw error;
1486
- }
1275
+ const aborted = await this.store.abortTransactionIfExpired({
1276
+ transactionId: record.id,
1277
+ expectedOwnerId: record.ownerId,
1278
+ expiresAtCutoff: dateIsoString(options.staleBefore),
1279
+ updatedAt: dateIsoString(this.#now())
1280
+ });
1281
+ if (aborted === void 0) {
1282
+ skippedTransactionIds.push(record.id);
1283
+ continue;
1284
+ }
1285
+ abortedTransactionIds.push(record.id);
1286
+ aborted.pendingBlockIds.forEach((id) => candidates.add(id));
1287
+ aborted.pendingSegmentIds.forEach((id) => segmentCandidates.add(id));
1288
+ } catch (error) {
1289
+ if (error instanceof TransactionRecordConflictError) {
1290
+ skippedTransactionIds.push(record.id);
1291
+ continue;
1292
+ }
1293
+ throw error;
1294
+ }
1295
+ }
1296
+ transactionCursor = page.nextCursor;
1297
+ } while (transactionCursor !== null);
1298
+ const removedBlockIds = [];
1299
+ const retainedBlockIds = [];
1300
+ const removedSegmentIds = [];
1301
+ const retainedSegmentIds = [];
1302
+ if (options.removePendingBlocks === false) {
1303
+ retainedBlockIds.push(...[...candidates].sort());
1304
+ retainedSegmentIds.push(...[...segmentCandidates].sort());
1305
+ } else if (candidates.size > 0 || segmentCandidates.size > 0) {
1306
+ const timestamp = dateIsoString(this.#now());
1307
+ const baseId = `recovery/${timestamp}/${this.#createId()}`;
1308
+ let suffix = 0;
1309
+ let job;
1310
+ for (; ; ) {
1311
+ const id = suffix === 0 ? baseId : `${baseId}/${String(suffix)}`;
1312
+ if (await this.store.getGarbageCollectionJob(id) !== void 0) {
1313
+ suffix += 1;
1314
+ continue;
1487
1315
  }
1488
- return removed;
1489
- }
1490
- async recover(options) {
1491
- const staleBefore = dateMilliseconds(options.staleBefore);
1492
- if (!Number.isFinite(staleBefore))
1493
- throw new TypeError("Recovery cutoff must be a valid date");
1494
- const abortedTransactionIds = [];
1495
- const skippedTransactionIds = [];
1496
- const candidates = new Set();
1497
- const segmentCandidates = new Set();
1498
- let transactionCursor = null;
1499
- do {
1500
- const page = await this.store.listTransactionPage(transactionCursor, 64);
1501
- for (const record of page.records) {
1502
- if (record.status !== "active" || Date.parse(record.expiresAt) > staleBefore)
1503
- continue;
1504
- try {
1505
- const aborted = await this.store.abortTransactionIfExpired({
1506
- transactionId: record.id,
1507
- expectedOwnerId: record.ownerId,
1508
- expiresAtCutoff: dateIsoString(options.staleBefore),
1509
- updatedAt: dateIsoString(this.#now()),
1510
- });
1511
- if (aborted === undefined) {
1512
- skippedTransactionIds.push(record.id);
1513
- continue;
1514
- }
1515
- abortedTransactionIds.push(record.id);
1516
- aborted.pendingBlockIds.forEach((id) => candidates.add(id));
1517
- aborted.pendingSegmentIds.forEach((id) => segmentCandidates.add(id));
1518
- }
1519
- catch (error) {
1520
- if (error instanceof TransactionRecordConflictError) {
1521
- skippedTransactionIds.push(record.id);
1522
- continue;
1523
- }
1524
- throw error;
1525
- }
1526
- }
1527
- transactionCursor = page.nextCursor;
1528
- } while (transactionCursor !== null);
1529
- const removedBlockIds = [];
1530
- const retainedBlockIds = [];
1531
- const removedSegmentIds = [];
1532
- const retainedSegmentIds = [];
1533
- if (options.removePendingBlocks === false) {
1534
- retainedBlockIds.push(...[...candidates].sort());
1535
- retainedSegmentIds.push(...[...segmentCandidates].sort());
1536
- }
1537
- else if (candidates.size > 0 || segmentCandidates.size > 0) {
1538
- const timestamp = dateIsoString(this.#now());
1539
- const baseId = `recovery/${timestamp}/${this.#createId()}`;
1540
- let suffix = 0;
1541
- let job;
1542
- for (;;) {
1543
- const id = suffix === 0 ? baseId : `${baseId}/${String(suffix)}`;
1544
- if ((await this.store.getGarbageCollectionJob(id)) !== undefined) {
1545
- suffix += 1;
1546
- continue;
1547
- }
1548
- try {
1549
- job = await this.store.createGarbageCollectionJob({
1550
- id,
1551
- candidateManifestVersions: [],
1552
- candidateSegmentIds: [...segmentCandidates],
1553
- candidateBlockIds: [...candidates],
1554
- leaseCutoff: timestamp,
1555
- createdAt: timestamp,
1556
- });
1557
- break;
1558
- }
1559
- catch (error) {
1560
- if ((await this.store.getGarbageCollectionJob(id)) === undefined)
1561
- throw error;
1562
- suffix += 1;
1563
- }
1564
- }
1565
- while (job.state !== "completed") {
1566
- try {
1567
- const step = await this.store.runGarbageCollectionStep({
1568
- jobId: job.id,
1569
- expectedRevision: job.revision,
1570
- maxItems: 128,
1571
- updatedAt: dateIsoString(this.#now()),
1572
- });
1573
- removedBlockIds.push(...step.reclaimedBlockIds);
1574
- retainedBlockIds.push(...step.retainedBlockIds);
1575
- removedSegmentIds.push(...step.reclaimedSegmentIds);
1576
- retainedSegmentIds.push(...step.retainedSegmentIds);
1577
- job = step.job;
1578
- }
1579
- catch (error) {
1580
- if (!(error instanceof GarbageCollectionJobConflictError))
1581
- throw error;
1582
- const latest = await this.store.getGarbageCollectionJob(job.id);
1583
- if (latest === undefined)
1584
- throw new Error(`Garbage collection job not found: ${job.id}`, { cause: error });
1585
- job = latest;
1586
- }
1587
- }
1316
+ try {
1317
+ job = await this.store.createGarbageCollectionJob({
1318
+ id,
1319
+ candidateManifestVersions: [],
1320
+ candidateSegmentIds: [...segmentCandidates],
1321
+ candidateBlockIds: [...candidates],
1322
+ leaseCutoff: timestamp,
1323
+ createdAt: timestamp
1324
+ });
1325
+ break;
1326
+ } catch (error) {
1327
+ if (await this.store.getGarbageCollectionJob(id) === void 0)
1328
+ throw error;
1329
+ suffix += 1;
1588
1330
  }
1589
- return {
1590
- abortedTransactionIds: abortedTransactionIds.sort(),
1591
- skippedTransactionIds: skippedTransactionIds.sort(),
1592
- removedBlockIds,
1593
- retainedBlockIds,
1594
- removedSegmentIds,
1595
- retainedSegmentIds,
1596
- };
1597
- }
1331
+ }
1332
+ while (job.state !== "completed") {
1333
+ try {
1334
+ const step = await this.store.runGarbageCollectionStep({
1335
+ jobId: job.id,
1336
+ expectedRevision: job.revision,
1337
+ maxItems: 128,
1338
+ updatedAt: dateIsoString(this.#now())
1339
+ });
1340
+ removedBlockIds.push(...step.reclaimedBlockIds);
1341
+ retainedBlockIds.push(...step.retainedBlockIds);
1342
+ removedSegmentIds.push(...step.reclaimedSegmentIds);
1343
+ retainedSegmentIds.push(...step.retainedSegmentIds);
1344
+ job = step.job;
1345
+ } catch (error) {
1346
+ if (!(error instanceof GarbageCollectionJobConflictError))
1347
+ throw error;
1348
+ const latest = await this.store.getGarbageCollectionJob(job.id);
1349
+ if (latest === void 0)
1350
+ throw new Error(`Garbage collection job not found: ${job.id}`, { cause: error });
1351
+ job = latest;
1352
+ }
1353
+ }
1354
+ }
1355
+ return {
1356
+ abortedTransactionIds: abortedTransactionIds.sort(),
1357
+ skippedTransactionIds: skippedTransactionIds.sort(),
1358
+ removedBlockIds,
1359
+ retainedBlockIds,
1360
+ removedSegmentIds,
1361
+ retainedSegmentIds
1362
+ };
1363
+ }
1598
1364
  }
1599
1365
  function validateTtl(ttlMs) {
1600
- if (!Number.isFinite(ttlMs) || ttlMs <= 0 || ttlMs > MAX_LEASE_TTL_MS) {
1601
- throw new RangeError(`Lease lifetime must be a positive number no greater than ${String(MAX_LEASE_TTL_MS)} milliseconds`);
1602
- }
1366
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0 || ttlMs > MAX_LEASE_TTL_MS) {
1367
+ throw new RangeError(`Lease lifetime must be a positive number no greater than ${String(MAX_LEASE_TTL_MS)} milliseconds`);
1368
+ }
1603
1369
  }
1604
1370
  function expiryAfter(now, ttlMs) {
1605
- validateTtl(ttlMs);
1606
- const expiresAt = dateMilliseconds(now) + ttlMs;
1607
- if (!Number.isSafeInteger(expiresAt) || expiresAt > 8_640_000_000_000_000) {
1608
- throw new RangeError("Lease expiry is outside the supported Date range");
1609
- }
1610
- return dateIsoString(new Date(expiresAt));
1371
+ validateTtl(ttlMs);
1372
+ const expiresAt = dateMilliseconds(now) + ttlMs;
1373
+ if (!Number.isSafeInteger(expiresAt) || expiresAt > 864e13) {
1374
+ throw new RangeError("Lease expiry is outside the supported Date range");
1375
+ }
1376
+ return dateIsoString(new Date(expiresAt));
1611
1377
  }
1612
1378
  function advanceRevision(revision, amount) {
1613
- if (!Number.isSafeInteger(revision) ||
1614
- revision < 0 ||
1615
- revision > Number.MAX_SAFE_INTEGER - amount) {
1616
- throw new RangeError("Transaction revision is outside the safe integer range");
1617
- }
1618
- return revision + amount;
1379
+ if (!Number.isSafeInteger(revision) || revision < 0 || revision > Number.MAX_SAFE_INTEGER - amount) {
1380
+ throw new RangeError("Transaction revision is outside the safe integer range");
1381
+ }
1382
+ return revision + amount;
1619
1383
  }
1620
1384
  function checkpointByteSum(total, addition) {
1621
- const next = total + addition;
1622
- if (!Number.isSafeInteger(next)) {
1623
- throw new RangeError("Transaction checkpoint size exceeds the safe integer range");
1624
- }
1625
- return next;
1385
+ const next = total + addition;
1386
+ if (!Number.isSafeInteger(next)) {
1387
+ throw new RangeError("Transaction checkpoint size exceeds the safe integer range");
1388
+ }
1389
+ return next;
1626
1390
  }
1627
1391
  function safeWholeNumberSum(values, label) {
1628
- const total = values.reduce((sum, value) => sum + value, 0);
1629
- if (!Number.isSafeInteger(total) || total < 0) {
1630
- throw new RangeError(`${label} exceeds the safe integer range`);
1631
- }
1632
- return total;
1392
+ const total = values.reduce((sum, value) => sum + value, 0);
1393
+ if (!Number.isSafeInteger(total) || total < 0) {
1394
+ throw new RangeError(`${label} exceeds the safe integer range`);
1395
+ }
1396
+ return total;
1633
1397
  }
1634
- /** Linear merge of two canonical postings for one term. */
1635
1398
  function mergeFtsPostingRows(left, right) {
1636
- if (left.term !== right.term)
1637
- throw new TypeError("Posting terms differ during transaction merge");
1638
- const rowIds = [];
1639
- const tf = [];
1640
- let leftIndex = 0;
1641
- let rightIndex = 0;
1642
- const push = (rowId, frequency) => {
1643
- const previous = rowIds[rowIds.length - 1];
1644
- if (previous === rowId) {
1645
- tf[tf.length - 1] = Math.max(tf[tf.length - 1] ?? 1, frequency);
1646
- return;
1647
- }
1648
- rowIds.push(rowId);
1649
- tf.push(frequency);
1650
- };
1651
- while (leftIndex < left.rowIds.length && rightIndex < right.rowIds.length) {
1652
- const leftRowId = left.rowIds[leftIndex] ?? 0n;
1653
- const rightRowId = right.rowIds[rightIndex] ?? 0n;
1654
- if (leftRowId <= rightRowId) {
1655
- push(leftRowId, left.tf[leftIndex] ?? 1);
1656
- leftIndex += 1;
1657
- }
1658
- if (rightRowId <= leftRowId) {
1659
- push(rightRowId, right.tf[rightIndex] ?? 1);
1660
- rightIndex += 1;
1661
- }
1662
- }
1663
- while (leftIndex < left.rowIds.length) {
1664
- push(left.rowIds[leftIndex] ?? 0n, left.tf[leftIndex] ?? 1);
1665
- leftIndex += 1;
1666
- }
1667
- while (rightIndex < right.rowIds.length) {
1668
- push(right.rowIds[rightIndex] ?? 0n, right.tf[rightIndex] ?? 1);
1669
- rightIndex += 1;
1670
- }
1671
- return { term: left.term, rowIds, tf };
1399
+ if (left.term !== right.term)
1400
+ throw new TypeError("Posting terms differ during transaction merge");
1401
+ const rowIds = [];
1402
+ const tf = [];
1403
+ let leftIndex = 0;
1404
+ let rightIndex = 0;
1405
+ const push = (rowId, frequency) => {
1406
+ const previous = rowIds[rowIds.length - 1];
1407
+ if (previous === rowId) {
1408
+ tf[tf.length - 1] = Math.max(tf[tf.length - 1] ?? 1, frequency);
1409
+ return;
1410
+ }
1411
+ rowIds.push(rowId);
1412
+ tf.push(frequency);
1413
+ };
1414
+ while (leftIndex < left.rowIds.length && rightIndex < right.rowIds.length) {
1415
+ const leftRowId = left.rowIds[leftIndex] ?? 0n;
1416
+ const rightRowId = right.rowIds[rightIndex] ?? 0n;
1417
+ if (leftRowId <= rightRowId) {
1418
+ push(leftRowId, left.tf[leftIndex] ?? 1);
1419
+ leftIndex += 1;
1420
+ }
1421
+ if (rightRowId <= leftRowId) {
1422
+ push(rightRowId, right.tf[rightIndex] ?? 1);
1423
+ rightIndex += 1;
1424
+ }
1425
+ }
1426
+ while (leftIndex < left.rowIds.length) {
1427
+ push(left.rowIds[leftIndex] ?? 0n, left.tf[leftIndex] ?? 1);
1428
+ leftIndex += 1;
1429
+ }
1430
+ while (rightIndex < right.rowIds.length) {
1431
+ push(right.rowIds[rightIndex] ?? 0n, right.tf[rightIndex] ?? 1);
1432
+ rightIndex += 1;
1433
+ }
1434
+ return { term: left.term, rowIds, tf };
1672
1435
  }
1673
1436
  function postingTokenCount(posting) {
1674
- return safeWholeNumberSum(posting.tf, "Posting token count");
1437
+ return safeWholeNumberSum(posting.tf, "Posting token count");
1675
1438
  }
1676
1439
  function requiredCompactionJobId(id) {
1677
- if (id === null)
1678
- throw new Error("Compaction source blocks require a compaction job");
1679
- return id;
1440
+ if (id === null)
1441
+ throw new Error("Compaction source blocks require a compaction job");
1442
+ return id;
1680
1443
  }
1681
1444
  function sameSegmentArtifacts(left, right) {
1682
- const leftColumns = Object.keys(left.columnBlockIds).sort();
1683
- const rightColumns = Object.keys(right.columnBlockIds).sort();
1684
- const leftSpans = left.rowIdSpans;
1685
- const rightSpans = right.rowIdSpans;
1686
- return (left.id === right.id &&
1687
- left.tableId === right.tableId &&
1688
- left.rowCount === right.rowCount &&
1689
- left.rowIdStart === right.rowIdStart &&
1690
- left.rowIdEndExclusive === right.rowIdEndExclusive &&
1691
- left.kind === right.kind &&
1692
- left.keyColumnId === right.keyColumnId &&
1693
- left.level === right.level &&
1694
- left.logicalOrder === right.logicalOrder &&
1695
- left.commitOrdinal === right.commitOrdinal &&
1696
- left.partitionOrdinal === right.partitionOrdinal &&
1697
- left.createdAt === right.createdAt &&
1698
- leftColumns.length === rightColumns.length &&
1699
- leftColumns.every((columnId, index) => {
1700
- if (columnId !== rightColumns[index])
1701
- return false;
1702
- const leftIds = left.columnBlockIds[columnId] ?? [];
1703
- const rightIds = right.columnBlockIds[columnId] ?? [];
1704
- return (leftIds.length === rightIds.length && leftIds.every((id, item) => id === rightIds[item]));
1705
- }) &&
1706
- leftSpans.length === rightSpans.length &&
1707
- leftSpans.every((span, index) => {
1708
- const other = rightSpans[index];
1709
- return (span.rowStart === other?.rowStart &&
1710
- span.rowCount === other.rowCount &&
1711
- span.rowIdStart === other.rowIdStart);
1712
- }));
1445
+ const leftColumns = Object.keys(left.columnBlockIds).sort();
1446
+ const rightColumns = Object.keys(right.columnBlockIds).sort();
1447
+ const leftSpans = left.rowIdSpans;
1448
+ const rightSpans = right.rowIdSpans;
1449
+ return left.id === right.id && left.tableId === right.tableId && left.rowCount === right.rowCount && left.rowIdStart === right.rowIdStart && left.rowIdEndExclusive === right.rowIdEndExclusive && left.kind === right.kind && left.keyColumnId === right.keyColumnId && left.level === right.level && left.logicalOrder === right.logicalOrder && left.commitOrdinal === right.commitOrdinal && left.partitionOrdinal === right.partitionOrdinal && left.createdAt === right.createdAt && leftColumns.length === rightColumns.length && leftColumns.every((columnId, index) => {
1450
+ if (columnId !== rightColumns[index])
1451
+ return false;
1452
+ const leftIds = left.columnBlockIds[columnId] ?? [];
1453
+ const rightIds = right.columnBlockIds[columnId] ?? [];
1454
+ return leftIds.length === rightIds.length && leftIds.every((id, item) => id === rightIds[item]);
1455
+ }) && leftSpans.length === rightSpans.length && leftSpans.every((span, index) => {
1456
+ const other = rightSpans[index];
1457
+ return span.rowStart === other?.rowStart && span.rowCount === other.rowCount && span.rowIdStart === other.rowIdStart;
1458
+ });
1713
1459
  }
1714
1460
  function transactionBlockBatches(blocks) {
1715
- const batches = [];
1716
- let batch = [];
1717
- let batchBytes = 0;
1718
- for (const block of blocks) {
1719
- assertTransactionArtifactBatchLimits([block], []);
1720
- if (batch.length === MAX_TRANSACTION_STAGE_BLOCKS ||
1721
- batchBytes + block.bytes.byteLength > MAX_TRANSACTION_STAGE_BYTES) {
1722
- batches.push(batch);
1723
- batch = [];
1724
- batchBytes = 0;
1725
- }
1726
- batch.push(block);
1727
- batchBytes += block.bytes.byteLength;
1728
- }
1729
- if (batch.length > 0)
1730
- batches.push(batch);
1731
- return batches;
1461
+ const batches = [];
1462
+ let batch = [];
1463
+ let batchBytes = 0;
1464
+ for (const block of blocks) {
1465
+ assertTransactionArtifactBatchLimits([block], []);
1466
+ if (batch.length === MAX_TRANSACTION_STAGE_BLOCKS || batchBytes + block.bytes.byteLength > MAX_TRANSACTION_STAGE_BYTES) {
1467
+ batches.push(batch);
1468
+ batch = [];
1469
+ batchBytes = 0;
1470
+ }
1471
+ batch.push(block);
1472
+ batchBytes += block.bytes.byteLength;
1473
+ }
1474
+ if (batch.length > 0)
1475
+ batches.push(batch);
1476
+ return batches;
1732
1477
  }
1733
1478
  function transactionArtifactBatches(blocks, segments) {
1734
- let blockBytes = 0;
1735
- for (const block of blocks) {
1736
- assertTransactionArtifactBatchLimits([block], []);
1737
- blockBytes += block.bytes.byteLength;
1738
- }
1739
- if (blocks.length <= MAX_TRANSACTION_STAGE_BLOCKS &&
1740
- segments.length <= MAX_TRANSACTION_STAGE_SEGMENTS &&
1741
- blockBytes <= MAX_TRANSACTION_STAGE_BYTES) {
1742
- return [{ blocks, segments }];
1743
- }
1744
- const batches = transactionBlockBatches(blocks).map((batch) => ({
1745
- blocks: batch,
1746
- segments: [],
1747
- }));
1748
- for (let start = 0; start < segments.length; start += MAX_TRANSACTION_STAGE_SEGMENTS) {
1749
- batches.push({
1750
- blocks: [],
1751
- segments: segments.slice(start, start + MAX_TRANSACTION_STAGE_SEGMENTS),
1752
- });
1753
- }
1754
- return batches;
1479
+ let blockBytes = 0;
1480
+ for (const block of blocks) {
1481
+ assertTransactionArtifactBatchLimits([block], []);
1482
+ blockBytes += block.bytes.byteLength;
1483
+ }
1484
+ if (blocks.length <= MAX_TRANSACTION_STAGE_BLOCKS && segments.length <= MAX_TRANSACTION_STAGE_SEGMENTS && blockBytes <= MAX_TRANSACTION_STAGE_BYTES) {
1485
+ return [{ blocks, segments }];
1486
+ }
1487
+ const batches = transactionBlockBatches(blocks).map((batch) => ({
1488
+ blocks: batch,
1489
+ segments: []
1490
+ }));
1491
+ for (let start = 0; start < segments.length; start += MAX_TRANSACTION_STAGE_SEGMENTS) {
1492
+ batches.push({
1493
+ blocks: [],
1494
+ segments: segments.slice(start, start + MAX_TRANSACTION_STAGE_SEGMENTS)
1495
+ });
1496
+ }
1497
+ return batches;
1755
1498
  }
1756
1499
  async function loadSnapshot(store, version) {
1757
- if (version === null)
1758
- return new Snapshot(store, null);
1759
- const manifest = await store.getManifest(version);
1760
- if (manifest === undefined || manifest.prunedAt !== undefined) {
1761
- throw new SnapshotManifestMissingError(version);
1762
- }
1763
- return new Snapshot(store, manifest.version);
1500
+ if (version === null)
1501
+ return new Snapshot(store, null);
1502
+ const manifest = await store.getManifest(version);
1503
+ if (manifest === void 0 || manifest.prunedAt !== void 0) {
1504
+ throw new SnapshotManifestMissingError(version);
1505
+ }
1506
+ return new Snapshot(store, manifest.version);
1764
1507
  }
1508
+ export {
1509
+ DEFAULT_TRANSACTION_TTL_MS,
1510
+ DatabaseTransaction,
1511
+ LeasedSnapshot,
1512
+ MAX_LEASE_TTL_MS,
1513
+ Snapshot,
1514
+ TransactionClosedError,
1515
+ TransactionManager
1516
+ };