@minnowdb/core 0.3.0 → 0.4.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 (276) hide show
  1. package/README.md +4 -2
  2. package/dist/block-format/block.d.ts +32 -13
  3. package/dist/block-format/block.js +179 -55
  4. package/dist/block-format/checksum.d.ts +2 -1
  5. package/dist/block-format/checksum.js +5 -2
  6. package/dist/block-format/codecs.d.ts +7 -2
  7. package/dist/block-format/codecs.js +53 -12
  8. package/dist/block-format/column.d.ts +3 -2
  9. package/dist/block-format/column.js +96 -30
  10. package/dist/block-format/index.d.ts +2 -2
  11. package/dist/block-format/index.js +2 -2
  12. package/dist/block-format/physical.d.ts +8 -2
  13. package/dist/block-format/physical.js +14 -6
  14. package/dist/block-format/types.d.ts +7 -3
  15. package/dist/block-format/types.js +0 -1
  16. package/dist/block-format/unicode.d.ts +11 -0
  17. package/dist/block-format/unicode.js +46 -0
  18. package/dist/date-value.d.ts +15 -0
  19. package/dist/date-value.js +64 -0
  20. package/dist/engine/artifact-cache.d.ts +0 -1
  21. package/dist/engine/artifact-cache.js +0 -1
  22. package/dist/engine/batch.d.ts +5 -2
  23. package/dist/engine/batch.js +0 -1
  24. package/dist/engine/buffered-writer.d.ts +2 -1
  25. package/dist/engine/buffered-writer.js +34 -9
  26. package/dist/engine/cache-limits.d.ts +25 -0
  27. package/dist/engine/cache-limits.js +25 -0
  28. package/dist/engine/catalog.d.ts +5 -6
  29. package/dist/engine/catalog.js +5 -5
  30. package/dist/engine/client.d.ts +27 -7
  31. package/dist/engine/client.js +197 -31
  32. package/dist/engine/database.d.ts +161 -38
  33. package/dist/engine/database.js +4320 -1523
  34. package/dist/engine/defaults.d.ts +0 -1
  35. package/dist/engine/defaults.js +2 -4
  36. package/dist/engine/errors.d.ts +19 -1
  37. package/dist/engine/errors.js +32 -2
  38. package/dist/engine/fts.d.ts +0 -6
  39. package/dist/engine/fts.js +39 -14
  40. package/dist/engine/group-index.d.ts +0 -1
  41. package/dist/engine/group-index.js +6 -11
  42. package/dist/engine/index.d.ts +16 -11
  43. package/dist/engine/index.js +12 -10
  44. package/dist/engine/join-index.d.ts +0 -1
  45. package/dist/engine/join-index.js +2 -2
  46. package/dist/engine/keyed-live.d.ts +57 -0
  47. package/dist/engine/keyed-live.js +226 -0
  48. package/dist/engine/live-api.d.ts +4 -0
  49. package/dist/engine/live-api.js +4 -0
  50. package/dist/engine/live.d.ts +43 -26
  51. package/dist/engine/live.js +499 -129
  52. package/dist/engine/memory.d.ts +2 -1
  53. package/dist/engine/memory.js +3 -2
  54. package/dist/engine/optimizer.d.ts +0 -1
  55. package/dist/engine/optimizer.js +4 -4
  56. package/dist/engine/query-api.d.ts +3 -0
  57. package/dist/engine/query-api.js +3 -0
  58. package/dist/engine/query-cache.d.ts +0 -1
  59. package/dist/engine/query-cache.js +4 -4
  60. package/dist/engine/query.d.ts +4 -7
  61. package/dist/engine/query.js +348 -155
  62. package/dist/engine/result-wire.d.ts +0 -1
  63. package/dist/engine/result-wire.js +2 -2
  64. package/dist/engine/schema-wire.d.ts +1 -2
  65. package/dist/engine/schema-wire.js +0 -1
  66. package/dist/engine/schema.d.ts +3 -8
  67. package/dist/engine/schema.js +41 -24
  68. package/dist/engine/sort-keys.d.ts +0 -1
  69. package/dist/engine/sort-keys.js +2 -2
  70. package/dist/engine/sql-domains.d.ts +6 -1
  71. package/dist/engine/sql-domains.js +209 -45
  72. package/dist/engine/sql-driver.d.ts +2 -2
  73. package/dist/engine/sql-driver.js +0 -1
  74. package/dist/engine/sql-json.d.ts +0 -1
  75. package/dist/engine/sql-json.js +53 -7
  76. package/dist/engine/sql-semantics.d.ts +8 -5
  77. package/dist/engine/sql-semantics.js +427 -65
  78. package/dist/engine/typed-live.d.ts +67 -0
  79. package/dist/engine/typed-live.js +349 -0
  80. package/dist/engine/vector.d.ts +7 -1
  81. package/dist/engine/vector.js +202 -58
  82. package/dist/engine/worker-host.d.ts +8 -2
  83. package/dist/engine/worker-host.js +616 -152
  84. package/dist/engine/worker.d.ts +0 -1
  85. package/dist/engine/worker.js +3 -3
  86. package/dist/engine/write-block-planner.d.ts +18 -0
  87. package/dist/engine/write-block-planner.js +112 -0
  88. package/dist/index.d.ts +0 -1
  89. package/dist/index.js +0 -1
  90. package/dist/plan/index.d.ts +0 -1
  91. package/dist/plan/index.js +0 -1
  92. package/dist/storage/index.d.ts +1 -1
  93. package/dist/storage/index.js +1 -1
  94. package/dist/storage/indexeddb.d.ts +74 -65
  95. package/dist/storage/indexeddb.js +12301 -2076
  96. package/dist/storage/memory.d.ts +56 -66
  97. package/dist/storage/memory.js +1004 -139
  98. package/dist/storage/opfs/files.d.ts +18 -7
  99. package/dist/storage/opfs/files.js +115 -28
  100. package/dist/storage/opfs/index.d.ts +1 -1
  101. package/dist/storage/opfs/index.js +1 -1
  102. package/dist/storage/opfs/leader.d.ts +403 -91
  103. package/dist/storage/opfs/leader.js +4306 -602
  104. package/dist/storage/opfs/rpc.d.ts +15 -3
  105. package/dist/storage/opfs/rpc.js +223 -2
  106. package/dist/storage/opfs/snapshot-ledger.d.ts +40 -0
  107. package/dist/storage/opfs/snapshot-ledger.js +281 -0
  108. package/dist/storage/opfs/store.d.ts +26 -121
  109. package/dist/storage/opfs/store.js +436 -388
  110. package/dist/storage/persistence.d.ts +37 -0
  111. package/dist/storage/persistence.js +78 -0
  112. package/dist/storage/snapshot-stream.d.ts +24 -0
  113. package/dist/storage/snapshot-stream.js +897 -0
  114. package/dist/storage/snapshot.d.ts +9 -99
  115. package/dist/storage/snapshot.js +33 -309
  116. package/dist/storage/toolkit/extents.d.ts +41 -7
  117. package/dist/storage/toolkit/extents.js +402 -39
  118. package/dist/storage/toolkit/index.d.ts +3 -4
  119. package/dist/storage/toolkit/index.js +27 -3
  120. package/dist/storage/toolkit/record-core.d.ts +174 -61
  121. package/dist/storage/toolkit/record-core.js +5401 -849
  122. package/dist/storage/toolkit/sync-file.d.ts +10 -1
  123. package/dist/storage/toolkit/sync-file.js +50 -2
  124. package/dist/storage/toolkit/wal.d.ts +23 -5
  125. package/dist/storage/toolkit/wal.js +89 -27
  126. package/dist/storage/toolkit/wire.d.ts +4 -6
  127. package/dist/storage/toolkit/wire.js +80 -25
  128. package/dist/storage/types.d.ts +1271 -218
  129. package/dist/storage/types.js +1471 -138
  130. package/dist/testing/block-store-conformance.d.ts +0 -1
  131. package/dist/testing/block-store-conformance.js +877 -138
  132. package/dist/testing/index.d.ts +42 -31
  133. package/dist/testing/index.js +100 -68
  134. package/dist/testing/opfs-shim.d.ts +13 -3
  135. package/dist/testing/opfs-shim.js +40 -6
  136. package/dist/testing/simulator.d.ts +0 -1
  137. package/dist/testing/simulator.js +43 -12
  138. package/dist/testing/sqllogictest.d.ts +0 -1
  139. package/dist/testing/sqllogictest.js +0 -1
  140. package/dist/transactions/index.d.ts +81 -20
  141. package/dist/transactions/index.js +1050 -268
  142. package/dist/worker-protocol/index.d.ts +3 -2
  143. package/dist/worker-protocol/index.js +2 -5
  144. package/package.json +49 -1
  145. package/dist/block-format/block.d.ts.map +0 -1
  146. package/dist/block-format/block.js.map +0 -1
  147. package/dist/block-format/checksum.d.ts.map +0 -1
  148. package/dist/block-format/checksum.js.map +0 -1
  149. package/dist/block-format/codecs.d.ts.map +0 -1
  150. package/dist/block-format/codecs.js.map +0 -1
  151. package/dist/block-format/column.d.ts.map +0 -1
  152. package/dist/block-format/column.js.map +0 -1
  153. package/dist/block-format/index.d.ts.map +0 -1
  154. package/dist/block-format/index.js.map +0 -1
  155. package/dist/block-format/physical.d.ts.map +0 -1
  156. package/dist/block-format/physical.js.map +0 -1
  157. package/dist/block-format/types.d.ts.map +0 -1
  158. package/dist/block-format/types.js.map +0 -1
  159. package/dist/engine/artifact-cache.d.ts.map +0 -1
  160. package/dist/engine/artifact-cache.js.map +0 -1
  161. package/dist/engine/batch.d.ts.map +0 -1
  162. package/dist/engine/batch.js.map +0 -1
  163. package/dist/engine/buffered-writer.d.ts.map +0 -1
  164. package/dist/engine/buffered-writer.js.map +0 -1
  165. package/dist/engine/catalog.d.ts.map +0 -1
  166. package/dist/engine/catalog.js.map +0 -1
  167. package/dist/engine/client.d.ts.map +0 -1
  168. package/dist/engine/client.js.map +0 -1
  169. package/dist/engine/coordinator.d.ts +0 -17
  170. package/dist/engine/coordinator.d.ts.map +0 -1
  171. package/dist/engine/coordinator.js +0 -60
  172. package/dist/engine/coordinator.js.map +0 -1
  173. package/dist/engine/database.d.ts.map +0 -1
  174. package/dist/engine/database.js.map +0 -1
  175. package/dist/engine/defaults.d.ts.map +0 -1
  176. package/dist/engine/defaults.js.map +0 -1
  177. package/dist/engine/errors.d.ts.map +0 -1
  178. package/dist/engine/errors.js.map +0 -1
  179. package/dist/engine/fts.d.ts.map +0 -1
  180. package/dist/engine/fts.js.map +0 -1
  181. package/dist/engine/group-index.d.ts.map +0 -1
  182. package/dist/engine/group-index.js.map +0 -1
  183. package/dist/engine/index.d.ts.map +0 -1
  184. package/dist/engine/index.js.map +0 -1
  185. package/dist/engine/join-index.d.ts.map +0 -1
  186. package/dist/engine/join-index.js.map +0 -1
  187. package/dist/engine/live.d.ts.map +0 -1
  188. package/dist/engine/live.js.map +0 -1
  189. package/dist/engine/memory.d.ts.map +0 -1
  190. package/dist/engine/memory.js.map +0 -1
  191. package/dist/engine/optimizer.d.ts.map +0 -1
  192. package/dist/engine/optimizer.js.map +0 -1
  193. package/dist/engine/query-cache.d.ts.map +0 -1
  194. package/dist/engine/query-cache.js.map +0 -1
  195. package/dist/engine/query.d.ts.map +0 -1
  196. package/dist/engine/query.js.map +0 -1
  197. package/dist/engine/result-wire.d.ts.map +0 -1
  198. package/dist/engine/result-wire.js.map +0 -1
  199. package/dist/engine/schema-wire.d.ts.map +0 -1
  200. package/dist/engine/schema-wire.js.map +0 -1
  201. package/dist/engine/schema.d.ts.map +0 -1
  202. package/dist/engine/schema.js.map +0 -1
  203. package/dist/engine/sort-keys.d.ts.map +0 -1
  204. package/dist/engine/sort-keys.js.map +0 -1
  205. package/dist/engine/sql-domains.d.ts.map +0 -1
  206. package/dist/engine/sql-domains.js.map +0 -1
  207. package/dist/engine/sql-driver.d.ts.map +0 -1
  208. package/dist/engine/sql-driver.js.map +0 -1
  209. package/dist/engine/sql-json.d.ts.map +0 -1
  210. package/dist/engine/sql-json.js.map +0 -1
  211. package/dist/engine/sql-semantics.d.ts.map +0 -1
  212. package/dist/engine/sql-semantics.js.map +0 -1
  213. package/dist/engine/vector.d.ts.map +0 -1
  214. package/dist/engine/vector.js.map +0 -1
  215. package/dist/engine/worker-host.d.ts.map +0 -1
  216. package/dist/engine/worker-host.js.map +0 -1
  217. package/dist/engine/worker.d.ts.map +0 -1
  218. package/dist/engine/worker.js.map +0 -1
  219. package/dist/index.d.ts.map +0 -1
  220. package/dist/index.js.map +0 -1
  221. package/dist/plan/index.d.ts.map +0 -1
  222. package/dist/plan/index.js.map +0 -1
  223. package/dist/storage/fixture-shape.d.ts +0 -42
  224. package/dist/storage/fixture-shape.d.ts.map +0 -1
  225. package/dist/storage/fixture-shape.js +0 -146
  226. package/dist/storage/fixture-shape.js.map +0 -1
  227. package/dist/storage/index.d.ts.map +0 -1
  228. package/dist/storage/index.js.map +0 -1
  229. package/dist/storage/indexeddb.d.ts.map +0 -1
  230. package/dist/storage/indexeddb.js.map +0 -1
  231. package/dist/storage/memory.d.ts.map +0 -1
  232. package/dist/storage/memory.js.map +0 -1
  233. package/dist/storage/opfs/files.d.ts.map +0 -1
  234. package/dist/storage/opfs/files.js.map +0 -1
  235. package/dist/storage/opfs/index.d.ts.map +0 -1
  236. package/dist/storage/opfs/index.js.map +0 -1
  237. package/dist/storage/opfs/leader.d.ts.map +0 -1
  238. package/dist/storage/opfs/leader.js.map +0 -1
  239. package/dist/storage/opfs/rpc.d.ts.map +0 -1
  240. package/dist/storage/opfs/rpc.js.map +0 -1
  241. package/dist/storage/opfs/store.d.ts.map +0 -1
  242. package/dist/storage/opfs/store.js.map +0 -1
  243. package/dist/storage/snapshot.d.ts.map +0 -1
  244. package/dist/storage/snapshot.js.map +0 -1
  245. package/dist/storage/toolkit/extents.d.ts.map +0 -1
  246. package/dist/storage/toolkit/extents.js.map +0 -1
  247. package/dist/storage/toolkit/index.d.ts.map +0 -1
  248. package/dist/storage/toolkit/index.js.map +0 -1
  249. package/dist/storage/toolkit/record-core.d.ts.map +0 -1
  250. package/dist/storage/toolkit/record-core.js.map +0 -1
  251. package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
  252. package/dist/storage/toolkit/sync-file.js.map +0 -1
  253. package/dist/storage/toolkit/wal.d.ts.map +0 -1
  254. package/dist/storage/toolkit/wal.js.map +0 -1
  255. package/dist/storage/toolkit/wire.d.ts.map +0 -1
  256. package/dist/storage/toolkit/wire.js.map +0 -1
  257. package/dist/storage/types.d.ts.map +0 -1
  258. package/dist/storage/types.js.map +0 -1
  259. package/dist/testing/block-store-conformance.d.ts.map +0 -1
  260. package/dist/testing/block-store-conformance.js.map +0 -1
  261. package/dist/testing/index.d.ts.map +0 -1
  262. package/dist/testing/index.js.map +0 -1
  263. package/dist/testing/opfs-shim.d.ts.map +0 -1
  264. package/dist/testing/opfs-shim.js.map +0 -1
  265. package/dist/testing/seeds.d.ts +0 -11
  266. package/dist/testing/seeds.d.ts.map +0 -1
  267. package/dist/testing/seeds.js +0 -50
  268. package/dist/testing/seeds.js.map +0 -1
  269. package/dist/testing/simulator.d.ts.map +0 -1
  270. package/dist/testing/simulator.js.map +0 -1
  271. package/dist/testing/sqllogictest.d.ts.map +0 -1
  272. package/dist/testing/sqllogictest.js.map +0 -1
  273. package/dist/transactions/index.d.ts.map +0 -1
  274. package/dist/transactions/index.js.map +0 -1
  275. package/dist/worker-protocol/index.d.ts.map +0 -1
  276. package/dist/worker-protocol/index.js.map +0 -1
