@minnowdb/core 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +12 -9
  2. package/dist/engine/client.d.ts +9 -1
  3. package/dist/engine/client.d.ts.map +1 -1
  4. package/dist/engine/client.js +29 -5
  5. package/dist/engine/client.js.map +1 -1
  6. package/dist/engine/database.d.ts +51 -6
  7. package/dist/engine/database.d.ts.map +1 -1
  8. package/dist/engine/database.js +2597 -865
  9. package/dist/engine/database.js.map +1 -1
  10. package/dist/engine/live.d.ts.map +1 -1
  11. package/dist/engine/live.js +35 -11
  12. package/dist/engine/live.js.map +1 -1
  13. package/dist/engine/query-cache.d.ts +13 -1
  14. package/dist/engine/query-cache.d.ts.map +1 -1
  15. package/dist/engine/query-cache.js +34 -5
  16. package/dist/engine/query-cache.js.map +1 -1
  17. package/dist/engine/query.d.ts +11 -1
  18. package/dist/engine/query.d.ts.map +1 -1
  19. package/dist/engine/query.js +49 -41
  20. package/dist/engine/query.js.map +1 -1
  21. package/dist/engine/result-wire.d.ts +68 -0
  22. package/dist/engine/result-wire.d.ts.map +1 -0
  23. package/dist/engine/result-wire.js +264 -0
  24. package/dist/engine/result-wire.js.map +1 -0
  25. package/dist/engine/sort-keys.d.ts +50 -12
  26. package/dist/engine/sort-keys.d.ts.map +1 -1
  27. package/dist/engine/sort-keys.js +371 -25
  28. package/dist/engine/sort-keys.js.map +1 -1
  29. package/dist/engine/vector.d.ts +11 -0
  30. package/dist/engine/vector.d.ts.map +1 -1
  31. package/dist/engine/vector.js +99 -203
  32. package/dist/engine/vector.js.map +1 -1
  33. package/dist/engine/worker-host.d.ts +17 -1
  34. package/dist/engine/worker-host.d.ts.map +1 -1
  35. package/dist/engine/worker-host.js +83 -13
  36. package/dist/engine/worker-host.js.map +1 -1
  37. package/dist/storage/index.d.ts +13 -0
  38. package/dist/storage/index.d.ts.map +1 -1
  39. package/dist/storage/index.js +13 -0
  40. package/dist/storage/index.js.map +1 -1
  41. package/dist/storage/indexeddb.d.ts +5 -1
  42. package/dist/storage/indexeddb.d.ts.map +1 -1
  43. package/dist/storage/indexeddb.js +484 -402
  44. package/dist/storage/indexeddb.js.map +1 -1
  45. package/dist/storage/memory.d.ts +24 -21
  46. package/dist/storage/memory.d.ts.map +1 -1
  47. package/dist/storage/memory.js +124 -1199
  48. package/dist/storage/memory.js.map +1 -1
  49. package/dist/storage/opfs/files.d.ts +64 -0
  50. package/dist/storage/opfs/files.d.ts.map +1 -0
  51. package/dist/storage/opfs/files.js +243 -0
  52. package/dist/storage/opfs/files.js.map +1 -0
  53. package/dist/storage/opfs/index.d.ts +3 -0
  54. package/dist/storage/opfs/index.d.ts.map +1 -0
  55. package/dist/storage/opfs/index.js +3 -0
  56. package/dist/storage/opfs/index.js.map +1 -0
  57. package/dist/storage/opfs/leader.d.ts +130 -0
  58. package/dist/storage/opfs/leader.d.ts.map +1 -0
  59. package/dist/storage/opfs/leader.js +1104 -0
  60. package/dist/storage/opfs/leader.js.map +1 -0
  61. package/dist/storage/opfs/rpc.d.ts +70 -0
  62. package/dist/storage/opfs/rpc.d.ts.map +1 -0
  63. package/dist/storage/opfs/rpc.js +58 -0
  64. package/dist/storage/opfs/rpc.js.map +1 -0
  65. package/dist/storage/opfs/store.d.ts +154 -0
  66. package/dist/storage/opfs/store.d.ts.map +1 -0
  67. package/dist/storage/opfs/store.js +958 -0
  68. package/dist/storage/opfs/store.js.map +1 -0
  69. package/dist/storage/toolkit/extents.d.ts +69 -0
  70. package/dist/storage/toolkit/extents.d.ts.map +1 -0
  71. package/dist/storage/toolkit/extents.js +175 -0
  72. package/dist/storage/toolkit/extents.js.map +1 -0
  73. package/dist/storage/toolkit/index.d.ts +30 -0
  74. package/dist/storage/toolkit/index.d.ts.map +1 -0
  75. package/dist/storage/toolkit/index.js +5 -0
  76. package/dist/storage/toolkit/index.js.map +1 -0
  77. package/dist/storage/toolkit/record-core.d.ts +252 -0
  78. package/dist/storage/toolkit/record-core.d.ts.map +1 -0
  79. package/dist/storage/toolkit/record-core.js +1670 -0
  80. package/dist/storage/toolkit/record-core.js.map +1 -0
  81. package/dist/storage/toolkit/sync-file.d.ts +29 -0
  82. package/dist/storage/toolkit/sync-file.d.ts.map +1 -0
  83. package/dist/storage/toolkit/sync-file.js +2 -0
  84. package/dist/storage/toolkit/sync-file.js.map +1 -0
  85. package/dist/storage/toolkit/wal.d.ts +22 -0
  86. package/dist/storage/toolkit/wal.d.ts.map +1 -0
  87. package/dist/storage/toolkit/wal.js +79 -0
  88. package/dist/storage/toolkit/wal.js.map +1 -0
  89. package/dist/storage/toolkit/wire.d.ts +25 -0
  90. package/dist/storage/toolkit/wire.d.ts.map +1 -0
  91. package/dist/storage/toolkit/wire.js +100 -0
  92. package/dist/storage/toolkit/wire.js.map +1 -0
  93. package/dist/storage/types.d.ts +343 -59
  94. package/dist/storage/types.d.ts.map +1 -1
  95. package/dist/storage/types.js +112 -11
  96. package/dist/storage/types.js.map +1 -1
  97. package/dist/testing/block-store-conformance.d.ts +51 -0
  98. package/dist/testing/block-store-conformance.d.ts.map +1 -0
  99. package/dist/testing/block-store-conformance.js +745 -0
  100. package/dist/testing/block-store-conformance.js.map +1 -0
  101. package/dist/testing/index.d.ts +3 -0
  102. package/dist/testing/index.d.ts.map +1 -1
  103. package/dist/testing/index.js +5 -0
  104. package/dist/testing/index.js.map +1 -1
  105. package/dist/testing/opfs-shim.d.ts +35 -0
  106. package/dist/testing/opfs-shim.d.ts.map +1 -0
  107. package/dist/testing/opfs-shim.js +295 -0
  108. package/dist/testing/opfs-shim.js.map +1 -0
  109. package/dist/transactions/index.d.ts +42 -1
  110. package/dist/transactions/index.d.ts.map +1 -1
  111. package/dist/transactions/index.js +219 -13
  112. package/dist/transactions/index.js.map +1 -1
  113. package/dist/worker-protocol/index.d.ts +2 -2
  114. package/dist/worker-protocol/index.d.ts.map +1 -1
  115. package/dist/worker-protocol/index.js +2 -1
  116. package/dist/worker-protocol/index.js.map +1 -1
  117. package/package.json +10 -2
@@ -1,25 +1,17 @@
1
- import { CompactionJobConflictError, createManifest, createGarbageCollectionJobRecord, advanceGarbageCollectionJobRecord, collectFtsCandidates, invalidateUncoveredFtsColumns, GarbageCollectionJobConflictError, LeaseConflictError, SnapshotManifestMissingError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyConflictError, normalizeCompactionJobRecord, normalizeSegmentRecord, updateCompactionJobRecord, updateTransactionRecord, WriteConflictError, } from "./types.js";
2
- import { selectLiveRecords, } from "./snapshot.js";
1
+ import { RecordCore, validateId, validateTempRunPage, validateTempRunPageIdentity, } from "./toolkit/record-core.js";
2
+ /**
3
+ * The in-process store: record semantics live in `RecordCore` (shared with the OPFS store),
4
+ * block and temp-page bytes live in Maps here, and atomicity comes from running every mutating
5
+ * record operation on a promise-chain queue — each queued body is synchronous, so no operation
6
+ * ever observes another mid-mutation.
7
+ */
3
8
  export class MemoryBlockStore {
4
9
  #blocks = new Map();
5
- #manifests = new Map();
6
- #transactions = new Map();
7
- #tables = new Map();
8
- #tableIdsByName = new Map();
9
- #segments = new Map();
10
- #leases = new Map();
11
- #compactionJobs = new Map();
12
- #garbageCollectionJobs = new Map();
13
- #nextRowIds = new Map();
14
- #nextAutoIncrement = new Map();
15
- #ftsBases = new Map();
16
- #ftsDeltas = new Map();
17
- #uniqueKeys = new Map();
18
10
  #tempRunPages = new Map();
19
- #tempOwners = new Map();
20
- #currentVersion = null;
21
- /** Advances on every catalog mutation; see `CatalogProbe` for the freshness contract. */
22
- #catalogEpoch = 0;
11
+ #core = new RecordCore({
12
+ hasBlock: (id) => this.#blocks.has(id),
13
+ blockByteLength: (id) => this.#blocks.get(id)?.byteLength,
14
+ });
23
15
  #commitQueue = Promise.resolve();
