@minnowdb/core 0.1.0 → 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 (149) hide show
  1. package/README.md +32 -18
  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
  118. package/dist/engine/dsl/db.d.ts +0 -137
  119. package/dist/engine/dsl/db.d.ts.map +0 -1
  120. package/dist/engine/dsl/db.js +0 -161
  121. package/dist/engine/dsl/db.js.map +0 -1
  122. package/dist/engine/dsl/expression.d.ts +0 -172
  123. package/dist/engine/dsl/expression.d.ts.map +0 -1
  124. package/dist/engine/dsl/expression.js +0 -397
  125. package/dist/engine/dsl/expression.js.map +0 -1
  126. package/dist/engine/dsl/index.d.ts +0 -8
  127. package/dist/engine/dsl/index.d.ts.map +0 -1
  128. package/dist/engine/dsl/index.js +0 -7
  129. package/dist/engine/dsl/index.js.map +0 -1
  130. package/dist/engine/dsl/live-query.d.ts +0 -45
  131. package/dist/engine/dsl/live-query.d.ts.map +0 -1
  132. package/dist/engine/dsl/live-query.js +0 -116
  133. package/dist/engine/dsl/live-query.js.map +0 -1
  134. package/dist/engine/dsl/mutations.d.ts +0 -123
  135. package/dist/engine/dsl/mutations.d.ts.map +0 -1
  136. package/dist/engine/dsl/mutations.js +0 -241
  137. package/dist/engine/dsl/mutations.js.map +0 -1
  138. package/dist/engine/dsl/select-query-builder.d.ts +0 -148
  139. package/dist/engine/dsl/select-query-builder.d.ts.map +0 -1
  140. package/dist/engine/dsl/select-query-builder.js +0 -377
  141. package/dist/engine/dsl/select-query-builder.js.map +0 -1
  142. package/dist/engine/dsl/sql-tag.d.ts +0 -46
  143. package/dist/engine/dsl/sql-tag.d.ts.map +0 -1
  144. package/dist/engine/dsl/sql-tag.js +0 -85
  145. package/dist/engine/dsl/sql-tag.js.map +0 -1
  146. package/dist/engine/dsl/types.d.ts +0 -133
  147. package/dist/engine/dsl/types.d.ts.map +0 -1
  148. package/dist/engine/dsl/types.js +0 -9
  149. package/dist/engine/dsl/types.js.map +0 -1