@@ -1,5 +1,8 @@
1
- import { activePostingStorageColumnIds, } from "./types.js";
2
- import { RecordCore, validateId, validateTempRunPage, validateTempRunPageIdentity, } from "./toolkit/record-core.js";
1
+ import { BlockReadBatchTooLargeError, MAX_SNAPSHOT_SESSION_TTL_MS, MAX_POSTING_BUILD_TTL_MS, MAX_SNAPSHOT_FRAME_BATCH_BYTES, MAX_SNAPSHOT_FRAME_BATCH_ITEMS, MAX_SNAPSHOT_METADATA_BATCH_BYTES, SNAPSHOT_FRAME_KINDS, MAX_TEMP_BYTES_PER_OWNER, MAX_TEMP_BYTES_TOTAL, MAX_TEMP_PAGES_PER_OWNER, MAX_TEMP_PAGES_TOTAL, MAX_TEMP_RUNS_PER_OWNER, MAX_TEMP_RUNS_TOTAL, PostingBuildConflictError, StorageResourceLimitError, MAX_BLOCK_READ_BATCH_BYTES, SnapshotImportConflictError, StorageCorruptionError, activePostingStorageColumnIds, assertStorageBulkReadItems, validateStorageId, assertTempRunPageBatchLimits, } from "./types.js";
2
+ import { verifyStoredBlock } from "../block-format/index.js";
3
+ import { crc32 } from "../block-format/checksum.js";
4
+ import { decodeSnapshotMetadataItems, encodeSnapshotFrameStreamFooter, encodeSnapshotFrameStreamHeader, encodeSnapshotMetadataPage, extendSnapshotFrameStreamChecksum, prepareSnapshotFrameStreamHeader, snapshotFrameEnvelopeParts, snapshotFrameStreamHeaderIdentity, } from "./snapshot-stream.js";
5
+ import { RecordCore, validateFtsPostingChunks, validateId, validateTempRunPage, validateTempRunPageIdentity, } from "./toolkit/record-core.js";
3
6
  /**
4
7
  * The in-process store: record semantics live in `RecordCore` (shared with the OPFS store),
5
8
  * block and temp-page bytes live in Maps here, and atomicity comes from running every mutating
@@ -8,54 +11,71 @@ import { RecordCore, validateId, validateTempRunPage, validateTempRunPageIdentit
8
11
  */
9
12
  export class MemoryBlockStore {
10
13
  #blocks = new Map();
14
+ #blockChecksums = new Map();
15
+ #physicalBlockBytes = 0;
11
16
  #tempRunPages = new Map();
17
+ #tempUsageByOwner = new Map();
18
+ #tempPageKeysByRun = new Map();
19
+ #tempBytesTotal = 0;
20
+ #tempPagesTotal = 0;
21
+ #tempRunsTotal = 0;
12
22
  #ftsBaseBuilds = new Map();
23
+ #snapshotFrameExport;
24
+ #snapshotFrameImport;
25
+ #completedSnapshotFrameImport;
13
26
  #core = new RecordCore({
14
27
  hasBlock: (id) => this.#blocks.has(id),
15
28
  blockByteLength: (id) => this.#blocks.get(id)?.byteLength,
29
+ blockChecksum: (id) => this.#blockChecksums.get(id),
16
30
  });
17
31
  #commitQueue = Promise.resolve();
18
- async addBlock(id, bytes) {
19
- return this.addBlocks([{ id, bytes }]);
20
- }
21
- async addBlocks(blocks) {
22
- const ids = new Set();
23
- for (const block of blocks) {
24
- validateId(block.id);
25
- if (ids.has(block.id) || this.#blocks.has(block.id)) {
26
- throw new Error(`Block already exists: ${block.id}`);
27
- }
28
- ids.add(block.id);
29
- }
30
- for (const block of blocks)
31
- this.#blocks.set(block.id, new Uint8Array(block.bytes));
32
- }
33
32
  async getBlock(id) {
33
+ validateStorageId(id, "Block ID");
34
34
  const bytes = this.#blocks.get(id);
35
35
  return bytes === undefined ? undefined : new Uint8Array(bytes);
36
36
  }
37
37
  async getBlocks(ids) {
38
+ assertStorageBulkReadItems(ids, "Block read batch");
39
+ for (const id of ids)
40
+ validateStorageId(id, "Block ID");
41
+ let bytes = 0;
42
+ for (const id of ids) {
43
+ bytes += this.#blocks.get(id)?.byteLength ?? 0;
44
+ if (!Number.isSafeInteger(bytes) || bytes > MAX_BLOCK_READ_BATCH_BYTES) {
45
+ throw new BlockReadBatchTooLargeError(bytes);
46
+ }
47
+ }
38
48
  return ids.map((id) => {
39
49
  const bytes = this.#blocks.get(id);
40
50
  return bytes === undefined ? undefined : new Uint8Array(bytes);
41
51
  });
42
52
  }