24
16
  async addBlock(id, bytes) {
25
17
  return this.addBlocks([{ id, bytes }]);
@@ -56,6 +48,13 @@ export class MemoryBlockStore {
56
48
  validateTempRunPage(page);
57
49
  this.#tempRunPages.set(tempRunPageKey(page.ownerId, page.runId, page.pageIndex), page.bytes.slice());
58
50
  }
51
+ async putTempRunPages(pages) {
52
+ for (const page of pages)
53
+ validateTempRunPage(page);
54
+ for (const page of pages) {
55
+ this.#tempRunPages.set(tempRunPageKey(page.ownerId, page.runId, page.pageIndex), page.bytes.slice());
56
+ }
57
+ }
59
58
  async getTempRunPage(ownerId, runId, pageIndex) {
60
59
  validateTempRunPageIdentity(ownerId, runId, pageIndex);
61
60
  return this.#tempRunPages.get(tempRunPageKey(ownerId, runId, pageIndex))?.slice();
@@ -69,967 +68,255 @@ export class MemoryBlockStore {
69
68
  }
70
69
  async removeTempOwner(ownerId) {
71
70
  validateId(ownerId);
72
- const prefix = `${String(ownerId.length)}:${ownerId}:`;
73
- for (const key of this.#tempRunPages.keys())
74
- if (key.startsWith(prefix))
75
- this.#tempRunPages.delete(key);
76
- this.#tempOwners.delete(ownerId);
71
+ this.#removeTempOwnerPages(ownerId);
72
+ this.#core.removeTempOwner(ownerId);
77
73
  }
78
74
  async createTempOwner(record) {
79
- validateTempOwnerRecord(record);
80
75
  return this.#runAtomic(() => {
81
- if (this.#tempOwners.has(record.ownerId)) {
82
- throw new Error(`Temp owner already exists: ${record.ownerId}`);
83
- }
84
- this.#tempOwners.set(record.ownerId, structuredClone(record));
76
+ this.#core.createTempOwner(record);
85
77
  });
86
78
  }
87
79
  async getTempOwner(ownerId) {
88
- validateId(ownerId);
89
- const record = this.#tempOwners.get(ownerId);
90
- return record === undefined ? undefined : structuredClone(record);
80
+ return this.#core.getTempOwner(ownerId);
91
81
  }
92
82
  async renewTempOwner(ownerId, expectedRevision, expiresAt) {
93
- validateLeaseExpiration(expiresAt);
94
- return this.#runAtomic(() => {
95
- const record = this.#tempOwners.get(ownerId);
96
- if (record?.revision !== expectedRevision) {
97
- throw new TempOwnerConflictError(ownerId, expectedRevision, record?.revision ?? null);
98
- }
99
- const renewed = { ...record, expiresAt, revision: record.revision + 1 };
100
- this.#tempOwners.set(ownerId, renewed);
101
- return structuredClone(renewed);
102
- });
83
+ return this.#runAtomic(() => this.#core.renewTempOwner(ownerId, expectedRevision, expiresAt));
103
84
  }
104
85
  async removeTempOwnerIfExpired(ownerId, expiresAtCutoff) {
105
- validateId(ownerId);
106
- const cutoff = Date.parse(expiresAtCutoff);
107
- if (!Number.isFinite(cutoff))
108
- throw new TypeError("Temp owner expiry cutoff must be valid");
109
86
  return this.#runAtomic(() => {
110
- const record = this.#tempOwners.get(ownerId);
111
- if (record !== undefined) {
112
- const expiresAt = Date.parse(record.expiresAt);
113
- if (Number.isFinite(expiresAt) && expiresAt > cutoff)
114
- return false;
115
- }
116
- const prefix = `${String(ownerId.length)}:${ownerId}:`;
117
- for (const key of this.#tempRunPages.keys())
118
- if (key.startsWith(prefix))
119
- this.#tempRunPages.delete(key);
120
- this.#tempOwners.delete(ownerId);
121
- return true;
87
+ const removed = this.#core.removeTempOwnerIfExpired(ownerId, expiresAtCutoff);
88
+ if (removed)
89
+ this.#removeTempOwnerPages(ownerId);
90
+ return removed;
122
91
  });
123
92
  }
124
93
  async listTempOwnerIdsPage(afterOwnerId, limit) {
125
- validatePageLimit(limit);
126
- const ownerIds = new Set(this.#tempOwners.keys());
94
+ const pageOwnerIds = [];
127
95
  for (const key of this.#tempRunPages.keys()) {
128
96
  const separator = key.indexOf(":");
129
97
  const length = Number(key.slice(0, separator));
130
- ownerIds.add(key.slice(separator + 1, separator + 1 + length));
98
+ pageOwnerIds.push(key.slice(separator + 1, separator + 1 + length));
131
99
  }
132
- const sorted = [...ownerIds]
133
- .filter((ownerId) => afterOwnerId === null || ownerId > afterOwnerId)
134
- .sort();
135
- const records = sorted.slice(0, limit);
136
- return {
137
- records,
138
- nextCursor: sorted.length > limit ? (records[records.length - 1] ?? null) : null,
139
- };
100
+ return this.#core.listTempOwnerIdsPage(afterOwnerId, limit, pageOwnerIds);
101
+ }
102
+ #removeTempOwnerPages(ownerId) {
103
+ const prefix = `${String(ownerId.length)}:${ownerId}:`;
104
+ for (const key of this.#tempRunPages.keys())
105
+ if (key.startsWith(prefix))
106
+ this.#tempRunPages.delete(key);
140
107
  }
141
108
  async addTable(record) {
142
- if (this.#tables.has(record.id))
143
- throw new Error(`Table already exists: ${record.id}`);
144
- if (this.#tableIdsByName.has(record.name))
145
- throw new Error(`Table name already exists: ${record.name}`);
146
- this.#tables.set(record.id, structuredClone(record));
147
- this.#tableIdsByName.set(record.name, record.id);
148
- this.#catalogEpoch += 1;
109
+ this.#core.addTable(record);
149
110
  }
150
111
  async getTable(id) {
151
- const record = this.#tables.get(id);
152
- return record === undefined ? undefined : structuredClone(record);
112
+ return this.#core.getTable(id);
153
113
  }
154
114
  async updateTable(id, expectedRevision, update) {
155
- return this.#runAtomic(() => {
156
- const record = this.#tables.get(id);
157
- const actualRevision = record === undefined ? null : (record.revision ?? 0);
158
- if (record === undefined || actualRevision !== expectedRevision) {
159
- throw new TableRecordConflictError(id, expectedRevision, actualRevision);
160
- }
161
- if (update.columns !== undefined)
162
- validateTableColumns(update.columns);
163
- const { ftsColumns: previousFts, triggers: previousTriggers, ...base } = record;
164
- const nextFts = update.ftsColumns === undefined ? previousFts : update.ftsColumns;
165
- const nextTriggers = update.triggers === undefined ? previousTriggers : update.triggers;
166
- const updated = {
167
- ...base,
168
- columns: update.columns === undefined ? record.columns : structuredClone(update.columns),
169
- ...(nextFts === null || nextFts === undefined
170
- ? {}
171
- : { ftsColumns: structuredClone(nextFts) }),
172
- ...(nextTriggers === null || nextTriggers === undefined
173
- ? {}
174
- : { triggers: structuredClone(nextTriggers) }),
175
- revision: expectedRevision + 1,
176
- };
177
- this.#tables.set(id, updated);
178
- this.#catalogEpoch += 1;
179
- return structuredClone(updated);
180
- });
115
+ return this.#runAtomic(() => this.#core.updateTable(id, expectedRevision, update));
181
116
  }
182
117
  async removeTable(id, expectedRevision) {
183
118
  return this.#runAtomic(() => {
184
- const record = this.#tables.get(id);
185
- const actualRevision = record === undefined ? null : (record.revision ?? 0);
186
- if (record === undefined || actualRevision !== expectedRevision) {
187
- throw new TableRecordConflictError(id, expectedRevision, actualRevision);
188
- }
189
- for (const [segmentId, segment] of this.#segments) {
190
- if (segment.tableId === id)
191
- this.#segments.delete(segmentId);
192
- }
193
- const owned = `${id}/`;
194
- for (const key of [...this.#ftsBases.keys()]) {
195
- if (key.startsWith(owned))
196
- this.#ftsBases.delete(key);
197
- }
198
- for (const key of [...this.#ftsDeltas.keys()]) {
199
- if (key.startsWith(owned))
200
- this.#ftsDeltas.delete(key);
201
- }
202
- for (const key of [...this.#nextAutoIncrement.keys()]) {
203
- if (key.startsWith(owned))
204
- this.#nextAutoIncrement.delete(key);
205
- }
206
- this.#uniqueKeys.delete(id);
207
- this.#nextRowIds.delete(id);
208
- this.#tableIdsByName.delete(record.name);
209
- this.#tables.delete(id);
210
- this.#catalogEpoch += 1;
119
+ this.#core.removeTable(id, expectedRevision);
211
120
  });
212
121
  }
213
122
  async writeFtsBase(tableId, columnId, input) {
214
123
  return this.#runAtomic(() => {
215
- const key = `${tableId}/${columnId}`;
216
- this.#ftsBases.set(key, structuredClone(input));
217
- const deltas = this.#ftsDeltas.get(key);
218
- if (deltas !== undefined) {
219
- for (const version of [...deltas.keys()]) {
220
- if (version <= input.coversVersion)
221
- deltas.delete(version);
222
- }
223
- }
124
+ this.#core.writeFtsBase(tableId, columnId, input);
224
125
  });
225
126
  }