@@ -0,0 +1,745 @@
1
+ import { CompactionJobConflictError, LeaseConflictError, SnapshotManifestMissingError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyConflictError, WriteConflictError, } from "../storage/types.js";
2
+ /** Runs every case in sequence; throws on the first failure, prefixed with the case name. */
3
+ export async function runBlockStoreConformance(target) {
4
+ for (const conformanceCase of blockStoreConformanceCases()) {
5
+ try {
6
+ await conformanceCase.run(target);
7
+ }
8
+ catch (error) {
9
+ const message = error instanceof Error ? error.message : String(error);
10
+ throw new Error(`[${conformanceCase.name}] ${message}`, { cause: error });
11
+ }
12
+ }
13
+ }
14
+ // ------------------------------------------------------------------------------------------
15
+ // Assertions — deliberately tiny and dependency-free.
16
+ // ------------------------------------------------------------------------------------------
17
+ function check(condition, message) {
18
+ if (!condition)
19
+ throw new Error(message);
20
+ }
21
+ function checkEqual(actual, expected, message) {
22
+ if (!deepEqual(actual, expected)) {
23
+ throw new Error(`${message}\n expected: ${render(expected)}\n actual: ${render(actual)}`);
24
+ }
25
+ }
26
+ async function checkThrows(work, errorClass, message) {
27
+ try {
28
+ await work();
29
+ }
30
+ catch (error) {
31
+ check(error instanceof errorClass, `${message}: threw ${render(error)} instead of ${errorClass.name}. Conflicts must be ` +
32
+ `thrown as the exact exported classes — the engine's retry loops match on them.`);
33
+ check(error.name === errorClass.name, `${message}: the error's own name is "${error.name}", not "${errorClass.name}" ` +
34
+ `— the worker client rehydrates errors by constructor name.`);
35
+ return error;
36
+ }
37
+ throw new Error(`${message}: resolved instead of throwing ${errorClass.name}`);
38
+ }
39
+ function deepEqual(left, right) {
40
+ if (Object.is(left, right))
41
+ return true;
42
+ if (left instanceof Uint8Array && right instanceof Uint8Array) {
43
+ return left.byteLength === right.byteLength && left.every((byte, at) => right[at] === byte);
44
+ }
45
+ if (Array.isArray(left) && Array.isArray(right)) {
46
+ return left.length === right.length && left.every((entry, at) => deepEqual(entry, right[at]));
47
+ }
48
+ if (typeof left === "object" &&
49
+ typeof right === "object" &&
50
+ left !== null &&
51
+ right !== null &&
52
+ !Array.isArray(left) &&
53
+ !Array.isArray(right)) {
54
+ const leftKeys = Object.keys(left).sort();
55
+ const rightKeys = Object.keys(right).sort();
56
+ return (deepEqual(leftKeys, rightKeys) &&
57
+ leftKeys.every((key) => deepEqual(left[key], right[key])));
58
+ }
59
+ return false;
60
+ }
61
+ function render(value) {
62
+ if (value instanceof Error)
63
+ return `${value.name}: ${value.message}`;
64
+ try {
65
+ return JSON.stringify(value, (_key, entry) => typeof entry === "bigint"
66
+ ? `${entry.toString()}n`
67
+ : entry instanceof Uint8Array
68
+ ? `Uint8Array(${String(entry.byteLength)})`
69
+ : entry);
70
+ }
71
+ catch {
72
+ return String(value);
73
+ }
74
+ }
75
+ // ------------------------------------------------------------------------------------------
76
+ // Fixtures — the smallest records that exercise each rule.
77
+ // ------------------------------------------------------------------------------------------
78
+ const T0 = "2026-01-01T00:00:00.000Z";
79
+ const LATER = "2026-01-01T01:00:00.000Z";
80
+ const FAR_FUTURE = "2999-01-01T00:00:00.000Z";
81
+ const PAST = "2000-01-01T00:00:00.000Z";
82
+ function table(name, keyed = false) {
83
+ return {
84
+ id: `table-${name}`,
85
+ name,
86
+ columns: [
87
+ { id: "col-id", name: "id", type: "number", nullable: false },
88
+ { id: "col-v", name: "v", type: "string", nullable: true },
89
+ ],
90
+ revision: 0,
91
+ createdAt: T0,
92
+ ...(keyed ? { uniqueKeyColumnId: "col-id" } : {}),
93
+ };
94
+ }
95
+ function transaction(id, snapshotVersion) {
96
+ return {
97
+ id,
98
+ snapshotVersion,
99
+ pendingBlockIds: [],
100
+ pendingSegmentIds: [],
101
+ status: "active",
102
+ revision: 0,
103
+ startedAt: T0,
104
+ updatedAt: T0,
105
+ committedVersion: null,
106
+ };
107
+ }
108
+ function segment(id, tableId, transactionId, blockId) {
109
+ return {
110
+ id,
111
+ tableId,
112
+ transactionId,
113
+ rowCount: 2,
114
+ rowIdStart: 1n,
115
+ rowIdEndExclusive: 3n,
116
+ columnBlockIds: { "col-id": [blockId], "col-v": [blockId] },
117
+ createdAt: T0,
118
+ };
119
+ }
120
+ /** Stages one block + one segment on a fresh transaction and commits it. */
121
+ async function commitOne(store, suffix, options = {}) {
122
+ const version = await store.getCurrentManifestVersion();
123
+ const transactionId = `txn-${suffix}`;
124
+ const blockId = `table/t/segment/${suffix}/part/000000`;
125
+ await store.createTransaction(transaction(transactionId, version));
126
+ await store.addBlock(blockId, Uint8Array.of(1, 2, 3, 4));
127
+ await store.addSegment(segment(`segment-${suffix}`, options.tableId ?? "table-t", transactionId, blockId));
128
+ await store.updateTransaction(transactionId, 0, {
129
+ pendingBlockIds: [blockId],
130
+ pendingSegmentIds: [`segment-${suffix}`],
131
+ updatedAt: T0,
132
+ });
133
+ const summary = await store.commitTransaction({
134
+ transactionId,
135
+ expectedTransactionRevision: 1,
136
+ expectedManifestVersion: version,
137
+ committedAt: LATER,
138
+ ...(options.keyTokens === undefined
139
+ ? {}
140
+ : {
141
+ uniqueKeyChanges: [
142
+ {
143
+ tableId: options.tableId ?? "table-t",
144
+ keyTokens: options.keyTokens,
145
+ requireAbsent: true,
146
+ },
147
+ ],
148
+ }),
149
+ });
150
+ return { version: summary.version, blockId };
151
+ }
152
+ // ------------------------------------------------------------------------------------------
153
+ // The cases.
154
+ // ------------------------------------------------------------------------------------------
155
+ export function blockStoreConformanceCases() {
156
+ return [
157
+ {
158
+ name: "blocks are immutable, copied both directions, and duplicates are refused",
159
+ async run(target) {
160
+ const store = await target.create();
161
+ const bytes = Uint8Array.of(1, 2, 3);
162
+ await store.addBlock("block-a", bytes);
163
+ bytes[0] = 99; // The store must not alias the caller's buffer.
164
+ const first = await store.getBlock("block-a");
165
+ check(first?.[0] === 1, "stored bytes changed with the caller's buffer");
166
+ first[1] = 99; // Nor may the caller's mutations reach the store.
167
+ const second = await store.getBlock("block-a");
168
+ check(second?.[1] === 2, "returned bytes alias the store's copy");
169
+ try {
170
+ await store.addBlock("block-a", Uint8Array.of(9));
171
+ throw new Error("a duplicate block id was accepted");
172
+ }
173
+ catch (error) {
174
+ check(error instanceof Error, "duplicate rejection must be an Error");
175
+ }
176
+ checkEqual(await store.getBlocks(["missing", "block-a"]).then((all) => all[0]), undefined, "getBlocks must be positional with undefined for missing ids");
177
+ store.close();
178
+ },
179
+ },
180
+ {
181
+ name: "a block batch with any duplicate writes nothing at all",
182
+ async run(target) {
183
+ const store = await target.create();
184
+ await store.addBlock("block-a", Uint8Array.of(1));
185
+ try {
186
+ await store.addBlocks([
187
+ { id: "block-b", bytes: Uint8Array.of(2) },
188
+ { id: "block-a", bytes: Uint8Array.of(3) },
189
+ { id: "block-c", bytes: Uint8Array.of(4) },
190
+ ]);
191
+ throw new Error("a batch containing an existing id was accepted");
192
+ }
193
+ catch {
194
+ // Expected; what matters is that nothing landed.
195
+ }
196
+ checkEqual(await store.getBlock("block-b"), undefined, "a failed batch leaked block-b");
197
+ checkEqual(await store.getBlock("block-c"), undefined, "a failed batch leaked block-c");
198
+ checkEqual(await store.listBlockIds(), ["block-a"], "listBlockIds must be sorted and exact");
199
+ store.close();
200
+ },
201
+ },
202
+ {
203
+ name: "table records round-trip, list sorted by name, and conflict by exact class",
204
+ async run(target) {
205
+ const store = await target.create();
206
+ await store.addTable(table("bravo"));
207
+ await store.addTable(table("alpha"));
208
+ const names = (await store.listTables()).map((record) => record.name);
209
+ checkEqual(names, ["alpha", "bravo"], "listTables must sort by name");
210
+ const byName = await store.getTableByName("alpha");
211
+ check(byName?.id === "table-alpha", "getTableByName must resolve the record");
212
+ await checkThrows(() => store.updateTable("table-alpha", 7, {}), TableRecordConflictError, "updateTable with a stale revision");
213
+ await checkThrows(() => store.removeTable("table-alpha", 7), TableRecordConflictError, "removeTable with a stale revision");
214
+ const updated = await store.updateTable("table-alpha", 0, {});
215
+ check(updated.revision === 1, "updateTable must advance the revision");
216
+ store.close();
217
+ },
218
+ },
219
+ {
220
+ name: "removing a table removes everything keyed to it",
221
+ async run(target) {
222
+ const store = await target.create();
223
+ await store.addTable(table("t", true));
224
+ const { blockId } = await commitOne(store, "one", {
225
+ keyTokens: ["number:1"],
226
+ tableId: "table-t",
227
+ });
228
+ await store.reserveRowIds("table-t", 10);
229
+ await store.removeTable("table-t", 0);
230
+ checkEqual(await store.listSegments("table-t"), [], "segments must go with the table");
231
+ checkEqual(await store.getExistingUniqueKeys("table-t", ["number:1"]), [], "unique keys must go with the table");
232
+ const range = await store.reserveRowIds("table-t", 1);
233
+ check(range.start === 1n, "row-id counters must reset with the table");
234
+ check((await store.getBlock(blockId)) !== undefined, "blocks are not the catalog's to delete");
235
+ store.close();
236
+ },
237
+ },
238
+ {
239
+ name: "counter reservations never overlap, even issued concurrently",
240
+ async run(target) {
241
+ const store = await target.create();
242
+ const ranges = await Promise.all(Array.from({ length: 8 }, () => store.reserveRowIds("table-t", 5)));
243
+ const starts = new Set(ranges.map((range) => range.start));
244
+ check(starts.size === 8, "concurrent reservations returned overlapping ranges");
245
+ for (const range of ranges) {
246
+ check(range.endExclusive - range.start === 5n, "a reservation returned the wrong span");
247
+ }
248
+ const bumped = await store.reserveAutoIncrement("table-t", "col-id", 3, 100n);
249
+ check(bumped.start >= 100n, "reserveAutoIncrement must honor atLeast");
250
+ store.close();
251
+ },
252
+ },
253
+ {
254
+ name: "a commit is atomic and visible: manifest, record, segments, keys together",
255
+ async run(target) {
256
+ const store = await target.create();
257
+ await store.addTable(table("t", true));
258
+ const { version, blockId } = await commitOne(store, "one", {
259
+ keyTokens: ["number:1"],
260
+ });
261
+ checkEqual(await store.getCurrentManifestVersion(), version, "the version must advance");
262
+ const manifest = await store.getCurrentManifest();
263
+ check(manifest?.blockIds.includes(blockId) === true, "the manifest must include the committed block");
264
+ const record = await store.getTransaction("txn-one");
265
+ check(record?.status === "committed" && record.committedVersion === version, "the transaction record must flip to committed with the published version");
266
+ checkEqual(await store.getExistingUniqueKeys("table-t", ["number:1", "number:2"]), ["number:1"], "committed unique keys must be queryable");
267
+ const finalized = await store.getSegment("segment-one");
268
+ check(finalized?.level === 0 && finalized.logicalOrder === version, "committed segments must be finalized with level and logical order");
269
+ store.close();
270
+ },
271
+ },
272
+ {
273
+ name: "commit conflicts are typed exactly, and a refused commit changes nothing",
274
+ async run(target) {
275
+ const store = await target.create();
276
+ await store.addTable(table("t", true));
277
+ await commitOne(store, "one");
278
+ const staleVersion = null; // The world has moved on from the empty database.
279
+ await store.createTransaction(transaction("txn-stale", staleVersion));
280
+ await checkThrows(() => store.commitTransaction({
281
+ transactionId: "txn-stale",
282
+ expectedTransactionRevision: 0,
283
+ expectedManifestVersion: staleVersion,
284
+ committedAt: LATER,
285
+ }), WriteConflictError, "a commit against a moved manifest");
286
+ const movedVersion = await store.getCurrentManifestVersion();
287
+ await checkThrows(() => store.commitTransaction({
288
+ transactionId: "txn-stale",
289
+ expectedTransactionRevision: 41,
290
+ expectedManifestVersion: movedVersion,
291
+ committedAt: LATER,
292
+ }), TransactionRecordConflictError, "a commit with a stale transaction revision");
293
+ const still = await store.getTransaction("txn-stale");
294
+ check(still?.status === "active", "a refused commit must leave the transaction active");
295
+ store.close();
296
+ },
297
+ },
298
+ {
299
+ name: "duplicate unique keys refuse with UniqueKeyConflictError and commit nothing",
300
+ async run(target) {
301
+ const store = await target.create();
302
+ await store.addTable(table("t", true));
303
+ const { version } = await commitOne(store, "one", { keyTokens: ["number:1"] });
304
+ await store.createTransaction(transaction("txn-dup", version));
305
+ await store.addBlock("block-dup", Uint8Array.of(9));
306
+ await store.updateTransaction("txn-dup", 0, {
307
+ pendingBlockIds: ["block-dup"],
308
+ updatedAt: T0,
309
+ });
310
+ await checkThrows(() => store.commitTransaction({
311
+ transactionId: "txn-dup",
312
+ expectedTransactionRevision: 1,
313
+ expectedManifestVersion: version,
314
+ committedAt: LATER,
315
+ uniqueKeyChanges: [
316
+ { tableId: "table-t", keyTokens: ["number:1"], requireAbsent: true },
317
+ ],
318
+ }), UniqueKeyConflictError, "a commit inserting an existing unique key");
319
+ checkEqual(await store.getCurrentManifestVersion(), version, "a key conflict must not advance the version");
320
+ store.close();
321
+ },
322
+ },
323
+ {
324
+ name: "publishManifest compare-and-swaps and refuses missing blocks",
325
+ async run(target) {
326
+ const store = await target.create();
327
+ await store.addBlock("block-a", Uint8Array.of(1));
328
+ await checkThrows(() => store.publishManifest({ expectedVersion: 3, blockIds: ["block-a"], createdAt: T0 }), WriteConflictError, "publishing against the wrong expected version");
329
+ try {
330
+ await store.publishManifest({
331
+ expectedVersion: null,
332
+ blockIds: ["missing"],
333
+ createdAt: T0,
334
+ });
335
+ throw new Error("a manifest referencing a missing block was published");
336
+ }
337
+ catch (error) {
338
+ check(!(error instanceof WriteConflictError), "missing blocks are not a version conflict");
339
+ }
340
+ const manifest = await store.publishManifest({
341
+ expectedVersion: null,
342
+ blockIds: ["block-a"],
343
+ createdAt: T0,
344
+ });
345
+ check(manifest.version === 0, "the first published version must be 0");
346
+ store.close();
347
+ },
348
+ },
349
+ {
350
+ name: "leases pin versions, renew by CAS, and expire honestly",
351
+ async run(target) {
352
+ const store = await target.create();
353
+ await store.addTable(table("t"));
354
+ const { version } = await commitOne(store, "one");
355
+ await store.createLease({
356
+ id: "lease-a",
357
+ kind: "reader",
358
+ manifestVersion: version,
359
+ ownerId: "owner-a",
360
+ expiresAt: FAR_FUTURE,
361
+ revision: 0,
362
+ });
363
+ await checkThrows(() => store.renewLease("lease-a", 9, FAR_FUTURE), LeaseConflictError, "renewing with a stale revision");
364
+ const renewed = await store.renewLease("lease-a", 0, FAR_FUTURE);
365
+ check(renewed.revision === 1, "a renewal must advance the revision");
366
+ checkEqual(await store.removeLeaseIfExpired("lease-a", 1, PAST), false, "an unexpired lease must not be removed");
367
+ checkEqual(await store.removeLeaseIfExpired("lease-a", 1, "3000-01-01T00:00:00.000Z"), true, "an expired lease must be removed");
368
+ store.close();
369
+ },
370
+ },
371
+ {
372
+ name: "garbage collection reclaims only unpinned, superseded state — atomically",
373
+ async run(target) {
374
+ const store = await target.create();
375
+ await store.addTable(table("t"));
376
+ const first = await commitOne(store, "one");
377
+ // Supersede the first commit's block so version 0 becomes collectable.
378
+ const transactionId = "txn-two";
379
+ await store.createTransaction(transaction(transactionId, first.version));
380
+ await store.addBlock("block-two", Uint8Array.of(5));
381
+ await store.addSegment(segment("segment-two", "table-t", transactionId, "block-two"));
382
+ await store.updateTransaction(transactionId, 0, {
383
+ pendingBlockIds: ["block-two"],
384
+ pendingSegmentIds: ["segment-two"],
385
+ updatedAt: T0,
386
+ });
387
+ await store.commitTransaction({
388
+ transactionId,
389
+ expectedTransactionRevision: 1,
390
+ expectedManifestVersion: first.version,
391
+ removedBlockIds: [first.blockId],
392
+ committedAt: LATER,
393
+ });
394
+ const job = await store.createGarbageCollectionJob({
395
+ id: "gc-1",
396
+ candidateManifestVersions: [first.version],
397
+ candidateSegmentIds: ["segment-one"],
398
+ candidateBlockIds: [first.blockId],
399
+ leaseCutoff: LATER,
400
+ createdAt: LATER,
401
+ });
402
+ const step = await store.runGarbageCollectionStep({
403
+ jobId: job.id,
404
+ expectedRevision: job.revision,
405
+ maxItems: 100,
406
+ updatedAt: LATER,
407
+ });
408
+ checkEqual(step.prunedManifestVersions, [first.version], "the superseded version must prune");
409
+ checkEqual(step.reclaimedBlockIds, [first.blockId], "the superseded block must reclaim");
410
+ checkEqual(await store.getBlock(first.blockId), undefined, "reclaimed bytes must be gone");
411
+ check((await store.getBlock("block-two")) !== undefined, "live bytes must survive");
412
+ const pruned = await store.getManifest(first.version);
413
+ check(pruned?.prunedAt !== undefined, "pruned manifests are tombstoned, not deleted");
414
+ store.close();
415
+ },
416
+ },
417
+ {
418
+ name: "temp spill pages are isolated scratch; owner records conflict by exact class",
419
+ async run(target) {
420
+ const store = await target.create();
421
+ await store.putTempRunPage({
422
+ ownerId: "o1",
423
+ runId: "r1",
424
+ pageIndex: 0,
425
+ bytes: Uint8Array.of(1),
426
+ });
427
+ const batched = store.putTempRunPages?.bind(store);
428
+ if (batched !== undefined) {
429
+ await batched([
430
+ { ownerId: "o1", runId: "r1", pageIndex: 1, bytes: Uint8Array.of(2) },
431
+ { ownerId: "o1", runId: "r2", pageIndex: 0, bytes: Uint8Array.of(3) },
432
+ ]);
433
+ checkEqual(await store.getTempRunPage("o1", "r1", 1), Uint8Array.of(2), "batched pages must read back identically");
434
+ }
435
+ await store.removeTempRun("o1", "r1");
436
+ checkEqual(await store.getTempRunPage("o1", "r1", 0), undefined, "a removed run must be gone");
437
+ await store.createTempOwner({ ownerId: "o1", expiresAt: FAR_FUTURE, revision: 0 });
438
+ await checkThrows(() => store.renewTempOwner("o1", 9, FAR_FUTURE), TempOwnerConflictError, "renewing a temp owner with a stale revision");
439
+ checkEqual(await store.removeTempOwnerIfExpired("o1", PAST), false, "an unexpired owner must survive the sweep");
440
+ const listed = await store.listTempOwnerIdsPage(null, 10);
441
+ check(listed.records.includes("o1"), "owners must be listable");
442
+ store.close();
443
+ },
444
+ },
445
+ {
446
+ name: "compaction job records update by CAS with the exact conflict class",
447
+ async run(target) {
448
+ const store = await target.create();
449
+ await store.addTable(table("t"));
450
+ const { version, blockId } = await commitOne(store, "one");
451
+ await store.createCompactionJob({
452
+ id: "job-1",
453
+ tableId: "table-t",
454
+ sourceManifestVersion: version,
455
+ sourceSegmentIds: ["segment-one"],
456
+ sourceBlockIds: [blockId],
457
+ outputBlockIds: [],
458
+ cursor: { sourceSegmentIndex: 0, sourceBlockIndex: 0 },
459
+ processedRows: 0,
460
+ sourceStoredBytes: 0,
461
+ outputStoredBytes: 0,
462
+ logicalBytes: 0,
463
+ targetLevel: 1,
464
+ state: "planned",
465
+ transactionId: null,
466
+ outputSegmentId: null,
467
+ publishedVersion: null,
468
+ revision: 0,
469
+ createdAt: T0,
470
+ updatedAt: T0,
471
+ });
472
+ await checkThrows(() => store.updateCompactionJob("job-1", 9, {
473
+ state: "running",
474
+ transactionId: "txn-one",
475
+ updatedAt: LATER,
476
+ }), CompactionJobConflictError, "updating a compaction job with a stale revision");
477
+ const updated = await store.updateCompactionJob("job-1", 0, {
478
+ state: "running",
479
+ transactionId: "txn-one",
480
+ updatedAt: LATER,
481
+ });
482
+ check(updated.revision === 1, "a job update must advance the revision");
483
+ store.close();
484
+ },
485
+ },
486
+ {
487
+ name: "full-text bases serve exact and prefix candidates with honest coverage",
488
+ async run(target) {
489
+ const store = await target.create();
490
+ await store.writeFtsBase("table-t", "col-v", {
491
+ coversVersion: 0,
492
+ chunks: [
493
+ [
494
+ { term: "minnow", rowIds: [1n, 3n], tf: [1, 2] },
495
+ { term: "minnows", rowIds: [2n], tf: [1] },
496
+ ],
497
+ [{ term: "shark", rowIds: [4n], tf: [1] }],
498
+ ],
499
+ totalTokens: 5,
500
+ });
501
+ const exact = await store.readFtsCandidates("table-t", "col-v", [{ term: "minnow", prefix: false }], 10);
502
+ checkEqual(exact.rowIdsByTerm, [[1n, 3n]], "exact terms must match exactly");
503
+ check(exact.coversVersion === 0 && exact.totalTokens === 5, "coverage and token totals must report the stored base");
504
+ const prefix = await store.readFtsCandidates("table-t", "col-v", [{ term: "minnow", prefix: true }], 10);
505
+ checkEqual(prefix.rowIdsByTerm, [[1n, 2n, 3n]], "prefix terms must match the term range, row ids ascending and unique");
506
+ store.close();
507
+ },
508
+ },
509
+ {
510
+ name: "optional atomic methods, when present, equal the sequential calls",
511
+ async run(target) {
512
+ const store = await target.create();
513
+ await store.addTable(table("t"));
514
+ const begin = store.beginTransaction?.bind(store);
515
+ if (begin !== undefined) {
516
+ const begun = await begin({
517
+ record: {
518
+ id: "txn-b",
519
+ pendingBlockIds: [],
520
+ pendingSegmentIds: [],
521
+ status: "active",
522
+ revision: 0,
523
+ startedAt: T0,
524
+ updatedAt: T0,
525
+ committedVersion: null,
526
+ },
527
+ reserveRowIds: { tableId: "table-t", count: 4 },
528
+ });
529
+ check(begun.record.snapshotVersion === (await store.getCurrentManifestVersion()), "beginTransaction must pin the current version");
530
+ check(begun.rowIds !== undefined && begun.rowIds.endExclusive - begun.rowIds.start === 4n, "beginTransaction must honor the reservation");
531
+ }
532
+ const stage = store.stageTransactionArtifacts?.bind(store);
533
+ if (stage !== undefined) {
534
+ await store.createTransaction(transaction("txn-s", await store.getCurrentManifestVersion()));
535
+ const staged = await stage({
536
+ transactionId: "txn-s",
537
+ expectedRevision: 0,
538
+ blocks: [{ id: "block-staged", bytes: Uint8Array.of(7) }],
539
+ segments: [segment("segment-staged", "table-t", "txn-s", "block-staged")],
540
+ updatedAt: LATER,
541
+ });
542
+ checkEqual(staged.pendingBlockIds, ["block-staged"], "staging must journal the block");
543
+ check((await store.getBlock("block-staged")) !== undefined, "staging must write the bytes");
544
+ check((await store.getSegment("segment-staged")) !== undefined, "staging must write the segment");
545
+ }
546
+ const probe = store.getCatalogProbe?.bind(store);
547
+ if (probe !== undefined) {
548
+ const before = await probe();
549
+ await store.addTable(table("probe-check"));
550
+ const after = await probe();
551
+ check(after.catalogEpoch > before.catalogEpoch, "a catalog mutation must advance the probe's epoch");
552
+ }
553
+ const catalogState = store.getQueryCatalogState?.bind(store);
554
+ if (catalogState !== undefined) {
555
+ const state = await catalogState(["t", "missing-table"]);
556
+ check(state.tables.length === 2 && state.tables[1] === undefined, "getQueryCatalogState must be positional with undefined for missing tables");
557
+ checkEqual(state.tables[0]?.id, (await store.getTableByName("t"))?.id, "getQueryCatalogState must return the same records the individual reads do");
558
+ }
559
+ store.close();
560
+ },
561
+ },
562
+ {
563
+ name: "the single-shot write, when present, commits atomically and refuses typed",
564
+ async run(target) {
565
+ const store = await target.create();
566
+ const write = store.writeTransaction?.bind(store);
567
+ if (write === undefined) {
568
+ store.close();
569
+ return;
570
+ }
571
+ await store.addTable(table("t", true));
572
+ const fresh = (id) => {
573
+ const { snapshotVersion: _pinned, ...record } = transaction(id, null);
574
+ void _pinned;
575
+ return record;
576
+ };
577
+ // A fresh record: begun, staged, and committed in the one step.
578
+ const first = await write({
579
+ transaction: { record: fresh("txn-w1") },
580
+ blocks: [{ id: "block-w1", bytes: Uint8Array.of(1) }],
581
+ segments: [segment("segment-w1", "table-t", "txn-w1", "block-w1")],
582
+ expectedManifestVersion: null,
583
+ uniqueKeyChanges: [{ tableId: "table-t", keyTokens: ["number:1"], requireAbsent: true }],
584
+ committedAt: LATER,
585
+ });
586
+ checkEqual(first.version, 0, "the first single-shot write must publish version 0");
587
+ const committed = await store.getTransaction("txn-w1");
588
+ check(committed?.status === "committed" &&
589
+ committed.committedVersion === 0 &&
590
+ committed.snapshotVersion === null, "the fresh record must end committed, pinned at the expected version");
591
+ checkEqual(committed.pendingBlockIds, ["block-w1"], "the single-shot write must journal its blocks on the record");
592
+ check((await store.getBlock("block-w1")) !== undefined &&
593
+ (await store.getSegment("segment-w1"))?.logicalOrder === 0, "the single-shot write must land bytes and finalized segments");
594
+ checkEqual(await store.getExistingUniqueKeys("table-t", ["number:1"]), ["number:1"], "the single-shot write must apply unique-key changes");
595
+ // A stale version refuses as WriteConflictError and leaves nothing behind — not even
596
+ // the record.
597
+ await checkThrows(() => write({
598
+ transaction: { record: fresh("txn-stale") },
599
+ blocks: [{ id: "block-stale", bytes: Uint8Array.of(2) }],
600
+ segments: [segment("segment-stale", "table-t", "txn-stale", "block-stale")],
601
+ expectedManifestVersion: null,
602
+ committedAt: LATER,
603
+ }), WriteConflictError, "a single-shot write against a moved manifest");
604
+ check((await store.getTransaction("txn-stale")) === undefined &&
605
+ (await store.getBlock("block-stale")) === undefined &&
606
+ (await store.getSegment("segment-stale")) === undefined, "a refused single-shot write must leave no record, block, or segment");
607
+ // So does a duplicate unique key.
608
+ await checkThrows(() => write({
609
+ transaction: { record: fresh("txn-dup") },
610
+ blocks: [{ id: "block-dup", bytes: Uint8Array.of(3) }],
611
+ segments: [segment("segment-dup", "table-t", "txn-dup", "block-dup")],
612
+ expectedManifestVersion: 0,
613
+ uniqueKeyChanges: [
614
+ { tableId: "table-t", keyTokens: ["number:1"], requireAbsent: true },
615
+ ],
616
+ committedAt: LATER,
617
+ }), UniqueKeyConflictError, "a single-shot write inserting an existing unique key");
618
+ check((await store.getTransaction("txn-dup")) === undefined &&
619
+ (await store.getBlock("block-dup")) === undefined, "a key conflict must leave no record or block");
620
+ checkEqual(await store.getCurrentManifestVersion(), 0, "refusals must not advance");
621
+ // A begun transaction — the shape a reservation needs — continues into the same step.
622
+ await store.createTransaction(transaction("txn-w2", 0));
623
+ await checkThrows(() => write({
624
+ transaction: { id: "txn-w2", expectedRevision: 4 },
625
+ blocks: [{ id: "block-w2", bytes: Uint8Array.of(4) }],
626
+ segments: [segment("segment-w2", "table-t", "txn-w2", "block-w2")],
627
+ expectedManifestVersion: 0,
628
+ committedAt: LATER,
629
+ }), TransactionRecordConflictError, "a single-shot write with a stale transaction revision");
630
+ check((await store.getBlock("block-w2")) === undefined, "a revision conflict must leave no block");
631
+ const second = await write({
632
+ transaction: { id: "txn-w2", expectedRevision: 0 },
633
+ blocks: [{ id: "block-w2", bytes: Uint8Array.of(4) }],
634
+ segments: [segment("segment-w2", "table-t", "txn-w2", "block-w2")],
635
+ expectedManifestVersion: 0,
636
+ removedBlockIds: ["block-w1"],
637
+ changedTableIds: ["table-t"],
638
+ committedAt: LATER,
639
+ });
640
+ checkEqual(second.version, 1, "the continued write must publish the next version");
641
+ checkEqual(second.changedTableIds, ["table-t"], "the summary must carry the change set");
642
+ const manifest = await store.getManifest(1);
643
+ checkEqual(manifest?.blockIds, ["block-w2"], "the continued write must add its blocks and apply supersessions");
644
+ check((await store.getTransaction("txn-w2"))?.status === "committed", "the begun record must end committed");
645
+ store.close();
646
+ },
647
+ },
648
+ {
649
+ name: "the lease move, when present, re-pins in place and refuses typed",
650
+ async run(target) {
651
+ const store = await target.create();
652
+ const move = store.moveLease?.bind(store);
653
+ if (move === undefined) {
654
+ store.close();
655
+ return;
656
+ }
657
+ await store.addTable(table("t"));
658
+ const first = await commitOne(store, "one");
659
+ const second = await commitOne(store, "two");
660
+ await store.createLease({
661
+ id: "lease-m",
662
+ kind: "reader",
663
+ manifestVersion: first.version,
664
+ ownerId: "owner-m",
665
+ expiresAt: LATER,
666
+ revision: 0,
667
+ });
668
+ await checkThrows(() => move("lease-m", 9, second.version, FAR_FUTURE), LeaseConflictError, "moving with a stale revision");
669
+ await checkThrows(() => move("lease-m", 0, 999, FAR_FUTURE), SnapshotManifestMissingError, "moving to a version with no manifest");
670
+ checkEqual(await store.getLease("lease-m"), {
671
+ id: "lease-m",
672
+ kind: "reader",
673
+ manifestVersion: first.version,
674
+ ownerId: "owner-m",
675
+ expiresAt: LATER,
676
+ revision: 0,
677
+ }, "a refused move must leave the lease exactly as it was");
678
+ const moved = await move("lease-m", 0, second.version, FAR_FUTURE);
679
+ checkEqual(moved, {
680
+ id: "lease-m",
681
+ kind: "reader",
682
+ manifestVersion: second.version,
683
+ ownerId: "owner-m",
684
+ expiresAt: FAR_FUTURE,
685
+ revision: 1,
686
+ }, "a move must re-pin, renew, and advance the revision");
687
+ checkEqual((await store.listLeases()).map((lease) => lease.id), ["lease-m"], "a move must keep the one record, not create a second");
688
+ store.close();
689
+ },
690
+ },
691
+ {
692
+ name: "everything committed survives a reopen",
693
+ async run(target) {
694
+ if (target.reopen === undefined)
695
+ return;
696
+ let store = await target.create();
697
+ await store.addTable(table("t", true));
698
+ let { version, blockId } = await commitOne(store, "one", { keyTokens: ["number:1"] });
699
+ const reserved = await store.reserveRowIds("table-t", 10);
700
+ // The single-shot write and the lease move, when present, must be just as durable.
701
+ const write = store.writeTransaction?.bind(store);
702
+ if (write !== undefined) {
703
+ const { snapshotVersion: _pinned, ...record } = transaction("txn-single", null);
704
+ void _pinned;
705
+ const summary = await write({
706
+ transaction: { record },
707
+ blocks: [{ id: "block-single", bytes: Uint8Array.of(1, 2, 3, 4) }],
708
+ segments: [segment("segment-single", "table-t", "txn-single", "block-single")],
709
+ expectedManifestVersion: version,
710
+ uniqueKeyChanges: [
711
+ { tableId: "table-t", keyTokens: ["number:1"], requireAbsent: false },
712
+ ],
713
+ committedAt: LATER,
714
+ });
715
+ version = summary.version;
716
+ blockId = "block-single";
717
+ }
718
+ const move = store.moveLease?.bind(store);
719
+ if (move !== undefined) {
720
+ await store.createLease({
721
+ id: "lease-durable",
722
+ kind: "reader",
723
+ manifestVersion: null,
724
+ ownerId: "owner",
725
+ expiresAt: FAR_FUTURE,
726
+ revision: 0,
727
+ });
728
+ await move("lease-durable", 0, version, FAR_FUTURE);
729
+ }
730
+ store = await target.reopen(store);
731
+ checkEqual(await store.getCurrentManifestVersion(), version, "the committed version must survive a reopen");
732
+ if (move !== undefined) {
733
+ checkEqual((await store.getLease("lease-durable"))?.manifestVersion, version, "a moved lease must survive a reopen at its new version");
734
+ }
735
+ checkEqual(await store.getBlock(blockId), Uint8Array.of(1, 2, 3, 4), "committed bytes must survive a reopen");
736
+ checkEqual(await store.getExistingUniqueKeys("table-t", ["number:1"]), ["number:1"], "unique-key membership must survive a reopen");
737
+ check((await store.getTableByName("t")) !== undefined, "the catalog must survive a reopen");
738
+ const next = await store.reserveRowIds("table-t", 1);
739
+ check(next.start >= reserved.endExclusive, "counters must continue past the reserved high-water mark after a reopen");
740
+ store.close();
741
+ },
742
+ },
743
+ ];
744
+ }
745
+ //# sourceMappingURL=block-store-conformance.js.map