43
- async removeBlock(id) {
44
- this.#blocks.delete(id);
53
+ async readManifestBlock(version, id) {
54
+ validateStorageId(id, "Block ID");
55
+ return this.#runAtomic(() => {
56
+ if (!this.#core.hasManifestBlocks(version, [id])[0])
57
+ return undefined;
58
+ const bytes = this.#blocks.get(id);
59
+ if (bytes === undefined) {
60
+ throw new StorageCorruptionError("memory", id, "manifest member payload is missing");
61
+ }
62
+ return new Uint8Array(bytes);
63
+ });
45
64
  }
46
- async listBlockIds() {
47
- return [...this.#blocks.keys()].sort();
65
+ async hasManifestBlocks(version, ids) {
66
+ assertStorageBulkReadItems(ids, "Manifest membership batch");
67
+ return this.#runAtomic(() => this.#core.hasManifestBlocks(version, ids));
48
68
  }
49
69
  async putTempRunPage(page) {
70
+ assertTempRunPageBatchLimits([page]);
50
71
  validateTempRunPage(page);
51
- this.#tempRunPages.set(tempRunPageKey(page.ownerId, page.runId, page.pageIndex), page.bytes.slice());
72
+ return this.#runAtomic(() => this.#putTempRunPages([page]));
52
73
  }
53
74
  async putTempRunPages(pages) {
75
+ assertTempRunPageBatchLimits(pages);
54
76
  for (const page of pages)
55
77
  validateTempRunPage(page);
56
- for (const page of pages) {
57
- this.#tempRunPages.set(tempRunPageKey(page.ownerId, page.runId, page.pageIndex), page.bytes.slice());
58
- }
78
+ return this.#runAtomic(() => this.#putTempRunPages(pages));
59
79
  }
60
80
  async getTempRunPage(ownerId, runId, pageIndex) {
61
81
  validateTempRunPageIdentity(ownerId, runId, pageIndex);
@@ -63,15 +83,14 @@ export class MemoryBlockStore {
63
83
  }
64
84
  async removeTempRun(ownerId, runId) {
65
85
  validateTempRunPageIdentity(ownerId, runId, 0);
66
- const prefix = tempRunPagePrefix(ownerId, runId);
67
- for (const key of this.#tempRunPages.keys())
68
- if (key.startsWith(prefix))
69
- this.#tempRunPages.delete(key);
86
+ return this.#runAtomic(() => this.#removeTempRunPages(ownerId, runId));
70
87
  }
71
88
  async removeTempOwner(ownerId) {
72
89
  validateId(ownerId);
73
- this.#removeTempOwnerPages(ownerId);
74
- this.#core.removeTempOwner(ownerId);
90
+ return this.#runAtomic(() => {
91
+ this.#removeTempOwnerPages(ownerId);
92
+ this.#core.removeTempOwner(ownerId);
93
+ });
75
94
  }
76
95
  async createTempOwner(record) {
77
96
  return this.#runAtomic(() => {
@@ -81,8 +100,8 @@ export class MemoryBlockStore {
81
100
  async getTempOwner(ownerId) {
82
101
  return this.#core.getTempOwner(ownerId);
83
102
  }
84
- async renewTempOwner(ownerId, expectedRevision, expiresAt) {
85
- return this.#runAtomic(() => this.#core.renewTempOwner(ownerId, expectedRevision, expiresAt));
103
+ async renewTempOwner(input) {
104
+ return this.#runAtomic(() => this.#core.renewTempOwner(input));
86
105
  }
87
106
  async removeTempOwnerIfExpired(ownerId, expiresAtCutoff) {
88
107
  return this.#runAtomic(() => {
@@ -101,26 +120,163 @@ export class MemoryBlockStore {
101
120
  }
102
121
  return this.#core.listTempOwnerIdsPage(afterOwnerId, limit, pageOwnerIds);
103
122
  }
123
+ async listExpiredTempOwnerPage(expiresAtCutoff, afterCursor, limit) {
124
+ return this.#core.listExpiredTempOwnerPage(expiresAtCutoff, afterCursor, limit);
125
+ }
126
+ #putTempRunPages(pages) {
127
+ const seenKeys = new Set();
128
+ const deltas = new Map();
129
+ let totalBytesDelta = 0;
130
+ let totalPagesDelta = 0;
131
+ let totalRunsDelta = 0;
132
+ for (const page of pages) {
133
+ if (this.#core.getTempOwner(page.ownerId) === undefined) {
134
+ throw new Error(`Temp owner does not exist: ${page.ownerId}`);
135
+ }
136
+ const key = tempRunPageKey(page.ownerId, page.runId, page.pageIndex);
137
+ if (seenKeys.has(key))
138
+ throw new TypeError(`Temp run page batch repeats ${key}`);
139
+ seenKeys.add(key);
140
+ const previousBytes = this.#tempRunPages.get(key)?.byteLength ?? 0;
141
+ const isNewPage = !this.#tempRunPages.has(key);
142
+ const delta = deltas.get(page.ownerId) ?? { bytes: 0, pages: 0, newRuns: new Set() };
143
+ delta.bytes += page.bytes.byteLength - previousBytes;
144
+ if (isNewPage)
145
+ delta.pages += 1;
146
+ const usage = this.#tempUsageByOwner.get(page.ownerId);
147
+ if (usage?.runs.has(page.runId) !== true && !delta.newRuns.has(page.runId)) {
148
+ delta.newRuns.add(page.runId);
149
+ }
150
+ deltas.set(page.ownerId, delta);
151
+ totalBytesDelta += page.bytes.byteLength - previousBytes;
152
+ if (isNewPage)
153
+ totalPagesDelta += 1;
154
+ }
155
+ for (const [ownerId, delta] of deltas) {
156
+ const usage = this.#tempUsageByOwner.get(ownerId);
157
+ const nextBytes = (usage?.bytes ?? 0) + delta.bytes;
158
+ const nextPages = (usage?.pages ?? 0) + delta.pages;
159
+ const nextRuns = (usage?.runs.size ?? 0) + delta.newRuns.size;
160
+ assertTempResourceLimit("temp owner byte", nextBytes, MAX_TEMP_BYTES_PER_OWNER);
161
+ assertTempResourceLimit("temp page", nextPages, MAX_TEMP_PAGES_PER_OWNER);
162
+ assertTempResourceLimit("temp run", nextRuns, MAX_TEMP_RUNS_PER_OWNER);
163
+ totalRunsDelta += delta.newRuns.size;
164
+ }
165
+ assertTempResourceLimit("temporary byte", this.#tempBytesTotal + totalBytesDelta, MAX_TEMP_BYTES_TOTAL);
166
+ assertTempResourceLimit("temporary page total", this.#tempPagesTotal + totalPagesDelta, MAX_TEMP_PAGES_TOTAL);
167
+ assertTempResourceLimit("temporary run total", this.#tempRunsTotal + totalRunsDelta, MAX_TEMP_RUNS_TOTAL);
168
+ for (const page of pages) {
169
+ const key = tempRunPageKey(page.ownerId, page.runId, page.pageIndex);
170
+ const previousBytes = this.#tempRunPages.get(key)?.byteLength ?? 0;
171
+ const isNewPage = !this.#tempRunPages.has(key);
172
+ const usage = this.#tempUsageByOwner.get(page.ownerId) ?? {
173
+ bytes: 0,
174
+ pages: 0,
175
+ runs: new Map(),
176
+ };
177
+ const previousRunPages = usage.runs.get(page.runId) ?? 0;
178
+ this.#tempRunPages.set(key, page.bytes.slice());
179
+ const runKey = tempRunPagePrefix(page.ownerId, page.runId);
180
+ const runKeys = this.#tempPageKeysByRun.get(runKey) ?? new Set();
181
+ runKeys.add(key);
182
+ this.#tempPageKeysByRun.set(runKey, runKeys);
183
+ usage.bytes += page.bytes.byteLength - previousBytes;
184
+ this.#tempBytesTotal += page.bytes.byteLength - previousBytes;
185
+ if (isNewPage) {
186
+ usage.pages += 1;
187
+ this.#tempPagesTotal += 1;
188
+ usage.runs.set(page.runId, previousRunPages + 1);
189
+ if (previousRunPages === 0)
190
+ this.#tempRunsTotal += 1;
191
+ }
192
+ this.#tempUsageByOwner.set(page.ownerId, usage);
193
+ }
194
+ }
195
+ #removeTempRunPages(ownerId, runId) {
196
+ const runKey = tempRunPagePrefix(ownerId, runId);
197
+ const keys = this.#tempPageKeysByRun.get(runKey);
198
+ if (keys === undefined)
199
+ return;
200
+ const usage = this.#tempUsageByOwner.get(ownerId);
201
+ let removedBytes = 0;
202
+ for (const key of keys) {
203
+ removedBytes += this.#tempRunPages.get(key)?.byteLength ?? 0;
204
+ this.#tempRunPages.delete(key);
205
+ }
206
+ this.#tempPageKeysByRun.delete(runKey);
207
+ this.#tempBytesTotal -= removedBytes;
208
+ this.#tempPagesTotal -= keys.size;
209
+ this.#tempRunsTotal -= 1;
210
+ if (usage !== undefined) {
211
+ usage.bytes -= removedBytes;
212
+ usage.pages -= keys.size;
213
+ usage.runs.delete(runId);
214
+ if (usage.pages === 0)
215
+ this.#tempUsageByOwner.delete(ownerId);
216
+ }
217
+ }
104
218
  #removeTempOwnerPages(ownerId) {
105
- const prefix = `${String(ownerId.length)}:${ownerId}:`;
106
- for (const key of this.#tempRunPages.keys())
107
- if (key.startsWith(prefix))
108
- this.#tempRunPages.delete(key);
219
+ const usage = this.#tempUsageByOwner.get(ownerId);
220
+ if (usage === undefined)
221
+ return;
222
+ for (const runId of [...usage.runs.keys()])
223
+ this.#removeTempRunPages(ownerId, runId);
109
224
  }
110
- async addTable(record) {
111
- this.#core.addTable(record);
225
+ async addTable(record, options = {}) {
226
+ this.#core.addTable(record, options);
112
227
  }
113
228
  async getTable(id) {
114
229
  return this.#core.getTable(id);
115
230
  }
116
231
  async updateTable(id, expectedRevision, update) {
117
- return this.#runAtomic(() => this.#core.updateTable(id, expectedRevision, update));
232
+ return this.#runAtomic(() => {
233
+ const before = this.#core.getTable(id);
234
+ const updated = this.#core.updateTable(id, expectedRevision, update);
235
+ if (before !== undefined)
236
+ this.#removeRetiredPostingBuilds(before, updated);
237
+ return updated;
238
+ });
239
+ }
240
+ async removeTable(id, expectedRevision, options = {}) {
241
+ return this.#runAtomic(() => {
242
+ this.#core.removeTable(id, expectedRevision, options);
243
+ this.#removeTablePostingBuilds(id);
244
+ });
118
245
  }
119
- async removeTable(id, expectedRevision) {
246
+ async dropTable(input) {
120
247
  return this.#runAtomic(() => {
121
- this.#core.removeTable(id, expectedRevision);
248
+ const summary = this.#core.dropTable(input);
249
+ this.#removeTablePostingBuilds(input.tableId);
250
+ return summary;
122
251
  });
123
252
  }
253
+ async dropTableColumn(input) {
254
+ return this.#runAtomic(() => {
255
+ const summary = this.#core.dropTableColumn(input);
256
+ this.#ftsBaseBuilds.delete(`${input.tableId}/${input.columnId}`);
257
+ return summary;
258
+ });
259
+ }
260
+ #removeRetiredPostingBuilds(before, after) {
261
+ const retained = new Set([
262
+ ...Object.keys(after.ftsColumns ?? {}),
263
+ ...Object.values(after.secondaryIndexes ?? {}).map((index) => index.storageColumnId),
264
+ ]);
265
+ for (const columnId of [
266
+ ...Object.keys(before.ftsColumns ?? {}),
267
+ ...Object.values(before.secondaryIndexes ?? {}).map((index) => index.storageColumnId),
268
+ ]) {
269
+ if (!retained.has(columnId))
270
+ this.#ftsBaseBuilds.delete(`${before.id}/${columnId}`);
271
+ }
272
+ }
273
+ #removeTablePostingBuilds(tableId) {
274
+ const prefix = `${tableId}/`;
275
+ for (const key of [...this.#ftsBaseBuilds.keys()]) {
276
+ if (key.startsWith(prefix))
277
+ this.#ftsBaseBuilds.delete(key);
278
+ }
279
+ }
124
280
  async writeFtsBase(tableId, columnId, input) {
125
281
  return this.#runAtomic(() => {
126
282
  this.#core.writeFtsBase(tableId, columnId, input);
@@ -132,31 +288,82 @@ export class MemoryBlockStore {
132
288
  this.#ftsBaseBuilds.delete(`${tableId}/${columnId}`);
133
289
  });