226
127
  async readFtsCandidates(tableId, columnId, terms, upToVersion) {
227
- const key = `${tableId}/${columnId}`;
228
- const base = this.#ftsBases.get(key);
229
- const deltas = this.#ftsDeltas.get(key) ??
230
- new Map();
231
- const chunkLists = [...(base?.chunks ?? [])];
232
- let deltaChunkCount = 0;
233
- let totalTokens = base?.totalTokens ?? 0;
234
- for (const [version, delta] of deltas) {
235
- if (version <= (base?.coversVersion ?? -1) || version > upToVersion)
236
- continue;
237
- deltaChunkCount += 1;
238
- totalTokens += delta.totalTokens;
239
- chunkLists.push(delta.postings);
240
- }
241
- return {
242
- ...collectFtsCandidates(chunkLists, terms),
243
- deltaChunkCount,
244
- totalTokens,
245
- coversVersion: base?.coversVersion ?? -1,
246
- };
247
- }
248
- /**
249
- * Applies a commit's full-text deltas, and closes the stale-writer race: a commit adding
250
- * segments to a table with an active index but no delta for one of its columns flips that
251
- * column to "invalid" so a rebuild self-heals instead of the data commit failing.
252
- */
253
- #applyFtsChanges(pendingSegments, changeList, version) {
254
- const changedTableIds = new Set(pendingSegments.map((segment) => segment.tableId));
255
- for (const tableId of changedTableIds) {
256
- const record = this.#tables.get(tableId);
257
- if (record === undefined)
258
- continue;
259
- const forTable = (changeList ?? []).find((entry) => entry.tableId === tableId);
260
- const covered = new Set(forTable?.columns.map((column) => column.columnId) ?? []);
261
- const invalidated = invalidateUncoveredFtsColumns(record, covered);
262
- if (invalidated !== undefined)
263
- this.#tables.set(record.id, invalidated);
264
- }
265
- for (const changes of changeList ?? [])
266
- this.#applyFtsEntry(changes, version);
267
- }
268
- #applyFtsEntry(changes, version) {
269
- for (const column of changes.columns) {
270
- const key = `${changes.tableId}/${column.columnId}`;
271
- const deltas = this.#ftsDeltas.get(key) ??
272
- new Map();
273
- deltas.set(version, {
274
- postings: structuredClone(column.postings),
275
- totalTokens: column.totalTokens,
276
- });
277
- this.#ftsDeltas.set(key, deltas);
278
- }
128
+ return this.#core.readFtsCandidates(tableId, columnId, terms, upToVersion);
279
129
  }
280
130
  async getTableByName(name) {
281
- const id = this.#tableIdsByName.get(name);
131
+ const id = this.#core.getTableIdByName(name);
282
132
  return id === undefined ? undefined : this.getTable(id);
283
133
  }