134
290
  }
135
- async beginFtsBaseBuild(tableId, columnId, buildId) {
291
+ async beginFtsBaseBuild(input) {
292
+ validatePostingBuildLifetime(input.createdAt, input.expiresAt);
293
+ validateId(input.tableId);
294
+ validateId(input.columnId);
295
+ validateId(input.buildId);
296
+ validateId(input.ownerId);
136
297
  return this.#runAtomic(() => {
137
- const table = this.#core.getTable(tableId);
138
- if (table === undefined || !activePostingStorageColumnIds(table).has(columnId)) {
139
- throw new Error(`Postings index is no longer active: ${tableId}/${columnId}`);
298
+ const key = `${input.tableId}/${input.columnId}`;
299
+ const current = this.#ftsBaseBuilds.get(key);
300
+ if (current !== undefined && Date.parse(current.expiresAt) > Date.parse(input.createdAt)) {
301
+ if (current.buildId === input.buildId && current.ownerId === input.ownerId)
302
+ return;
303
+ throw new PostingBuildConflictError(current.buildId, current.ownerId, "another live build owns the column");
140
304
  }
141
- this.#ftsBaseBuilds.set(`${tableId}/${columnId}`, { buildId, chunks: new Map() });
305
+ const table = this.#core.getTable(input.tableId);
306
+ if (table === undefined || !activePostingStorageColumnIds(table).has(input.columnId)) {
307
+ throw new Error(`Postings index is no longer active: ${key}`);
308
+ }
309
+ this.#ftsBaseBuilds.set(key, {
310
+ buildId: input.buildId,
311
+ ownerId: input.ownerId,
312
+ createdAt: input.createdAt,
313
+ expiresAt: input.expiresAt,
314
+ chunks: new Map(),
315
+ });
142
316
  });
143
317
  }
144
- async writeFtsBaseBuildChunk(tableId, columnId, buildId, ordinal, chunk) {
318
+ async renewFtsBaseBuild(input) {
319
+ validatePostingBuildLifetime(input.expiresAtCutoff, input.expiresAt);
145
320
  return this.#runAtomic(() => {
146
- const build = this.#ftsBaseBuilds.get(`${tableId}/${columnId}`);
147
- if (build?.buildId !== buildId)
148
- throw new Error(`Full-text base build changed: ${buildId}`);
149
- build.chunks.set(ordinal, structuredClone(chunk));
321
+ const build = this.#requirePostingBuild(input);
322
+ build.expiresAt = input.expiresAt;
150
323
  });
151
324
  }
152
- async finishFtsBaseBuild(tableId, columnId, buildId, input) {
325
+ async writeFtsBaseBuildChunk(input) {
326
+ validatePostingBuildLifetime(input.expiresAtCutoff, input.expiresAt);
153
327
  return this.#runAtomic(() => {
154
- const key = `${tableId}/${columnId}`;
155
- const build = this.#ftsBaseBuilds.get(key);
156
- if (build?.buildId !== buildId)
157
- throw new Error(`Full-text base build changed: ${buildId}`);
158
- const table = this.#core.getTable(tableId);
159
- if (table === undefined || !activePostingStorageColumnIds(table).has(columnId)) {
328
+ const build = this.#requirePostingBuild(input);
329
+ if (!Number.isSafeInteger(input.ordinal) || input.ordinal < 0) {
330
+ throw new RangeError(`Full-text base chunk ordinal is invalid: ${String(input.ordinal)}`);
331
+ }
332
+ validateFtsPostingChunks([input.chunk], "Full-text base build");
333
+ const replay = build.chunks.get(input.ordinal);
334
+ if (replay !== undefined) {
335
+ if (!samePostingChunk(replay, input.chunk)) {
336
+ throw new PostingBuildConflictError(input.buildId, input.ownerId, "chunk replay changed");
337
+ }
338
+ build.expiresAt = input.expiresAt;
339
+ return;
340
+ }
341
+ if (input.ordinal !== build.chunks.size) {
342
+ throw new Error(`Full-text base chunk is out of order: ${String(input.ordinal)}`);
343
+ }
344
+ // Chunks are row windows, not term partitions. Terms are sorted inside each chunk, but
345
+ // adjacent chunks can legitimately overlap or restart at an earlier term.
346
+ build.chunks.set(input.ordinal, structuredClone(input.chunk));
347
+ build.expiresAt = input.expiresAt;
348
+ });
349
+ }
350
+ async finishFtsBaseBuild(input) {
351
+ return this.#runAtomic(() => {
352
+ const key = `${input.tableId}/${input.columnId}`;
353
+ const build = this.#requirePostingBuild(input);
354
+ if (!Number.isSafeInteger(input.coversVersion) ||
355
+ input.coversVersion < -1 ||
356
+ !Number.isSafeInteger(input.chunkCount) ||
357
+ input.chunkCount < 0 ||
358
+ !Number.isSafeInteger(input.totalTokens) ||
359
+ input.totalTokens < 0) {
360
+ throw new TypeError("Full-text base build metadata is invalid");
361
+ }
362
+ if (input.chunkCount !== build.chunks.size) {
363
+ throw new Error(`Full-text base build is incomplete: ${input.buildId}`);
364
+ }
365
+ const table = this.#core.getTable(input.tableId);
366
+ if (table === undefined || !activePostingStorageColumnIds(table).has(input.columnId)) {
160
367
  this.#ftsBaseBuilds.delete(key);
161
368
  return;
162
369
  }
@@ -167,7 +374,7 @@ export class MemoryBlockStore {
167
374
  }
168
375
  return chunk;
169
376
  });
170
- this.#core.writeFtsBase(tableId, columnId, {
377
+ this.#core.writeFtsBase(input.tableId, input.columnId, {
171
378
  coversVersion: input.coversVersion,
172
379
  chunks,
173
380
  totalTokens: input.totalTokens,
@@ -175,18 +382,35 @@ export class MemoryBlockStore {
175
382
  this.#ftsBaseBuilds.delete(key);
176
383
  });
177
384
  }
178
- async abortFtsBaseBuild(tableId, columnId, buildId) {
385
+ async abortFtsBaseBuild(input) {
179
386
  return this.#runAtomic(() => {
180
- const key = `${tableId}/${columnId}`;
181
- if (this.#ftsBaseBuilds.get(key)?.buildId === buildId)
182
- this.#ftsBaseBuilds.delete(key);
387
+ const key = `${input.tableId}/${input.columnId}`;
388
+ const build = this.#ftsBaseBuilds.get(key);
389
+ if (build === undefined)
390
+ return;
391
+ if (build.buildId !== input.buildId ||
392
+ (build.ownerId !== input.ownerId &&
393
+ Date.parse(build.expiresAt) > Date.parse(input.expiresAtCutoff))) {
394
+ throw new PostingBuildConflictError(build.buildId, build.ownerId, "abort ownership changed");
395
+ }
396
+ this.#ftsBaseBuilds.delete(key);
183
397
  });
184
398
  }
185
- async readFtsCandidates(tableId, columnId, terms, upToVersion) {
186
- return this.#core.readFtsCandidates(tableId, columnId, terms, upToVersion);
399
+ #requirePostingBuild(input) {
400
+ const build = this.#ftsBaseBuilds.get(`${input.tableId}/${input.columnId}`);
401
+ if (build?.buildId !== input.buildId || build.ownerId !== input.ownerId) {
402
+ throw new PostingBuildConflictError(build?.buildId ?? input.buildId, build?.ownerId ?? input.ownerId, "session is missing or owned by another caller");
403
+ }
404
+ if (Date.parse(build.expiresAt) <= Date.parse(input.expiresAtCutoff)) {
405
+ throw new PostingBuildConflictError(build.buildId, build.ownerId, "session is expired");
406
+ }
407
+ return build;
408
+ }
409
+ async readFtsCandidates(tableId, columnId, terms, upToVersion, maxRowIds) {
410
+ return this.#core.readFtsCandidates(tableId, columnId, terms, upToVersion, maxRowIds);
187
411
  }
188
- async readFtsPostings(tableId, columnId, upToVersion) {
189
- return this.#core.readFtsPostings(tableId, columnId, upToVersion);
412
+ async readFtsPostings(tableId, columnId, upToVersion, maxRowIds, maxRetainedBytes) {
413
+ return this.#core.readFtsPostings(tableId, columnId, upToVersion, maxRowIds, maxRetainedBytes);
190
414
  }
191
415
  async getTableByName(name) {
192
416
  const id = this.#core.getTableIdByName(name);
@@ -195,20 +419,20 @@ export class MemoryBlockStore {
195
419
  async listTables() {
196
420
  return this.#core.listTables();
197
421
  }
198
- async addSegment(record) {
199
- this.#core.addSegment(record);
200
- }
201
422
  async getSegment(id) {
202
423
  return this.#core.getSegment(id);
203
424
  }
204
- async listSegments(tableId) {
205
- return this.#core.listSegments(tableId);
206
- }
207
425
  async listSegmentPage(afterId, limit) {
208
426
  return this.#core.listSegmentPage(afterId, limit);
209
427
  }
210
- async removeSegment(id) {
211
- this.#core.removeSegment(id);
428
+ async listTableSegmentPage(tableId, afterId, limit) {
429
+ return this.#core.listTableSegmentPage(tableId, afterId, limit);
430
+ }
431
+ async removeAbortedSegment(id, expectedTransactionId) {
432
+ return this.#runAtomic(() => this.#core.removeAbortedSegment(id, expectedTransactionId));
433
+ }
434
+ async adoptAbortedSegment(input) {
435
+ return this.#runAtomic(() => this.#core.adoptAbortedSegment(input));
212
436
  }
213
437
  async reserveRowIds(tableId, count) {
214
438
  return this.#core.reserveRowIds(tableId, count);
@@ -217,18 +441,69 @@ export class MemoryBlockStore {
217
441
  return this.#runAtomic(() => this.#core.reserveAutoIncrement(tableId, columnId, count, atLeast));
218
442
  }
219
443
  async getExistingUniqueKeys(tableId, keyTokens) {
444
+ assertStorageBulkReadItems(keyTokens, "Unique-key lookup batch");
220
445
  return this.#core.getExistingUniqueKeys(tableId, keyTokens);
221
446
  }
447
+ async beginUniqueKeyBuild(input) {
448
+ return this.#runAtomic(() => this.#core.beginUniqueKeyBuild(input));
449
+ }
450
+ async getUniqueKeyBuild(buildId) {
451
+ return this.#core.getUniqueKeyBuild(buildId);
452
+ }
453
+ async renewUniqueKeyBuild(input) {
454
+ return this.#runAtomic(() => this.#core.renewUniqueKeyBuild(input));
455
+ }
456
+ async appendUniqueKeyBuildChunk(input) {
457
+ return this.#runAtomic(() => this.#core.appendUniqueKeyBuildChunk(input));
458
+ }
459
+ async finishUniqueKeyBuild(input) {
460
+ return this.#runAtomic(() => this.#core.finishUniqueKeyBuild(input));
461
+ }
462
+ async abortUniqueKeyBuild(input) {
463
+ return this.#runAtomic(() => this.#core.abortUniqueKeyBuild(input));
464
+ }
222
465
  async getCurrentManifestVersion() {
223
466
  return this.#core.getCurrentManifestVersion();
224
467
  }
225
468
  async getCatalogProbe() {
226
469
  return this.#core.getCatalogProbe();
227
470
  }
228
- async getQueryCatalogState(tableNames) {
229
- // RecordCore filters before cloning, which matters after a long-running database has
230
- // accumulated superseded history that collection still retains for recent snapshots.
231
- return this.#core.getQueryCatalogState(tableNames);
471
+ async getQueryCatalogState(names) {
472
+ assertStorageBulkReadItems(names, "Query catalog table batch");
473
+ return this.#runAtomic(() => {
474
+ const probe = this.#core.getCatalogProbe();
475
+ const tables = names.map((name) => {
476
+ const id = this.#core.getTableIdByName(name);
477
+ return id === undefined ? undefined : this.#core.getTable(id);
478
+ });
479
+ const segments = [];
480
+ for (const table of tables) {
481
+ if (table === undefined)
482
+ continue;
483
+ let cursor = null;
484
+ do {
485
+ const page = this.#core.listTableSegmentPage(table.id, cursor, 1_024);
486
+ segments.push(...page.records);
487
+ cursor = page.nextCursor;
488
+ } while (cursor !== null);
489
+ }
490
+ segments.sort((left, right) => left.id.localeCompare(right.id));
491
+ const transactionIds = [...new Set(segments.map((segment) => segment.transactionId))];
492
+ const transactions = [];
493
+ for (let start = 0; start < transactionIds.length; start += 64) {
494
+ for (const record of this.#core.getTransactions(transactionIds.slice(start, start + 64))) {
495
+ if (record !== undefined)
496
+ transactions.push(record);
497
+ }
498
+ }
499
+ return {
500
+ manifestVersion: probe.manifestVersion,
501
+ tables,
502
+ segments,
503
+ transactions,
504
+ catalogEpoch: probe.catalogEpoch,
505
+ };
506
+ });
232
507
  }
233
508
  async getCurrentManifest() {
234
509
  return this.#core.getCurrentManifest();
@@ -236,15 +511,15 @@ export class MemoryBlockStore {
236
511
  async getManifest(version) {
237
512
  return this.#core.getManifest(version);
238
513
  }
239
- async listManifests() {
240
- return this.#core.listManifests();
514
+ async listManifestBlockPage(input) {
515
+ return this.#core.listManifestBlockPage(input);
516
+ }
517
+ async listRetiredManifestBlockPage(input) {
518
+ return this.#core.listRetiredManifestBlockPage(input);
241
519
  }
242
520
  async listManifestPage(afterVersion, limit) {
243
521
  return this.#core.listManifestPage(afterVersion, limit);
244
522
  }
245
- async publishManifest(input) {
246
- return this.#runAtomic(() => this.#core.publishManifest(input));
247
- }
248
523
  async beginTransaction(input) {
249
524
  return this.#runAtomic(() => this.#core.beginTransaction(input));
250
525
  }
@@ -257,23 +532,35 @@ export class MemoryBlockStore {
257
532
  return this.#core.getTransaction(id);
258
533
  }
259
534
  async getTransactions(ids) {
535
+ assertStorageBulkReadItems(ids, "Transaction read batch");
260
536
  return this.#core.getTransactions(ids);
261
537
  }
262
- async listTransactions() {
263
- return this.#core.listTransactions();
264
- }
265
538
  async listTransactionPage(afterId, limit) {
266
539
  return this.#core.listTransactionPage(afterId, limit);
267
540
  }
268
541
  async updateTransaction(id, expectedRevision, update) {
269
542
  return this.#runAtomic(() => this.#core.updateTransaction(id, expectedRevision, update));
270
543
  }
544
+ async renewTransaction(input) {
545
+ return this.#runAtomic(() => this.#core.renewTransaction(input));
546
+ }
547
+ async abortTransactionIfExpired(input) {
548
+ return this.#runAtomic(() => this.#core.abortTransactionIfExpired(input));
549
+ }
271
550
  async stageTransactionArtifacts(input) {
272
551
  return this.#runAtomic(() => {
273
552
  const updated = this.#core.stageTransactionArtifacts(input);
274
553
  // The record half validated everything; the bytes land in this same atomic step.
275
554
  for (const block of input.blocks)
276
- this.#blocks.set(block.id, new Uint8Array(block.bytes));
555
+ this.#putBlock(block.id, block.bytes);
556
+ return updated;
557
+ });
558
+ }
559
+ async rollbackTransactionArtifacts(input) {
560
+ return this.#runAtomic(() => {
561
+ const updated = this.#core.rollbackTransactionArtifacts(input);
562
+ for (const id of input.removeBlockIds)
563
+ this.#deleteBlock(id);
277
564
  return updated;
278
565
  });
279
566
  }
@@ -285,7 +572,7 @@ export class MemoryBlockStore {
285
572
  const summary = this.#core.writeTransaction(input);
286
573
  // The record half validated everything; the bytes land in this same atomic step.
287
574
  for (const block of input.blocks)
288
- this.#blocks.set(block.id, new Uint8Array(block.bytes));
575
+ this.#putBlock(block.id, block.bytes);
289
576
  return summary;
290
577
  });
291
578
  }
@@ -300,19 +587,20 @@ export class MemoryBlockStore {
300
587
  async listLeases() {
301
588
  return this.#core.listLeases();
302
589
  }
303
- async renewLease(id, expectedRevision, expiresAt) {
304
- return this.#runAtomic(() => this.#core.renewLease(id, expectedRevision, expiresAt));
590
+ async listExpiredLeasePage(expiresAtCutoff, afterCursor, limit) {
591
+ return this.#core.listExpiredLeasePage(expiresAtCutoff, afterCursor, limit);
592
+ }
593
+ async renewLease(input) {
594
+ return this.#runAtomic(() => this.#core.renewLease(input));
305
595
  }
306
- async moveLease(id, expectedRevision, manifestVersion, expiresAt) {
307
- return this.#runAtomic(() => this.#core.moveLease(id, expectedRevision, manifestVersion, expiresAt));
596
+ async moveLease(input) {
597
+ return this.#runAtomic(() => this.#core.moveLease(input));
308
598
  }
309
599
  async removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff) {
310
600
  return this.#runAtomic(() => this.#core.removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff));
311
601
  }
312
- async removeLease(id) {
313
- return this.#runAtomic(() => {
314
- this.#core.removeLease(id);
315
- });
602
+ async removeLease(input) {
603
+ return this.#runAtomic(() => this.#core.removeLease(input));
316
604
  }
317
605
  async createCompactionJob(record) {
318
606
  this.#core.createCompactionJob(record);
@@ -333,71 +621,551 @@ export class MemoryBlockStore {
333
621
  return this.#runAtomic(() => this.#core.cancelCompactionJob(id, expectedRevision, cancelledAt));
334
622
  }
335
623
  async removeCompactionJob(id) {
336
- this.#core.removeCompactionJob(id);
624
+ return this.#core.removeCompactionJob(id);
337
625
  }
338
626
  async createGarbageCollectionJob(input) {
339
627
  return this.#runAtomic(() => this.#core.createGarbageCollectionJob(input));
340
628
  }
629
+ async updateGarbageCollectionPlanning(input) {
630
+ return this.#runAtomic(() => this.#core.updateGarbageCollectionPlanning(input));
631
+ }
341
632
  async getGarbageCollectionJob(id) {
342
633
  return this.#core.getGarbageCollectionJob(id);
343
634
  }
344
635
  async listGarbageCollectionJobs() {
345
636
  return this.#core.listGarbageCollectionJobs();
346
637
  }
638
+ async listGarbageCollectionJobPage(afterId, limit) {
639
+ return this.#core.listGarbageCollectionJobPage(afterId, limit);
640
+ }
347
641
  async runGarbageCollectionStep(input) {
348
642
  return this.#runAtomic(() => {
349
643
  const result = this.#core.runGarbageCollectionStep(input);
350
- result.reclaimedBlockIds.forEach((id) => this.#blocks.delete(id));
644
+ result.reclaimedBlockIds.forEach((id) => this.#deleteBlock(id));
351
645
  return result;
352
646
  });
353
647
  }
354
- async removePrunedManifestRecords() {
355
- return this.#runAtomic(() => this.#core.removePrunedManifestRecords().length);
648
+ async removePrunedManifestRecords(maxItems) {
649
+ return this.#runAtomic(() => this.#core.removePrunedManifestRecords(maxItems));
356
650
  }
357
651
  async removeGarbageCollectionJob(id) {
358
652
  return this.#runAtomic(() => {
359
653
  this.#core.removeGarbageCollectionJob(id);
360
654
  });
361
655
  }
362
- /**
363
- * Copies the current version out as a portable snapshot. Runs on the commit queue, so it sees
364
- * one consistent version rather than a commit in progress. What the snapshot carries and
365
- * drops is `RecordCore.exportSnapshot`'s contract.
366
- */
367
- exportSnapshot() {
368
- return this.#runAtomic(() => this.#core.exportSnapshot((id) => this.#blocks.get(id)));
369
- }
370
- /**
371
- * Loads a snapshot into this store, which must be empty — the same contract as the IndexedDB
372
- * store, so a caller holding a `BlockStore` can restore into either without asking which it
373
- * has. The load runs on the commit queue, so it cannot interleave with a commit.
374
- *
375
- * Progress is reported for parity with the IndexedDB store rather than because it is needed:
376
- * an in-memory load is a handful of map writes with no transaction to break it into, so it
377
- * reports the start and then the end.
378
- */
379
- async importSnapshot(snapshot, options = {}) {
380
- const totalBytes = snapshot.blocks.reduce((total, block) => total + block.bytes.byteLength, 0);
381
- options.onProgress?.({ phase: "blocks", writtenBytes: 0, totalBytes });
382
- await this.#runAtomic(() => {
383
- this.#core.importSnapshot(snapshot, (id, bytes) => this.#blocks.set(id, bytes.slice()));
384
- });
385
- options.onProgress?.({ phase: "done", writtenBytes: totalBytes, totalBytes });
386
- }
387
- /** Builds a store holding exactly what the snapshot captured. */
388
- static fromSnapshot(snapshot) {
389
- const store = new MemoryBlockStore();
390
- store.#core.loadSnapshot(snapshot, (id, bytes) => store.#blocks.set(id, bytes.slice()));
391
- return store;
656
+ async beginSnapshotFrameExport(input) {
657
+ validateSnapshotSessionLifetime(input.createdAt, input.expiresAt);
658
+ validateId(input.ownerId);
659
+ return this.#runAtomic(() => {
660
+ const current = this.#snapshotFrameExport;
661
+ if (current !== undefined) {
662
+ if (Date.parse(current.session.expiresAt) > Date.parse(input.createdAt)) {
663
+ throw new SnapshotImportConflictError(current.session.sessionId, current.session.ownerId, "another snapshot export is still live");
664
+ }
665
+ this.#core.removeLease({
666
+ id: current.session.sessionId,
667
+ ownerId: current.session.ownerId,
668
+ });
669
+ this.#snapshotFrameExport = undefined;
670
+ }
671
+ const manifest = this.#core.snapshotFrameManifest();
672
+ const metadataFrames = [];
673
+ const summaries = emptySnapshotObservedSummaries();
674
+ const append = (item) => {
675
+ const payload = encodeSnapshotMetadataPage([item]);
676
+ const kind = snapshotMetadataFrameKind(item);
677
+ const frame = {
678
+ sequence: metadataFrames.length,
679
+ kind,
680
+ itemCount: 1,
681
+ key: null,
682
+ payload,
683
+ checksum: crc32(payload),
684
+ };
685
+ metadataFrames.push(frame);
686
+ const summary = summaries[kind];
687
+ summary.frameCount += 1;
688
+ summary.itemCount += 1;
689
+ summary.storedBytes = safeSnapshotByteSum(summary.storedBytes, payload.byteLength);
690
+ };
691
+ for (const item of this.#core.snapshotFrameMetadataItems())
692
+ append(item);
693
+ for (const item of this.#core.snapshotFramePostingItems())
694
+ append(item);
695
+ summaries.block = {
696
+ frameCount: manifest.liveBlockCount,
697
+ itemCount: manifest.liveBlockCount,
698
+ storedBytes: manifest.liveBlockBytes,
699
+ };
700
+ const header = prepareSnapshotFrameStreamHeader({
701
+ formatVersion: 1,
702
+ databaseVersion: manifest.version,
703
+ createdAt: input.createdAt,
704
+ kinds: summaries,
705
+ });
706
+ const sessionId = `snapshot-export/${crypto.randomUUID()}`;
707
+ this.#core.createLease({
708
+ id: sessionId,
709
+ kind: "backup",
710
+ manifestVersion: manifest.version,
711
+ ownerId: input.ownerId,
712
+ createdAt: input.createdAt,
713
+ expiresAt: input.expiresAt,
714
+ revision: 0,
715
+ });
716
+ const session = { sessionId, ownerId: input.ownerId, expiresAt: input.expiresAt, header };
717
+ this.#snapshotFrameExport = {
718
+ session,
719
+ manifestVersion: manifest.version,
720
+ metadataFrames,
721
+ nextSequence: 0,
722
+ lastBlockId: null,
723
+ };
724
+ return structuredClone(session);
725
+ });
726
+ }
727
+ async readSnapshotExportFrame(input) {
728
+ validateSnapshotSessionLifetime(input.expiresAtCutoff, input.expiresAt);
729
+ if (!Number.isSafeInteger(input.sequence) || input.sequence < 0) {
730
+ throw new RangeError("Snapshot export sequence is invalid");
731
+ }
732
+ return this.#runAtomic(() => {
733
+ const state = this.#snapshotFrameExport;
734
+ if (state?.session.sessionId !== input.sessionId || state.session.ownerId !== input.ownerId) {
735
+ throw new SnapshotImportConflictError(input.sessionId, input.ownerId, "export session is missing or owned by another caller");
736
+ }
737
+ if (input.sequence === state.nextSequence - 1 && state.lastFrame !== undefined) {
738
+ return copySnapshotFrame(state.lastFrame);
739
+ }
740
+ if (input.sequence !== state.nextSequence) {
741
+ throw new RangeError("Snapshot export frames must be pulled in contiguous order");
742
+ }
743
+ const lease = this.#core.getLease(input.sessionId);
744
+ if (lease === undefined) {
745
+ throw new SnapshotImportConflictError(input.sessionId, input.ownerId, "lease is missing");
746
+ }
747
+ this.#core.renewLease({
748
+ id: lease.id,
749
+ expectedRevision: lease.revision,
750
+ expiresAtCutoff: input.expiresAtCutoff,
751
+ expiresAt: input.expiresAt,
752
+ });
753
+ state.session = { ...state.session, expiresAt: input.expiresAt };
754
+ const metadata = state.metadataFrames[input.sequence];
755
+ let frame;
756
+ if (metadata !== undefined) {
757
+ frame = metadata;
758
+ }
759
+ else {
760
+ const blockPage = this.#core.listManifestBlockPage({
761
+ version: state.manifestVersion,
762
+ afterBlockId: state.lastBlockId,
763
+ limit: 1,
764
+ });
765
+ const record = blockPage.records[0];
766
+ if (record === undefined)
767
+ return undefined;
768
+ const bytes = this.#blocks.get(record.blockId);
769
+ if (bytes?.byteLength !== record.byteLength || crc32(bytes) !== record.checksum) {
770
+ throw new StorageCorruptionError("memory", record.blockId, "snapshot block disagrees with its manifest provenance");
771
+ }
772
+ frame = {
773
+ sequence: input.sequence,
774
+ kind: "block",
775
+ itemCount: 1,
776
+ key: record.blockId,
777
+ payload: bytes.slice(),
778
+ checksum: record.checksum,
779
+ };
780
+ state.lastBlockId = record.blockId;
781
+ }
782
+ state.lastFrame = copySnapshotFrame(frame);
783
+ state.nextSequence += 1;
784
+ return copySnapshotFrame(frame);
785
+ });
786
+ }
787
+ async closeSnapshotFrameExport(input) {
788
+ return this.#runAtomic(() => {
789
+ const state = this.#snapshotFrameExport;
790
+ if (state === undefined)
791
+ return false;
792
+ if (state.session.sessionId !== input.sessionId || state.session.ownerId !== input.ownerId) {
793
+ throw new SnapshotImportConflictError(input.sessionId, input.ownerId, "export session belongs to another caller");
794
+ }
795
+ this.#core.removeLease({ id: input.sessionId, ownerId: input.ownerId });
796
+ this.#snapshotFrameExport = undefined;
797
+ return true;
798
+ });
799
+ }
800
+ async beginSnapshotFrameImport(input) {
801
+ validateSnapshotSessionLifetime(input.createdAt, input.expiresAt);
802
+ validateId(input.ownerId);
803
+ const header = prepareSnapshotFrameStreamHeader(input.header);
804
+ if (snapshotFrameStreamHeaderIdentity(header) !== input.identity) {
805
+ throw new TypeError("Snapshot import identity does not match its canonical header");
806
+ }
807
+ return this.#runAtomic(() => {
808
+ const current = this.#snapshotFrameImport;
809
+ if (current !== undefined) {
810
+ const expired = Date.parse(current.session.expiresAt) <= Date.parse(input.createdAt);
811
+ const sameIdentity = current.session.identity === input.identity;
812
+ const sameHeader = sameSnapshotFrameHeader(current.header, header);
813
+ if (expired && (!sameIdentity || !sameHeader)) {
814
+ if (!current.completedReplay)
815
+ this.#clearPhysicalBlocks();
816
+ this.#snapshotFrameImport = undefined;
817
+ }
818
+ else {
819
+ if (!expired && current.session.identity !== input.identity) {
820
+ throw new SnapshotImportConflictError(current.session.identity, current.session.ownerId, "another snapshot import is still live");
821
+ }
822
+ if (!expired && current.session.ownerId !== input.ownerId) {
823
+ throw new SnapshotImportConflictError(current.session.identity, current.session.ownerId, "snapshot import is owned by another caller");
824
+ }
825
+ if (!sameHeader) {
826
+ throw new SnapshotImportConflictError(current.session.identity, current.session.ownerId, "snapshot import header changed");
827
+ }
828
+ current.session = {
829
+ ...current.session,
830
+ ownerId: input.ownerId,
831
+ expiresAt: input.expiresAt,
832
+ };
833
+ return structuredClone(current.session);
834
+ }
835
+ }
836
+ const completed = this.#completedSnapshotFrameImport;
837
+ const completedReplay = completed?.identity === input.identity &&
838
+ completed.version === this.#core.getCurrentManifestVersion();
839
+ if (completedReplay && !sameSnapshotFrameHeader(completed.header, header)) {
840
+ throw new SnapshotImportConflictError(input.identity, input.ownerId, "completed snapshot header changed");
841
+ }
842
+ if (!completedReplay && this.#core.getCurrentManifestVersion() !== null) {
843
+ throw new Error("Snapshot store already holds a database");
844
+ }
845
+ if (!completedReplay && this.#blocks.size > 0) {
846
+ throw new StorageCorruptionError("memory", "snapshot-import", "empty target contains unowned payloads");
847
+ }
848
+ const session = {
849
+ identity: input.identity,
850
+ ownerId: input.ownerId,
851
+ version: header.databaseVersion,
852
+ createdAt: input.createdAt,
853
+ expiresAt: input.expiresAt,
854
+ nextSequence: 0,
855
+ stagedBytes: 0,
856
+ };
857
+ this.#snapshotFrameImport = {
858
+ session,
859
+ header,
860
+ metadataFrames: completedReplay ? this.#captureSnapshotMetadataFrames() : [],
861
+ observed: emptySnapshotObservedSummaries(),
862
+ checksum: 0,
863
+ itemCount: 0,
864
+ previousKindIndex: 0,
865
+ lastBlockId: null,
866
+ lastBatchFrames: [],
867
+ completedReplay,
868
+ };
869
+ return structuredClone(session);
870
+ });
871
+ }
872
+ async renewSnapshotFrameImport(input) {
873
+ validateSnapshotSessionLifetime(input.expiresAtCutoff, input.expiresAt);
874
+ return this.#runAtomic(() => {
875
+ const state = this.#requireLiveSnapshotFrameImport(input);
876
+ state.session = { ...state.session, expiresAt: input.expiresAt };
877
+ return structuredClone(state.session);
878
+ });
879
+ }
880
+ async appendSnapshotImportFrames(input) {
881
+ validateSnapshotSessionLifetime(input.expiresAtCutoff, input.expiresAt);
882
+ if (input.frames.length < 1 || input.frames.length > MAX_SNAPSHOT_FRAME_BATCH_ITEMS) {
883
+ throw new RangeError("Snapshot import frame batch item count is out of bounds");
884
+ }
885
+ let batchBytes = 0;
886
+ let metadataBatchBytes = 0;
887
+ for (const frame of input.frames) {
888
+ batchBytes = safeSnapshotByteSum(batchBytes, frame.payload.byteLength);
889
+ if (batchBytes > MAX_SNAPSHOT_FRAME_BATCH_BYTES) {
890
+ throw new RangeError("Snapshot import frame batch exceeds its byte limit");
891
+ }
892
+ if (frame.kind !== "block") {
893
+ metadataBatchBytes = safeSnapshotByteSum(metadataBatchBytes, frame.payload.byteLength);
894
+ if (metadataBatchBytes > MAX_SNAPSHOT_METADATA_BATCH_BYTES) {
895
+ throw new RangeError("Snapshot metadata frame batch exceeds its byte limit");
896
+ }
897
+ }
898
+ }
899
+ return this.#runAtomic(() => {
900
+ const state = this.#requireLiveSnapshotFrameImport(input);
901
+ if (state.lastBatchFrames.length === input.frames.length &&
902
+ input.frames[0]?.sequence === state.session.nextSequence - state.lastBatchFrames.length) {
903
+ if (!state.lastBatchFrames.every((frame, index) => sameSnapshotFrame(frame, input.frames[index]))) {
904
+ throw new SnapshotImportConflictError(input.identity, input.ownerId, "replayed snapshot frame bytes changed");
905
+ }
906
+ state.session = { ...state.session, expiresAt: input.expiresAt };
907
+ return structuredClone(state.session);
908
+ }
909
+ if (input.frames[0]?.sequence !== state.session.nextSequence) {
910
+ throw new RangeError("Snapshot import frame batch is not the next contiguous sequence");
911
+ }
912
+ const observed = structuredClone(state.observed);
913
+ let checksum = state.checksum;
914
+ let itemCount = state.itemCount;
915
+ let previousKindIndex = state.previousKindIndex;
916
+ let lastBlockId = state.lastBlockId;
917
+ let nextSequence = state.session.nextSequence;
918
+ let stagedBytes = state.session.stagedBytes;
919
+ const metadataFrames = [];
920
+ const blockFrames = [];
921
+ for (const frame of input.frames) {
922
+ if (frame.sequence !== nextSequence || crc32(frame.payload) !== frame.checksum) {
923
+ throw new Error("Snapshot import frame sequence or checksum is invalid");
924
+ }
925
+ const envelope = snapshotFrameEnvelopeParts(frame);
926
+ const kindIndex = SNAPSHOT_FRAME_KINDS.indexOf(frame.kind);
927
+ if (kindIndex < previousKindIndex)
928
+ throw new Error("Snapshot frame kinds are out of order");
929
+ previousKindIndex = kindIndex;
930
+ if (frame.kind === "block") {
931
+ if (frame.key === null || (lastBlockId !== null && frame.key <= lastBlockId)) {
932
+ throw new Error("Snapshot block frames are not in canonical ID order");
933
+ }
934
+ verifyStoredBlock(frame.payload);
935
+ if (state.completedReplay) {
936
+ const current = this.#blocks.get(frame.key);
937
+ if (current === undefined || !sameSnapshotBytes(current, frame.payload)) {
938
+ throw new SnapshotImportConflictError(input.identity, input.ownerId, `completed snapshot block changed: ${frame.key}`);
939
+ }
940
+ }
941
+ else
942
+ blockFrames.push(copySnapshotFrame(frame));
943
+ lastBlockId = frame.key;
944
+ }
945
+ else {
946
+ if (frame.itemCount !== 1) {
947
+ throw new TypeError("Snapshot v1 metadata frames contain exactly one item");
948
+ }
949
+ const items = decodeSnapshotMetadataItems(frame.kind, frame.payload);
950
+ if (items.length !== frame.itemCount) {
951
+ throw new Error("Snapshot metadata item count disagrees with its frame");
952
+ }
953
+ if (state.completedReplay) {
954
+ const expected = state.metadataFrames[frame.sequence];
955
+ if (!sameSnapshotFrame(expected, frame)) {
956
+ throw new SnapshotImportConflictError(input.identity, input.ownerId, "completed snapshot metadata changed");
957
+ }
958
+ }
959
+ else
960
+ metadataFrames.push(copySnapshotFrame(frame));
961
+ }
962
+ checksum = extendSnapshotFrameStreamChecksum(checksum, envelope);
963
+ itemCount = safeSnapshotByteSum(itemCount, frame.itemCount);
964
+ stagedBytes = safeSnapshotByteSum(stagedBytes, frame.payload.byteLength);
965
+ const summary = observed[frame.kind];
966
+ summary.frameCount += 1;
967
+ summary.itemCount = safeSnapshotByteSum(summary.itemCount, frame.itemCount);
968
+ summary.storedBytes = safeSnapshotByteSum(summary.storedBytes, frame.payload.byteLength);
969
+ const expected = state.header.kinds[frame.kind];
970
+ if (summary.frameCount > expected.frameCount ||
971
+ summary.itemCount > expected.itemCount ||
972
+ summary.storedBytes > expected.storedBytes) {
973
+ throw new Error(`Snapshot ${frame.kind} frames exceed their header summary`);
974
+ }
975
+ nextSequence += 1;
976
+ }
977
+ for (const frame of metadataFrames)
978
+ state.metadataFrames.push(frame);
979
+ for (const frame of blockFrames) {
980
+ if (frame.key === null)
981
+ throw new Error("Snapshot block frame lost its ID");
982
+ this.#putBlock(frame.key, frame.payload);
983
+ }
984
+ state.observed = observed;
985
+ state.checksum = checksum;
986
+ state.itemCount = itemCount;
987
+ state.previousKindIndex = previousKindIndex;
988
+ state.lastBlockId = lastBlockId;
989
+ state.lastBatchFrames = input.frames.map(copySnapshotFrame);
990
+ state.session = {
991
+ ...state.session,
992
+ expiresAt: input.expiresAt,
993
+ nextSequence,
994
+ stagedBytes,
995
+ };
996
+ return structuredClone(state.session);
997
+ });
998
+ }
999
+ async finishSnapshotFrameImport(input) {
1000
+ encodeSnapshotFrameStreamFooter(input.footer);
1001
+ return this.#runAtomic(() => {
1002
+ const state = this.#requireLiveSnapshotFrameImport(input);
1003
+ const expectedFrameCount = snapshotFrameCount(state.header);
1004
+ if (input.footer.frameCount !== expectedFrameCount ||
1005
+ input.footer.frameCount !== state.session.nextSequence ||
1006
+ input.footer.itemCount !== state.itemCount ||
1007
+ input.footer.storedBytes !== state.session.stagedBytes ||
1008
+ input.footer.checksum !== state.checksum) {
1009
+ throw new Error("Snapshot import footer does not match its staged frames");
1010
+ }
1011
+ for (const kind of SNAPSHOT_FRAME_KINDS) {
1012
+ const actual = state.observed[kind];
1013
+ const expected = state.header.kinds[kind];
1014
+ if (actual.frameCount !== expected.frameCount ||
1015
+ actual.itemCount !== expected.itemCount ||
1016
+ actual.storedBytes !== expected.storedBytes) {
1017
+ throw new Error(`Snapshot import ${kind} generation is incomplete`);
1018
+ }
1019
+ }
1020
+ if (!state.completedReplay) {
1021
+ const items = function* () {
1022
+ for (const frame of state.metadataFrames) {
1023
+ if (frame.kind === "block")
1024
+ continue;
1025
+ yield* decodeSnapshotMetadataItems(frame.kind, frame.payload);
1026
+ }
1027
+ };
1028
+ const blocks = function* (store) {
1029
+ for (const [blockId, bytes] of store.#blocks) {
1030
+ yield { blockId, byteLength: bytes.byteLength, checksum: crc32(bytes) };
1031
+ }
1032
+ };
1033
+ this.#core.loadSnapshotFrameItems(state.header, items(), blocks(this));
1034
+ }
1035
+ this.#completedSnapshotFrameImport = {
1036
+ identity: state.session.identity,
1037
+ version: state.header.databaseVersion,
1038
+ header: structuredClone(state.header),
1039
+ };
1040
+ this.#snapshotFrameImport = undefined;
1041
+ });
1042
+ }
1043
+ async cancelSnapshotFrameImport(input) {
1044
+ return this.#runAtomic(() => {
1045
+ const state = this.#snapshotFrameImport;
1046
+ if (state?.session.identity !== input.identity || state.session.ownerId !== input.ownerId) {
1047
+ throw new SnapshotImportConflictError(input.identity, input.ownerId, "session is missing or owned by another caller");
1048
+ }
1049
+ const result = {
1050
+ identity: input.identity,
1051
+ removedBlockCount: state.completedReplay ? 0 : state.observed.block.itemCount,
1052
+ removedBytes: state.completedReplay ? 0 : state.observed.block.storedBytes,
1053
+ };
1054
+ if (!state.completedReplay)
1055
+ this.#clearPhysicalBlocks();
1056
+ this.#snapshotFrameImport = undefined;
1057
+ return result;
1058
+ });
1059
+ }
1060
+ #requireLiveSnapshotFrameImport(input) {
1061
+ const state = this.#snapshotFrameImport;
1062
+ if (state?.session.identity !== input.identity || state.session.ownerId !== input.ownerId) {
1063
+ throw new SnapshotImportConflictError(input.identity, input.ownerId, "session is missing or owned by another caller");
1064
+ }
1065
+ if (Date.parse(state.session.expiresAt) <= Date.parse(input.expiresAtCutoff)) {
1066
+ throw new SnapshotImportConflictError(input.identity, input.ownerId, "session is expired");
1067
+ }
1068
+ return state;
1069
+ }
1070
+ #captureSnapshotMetadataFrames() {
1071
+ const frames = [];
1072
+ const append = (item) => {
1073
+ const payload = encodeSnapshotMetadataPage([item]);
1074
+ frames.push({
1075
+ sequence: frames.length,
1076
+ kind: snapshotMetadataFrameKind(item),
1077
+ itemCount: 1,
1078
+ key: null,
1079
+ payload,
1080
+ checksum: crc32(payload),
1081
+ });
1082
+ };
1083
+ for (const item of this.#core.snapshotFrameMetadataItems())
1084
+ append(item);
1085
+ for (const item of this.#core.snapshotFramePostingItems())
1086
+ append(item);
1087
+ return frames;
1088
+ }
1089
+ async inspectInterruptedImport() {
1090
+ const framed = this.#snapshotFrameImport;
1091
+ if (framed !== undefined) {
1092
+ return {
1093
+ identity: framed.session.identity,
1094
+ version: framed.session.version,
1095
+ createdAt: framed.session.createdAt,
1096
+ stagedBlockCount: framed.observed.block.itemCount,
1097
+ stagedBytes: framed.session.stagedBytes,
1098
+ };
1099
+ }
1100
+ return null;
1101
+ }
1102
+ async abortInterruptedImport(identity) {
1103
+ return this.#runAtomic(() => {
1104
+ const framed = this.#snapshotFrameImport;
1105
+ if (framed?.session.identity === identity) {
1106
+ const result = {
1107
+ identity,
1108
+ removedBlockCount: framed.completedReplay ? 0 : framed.observed.block.itemCount,
1109
+ removedBytes: framed.completedReplay ? 0 : framed.observed.block.storedBytes,
1110
+ };
1111
+ if (!framed.completedReplay)
1112
+ this.#clearPhysicalBlocks();
1113
+ this.#snapshotFrameImport = undefined;
1114
+ return result;
1115
+ }
1116
+ throw new Error(`Interrupted snapshot import not found: ${identity}`);
1117
+ });
392
1118
  }