284
134
  async listTables() {
285
- return [...this.#tables.values()]
286
- .sort((left, right) => left.name.localeCompare(right.name))
287
- .map((record) => structuredClone(record));
135
+ return this.#core.listTables();
288
136
  }
289
137
  async addSegment(record) {
290
- const normalized = normalizeSegmentRecord(record);
291
- if (this.#segments.has(normalized.id)) {
292
- throw new Error(`Segment already exists: ${normalized.id}`);
293
- }
294
- this.#segments.set(normalized.id, normalized);
138
+ this.#core.addSegment(record);
295
139
  }
296
140
  async getSegment(id) {
297
- const record = this.#segments.get(id);
298
- return record === undefined ? undefined : normalizeSegmentRecord(record);
141
+ return this.#core.getSegment(id);
299
142
  }
300
143
  async listSegments(tableId) {
301
- return [...this.#segments.values()]
302
- .filter((record) => tableId === undefined || record.tableId === tableId)
303
- .sort((left, right) => left.id.localeCompare(right.id))
304
- .map((record) => normalizeSegmentRecord(record));
144
+ return this.#core.listSegments(tableId);
305
145
  }
306
146
  async removeSegment(id) {
307
- this.#segments.delete(id);
147
+ this.#core.removeSegment(id);
308
148
  }
309
149
  async reserveRowIds(tableId, count) {
310
- validateCount(count);
311
- const start = this.#nextRowIds.get(tableId) ?? 1n;
312
- const endExclusive = start + BigInt(count);
313
- this.#nextRowIds.set(tableId, endExclusive);
314
- return { start, endExclusive };
150
+ return this.#core.reserveRowIds(tableId, count);
315
151
  }
316
152
  async reserveAutoIncrement(tableId, columnId, count, atLeast) {
317
- validateAutoIncrementReservation(count, atLeast);
318
- return this.#runAtomic(() => this.#reserveAutoIncrement(tableId, columnId, count, atLeast));
319
- }
320
- #reserveAutoIncrement(tableId, columnId, count, atLeast) {
321
- const key = `${tableId}/${columnId}`;
322
- const current = this.#nextAutoIncrement.get(key) ?? 1n;
323
- const floor = atLeast ?? 1n;
324
- const start = current > floor ? current : floor;
325
- const endExclusive = start + BigInt(count);
326
- this.#nextAutoIncrement.set(key, endExclusive);
327
- return { start, endExclusive };
153
+ return this.#runAtomic(() => this.#core.reserveAutoIncrement(tableId, columnId, count, atLeast));
328
154
  }
329
155
  async getExistingUniqueKeys(tableId, keyTokens) {
330
- const existing = this.#uniqueKeys.get(tableId);
331
- if (existing === undefined)
332
- return [];
333
- return [...new Set(keyTokens)].filter((token) => existing.has(token)).sort();
156
+ return this.#core.getExistingUniqueKeys(tableId, keyTokens);
334
157
  }
335
158
  async getCurrentManifestVersion() {
336
- return this.#currentVersion;
159
+ return this.#core.getCurrentManifestVersion();
337
160
  }
338
161
  async getCatalogProbe() {
339
- return { manifestVersion: this.#currentVersion, catalogEpoch: this.#catalogEpoch };
162
+ return this.#core.getCatalogProbe();
340
163
  }
164
+ /**
165
+ * Composed over this store's own methods rather than delegated to the core, so a subclass
166
+ * that instruments `getTransactions` or `listSegments` observes these internal reads too —
167
+ * the engine's read-count tests rely on that.
168
+ */
341
169
  async getQueryCatalogState(tableNames) {
342
170
  const tables = await Promise.all(tableNames.map((name) => this.getTableByName(name)));
343
171
  const foundTableIds = new Set(tables.filter((table) => table !== undefined).map((table) => table.id));
344
172
  const segments = (await this.listSegments()).filter((record) => foundTableIds.has(record.tableId));
345
173
  const transactionIds = [...new Set(segments.map((segment) => segment.transactionId))];
346
174
  const transactions = (await this.getTransactions(transactionIds)).filter((record) => record !== undefined);
175
+ const probe = this.#core.getCatalogProbe();
347
176
  return {
348
- manifestVersion: this.#currentVersion,
177
+ manifestVersion: probe.manifestVersion,
349
178
  tables,
350
179
  segments,
351
180
  transactions,
352
- catalogEpoch: this.#catalogEpoch,
181
+ catalogEpoch: probe.catalogEpoch,
353
182
  };
354
183
  }
355
184
  async getCurrentManifest() {
356
- const manifest = this.#currentVersion === null ? undefined : this.#manifests.get(this.#currentVersion);
357
- return manifest === undefined ? undefined : structuredClone(manifest);
185
+ return this.#core.getCurrentManifest();
358
186
  }
359
187
  async getManifest(version) {
360
- const manifest = this.#manifests.get(version);
361
- return manifest === undefined ? undefined : structuredClone(manifest);
188
+ return this.#core.getManifest(version);
362
189
  }
363
190
  async listManifests() {
364
- return [...this.#manifests.values()]
365
- .sort((left, right) => left.version - right.version)
366
- .map((manifest) => structuredClone(manifest));
191
+ return this.#core.listManifests();
367
192
  }
368
193
  async listManifestPage(afterVersion, limit) {
369
- validatePageLimit(limit);
370
- const records = [...this.#manifests.values()]
371
- .filter((manifest) => afterVersion === null || manifest.version > afterVersion)
372
- .sort((left, right) => left.version - right.version)
373
- .slice(0, limit)
374
- .map((manifest) => structuredClone(manifest));
375
- return {
376
- records,
377
- nextCursor: records.length === limit ? (records.at(-1)?.version ?? null) : null,
378
- };
194
+ return this.#core.listManifestPage(afterVersion, limit);
379
195
  }
380
196
  async publishManifest(input) {
381
- let resolveResult;
382
- let rejectResult;
383
- const result = new Promise((resolve, reject) => {
384
- resolveResult = resolve;
385
- rejectResult = reject;
386
- });
387
- this.#commitQueue = this.#commitQueue.then(() => {
388
- try {
389
- if (this.#currentVersion !== input.expectedVersion) {
390
- throw new WriteConflictError(input.expectedVersion, this.#currentVersion);
391
- }
392
- for (const id of input.blockIds) {
393
- if (!this.#blocks.has(id))
394
- throw new Error(`Manifest references missing block: ${id}`);
395
- }
396
- const manifest = createManifest(input);
397
- this.#manifests.set(manifest.version, manifest);
398
- this.#currentVersion = manifest.version;
399
- this.#catalogEpoch += 1;
400
- resolveResult(structuredClone(manifest));
401
- }
402
- catch (error) {
403
- rejectResult(error);
404
- }
405
- });
406
- return result;
197
+ return this.#runAtomic(() => this.#core.publishManifest(input));
407
198
  }
408
199
  async beginTransaction(input) {
409
- if (input.record.pendingBlockIds.length > 0 || input.record.pendingSegmentIds.length > 0) {
410
- throw new TypeError("A fresh transaction cannot begin with pending artifacts");
411
- }
412
- return this.#runAtomic(() => {
413
- const record = {
414
- ...structuredClone(input.record),
415
- snapshotVersion: this.#currentVersion,
416
- };
417
- assertSnapshotAvailable(record.snapshotVersion, this.#manifests, this.#blocks);
418
- if (this.#transactions.has(record.id)) {
419
- throw new Error(`Transaction already exists: ${record.id}`);
420
- }
421
- this.#transactions.set(record.id, record);
422
- let rowIds;
423
- if (input.reserveRowIds !== undefined) {
424
- validateCount(input.reserveRowIds.count);
425
- const current = this.#nextRowIds.get(input.reserveRowIds.tableId) ?? 1n;
426
- const endExclusive = current + BigInt(input.reserveRowIds.count);
427
- this.#nextRowIds.set(input.reserveRowIds.tableId, endExclusive);
428
- rowIds = { start: current, endExclusive };
429
- }
430
- let autoIncrementValues;
431
- if (input.reserveAutoIncrement !== undefined) {
432
- const { tableId, columnId, count, atLeast } = input.reserveAutoIncrement;
433
- validateAutoIncrementReservation(count, atLeast);
434
- autoIncrementValues = this.#reserveAutoIncrement(tableId, columnId, count, atLeast);
435
- }
436
- return {
437
- record: structuredClone(record),
438
- ...(rowIds === undefined ? {} : { rowIds }),
439
- ...(autoIncrementValues === undefined ? {} : { autoIncrementValues }),
440
- };
441
- });
200
+ return this.#runAtomic(() => this.#core.beginTransaction(input));
442
201
  }
443
202
  async createTransaction(record) {
444
203
  return this.#runAtomic(() => {
445
- if (this.#transactions.has(record.id)) {
446
- throw new Error(`Transaction already exists: ${record.id}`);
447
- }
448
- assertSnapshotAvailable(record.snapshotVersion, this.#manifests, this.#blocks);
449
- assertPendingArtifactsAvailable(record, this.#blocks, this.#segments);
450
- this.#transactions.set(record.id, structuredClone(record));
204
+ this.#core.createTransaction(record);
451
205
  });
452
206
  }
453
207
  async getTransaction(id) {
454
- const record = this.#transactions.get(id);
455
- return record === undefined ? undefined : structuredClone(record);
208
+ return this.#core.getTransaction(id);
456
209
  }
457
210
  async getTransactions(ids) {
458
- return ids.map((id) => {
459
- const record = this.#transactions.get(id);
460
- return record === undefined ? undefined : structuredClone(record);
461
- });
211
+ return this.#core.getTransactions(ids);
462
212
  }
463
213
  async listTransactions() {
464
- return [...this.#transactions.values()]
465
- .sort((left, right) => left.startedAt.localeCompare(right.startedAt) || left.id.localeCompare(right.id))
466
- .map((record) => structuredClone(record));
214
+ return this.#core.listTransactions();
467
215
  }
468
216
  async listTransactionPage(afterId, limit) {
469
- validatePageLimit(limit);
470
- const records = [...this.#transactions.values()]
471
- .filter((record) => afterId === null || record.id > afterId)
472
- .sort((left, right) => left.id.localeCompare(right.id))
473
- .slice(0, limit)
474
- .map((record) => structuredClone(record));
475
- return { records, nextCursor: records.length === limit ? (records.at(-1)?.id ?? null) : null };
217
+ return this.#core.listTransactionPage(afterId, limit);
476
218
  }
477
219
  async updateTransaction(id, expectedRevision, update) {
478
- return this.#runAtomic(() => {
479
- const current = this.#transactions.get(id);
480
- if (current?.revision !== expectedRevision) {
481
- throw new TransactionRecordConflictError(id, expectedRevision, current?.revision ?? null);
482
- }
483
- assertGenericTransactionUpdateAllowed(current, update);
484
- const updated = updateTransactionRecord(current, update);
485
- if (update.snapshotVersion !== undefined) {
486
- assertSnapshotAvailable(updated.snapshotVersion, this.#manifests, this.#blocks);
487
- }
488
- assertPendingArtifactsAvailable(updated, this.#blocks, this.#segments, update.pendingBlockIds !== undefined, update.pendingSegmentIds !== undefined);
489
- this.#transactions.set(id, updated);
490
- return structuredClone(updated);
491
- });
220
+ return this.#runAtomic(() => this.#core.updateTransaction(id, expectedRevision, update));
492
221
  }
493
222
  async stageTransactionArtifacts(input) {
494
223
  return this.#runAtomic(() => {
495
- const ids = new Set();
496
- for (const block of input.blocks) {
497
- validateId(block.id);
498
- if (ids.has(block.id) || this.#blocks.has(block.id)) {
499
- throw new Error(`Block already exists: ${block.id}`);
500
- }
501
- ids.add(block.id);
502
- }
503
- for (const segment of input.segments) {
504
- if (this.#segments.has(segment.id)) {
505
- throw new Error(`Segment already exists: ${segment.id}`);
506
- }
507
- }
508
- const current = this.#transactions.get(input.transactionId);
509
- if (current?.revision !== input.expectedRevision) {
510
- throw new TransactionRecordConflictError(input.transactionId, input.expectedRevision, current?.revision ?? null);
511
- }
512
- const update = {
513
- pendingBlockIds: [...current.pendingBlockIds, ...input.blocks.map((block) => block.id)],
514
- pendingSegmentIds: [
515
- ...current.pendingSegmentIds,
516
- ...input.segments.map((segment) => segment.id),
517
- ],
518
- updatedAt: input.updatedAt,
519
- };
520
- assertGenericTransactionUpdateAllowed(current, update);
521
- const updated = updateTransactionRecord(current, update);
522
- // Only previously journaled artifacts need existence checks; the new ones land with the
523
- // journal update in this same atomic step.
524
- assertPendingArtifactsAvailable(current, this.#blocks, this.#segments, true, true);
224
+ const updated = this.#core.stageTransactionArtifacts(input);
225
+ // The record half validated everything; the bytes land in this same atomic step.
525
226
  for (const block of input.blocks)
526
227
  this.#blocks.set(block.id, new Uint8Array(block.bytes));
527
- for (const segment of input.segments) {
528
- this.#segments.set(segment.id, normalizeSegmentRecord(segment));
529
- }
530
- this.#transactions.set(input.transactionId, updated);
531
- return structuredClone(updated);
228
+ return updated;
532
229
  });
533
230
  }
534
231
  async commitTransaction(input) {
535
- let resolveResult;
536
- let rejectResult;
537
- const result = new Promise((resolve, reject) => {
538
- resolveResult = resolve;
539
- rejectResult = reject;
540
- });
541
- this.#commitQueue = this.#commitQueue.then(() => {
542
- try {
543
- const transaction = this.#transactions.get(input.transactionId);
544
- if (transaction?.revision !== input.expectedTransactionRevision ||
545
- transaction.status !== "active") {
546
- throw new TransactionRecordConflictError(input.transactionId, input.expectedTransactionRevision, transaction?.revision ?? null);
547
- }
548
- if (this.#currentVersion !== input.expectedManifestVersion) {
549
- throw new WriteConflictError(input.expectedManifestVersion, this.#currentVersion);
550
- }
551
- if (transaction.snapshotVersion !== input.expectedManifestVersion) {
552
- throw new Error("Transaction snapshot does not match the expected manifest");
553
- }
554
- const baseManifest = input.expectedManifestVersion === null
555
- ? undefined
556
- : this.#manifests.get(input.expectedManifestVersion);
557
- if (input.expectedManifestVersion !== null && baseManifest === undefined) {
558
- throw new Error(`Snapshot manifest is missing: ${String(input.expectedManifestVersion)}`);
559
- }
560
- const baseBlockIds = baseManifest?.blockIds ?? [];
561
- const removedBlockIds = [...new Set(input.removedBlockIds ?? [])];
562
- const baseBlockIdSet = new Set(baseBlockIds);
563
- for (const id of removedBlockIds) {
564
- if (!baseBlockIdSet.has(id)) {
565
- throw new Error(`Cannot supersede a block outside the transaction snapshot: ${id}`);
566
- }
567
- if (transaction.pendingBlockIds.includes(id)) {
568
- throw new Error(`Cannot supersede a pending block: ${id}`);
569
- }
570
- }
571
- const removedBlockIdSet = new Set(removedBlockIds);
572
- // The published list derives from the stored base plus this commit's delta; the memory
573
- // store keeps full manifests internally since cloning in memory is cheap.
574
- const nextBlockIds = [
575
- ...baseBlockIds.filter((id) => !removedBlockIdSet.has(id)),
576
- ...transaction.pendingBlockIds,
577
- ];
578
- for (const id of transaction.pendingBlockIds) {
579
- if (!this.#blocks.has(id))
580
- throw new Error(`Manifest references missing block: ${id}`);
581
- }
582
- const pendingSegments = transaction.pendingSegmentIds.map((id) => {
583
- const segment = this.#segments.get(id);
584
- if (segment === undefined)
585
- throw new Error(`Transaction references missing segment: ${id}`);
586
- if (segment.transactionId !== transaction.id) {
587
- throw new Error(`Segment ${id} belongs to another transaction`);
588
- }
589
- return segment;
590
- });
591
- const uniqueKeyEntries = input.uniqueKeyChanges ?? [];
592
- {
593
- // Entries apply in operation order over per-table working sets, so a scope that
594
- // inserts the same key twice conflicts exactly like two separate commits would.
595
- const working = new Map();
596
- for (const entry of uniqueKeyEntries) {
597
- let tokens = working.get(entry.tableId);
598
- if (tokens === undefined) {
599
- tokens = new Set(this.#uniqueKeys.get(entry.tableId) ?? []);
600
- working.set(entry.tableId, tokens);
601
- }
602
- for (const token of entry.keyTokens) {
603
- if (entry.remove === true) {
604
- tokens.delete(token);
605
- continue;
606
- }
607
- if (entry.requireAbsent && tokens.has(token)) {
608
- throw new UniqueKeyConflictError(entry.tableId, token);
609
- }
610
- tokens.add(token);
611
- }
612
- }
613
- }
614
- const manifest = createManifest({
615
- expectedVersion: input.expectedManifestVersion,
616
- blockIds: nextBlockIds,
617
- createdAt: input.committedAt,
618
- ...(input.changedTableIds === undefined
619
- ? {}
620
- : { changedTableIds: input.changedTableIds }),
621
- });
622
- const committed = updateTransactionRecord(transaction, {
623
- status: "committed",
624
- committedVersion: manifest.version,
625
- updatedAt: input.committedAt,
626
- });
627
- this.#manifests.set(manifest.version, manifest);
628
- this.#currentVersion = manifest.version;
629
- this.#transactions.set(transaction.id, committed);
630
- for (const segment of pendingSegments) {
631
- this.#segments.set(segment.id, {
632
- ...segment,
633
- level: segment.level ?? 0,
634
- logicalOrder: segment.logicalOrder ?? manifest.version,
635
- });
636
- }
637
- for (const entry of uniqueKeyEntries) {
638
- const existing = this.#uniqueKeys.get(entry.tableId) ?? new Set();
639
- entry.keyTokens.forEach((token) => {
640
- if (entry.remove === true)
641
- existing.delete(token);
642
- else
643
- existing.add(token);
644
- });
645
- this.#uniqueKeys.set(entry.tableId, existing);
646
- }
647
- this.#applyFtsChanges(pendingSegments, input.ftsChanges, manifest.version);
648
- this.#catalogEpoch += 1;
649
- // Match the IndexedDB store's observable commit shape: the summary without blockIds.
650
- const { blockIds: _resolved, ...summary } = manifest;
651
- void _resolved;
652
- resolveResult(structuredClone(summary));
653
- }
654
- catch (error) {
655
- rejectResult(error);
656
- }
232
+ return this.#runAtomic(() => this.#core.commitTransaction(input));
233
+ }
234
+ async writeTransaction(input) {
235
+ return this.#runAtomic(() => {
236
+ const summary = this.#core.writeTransaction(input);
237
+ // The record half validated everything; the bytes land in this same atomic step.
238
+ for (const block of input.blocks)
239
+ this.#blocks.set(block.id, new Uint8Array(block.bytes));
240
+ return summary;
657
241
  });
658
- return result;
659
242
  }
660
243
  async createLease(record) {
661
- validateLeaseExpiration(record.expiresAt);
662
244
  return this.#runAtomic(() => {
663
- if (this.#leases.has(record.id))
664
- throw new Error(`Lease already exists: ${record.id}`);
665
- assertSnapshotAvailable(record.manifestVersion, this.#manifests, this.#blocks);
666
- this.#leases.set(record.id, structuredClone(record));
245
+ this.#core.createLease(record);
667
246
  });
668
247
  }
669
248
  async getLease(id) {
670
- const record = this.#leases.get(id);
671
- return record === undefined ? undefined : structuredClone(record);
249
+ return this.#core.getLease(id);
672
250
  }
673
251
  async listLeases() {
674
- return [...this.#leases.values()]
675
- .sort((left, right) => left.id.localeCompare(right.id))
676
- .map((record) => structuredClone(record));
252
+ return this.#core.listLeases();
677
253
  }
678
254
  async renewLease(id, expectedRevision, expiresAt) {
679
- validateLeaseExpiration(expiresAt);
680
- return this.#runAtomic(() => {
681
- const record = this.#leases.get(id);
682
- if (record?.revision !== expectedRevision) {
683
- throw new LeaseConflictError(id, expectedRevision, record?.revision ?? null);
684
- }
685
- assertSnapshotAvailable(record.manifestVersion, this.#manifests, this.#blocks);
686
- const renewed = { ...record, expiresAt, revision: record.revision + 1 };
687
- this.#leases.set(id, renewed);
688
- return structuredClone(renewed);
689
- });
255
+ return this.#runAtomic(() => this.#core.renewLease(id, expectedRevision, expiresAt));
256
+ }
257
+ async moveLease(id, expectedRevision, manifestVersion, expiresAt) {
258
+ return this.#runAtomic(() => this.#core.moveLease(id, expectedRevision, manifestVersion, expiresAt));
690
259
  }
691
260
  async removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff) {
692
- const cutoff = Date.parse(expiresAtCutoff);
693
- if (!Number.isFinite(cutoff))
694
- throw new TypeError("Lease expiry cutoff must be valid");
695
- return this.#runAtomic(() => {
696
- const record = this.#leases.get(id);
697
- if (record?.revision !== expectedRevision) {
698
- throw new LeaseConflictError(id, expectedRevision, record?.revision ?? null);
699
- }
700
- const expiresAt = Date.parse(record.expiresAt);
701
- if (!Number.isFinite(expiresAt) || expiresAt > cutoff)
702
- return false;
703
- this.#leases.delete(id);
704
- return true;
705
- });
261
+ return this.#runAtomic(() => this.#core.removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff));
706
262
  }