393
1119
  /** Block and spill bytes held; the record maps are negligible beside them. */
394
1120
  async getLogicalStorageBytes() {
395
- let total = 0;
396
- for (const bytes of this.#blocks.values())
397
- total += bytes.byteLength;
398
- for (const bytes of this.#tempRunPages.values())
399
- total += bytes.byteLength;
400
- return total;
1121
+ return this.#physicalBlockBytes + this.#tempBytesTotal;
1122
+ }
1123
+ async getStorageStats() {
1124
+ return this.#runAtomic(() => {
1125
+ const current = this.#core.getCurrentManifest();
1126
+ const temporaryBytes = this.#tempBytesTotal;
1127
+ const manifestCount = countRecordPages((cursor) => this.#core.listManifestPage(cursor, 1_024));
1128
+ const transactionCount = countRecordPages((cursor) => this.#core.listTransactionPage(cursor, 1_024));
1129
+ const segmentCount = countRecordPages((cursor) => this.#core.listSegmentPage(cursor, 1_024));
1130
+ const liveBlockCount = current?.liveBlockCount ?? 0;
1131
+ const liveBlockBytes = current?.liveBlockBytes ?? 0;
1132
+ if (liveBlockCount > this.#blocks.size || liveBlockBytes > this.#physicalBlockBytes) {
1133
+ throw new StorageCorruptionError("memory", "manifest", "live block summary exceeds physical payloads");
1134
+ }
1135
+ return {
1136
+ backend: "memory",
1137
+ logicalBytes: this.#physicalBlockBytes + temporaryBytes,
1138
+ physicalBytes: this.#physicalBlockBytes + temporaryBytes,
1139
+ liveBlockCount,
1140
+ obsoleteBlockCount: this.#blocks.size - liveBlockCount,
1141
+ liveBlockBytes,
1142
+ obsoleteBlockBytes: this.#physicalBlockBytes - liveBlockBytes,
1143
+ temporaryBytes,
1144
+ walBytes: null,
1145
+ checkpointBytes: null,
1146
+ orphanBytes: null,
1147
+ manifestCount,
1148
+ transactionCount,
1149
+ segmentCount,
1150
+ };
1151
+ });
1152
+ }
1153
+ #putBlock(id, bytes) {
1154
+ const stored = new Uint8Array(bytes);
1155
+ const previous = this.#blocks.get(id);
1156
+ this.#physicalBlockBytes += stored.byteLength - (previous?.byteLength ?? 0);
1157
+ this.#blocks.set(id, stored);
1158
+ this.#blockChecksums.set(id, crc32(stored));
1159
+ }
1160
+ #deleteBlock(id) {
1161
+ this.#physicalBlockBytes -= this.#blocks.get(id)?.byteLength ?? 0;
1162
+ this.#blocks.delete(id);
1163
+ this.#blockChecksums.delete(id);
1164
+ }
1165
+ #clearPhysicalBlocks() {
1166
+ this.#blocks.clear();
1167
+ this.#blockChecksums.clear();
1168
+ this.#physicalBlockBytes = 0;
401
1169
  }
402
1170
  close() {
403
1171
  // The in-memory implementation owns no external resources.
@@ -411,6 +1179,10 @@ export class MemoryBlockStore {
411
1179
  });
412
1180
  this.#commitQueue = this.#commitQueue.then(() => {
413
1181
  try {
1182
+ if (this.#completedSnapshotFrameImport !== undefined &&
1183
+ this.#core.getCurrentManifestVersion() !== this.#completedSnapshotFrameImport.version) {
1184
+ this.#completedSnapshotFrameImport = undefined;
1185
+ }
414
1186
  resolveResult(operation());
415
1187
  }
416
1188
  catch (error) {
@@ -426,4 +1198,97 @@ function tempRunPageKey(ownerId, runId, pageIndex) {
426
1198
  function tempRunPagePrefix(ownerId, runId) {
427
1199
  return `${String(ownerId.length)}:${ownerId}:${String(runId.length)}:${runId}:`;
428
1200
  }
429
- //# sourceMappingURL=memory.js.map
1201
+ function countRecordPages(read) {
1202
+ let count = 0;
1203
+ let cursor = null;
1204
+ do {
1205
+ const page = read(cursor);
1206
+ count += page.records.length;
1207
+ if (!Number.isSafeInteger(count))
1208
+ throw new RangeError("Storage record count overflow");
1209
+ cursor = page.nextCursor;
1210
+ } while (cursor !== null);
1211
+ return count;
1212
+ }
1213
+ function assertTempResourceLimit(resource, actual, limit) {
1214
+ if (!Number.isSafeInteger(actual) || actual < 0 || actual > limit) {
1215
+ throw new StorageResourceLimitError(resource, actual, limit);
1216
+ }
1217
+ }
1218
+ function validateSnapshotSessionLifetime(cutoff, expiresAt) {
1219
+ const cutoffMs = Date.parse(cutoff);
1220
+ const expiresAtMs = Date.parse(expiresAt);
1221
+ if (!Number.isFinite(cutoffMs) || !Number.isFinite(expiresAtMs)) {
1222
+ throw new TypeError("Snapshot session timestamps are invalid");
1223
+ }
1224
+ if (expiresAtMs <= cutoffMs) {
1225
+ throw new RangeError("Snapshot session expiration must be after its cutoff");
1226
+ }
1227
+ if (expiresAtMs - cutoffMs > MAX_SNAPSHOT_SESSION_TTL_MS) {
1228
+ throw new RangeError("Snapshot session exceeds the maximum lifetime");
1229
+ }
1230
+ }
1231
+ function validatePostingBuildLifetime(cutoff, expiresAt) {
1232
+ const cutoffMs = Date.parse(cutoff);
1233
+ const expiresAtMs = Date.parse(expiresAt);
1234
+ if (!Number.isFinite(cutoffMs) || !Number.isFinite(expiresAtMs)) {
1235
+ throw new TypeError("Posting build timestamps are invalid");
1236
+ }
1237
+ if (expiresAtMs <= cutoffMs || expiresAtMs - cutoffMs > MAX_POSTING_BUILD_TTL_MS) {
1238
+ throw new RangeError("Posting build expiration is outside its bounded lifetime");
1239
+ }
1240
+ }
1241
+ function samePostingChunk(left, right) {
1242
+ return (left.length === right.length &&
1243
+ left.every((posting, postingIndex) => {
1244
+ const other = right[postingIndex];
1245
+ return (other?.term === posting.term &&
1246
+ posting.rowIds.length === other.rowIds.length &&
1247
+ posting.rowIds.every((rowId, index) => rowId === other.rowIds[index]) &&
1248
+ posting.tf.length === other.tf.length &&
1249
+ posting.tf.every((frequency, index) => frequency === other.tf[index]));
1250
+ }));
1251
+ }
1252
+ function safeSnapshotByteSum(total, byteLength) {
1253
+ const result = total + byteLength;
1254
+ if (!Number.isSafeInteger(result) || result < 0) {
1255
+ throw new RangeError("Snapshot payload is too large");
1256
+ }
1257
+ return result;
1258
+ }
1259
+ function emptySnapshotObservedSummaries() {
1260
+ return Object.fromEntries(SNAPSHOT_FRAME_KINDS.map((kind) => [kind, { frameCount: 0, itemCount: 0, storedBytes: 0 }]));
1261
+ }
1262
+ function snapshotMetadataFrameKind(item) {
1263
+ if (item.kind === "table")
1264
+ return "catalog-page";
1265
+ if (item.kind === "segment")
1266
+ return "segment-page";
1267
+ if (item.kind === "transaction")
1268
+ return "transaction-page";
1269
+ if (item.kind === "unique-generation" || item.kind === "unique-chunk")
1270
+ return "unique-page";
1271
+ return "posting-page";
1272
+ }
1273
+ function copySnapshotFrame(frame) {
1274
+ return { ...frame, payload: frame.payload.slice() };
1275
+ }
1276
+ function sameSnapshotFrameHeader(left, right) {
1277
+ return sameSnapshotBytes(encodeSnapshotFrameStreamHeader(left), encodeSnapshotFrameStreamHeader(right));
1278
+ }
1279
+ function sameSnapshotFrame(left, right) {
1280
+ if (left?.sequence === undefined || right?.sequence === undefined)
1281
+ return false;
1282
+ return (left.sequence === right.sequence &&
1283
+ left.kind === right.kind &&
1284
+ left.itemCount === right.itemCount &&
1285
+ left.key === right.key &&
1286
+ left.checksum === right.checksum &&
1287
+ sameSnapshotBytes(left.payload, right.payload));
1288
+ }
1289
+ function snapshotFrameCount(header) {
1290
+ return SNAPSHOT_FRAME_KINDS.reduce((total, kind) => safeSnapshotByteSum(total, header.kinds[kind].frameCount), 0);
1291
+ }
1292
+ function sameSnapshotBytes(left, right) {
1293
+ return left.byteLength === right.byteLength && left.every((byte, index) => right[index] === byte);
1294
+ }