707
263
  async removeLease(id) {
708
264
  return this.#runAtomic(() => {
709
- this.#leases.delete(id);
265
+ this.#core.removeLease(id);
710
266
  });
711
267
  }
712
268
  async createCompactionJob(record) {
713
- const normalized = normalizeCompactionJobRecord(record);
714
- if (this.#compactionJobs.has(normalized.id)) {
715
- throw new Error(`Compaction job already exists: ${normalized.id}`);
716
- }
717
- this.#compactionJobs.set(normalized.id, normalized);
269
+ this.#core.createCompactionJob(record);
718
270
  }
719
271
  async getCompactionJob(id) {
720
- const record = this.#compactionJobs.get(id);
721
- return record === undefined ? undefined : structuredClone(record);
272
+ return this.#core.getCompactionJob(id);
722
273
  }
723
274
  async listCompactionJobs(tableId) {
724
- return [...this.#compactionJobs.values()]
725
- .filter((record) => tableId === undefined || record.tableId === tableId)
726
- .sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id))
727
- .map((record) => structuredClone(record));
275
+ return this.#core.listCompactionJobs(tableId);
728
276
  }
729
277
  async listCompactionJobPage(afterId, limit) {
730
- validatePageLimit(limit);
731
- const records = [...this.#compactionJobs.values()]
732
- .filter((record) => afterId === null || record.id > afterId)
733
- .sort((left, right) => left.id.localeCompare(right.id))
734
- .slice(0, limit)
735
- .map((record) => structuredClone(record));
736
- return { records, nextCursor: records.length === limit ? (records.at(-1)?.id ?? null) : null };
278
+ return this.#core.listCompactionJobPage(afterId, limit);
737
279
  }
738
280
  async updateCompactionJob(id, expectedRevision, update) {
739
- if (update.state === "cancelled") {
740
- throw new TypeError("Use cancelCompactionJob to cancel a compaction job");
741
- }
742
- const current = this.#compactionJobs.get(id);
743
- if (current?.revision !== expectedRevision) {
744
- throw new CompactionJobConflictError(id, expectedRevision, current?.revision ?? null);
745
- }
746
- const updated = updateCompactionJobRecord(current, update);
747
- this.#compactionJobs.set(id, updated);
748
- return structuredClone(updated);
281
+ return this.#core.updateCompactionJob(id, expectedRevision, update);
749
282
  }
750
283
  async cancelCompactionJob(id, expectedRevision, cancelledAt) {
751
- let resolveResult;
752
- let rejectResult;
753
- const result = new Promise((resolve, reject) => {
754
- resolveResult = resolve;
755
- rejectResult = reject;
756
- });
757
- this.#commitQueue = this.#commitQueue.then(() => {
758
- try {
759
- const current = this.#compactionJobs.get(id);
760
- if (current?.revision !== expectedRevision) {
761
- throw new CompactionJobConflictError(id, expectedRevision, current?.revision ?? null);
762
- }
763
- if (isTerminalCompactionJob(current)) {
764
- resolveResult(structuredClone(current));
765
- return;
766
- }
767
- const transaction = current.transactionId === null
768
- ? undefined
769
- : this.#transactions.get(current.transactionId);
770
- if (transaction?.status === "committed") {
771
- if (transaction.committedVersion === null) {
772
- throw new Error(`Committed transaction has no manifest version: ${transaction.id}`);
773
- }
774
- const published = updateCompactionJobRecord(current, {
775
- state: "published",
776
- publishedVersion: transaction.committedVersion,
777
- updatedAt: cancelledAt,
778
- error: null,
779
- });
780
- this.#compactionJobs.set(id, published);
781
- resolveResult(structuredClone(published));
782
- return;
783
- }
784
- const cancelled = updateCompactionJobRecord(current, {
785
- state: "cancelled",
786
- updatedAt: cancelledAt,
787
- error: null,
788
- });
789
- const abortedTransaction = transaction?.status === "active"
790
- ? updateTransactionRecord(transaction, {
791
- status: "aborted",
792
- updatedAt: cancelledAt,
793
- committedVersion: null,
794
- })
795
- : undefined;
796
- if (abortedTransaction !== undefined) {
797
- this.#transactions.set(abortedTransaction.id, abortedTransaction);
798
- }
799
- this.#compactionJobs.set(id, cancelled);
800
- resolveResult(structuredClone(cancelled));
801
- }
802
- catch (error) {
803
- rejectResult(error);
804
- }
805
- });
806
- return result;
284
+ return this.#runAtomic(() => this.#core.cancelCompactionJob(id, expectedRevision, cancelledAt));
807
285
  }
808
286
  async removeCompactionJob(id) {
809
- this.#compactionJobs.delete(id);
287
+ this.#core.removeCompactionJob(id);
810
288
  }
811
289
  async createGarbageCollectionJob(input) {
812
- const record = createGarbageCollectionJobRecord(input);
813
- return this.#runAtomic(() => {
814
- if (this.#garbageCollectionJobs.has(record.id)) {
815
- throw new Error(`Garbage collection job already exists: ${record.id}`);
816
- }
817
- assertGarbageCollectionCandidateProvenance(record, this.#manifests, this.#segments, this.#transactions, this.#compactionJobs);
818
- this.#garbageCollectionJobs.set(record.id, record);
819
- return structuredClone(record);
820
- });
290
+ return this.#runAtomic(() => this.#core.createGarbageCollectionJob(input));
821
291
  }
822
292
  async getGarbageCollectionJob(id) {
823
- const record = this.#garbageCollectionJobs.get(id);
824
- return record === undefined ? undefined : structuredClone(record);
293
+ return this.#core.getGarbageCollectionJob(id);
825
294
  }
826
295
  async listGarbageCollectionJobs() {
827
- return [...this.#garbageCollectionJobs.values()]
828
- .sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id))
829
- .map((record) => structuredClone(record));
296
+ return this.#core.listGarbageCollectionJobs();
830
297
  }
831
298
  async runGarbageCollectionStep(input) {
832
- validateGarbageCollectionStepInput(input);
833
299
  return this.#runAtomic(() => {
834
- const current = this.#garbageCollectionJobs.get(input.jobId);
835
- if (current?.revision !== input.expectedRevision) {
836
- throw new GarbageCollectionJobConflictError(input.jobId, input.expectedRevision, current?.revision ?? null);
837
- }
838
- if (current.state === "completed")
839
- return emptyGarbageCollectionStep(current);
840
- const prunedManifestVersions = [];
841
- const alreadyPrunedManifestVersions = [];
842
- const retainedManifestVersions = [];
843
- const missingManifestVersions = [];
844
- const reclaimedSegmentIds = [];
845
- const retainedSegmentIds = [];
846
- const missingSegmentIds = [];
847
- const reclaimedBlockIds = [];
848
- const retainedBlockIds = [];
849
- const missingBlockIds = [];
850
- let reclaimedBlockBytes = 0;
851
- let remaining = input.maxItems;
852
- const leaseCutoff = Date.parse(current.leaseCutoff);
853
- assertGarbageCollectionPinsAvailable(this.#currentVersion, this.#transactions, this.#leases.values(), this.#compactionJobs.values(), leaseCutoff, this.#manifests, this.#blocks);
854
- let manifestIndex = current.cursor.manifestIndex;
855
- while (remaining > 0 && manifestIndex < current.candidateManifestVersions.length) {
856
- const version = current.candidateManifestVersions[manifestIndex];
857
- if (version === undefined)
858
- throw new Error("Garbage collection manifest cursor is invalid");
859
- const manifest = this.#manifests.get(version);
860
- if (manifest === undefined)
861
- missingManifestVersions.push(version);
862
- else if (manifest.prunedAt !== undefined)
863
- alreadyPrunedManifestVersions.push(version);
864
- else if (isManifestVersionPinned(version, this.#currentVersion, this.#transactions, this.#leases.values(), this.#compactionJobs.values(), leaseCutoff))
865
- retainedManifestVersions.push(version);
866
- else
867
- prunedManifestVersions.push(version);
868
- manifestIndex += 1;
869
- remaining -= 1;
870
- }
871
- const prunedManifestVersionSet = new Set(prunedManifestVersions);
872
- assertRemainingManifestBlocksAvailable(this.#manifests.values(), prunedManifestVersionSet, this.#blocks);
873
- let segmentIndex = current.cursor.segmentIndex;
874
- const segmentIdsToExamine = manifestIndex === current.candidateManifestVersions.length
875
- ? current.candidateSegmentIds.slice(segmentIndex, segmentIndex + remaining)
876
- : [];
877
- const blockCapacity = Math.max(0, remaining - segmentIdsToExamine.length);
878
- const blockIdsToExamine = manifestIndex === current.candidateManifestVersions.length &&
879
- segmentIndex + segmentIdsToExamine.length === current.candidateSegmentIds.length
880
- ? current.candidateBlockIds.slice(current.cursor.blockIndex, current.cursor.blockIndex + blockCapacity)
881
- : [];
882
- const roots = collectBoundedPhysicalRoots(segmentIdsToExamine, blockIdsToExamine, this.#manifests.values(), prunedManifestVersionSet, this.#segments, this.#transactions, this.#compactionJobs.values());
883
- while (remaining > 0 &&
884
- manifestIndex === current.candidateManifestVersions.length &&
885
- segmentIndex < current.candidateSegmentIds.length) {
886
- const id = current.candidateSegmentIds[segmentIndex];
887
- if (id === undefined)
888
- throw new Error("Garbage collection segment cursor is invalid");
889
- if (!this.#segments.has(id))
890
- missingSegmentIds.push(id);
891
- else if (roots.segmentIds.has(id))
892
- retainedSegmentIds.push(id);
893
- else
894
- reclaimedSegmentIds.push(id);
895
- segmentIndex += 1;
896
- remaining -= 1;
897
- }
898
- let blockIndex = current.cursor.blockIndex;
899
- while (remaining > 0 &&
900
- manifestIndex === current.candidateManifestVersions.length &&
901
- segmentIndex === current.candidateSegmentIds.length &&
902
- blockIndex < current.candidateBlockIds.length) {
903
- const id = current.candidateBlockIds[blockIndex];
904
- if (id === undefined)
905
- throw new Error("Garbage collection block cursor is invalid");
906
- const bytes = this.#blocks.get(id);
907
- if (bytes === undefined)
908
- missingBlockIds.push(id);
909
- else if (roots.blockIds.has(id))
910
- retainedBlockIds.push(id);
911
- else {
912
- reclaimedBlockIds.push(id);
913
- reclaimedBlockBytes = safeStorageSum(reclaimedBlockBytes, bytes.byteLength);
914
- }
915
- blockIndex += 1;
916
- remaining -= 1;
917
- }
918
- const updated = advanceGarbageCollectionJobRecord(current, {
919
- examinedManifestCount: prunedManifestVersions.length +
920
- alreadyPrunedManifestVersions.length +
921
- retainedManifestVersions.length +
922
- missingManifestVersions.length,
923
- prunedManifestCount: prunedManifestVersions.length,
924
- alreadyPrunedManifestCount: alreadyPrunedManifestVersions.length,
925
- retainedManifestCount: retainedManifestVersions.length,
926
- missingManifestCount: missingManifestVersions.length,
927
- examinedSegmentCount: reclaimedSegmentIds.length + retainedSegmentIds.length + missingSegmentIds.length,
928
- reclaimedSegmentCount: reclaimedSegmentIds.length,
929
- retainedSegmentCount: retainedSegmentIds.length,
930
- missingSegmentCount: missingSegmentIds.length,
931
- examinedBlockCount: reclaimedBlockIds.length + retainedBlockIds.length + missingBlockIds.length,
932
- reclaimedBlockCount: reclaimedBlockIds.length,
933
- retainedBlockCount: retainedBlockIds.length,
934
- missingBlockCount: missingBlockIds.length,
935
- reclaimedBlockBytes,
936
- updatedAt: input.updatedAt,
937
- });
938
- prunedManifestVersions.forEach((version) => {
939
- const manifest = this.#manifests.get(version);
940
- if (manifest !== undefined)
941
- this.#manifests.set(version, { ...manifest, prunedAt: input.updatedAt });
942
- });
943
- reclaimedSegmentIds.forEach((id) => this.#segments.delete(id));
944
- reclaimedBlockIds.forEach((id) => this.#blocks.delete(id));
945
- this.#garbageCollectionJobs.set(updated.id, updated);
946
- return {
947
- job: structuredClone(updated),
948
- prunedManifestVersions,
949
- alreadyPrunedManifestVersions,
950
- retainedManifestVersions,
951
- missingManifestVersions,
952
- reclaimedSegmentIds,
953
- retainedSegmentIds,
954
- missingSegmentIds,
955
- reclaimedBlockIds,
956
- retainedBlockIds,
957
- missingBlockIds,
958
- reclaimedBlockBytes,
959
- };
300
+ const result = this.#core.runGarbageCollectionStep(input);
301
+ result.reclaimedBlockIds.forEach((id) => this.#blocks.delete(id));
302
+ return result;
960
303
  });
961
304
  }
305
+ async removePrunedManifestRecords() {
306
+ return this.#runAtomic(() => this.#core.removePrunedManifestRecords().length);
307
+ }
962
308
  async removeGarbageCollectionJob(id) {
963
309
  return this.#runAtomic(() => {
964
- this.#garbageCollectionJobs.delete(id);
310
+ this.#core.removeGarbageCollectionJob(id);
965
311
  });
966
312
  }
967
313
  /**
968
314
  * Copies the current version out as a portable snapshot. Runs on the commit queue, so it sees
969
- * one consistent version rather than a commit in progress.
970
- *
971
- * Only blocks the current manifest still points at are copied, which drops every superseded
972
- * block a compaction left behind, and only full-text bases that already cover this exact
973
- * version are carried; any other indexed column is exported as `invalid` so the loaded
974
- * database rebuilds it. That costs a rebuild, never a wrong answer — the index is a pruning
975
- * accelerator and the scan re-verifies every candidate.
315
+ * one consistent version rather than a commit in progress. What the snapshot carries and
316
+ * drops is `RecordCore.exportSnapshot`'s contract.
976
317
  */
977
318
  exportSnapshot() {
978
- return this.#runAtomic(() => {
979
- const version = this.#currentVersion;
980
- if (version === null)
981
- throw new Error("There is no committed version to snapshot");
982
- const manifest = this.#manifests.get(version);
983
- if (manifest === undefined)
984
- throw new SnapshotManifestMissingError(version);
985
- const liveBlockIds = new Set(manifest.blockIds);
986
- const blocks = manifest.blockIds.map((id) => {
987
- const bytes = this.#blocks.get(id);
988
- if (bytes === undefined)
989
- throw new Error(`Manifest references missing block: ${id}`);
990
- return { id, bytes: bytes.slice() };
991
- });
992
- const { segments, transactions } = selectLiveRecords({
993
- liveBlockIds,
994
- segments: [...this.#segments.values()].map((record) => normalizeSegmentRecord(record)),
995
- transactions: [...this.#transactions.values()],
996
- version,
997
- });
998
- const tables = [...this.#tables.values()].map((table) => {
999
- const fts = [];
1000
- const ftsColumns = { ...(table.ftsColumns ?? {}) };
1001
- for (const [columnId, state] of Object.entries(ftsColumns)) {
1002
- const base = this.#ftsBases.get(`${table.id}/${columnId}`);
1003
- if (state.state === "ready" && base?.coversVersion === version) {
1004
- fts.push({ columnId, ...structuredClone(base) });
1005
- }
1006
- else {
1007
- ftsColumns[columnId] = { ...state, state: "invalid" };
1008
- }
1009
- }
1010
- return {
1011
- record: {
1012
- ...structuredClone(table),
1013
- ...(Object.keys(ftsColumns).length === 0 ? {} : { ftsColumns }),
1014
- },
1015
- nextRowId: this.#nextRowIds.get(table.id) ?? 1n,
1016
- autoIncrement: table.columns.flatMap((column) => {
1017
- const next = this.#nextAutoIncrement.get(`${table.id}/${column.id}`);
1018
- return next === undefined ? [] : [{ columnId: column.id, next }];
1019
- }),
1020
- uniqueKeyTokens: [...(this.#uniqueKeys.get(table.id) ?? [])],
1021
- fts,
1022
- };
1023
- });
1024
- return {
1025
- version,
1026
- createdAt: new Date().toISOString(),
1027
- tables,
1028
- segments,
1029
- transactions,
1030
- blocks,
1031
- };
1032
- });
319
+ return this.#runAtomic(() => this.#core.exportSnapshot((id) => this.#blocks.get(id)));
1033
320
  }
1034
321
  /**
1035
322
  * Loads a snapshot into this store, which must be empty — the same contract as the IndexedDB
@@ -1044,59 +331,24 @@ export class MemoryBlockStore {
1044
331
  const totalBytes = snapshot.blocks.reduce((total, block) => total + block.bytes.byteLength, 0);
1045
332
  options.onProgress?.({ phase: "blocks", writtenBytes: 0, totalBytes });
1046
333
  await this.#runAtomic(() => {
1047
- if (this.#currentVersion !== null)
1048
- throw new Error("This store already holds a database");
1049
- if (this.#tables.size > 0)
1050
- throw new Error("This store already holds a catalog");
1051
- this.#loadSnapshot(snapshot);
334
+ this.#core.importSnapshot(snapshot, (id, bytes) => this.#blocks.set(id, bytes.slice()));
1052
335
  });
1053
336
  options.onProgress?.({ phase: "done", writtenBytes: totalBytes, totalBytes });
1054
337
  }
1055
338
  /** Builds a store holding exactly what the snapshot captured. */
1056
339
  static fromSnapshot(snapshot) {
1057
340
  const store = new MemoryBlockStore();
1058
- store.#loadSnapshot(snapshot);
341
+ store.#core.loadSnapshot(snapshot, (id, bytes) => store.#blocks.set(id, bytes.slice()));
1059
342
  return store;
1060
343
  }
1061
- #loadSnapshot(snapshot) {
1062
- for (const block of snapshot.blocks)
1063
- this.#blocks.set(block.id, block.bytes.slice());
1064
- for (const segment of snapshot.segments) {
1065
- this.#segments.set(segment.id, normalizeSegmentRecord(segment));
1066
- }
1067
- for (const record of snapshot.transactions) {
1068
- this.#transactions.set(record.id, structuredClone(record));
1069
- }
1070
- for (const table of snapshot.tables) {
1071
- const record = structuredClone(table.record);
1072
- this.#tables.set(record.id, record);
1073
- this.#tableIdsByName.set(record.name, record.id);
1074
- this.#nextRowIds.set(record.id, table.nextRowId);
1075
- for (const entry of table.autoIncrement) {
1076
- this.#nextAutoIncrement.set(`${record.id}/${entry.columnId}`, entry.next);
1077
- }
1078
- if (table.uniqueKeyTokens.length > 0) {
1079
- this.#uniqueKeys.set(record.id, new Set(table.uniqueKeyTokens));
1080
- }
1081
- for (const index of table.fts) {
1082
- this.#ftsBases.set(`${record.id}/${index.columnId}`, {
1083
- coversVersion: index.coversVersion,
1084
- chunks: structuredClone(index.chunks),
1085
- totalTokens: index.totalTokens,
1086
- });
1087
- }
1088
- }
1089
- // One checkpoint, not the captured history: a snapshot restores as a single readable
1090
- // version, and the version number itself is preserved so committed transactions still
1091
- // sort and compare against it.
1092
- this.#manifests.set(snapshot.version, {
1093
- version: snapshot.version,
1094
- previousVersion: null,
1095
- blockIds: snapshot.blocks.map((block) => block.id),
1096
- createdAt: snapshot.createdAt,
1097
- });
1098
- this.#currentVersion = snapshot.version;
1099
- this.#catalogEpoch += 1;
344
+ /** Block and spill bytes held; the record maps are negligible beside them. */
345
+ async getLogicalStorageBytes() {
346
+ let total = 0;
347
+ for (const bytes of this.#blocks.values())
348
+ total += bytes.byteLength;
349
+ for (const bytes of this.#tempRunPages.values())
350
+ total += bytes.byteLength;
351
+ return total;
1100
352
  }
1101
353
  close() {
1102
354
  // The in-memory implementation owns no external resources.
@@ -1119,337 +371,10 @@ export class MemoryBlockStore {
1119
371
  return result;
1120
372
  }
1121
373
  }
1122
- function validateId(id) {
1123
- if (id.length === 0)
1124
- throw new TypeError("Block ID cannot be empty");
1125
- }
1126
- function validateCount(count) {
1127
- if (!Number.isSafeInteger(count) || count <= 0) {
1128
- throw new RangeError("Row ID reservation count must be a positive whole number");
1129
- }
1130
- }
1131
- function validateAutoIncrementReservation(count, atLeast) {
1132
- if (!Number.isSafeInteger(count) || count < 0) {
1133
- throw new RangeError("Auto-increment reservation count must be a non-negative whole number");
1134
- }
1135
- if (atLeast !== undefined && atLeast < 1n) {
1136
- throw new RangeError("Auto-increment bump target must be at least 1");
1137
- }
1138
- }
1139
- function isTerminalCompactionJob(record) {
1140
- return record.state === "published" || record.state === "cancelled" || record.state === "aborted";
1141
- }
1142
- function assertGenericTransactionUpdateAllowed(record, update) {
1143
- if (record.status !== "active") {
1144
- throw new TypeError(`Only active transactions can be updated; found ${record.status}`);
1145
- }
1146
- if (update.status === "committed") {
1147
- throw new TypeError("Use commitTransaction to commit a transaction");
1148
- }
1149
- if (Reflect.has(update, "committedVersion")) {
1150
- throw new TypeError("Only commitTransaction can set a committed transaction version");
1151
- }
1152
- }
1153
- function assertSnapshotAvailable(version, manifests, blocks) {
1154
- if (version === null)
1155
- return;
1156
- const manifest = manifests.get(version);
1157
- if (manifest === undefined ||
1158
- manifest.prunedAt !== undefined ||
1159
- manifest.blockIds.some((id) => !blocks.has(id))) {
1160
- throw new SnapshotManifestMissingError(version);
1161
- }
1162
- }
1163
- function assertGarbageCollectionPinsAvailable(currentVersion, transactions, leases, compactionJobs, leaseCutoff, manifests, blocks) {
1164
- if (currentVersion !== null)
1165
- assertSnapshotAvailable(currentVersion, manifests, blocks);
1166
- for (const transaction of transactions.values()) {
1167
- if (transaction.status === "active") {
1168
- assertSnapshotAvailable(transaction.snapshotVersion, manifests, blocks);
1169
- }
1170
- }
1171
- for (const lease of leases) {
1172
- const expiresAt = Date.parse(lease.expiresAt);
1173
- if (!Number.isFinite(expiresAt) || expiresAt > leaseCutoff) {
1174
- assertSnapshotAvailable(lease.manifestVersion, manifests, blocks);
1175
- }
1176
- }
1177
- for (const job of compactionJobs) {
1178
- if (isTerminalCompactionJob(job))
1179
- continue;
1180
- assertSnapshotAvailable(job.sourceManifestVersion, manifests, blocks);
1181
- const linkedTransaction = job.transactionId === null ? undefined : transactions.get(job.transactionId);
1182
- if (linkedTransaction?.status === "committed") {
1183
- if (linkedTransaction.committedVersion === null) {
1184
- throw new Error(`Committed transaction has no manifest version: ${linkedTransaction.id}`);
1185
- }
1186
- assertSnapshotAvailable(linkedTransaction.committedVersion, manifests, blocks);
1187
- }
1188
- }
1189
- }
1190
- function assertPendingArtifactsAvailable(transaction, blocks, segments, validateBlocks = true, validateSegments = true) {
1191
- const missingBlockId = validateBlocks
1192
- ? transaction.pendingBlockIds.find((id) => !blocks.has(id))
1193
- : undefined;
1194
- if (missingBlockId !== undefined) {
1195
- throw new Error(`Transaction references missing pending block: ${missingBlockId}`);
1196
- }
1197
- const missingSegmentId = validateSegments
1198
- ? transaction.pendingSegmentIds.find((id) => !segments.has(id))
1199
- : undefined;
1200
- if (missingSegmentId !== undefined) {
1201
- throw new Error(`Transaction references missing pending segment: ${missingSegmentId}`);
1202
- }
1203
- }
1204
- function assertRemainingManifestBlocksAvailable(manifests, newlyPrunedVersions, blocks) {
1205
- for (const manifest of manifests) {
1206
- if (manifest.prunedAt !== undefined || newlyPrunedVersions.has(manifest.version))
1207
- continue;
1208
- if (manifest.blockIds.some((id) => !blocks.has(id))) {
1209
- throw new SnapshotManifestMissingError(manifest.version);
1210
- }
1211
- }
1212
- }
1213
- function assertGarbageCollectionCandidateProvenance(job, manifests, segments, transactions, compactionJobs) {
1214
- for (const version of job.candidateManifestVersions) {
1215
- if (!manifests.has(version)) {
1216
- throw new Error(`Garbage collection candidate manifest is missing: ${String(version)}`);
1217
- }
1218
- }
1219
- const blockHasProvenance = (id) => {
1220
- for (const version of job.candidateManifestVersions) {
1221
- if (manifests.get(version)?.blockIds.includes(id))
1222
- return true;
1223
- }
1224
- for (const transaction of transactions.values()) {
1225
- if (transaction.status === "aborted" && transaction.pendingBlockIds.includes(id))
1226
- return true;
1227
- }
1228
- for (const compaction of compactionJobs.values()) {
1229
- if (isTerminalCompactionJob(compaction) &&
1230
- (compaction.sourceBlockIds.includes(id) || compaction.outputBlockIds.includes(id))) {
1231
- return true;
1232
- }
1233
- }
1234
- return false;
1235
- };
1236
- const unprovenBlockId = job.candidateBlockIds.find((id) => !blockHasProvenance(id));
1237
- if (unprovenBlockId !== undefined) {
1238
- throw new Error(`Garbage collection block candidate has no persisted provenance: ${unprovenBlockId}`);
1239
- }
1240
- const unprovenSegmentId = job.candidateSegmentIds.find((id) => {
1241
- for (const transaction of transactions.values()) {
1242
- if (transaction.status === "aborted" && transaction.pendingSegmentIds.includes(id)) {
1243
- return false;
1244
- }
1245
- }
1246
- for (const compaction of compactionJobs.values()) {
1247
- if (isTerminalCompactionJob(compaction) &&
1248
- (compaction.sourceSegmentIds.includes(id) || compaction.outputSegmentId === id)) {
1249
- return false;
1250
- }
1251
- }
1252
- const segment = segments.get(id);
1253
- const blockIds = segment === undefined ? [] : segmentBlockIds(segment);
1254
- return blockIds.length === 0 || !blockIds.every(blockHasProvenance);
1255
- });
1256
- if (unprovenSegmentId !== undefined) {
1257
- throw new Error(`Garbage collection segment candidate has no persisted provenance: ${unprovenSegmentId}`);
1258
- }
1259
- }
1260
- function isManifestVersionPinned(version, currentVersion, transactions, leases, compactionJobs, leaseCutoff) {
1261
- if (currentVersion === version)
1262
- return true;
1263
- for (const transaction of transactions.values()) {
1264
- if (transaction.status === "active" && transaction.snapshotVersion === version)
1265
- return true;
1266
- }
1267
- for (const lease of leases) {
1268
- const expiresAt = Date.parse(lease.expiresAt);
1269
- if (lease.manifestVersion === version &&
1270
- (!Number.isFinite(expiresAt) || expiresAt > leaseCutoff)) {
1271
- return true;
1272
- }
1273
- }
1274
- for (const job of compactionJobs) {
1275
- if (isTerminalCompactionJob(job))
1276
- continue;
1277
- if (job.sourceManifestVersion === version)
1278
- return true;
1279
- const linkedTransaction = job.transactionId === null ? undefined : transactions.get(job.transactionId);
1280
- if (linkedTransaction?.status === "committed") {
1281
- if (linkedTransaction.committedVersion === null) {
1282
- throw new Error(`Committed transaction has no manifest version: ${linkedTransaction.id}`);
1283
- }
1284
- if (linkedTransaction.committedVersion === version)
1285
- return true;
1286
- }
1287
- }
1288
- return false;
1289
- }
1290
- const MAX_GARBAGE_COLLECTION_ROOT_DEPENDENCIES = 4_096;
1291
- function collectBoundedPhysicalRoots(candidateSegmentIds, candidateBlockIds, manifests, newlyPrunedVersions, segments, transactions, compactionJobs) {
1292
- const candidateBlocks = new Set(candidateBlockIds);
1293
- const probeBlockIds = new Set(candidateBlockIds);
1294
- const probeSegmentIds = new Set(candidateSegmentIds);
1295
- const relatedSegments = new Map();
1296
- let dependencyOverflow = false;
1297
- for (const segment of segments.values()) {
1298
- const ids = segmentBlockIds(segment);
1299
- if (!candidateSegmentIds.includes(segment.id) && !ids.some((id) => candidateBlocks.has(id))) {
1300
- continue;
1301
- }
1302
- const newBlockIds = new Set(ids.filter((id) => !probeBlockIds.has(id)));
1303
- if (relatedSegments.size >= MAX_GARBAGE_COLLECTION_ROOT_DEPENDENCIES ||
1304
- probeBlockIds.size + newBlockIds.size > MAX_GARBAGE_COLLECTION_ROOT_DEPENDENCIES) {
1305
- dependencyOverflow = true;
1306
- break;
1307
- }
1308
- relatedSegments.set(segment.id, segment);
1309
- probeSegmentIds.add(segment.id);
1310
- ids.forEach((id) => probeBlockIds.add(id));
1311
- }
1312
- if (dependencyOverflow) {
1313
- return { blockIds: new Set(candidateBlockIds), segmentIds: new Set(candidateSegmentIds) };
1314
- }
1315
- const directBlockRoots = new Set();
1316
- const directSegmentRoots = new Set();
1317
- for (const manifest of manifests) {
1318
- if (manifest.prunedAt !== undefined || newlyPrunedVersions.has(manifest.version))
1319
- continue;
1320
- for (const id of manifest.blockIds)
1321
- if (probeBlockIds.has(id))
1322
- directBlockRoots.add(id);
1323
- }
1324
- for (const transaction of transactions.values()) {
1325
- if (transaction.status !== "active")
1326
- continue;
1327
- for (const id of transaction.pendingBlockIds)
1328
- if (probeBlockIds.has(id))
1329
- directBlockRoots.add(id);
1330
- for (const id of transaction.pendingSegmentIds)
1331
- if (probeSegmentIds.has(id))
1332
- directSegmentRoots.add(id);
1333
- }
1334
- for (const job of compactionJobs) {
1335
- if (isTerminalCompactionJob(job))
1336
- continue;
1337
- for (const id of job.sourceBlockIds)
1338
- if (probeBlockIds.has(id))
1339
- directBlockRoots.add(id);
1340
- for (const id of job.outputBlockIds)
1341
- if (probeBlockIds.has(id))
1342
- directBlockRoots.add(id);
1343
- for (const id of job.sourceSegmentIds)
1344
- if (probeSegmentIds.has(id))
1345
- directSegmentRoots.add(id);
1346
- if (job.outputSegmentId !== null && probeSegmentIds.has(job.outputSegmentId)) {
1347
- directSegmentRoots.add(job.outputSegmentId);
1348
- }
1349
- }
1350
- const rootedBlockIds = new Set();
1351
- const rootedSegmentIds = new Set();
1352
- for (const segment of relatedSegments.values()) {
1353
- const ids = segmentBlockIds(segment);
1354
- if (directSegmentRoots.has(segment.id) ||
1355
- transactions.get(segment.transactionId)?.status === "active" ||
1356
- (ids.length > 0 && ids.every((id) => directBlockRoots.has(id)))) {
1357
- if (candidateSegmentIds.includes(segment.id))
1358
- rootedSegmentIds.add(segment.id);
1359
- for (const id of ids)
1360
- if (candidateBlocks.has(id))
1361
- rootedBlockIds.add(id);
1362
- }
1363
- }
1364
- for (const id of candidateBlockIds)
1365
- if (directBlockRoots.has(id))
1366
- rootedBlockIds.add(id);
1367
- for (const id of candidateSegmentIds)
1368
- if (directSegmentRoots.has(id))
1369
- rootedSegmentIds.add(id);
1370
- return { blockIds: rootedBlockIds, segmentIds: rootedSegmentIds };
1371
- }
1372
- function segmentBlockIds(segment) {
1373
- return [...new Set(Object.values(segment.columnBlockIds).flat())];
1374
- }
1375
- function validateGarbageCollectionStepInput(input) {
1376
- if (input.jobId.length === 0)
1377
- throw new TypeError("Garbage collection job ID cannot be empty");
1378
- if (!Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0) {
1379
- throw new RangeError("Garbage collection expected revision must be a non-negative whole number");
1380
- }
1381
- if (!Number.isSafeInteger(input.maxItems) || input.maxItems <= 0) {
1382
- throw new RangeError("Garbage collection item limit must be a positive whole number");
1383
- }
1384
- if (input.updatedAt.length === 0 || !Number.isFinite(Date.parse(input.updatedAt))) {
1385
- throw new TypeError("Garbage collection update timestamp must be valid");
1386
- }
1387
- }
1388
- function validatePageLimit(limit) {
1389
- if (!Number.isSafeInteger(limit) || limit <= 0) {
1390
- throw new RangeError("Storage page limit must be a positive whole number");
1391
- }
1392
- }
1393
- function validateTempRunPage(page) {
1394
- validateTempRunPageIdentity(page.ownerId, page.runId, page.pageIndex);
1395
- if (!(page.bytes instanceof Uint8Array))
1396
- throw new TypeError("Temp run page bytes are invalid");
1397
- }
1398
- function validateTempRunPageIdentity(ownerId, runId, pageIndex) {
1399
- validateId(ownerId);
1400
- validateId(runId);
1401
- if (!Number.isSafeInteger(pageIndex) || pageIndex < 0) {
1402
- throw new RangeError("Temp run page index must be a non-negative whole number");
1403
- }
1404
- }
1405
- function validateTableColumns(columns) {
1406
- if (columns.length === 0)
1407
- throw new TypeError("A table needs at least one column");
1408
- const ids = new Set(columns.map(({ id }) => id));
1409
- const names = new Set(columns.map(({ name }) => name));
1410
- if (ids.size !== columns.length || names.size !== columns.length) {
1411
- throw new TypeError("Table columns must have unique IDs and names");
1412
- }
1413
- }
1414
- function validateTempOwnerRecord(record) {
1415
- validateId(record.ownerId);
1416
- validateLeaseExpiration(record.expiresAt);
1417
- if (record.revision !== 0) {
1418
- throw new RangeError("Temp owner record must be created at revision zero");
1419
- }
1420
- }
1421
374
  function tempRunPageKey(ownerId, runId, pageIndex) {
1422
375
  return `${String(ownerId.length)}:${ownerId}:${String(runId.length)}:${runId}:${String(pageIndex)}`;
1423
376
  }
1424
377
  function tempRunPagePrefix(ownerId, runId) {
1425
378
  return `${String(ownerId.length)}:${ownerId}:${String(runId.length)}:${runId}:`;
1426
379
  }
1427
- function validateLeaseExpiration(expiresAt) {
1428
- if (expiresAt.length === 0 || !Number.isFinite(Date.parse(expiresAt))) {
1429
- throw new TypeError("Lease expiration must be valid");
1430
- }
1431
- }
1432
- function emptyGarbageCollectionStep(job) {
1433
- return {
1434
- job: structuredClone(job),
1435
- prunedManifestVersions: [],
1436
- alreadyPrunedManifestVersions: [],
1437
- retainedManifestVersions: [],
1438
- missingManifestVersions: [],
1439
- reclaimedSegmentIds: [],
1440
- retainedSegmentIds: [],
1441
- missingSegmentIds: [],
1442
- reclaimedBlockIds: [],
1443
- retainedBlockIds: [],
1444
- missingBlockIds: [],
1445
- reclaimedBlockBytes: 0,
1446
- };
1447
- }
1448
- function safeStorageSum(left, right) {
1449
- const total = left + right;
1450
- if (!Number.isSafeInteger(total)) {
1451
- throw new RangeError("Garbage collection reclaimed block bytes exceed the safe range");
1452
- }
1453
- return total;
1454
- }
1455
380
  //# sourceMappingURL=memory.js.map