@minnowdb/core 0.2.1 → 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 (270) hide show
  1. package/README.md +23 -32
  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 +2 -1
  21. package/dist/engine/artifact-cache.js +5 -1
  22. package/dist/engine/batch.d.ts +11 -3
  23. package/dist/engine/batch.js +17 -7
  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 +24 -5
  29. package/dist/engine/catalog.js +34 -7
  30. package/dist/engine/client.d.ts +29 -7
  31. package/dist/engine/client.js +216 -40
  32. package/dist/engine/database.d.ts +224 -39
  33. package/dist/engine/database.js +7475 -1539
  34. package/dist/engine/defaults.d.ts +4 -8
  35. package/dist/engine/defaults.js +46 -22
  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 +41 -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 -10
  43. package/dist/engine/index.js +12 -9
  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 +504 -126
  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 +262 -27
  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 +1 -2
  59. package/dist/engine/query-cache.js +7 -5
  60. package/dist/engine/query.d.ts +134 -29
  61. package/dist/engine/query.js +1668 -372
  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 +6 -3
  65. package/dist/engine/schema-wire.js +40 -34
  66. package/dist/engine/schema.d.ts +156 -80
  67. package/dist/engine/schema.js +570 -108
  68. package/dist/engine/sort-keys.d.ts +4 -4
  69. package/dist/engine/sort-keys.js +44 -29
  70. package/dist/engine/sql-domains.d.ts +31 -0
  71. package/dist/engine/sql-domains.js +585 -0
  72. package/dist/engine/sql-driver.d.ts +19 -0
  73. package/dist/engine/sql-driver.js +1 -0
  74. package/dist/engine/sql-json.d.ts +7 -9
  75. package/dist/engine/sql-json.js +75 -15
  76. package/dist/engine/sql-semantics.d.ts +8 -3
  77. package/dist/engine/sql-semantics.js +458 -30
  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 +12 -2
  81. package/dist/engine/vector.js +635 -119
  82. package/dist/engine/worker-host.d.ts +9 -2
  83. package/dist/engine/worker-host.js +620 -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 +3 -4
  91. package/dist/plan/index.js +3 -4
  92. package/dist/storage/index.d.ts +1 -1
  93. package/dist/storage/index.js +1 -1
  94. package/dist/storage/indexeddb.d.ts +99 -51
  95. package/dist/storage/indexeddb.js +13448 -2660
  96. package/dist/storage/memory.d.ts +64 -57
  97. package/dist/storage/memory.js +1042 -128
  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 +409 -75
  103. package/dist/storage/opfs/leader.js +4621 -650
  104. package/dist/storage/opfs/rpc.d.ts +15 -3
  105. package/dist/storage/opfs/rpc.js +224 -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 +33 -99
  109. package/dist/storage/opfs/store.js +453 -364
  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 -85
  115. package/dist/storage/snapshot.js +33 -265
  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 +4 -5
  119. package/dist/storage/toolkit/index.js +28 -4
  120. package/dist/storage/toolkit/record-core.d.ts +182 -55
  121. package/dist/storage/toolkit/record-core.js +6158 -1331
  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 +15 -2
  127. package/dist/storage/toolkit/wire.js +199 -10
  128. package/dist/storage/types.d.ts +1439 -204
  129. package/dist/storage/types.js +1745 -141
  130. package/dist/testing/block-store-conformance.d.ts +0 -1
  131. package/dist/testing/block-store-conformance.js +1009 -119
  132. package/dist/testing/index.d.ts +46 -26
  133. package/dist/testing/index.js +112 -57
  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 +97 -0
  137. package/dist/testing/simulator.js +591 -0
  138. package/dist/testing/sqllogictest.d.ts +89 -0
  139. package/dist/testing/sqllogictest.js +434 -0
  140. package/dist/transactions/index.d.ts +92 -16
  141. package/dist/transactions/index.js +1074 -236
  142. package/dist/worker-protocol/index.d.ts +3 -2
  143. package/dist/worker-protocol/index.js +2 -5
  144. package/package.json +53 -3
  145. package/postgres-feature-profile.json +223 -0
  146. package/sql-feature-matrix.json +172 -252
  147. package/dist/block-format/block.d.ts.map +0 -1
  148. package/dist/block-format/block.js.map +0 -1
  149. package/dist/block-format/checksum.d.ts.map +0 -1
  150. package/dist/block-format/checksum.js.map +0 -1
  151. package/dist/block-format/codecs.d.ts.map +0 -1
  152. package/dist/block-format/codecs.js.map +0 -1
  153. package/dist/block-format/column.d.ts.map +0 -1
  154. package/dist/block-format/column.js.map +0 -1
  155. package/dist/block-format/index.d.ts.map +0 -1
  156. package/dist/block-format/index.js.map +0 -1
  157. package/dist/block-format/physical.d.ts.map +0 -1
  158. package/dist/block-format/physical.js.map +0 -1
  159. package/dist/block-format/types.d.ts.map +0 -1
  160. package/dist/block-format/types.js.map +0 -1
  161. package/dist/engine/artifact-cache.d.ts.map +0 -1
  162. package/dist/engine/artifact-cache.js.map +0 -1
  163. package/dist/engine/batch.d.ts.map +0 -1
  164. package/dist/engine/batch.js.map +0 -1
  165. package/dist/engine/buffered-writer.d.ts.map +0 -1
  166. package/dist/engine/buffered-writer.js.map +0 -1
  167. package/dist/engine/catalog.d.ts.map +0 -1
  168. package/dist/engine/catalog.js.map +0 -1
  169. package/dist/engine/client.d.ts.map +0 -1
  170. package/dist/engine/client.js.map +0 -1
  171. package/dist/engine/coordinator.d.ts +0 -17
  172. package/dist/engine/coordinator.d.ts.map +0 -1
  173. package/dist/engine/coordinator.js +0 -60
  174. package/dist/engine/coordinator.js.map +0 -1
  175. package/dist/engine/database.d.ts.map +0 -1
  176. package/dist/engine/database.js.map +0 -1
  177. package/dist/engine/defaults.d.ts.map +0 -1
  178. package/dist/engine/defaults.js.map +0 -1
  179. package/dist/engine/errors.d.ts.map +0 -1
  180. package/dist/engine/errors.js.map +0 -1
  181. package/dist/engine/fts.d.ts.map +0 -1
  182. package/dist/engine/fts.js.map +0 -1
  183. package/dist/engine/group-index.d.ts.map +0 -1
  184. package/dist/engine/group-index.js.map +0 -1
  185. package/dist/engine/index.d.ts.map +0 -1
  186. package/dist/engine/index.js.map +0 -1
  187. package/dist/engine/join-index.d.ts.map +0 -1
  188. package/dist/engine/join-index.js.map +0 -1
  189. package/dist/engine/live.d.ts.map +0 -1
  190. package/dist/engine/live.js.map +0 -1
  191. package/dist/engine/memory.d.ts.map +0 -1
  192. package/dist/engine/memory.js.map +0 -1
  193. package/dist/engine/optimizer.d.ts.map +0 -1
  194. package/dist/engine/optimizer.js.map +0 -1
  195. package/dist/engine/query-cache.d.ts.map +0 -1
  196. package/dist/engine/query-cache.js.map +0 -1
  197. package/dist/engine/query.d.ts.map +0 -1
  198. package/dist/engine/query.js.map +0 -1
  199. package/dist/engine/result-wire.d.ts.map +0 -1
  200. package/dist/engine/result-wire.js.map +0 -1
  201. package/dist/engine/schema-wire.d.ts.map +0 -1
  202. package/dist/engine/schema-wire.js.map +0 -1
  203. package/dist/engine/schema.d.ts.map +0 -1
  204. package/dist/engine/schema.js.map +0 -1
  205. package/dist/engine/sort-keys.d.ts.map +0 -1
  206. package/dist/engine/sort-keys.js.map +0 -1
  207. package/dist/engine/sql-json.d.ts.map +0 -1
  208. package/dist/engine/sql-json.js.map +0 -1
  209. package/dist/engine/sql-semantics.d.ts.map +0 -1
  210. package/dist/engine/sql-semantics.js.map +0 -1
  211. package/dist/engine/vector.d.ts.map +0 -1
  212. package/dist/engine/vector.js.map +0 -1
  213. package/dist/engine/worker-host.d.ts.map +0 -1
  214. package/dist/engine/worker-host.js.map +0 -1
  215. package/dist/engine/worker.d.ts.map +0 -1
  216. package/dist/engine/worker.js.map +0 -1
  217. package/dist/index.d.ts.map +0 -1
  218. package/dist/index.js.map +0 -1
  219. package/dist/plan/index.d.ts.map +0 -1
  220. package/dist/plan/index.js.map +0 -1
  221. package/dist/storage/fixture-shape.d.ts +0 -42
  222. package/dist/storage/fixture-shape.d.ts.map +0 -1
  223. package/dist/storage/fixture-shape.js +0 -146
  224. package/dist/storage/fixture-shape.js.map +0 -1
  225. package/dist/storage/index.d.ts.map +0 -1
  226. package/dist/storage/index.js.map +0 -1
  227. package/dist/storage/indexeddb.d.ts.map +0 -1
  228. package/dist/storage/indexeddb.js.map +0 -1
  229. package/dist/storage/memory.d.ts.map +0 -1
  230. package/dist/storage/memory.js.map +0 -1
  231. package/dist/storage/opfs/files.d.ts.map +0 -1
  232. package/dist/storage/opfs/files.js.map +0 -1
  233. package/dist/storage/opfs/index.d.ts.map +0 -1
  234. package/dist/storage/opfs/index.js.map +0 -1
  235. package/dist/storage/opfs/leader.d.ts.map +0 -1
  236. package/dist/storage/opfs/leader.js.map +0 -1
  237. package/dist/storage/opfs/rpc.d.ts.map +0 -1
  238. package/dist/storage/opfs/rpc.js.map +0 -1
  239. package/dist/storage/opfs/store.d.ts.map +0 -1
  240. package/dist/storage/opfs/store.js.map +0 -1
  241. package/dist/storage/snapshot.d.ts.map +0 -1
  242. package/dist/storage/snapshot.js.map +0 -1
  243. package/dist/storage/toolkit/extents.d.ts.map +0 -1
  244. package/dist/storage/toolkit/extents.js.map +0 -1
  245. package/dist/storage/toolkit/index.d.ts.map +0 -1
  246. package/dist/storage/toolkit/index.js.map +0 -1
  247. package/dist/storage/toolkit/record-core.d.ts.map +0 -1
  248. package/dist/storage/toolkit/record-core.js.map +0 -1
  249. package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
  250. package/dist/storage/toolkit/sync-file.js.map +0 -1
  251. package/dist/storage/toolkit/wal.d.ts.map +0 -1
  252. package/dist/storage/toolkit/wal.js.map +0 -1
  253. package/dist/storage/toolkit/wire.d.ts.map +0 -1
  254. package/dist/storage/toolkit/wire.js.map +0 -1
  255. package/dist/storage/types.d.ts.map +0 -1
  256. package/dist/storage/types.js.map +0 -1
  257. package/dist/testing/block-store-conformance.d.ts.map +0 -1
  258. package/dist/testing/block-store-conformance.js.map +0 -1
  259. package/dist/testing/index.d.ts.map +0 -1
  260. package/dist/testing/index.js.map +0 -1
  261. package/dist/testing/opfs-shim.d.ts.map +0 -1
  262. package/dist/testing/opfs-shim.js.map +0 -1
  263. package/dist/testing/seeds.d.ts +0 -11
  264. package/dist/testing/seeds.d.ts.map +0 -1
  265. package/dist/testing/seeds.js +0 -50
  266. package/dist/testing/seeds.js.map +0 -1
  267. package/dist/transactions/index.d.ts.map +0 -1
  268. package/dist/transactions/index.js.map +0 -1
  269. package/dist/worker-protocol/index.d.ts.map +0 -1
  270. package/dist/worker-protocol/index.js.map +0 -1
@@ -1,4 +1,8 @@
1
- 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";
2
6
  /**
3
7
  * The in-process store: record semantics live in `RecordCore` (shared with the OPFS store),
4
8
  * block and temp-page bytes live in Maps here, and atomicity comes from running every mutating
@@ -7,53 +11,71 @@ import { RecordCore, validateId, validateTempRunPage, validateTempRunPageIdentit
7
11
  */
8
12
  export class MemoryBlockStore {
9
13
  #blocks = new Map();
14
+ #blockChecksums = new Map();
15
+ #physicalBlockBytes = 0;
10
16
  #tempRunPages = new Map();
17
+ #tempUsageByOwner = new Map();
18
+ #tempPageKeysByRun = new Map();
19
+ #tempBytesTotal = 0;
20
+ #tempPagesTotal = 0;
21
+ #tempRunsTotal = 0;
22
+ #ftsBaseBuilds = new Map();
23
+ #snapshotFrameExport;
24
+ #snapshotFrameImport;
25
+ #completedSnapshotFrameImport;
11
26
  #core = new RecordCore({
12
27
  hasBlock: (id) => this.#blocks.has(id),
13
28
  blockByteLength: (id) => this.#blocks.get(id)?.byteLength,
29
+ blockChecksum: (id) => this.#blockChecksums.get(id),
14
30
  });
15
31
  #commitQueue = Promise.resolve();
16
- async addBlock(id, bytes) {
17
- return this.addBlocks([{ id, bytes }]);
18
- }
19
- async addBlocks(blocks) {
20
- const ids = new Set();
21
- for (const block of blocks) {
22
- validateId(block.id);
23
- if (ids.has(block.id) || this.#blocks.has(block.id)) {
24
- throw new Error(`Block already exists: ${block.id}`);
25
- }
26
- ids.add(block.id);
27
- }
28
- for (const block of blocks)
29
- this.#blocks.set(block.id, new Uint8Array(block.bytes));
30
- }
31
32
  async getBlock(id) {
33
+ validateStorageId(id, "Block ID");
32
34
  const bytes = this.#blocks.get(id);
33
35
  return bytes === undefined ? undefined : new Uint8Array(bytes);
34
36
  }
35
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
+ }
36
48
  return ids.map((id) => {
37
49
  const bytes = this.#blocks.get(id);
38
50
  return bytes === undefined ? undefined : new Uint8Array(bytes);
39
51
  });
40
52
  }
41
- async removeBlock(id) {
42
- 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
+ });
43
64
  }
44
- async listBlockIds() {
45
- 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));
46
68
  }
47
69
  async putTempRunPage(page) {
70
+ assertTempRunPageBatchLimits([page]);
48
71
  validateTempRunPage(page);
49
- this.#tempRunPages.set(tempRunPageKey(page.ownerId, page.runId, page.pageIndex), page.bytes.slice());
72
+ return this.#runAtomic(() => this.#putTempRunPages([page]));
50
73
  }
51
74
  async putTempRunPages(pages) {
75
+ assertTempRunPageBatchLimits(pages);
52
76
  for (const page of pages)
53
77
  validateTempRunPage(page);
54
- for (const page of pages) {
55
- this.#tempRunPages.set(tempRunPageKey(page.ownerId, page.runId, page.pageIndex), page.bytes.slice());
56
- }
78
+ return this.#runAtomic(() => this.#putTempRunPages(pages));
57
79
  }
58
80
  async getTempRunPage(ownerId, runId, pageIndex) {
59
81
  validateTempRunPageIdentity(ownerId, runId, pageIndex);
@@ -61,15 +83,14 @@ export class MemoryBlockStore {
61
83
  }
62
84
  async removeTempRun(ownerId, runId) {
63
85
  validateTempRunPageIdentity(ownerId, runId, 0);
64
- const prefix = tempRunPagePrefix(ownerId, runId);
65
- for (const key of this.#tempRunPages.keys())
66
- if (key.startsWith(prefix))
67
- this.#tempRunPages.delete(key);
86
+ return this.#runAtomic(() => this.#removeTempRunPages(ownerId, runId));
68
87
  }
69
88
  async removeTempOwner(ownerId) {
70
89
  validateId(ownerId);
71
- this.#removeTempOwnerPages(ownerId);
72
- this.#core.removeTempOwner(ownerId);
90
+ return this.#runAtomic(() => {
91
+ this.#removeTempOwnerPages(ownerId);
92
+ this.#core.removeTempOwner(ownerId);
93
+ });
73
94
  }
74
95
  async createTempOwner(record) {
75
96
  return this.#runAtomic(() => {
@@ -79,8 +100,8 @@ export class MemoryBlockStore {
79
100
  async getTempOwner(ownerId) {
80
101
  return this.#core.getTempOwner(ownerId);
81
102
  }
82
- async renewTempOwner(ownerId, expectedRevision, expiresAt) {
83
- return this.#runAtomic(() => this.#core.renewTempOwner(ownerId, expectedRevision, expiresAt));
103
+ async renewTempOwner(input) {
104
+ return this.#runAtomic(() => this.#core.renewTempOwner(input));
84
105
  }
85
106
  async removeTempOwnerIfExpired(ownerId, expiresAtCutoff) {
86
107
  return this.#runAtomic(() => {
@@ -99,33 +120,297 @@ export class MemoryBlockStore {
99
120
  }
100
121
  return this.#core.listTempOwnerIdsPage(afterOwnerId, limit, pageOwnerIds);
101
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
+ }
102
218
  #removeTempOwnerPages(ownerId) {
103
- const prefix = `${String(ownerId.length)}:${ownerId}:`;
104
- for (const key of this.#tempRunPages.keys())
105
- if (key.startsWith(prefix))
106
- this.#tempRunPages.delete(key);
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);
107
224
  }
108
- async addTable(record) {
109
- this.#core.addTable(record);
225
+ async addTable(record, options = {}) {
226
+ this.#core.addTable(record, options);
110
227
  }
111
228
  async getTable(id) {
112
229
  return this.#core.getTable(id);
113
230
  }
114
231
  async updateTable(id, expectedRevision, update) {
115
- 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
+ });
116
245
  }
117
- async removeTable(id, expectedRevision) {
246
+ async dropTable(input) {
118
247
  return this.#runAtomic(() => {
119
- this.#core.removeTable(id, expectedRevision);
248
+ const summary = this.#core.dropTable(input);
249
+ this.#removeTablePostingBuilds(input.tableId);
250
+ return summary;
251
+ });
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;
120
258
  });
121
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
+ }
122
280
  async writeFtsBase(tableId, columnId, input) {
123
281
  return this.#runAtomic(() => {
124
282
  this.#core.writeFtsBase(tableId, columnId, input);
125
283
  });
126
284
  }
127
- async readFtsCandidates(tableId, columnId, terms, upToVersion) {
128
- return this.#core.readFtsCandidates(tableId, columnId, terms, upToVersion);
285
+ async removeFtsColumn(tableId, columnId) {
286
+ return this.#runAtomic(() => {
287
+ this.#core.removeFtsColumn(tableId, columnId);
288
+ this.#ftsBaseBuilds.delete(`${tableId}/${columnId}`);
289
+ });
290
+ }
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);
297
+ return this.#runAtomic(() => {
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");
304
+ }
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
+ });
316
+ });
317
+ }
318
+ async renewFtsBaseBuild(input) {
319
+ validatePostingBuildLifetime(input.expiresAtCutoff, input.expiresAt);
320
+ return this.#runAtomic(() => {
321
+ const build = this.#requirePostingBuild(input);
322
+ build.expiresAt = input.expiresAt;
323
+ });
324
+ }
325
+ async writeFtsBaseBuildChunk(input) {
326
+ validatePostingBuildLifetime(input.expiresAtCutoff, input.expiresAt);
327
+ return this.#runAtomic(() => {
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)) {
367
+ this.#ftsBaseBuilds.delete(key);
368
+ return;
369
+ }
370
+ const chunks = Array.from({ length: input.chunkCount }, (_, ordinal) => {
371
+ const chunk = build.chunks.get(ordinal);
372
+ if (chunk === undefined) {
373
+ throw new Error(`Full-text base chunk is missing: ${String(ordinal)}`);
374
+ }
375
+ return chunk;
376
+ });
377
+ this.#core.writeFtsBase(input.tableId, input.columnId, {
378
+ coversVersion: input.coversVersion,
379
+ chunks,
380
+ totalTokens: input.totalTokens,
381
+ });
382
+ this.#ftsBaseBuilds.delete(key);
383
+ });
384
+ }
385
+ async abortFtsBaseBuild(input) {
386
+ return this.#runAtomic(() => {
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);
397
+ });
398
+ }
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);
411
+ }
412
+ async readFtsPostings(tableId, columnId, upToVersion, maxRowIds, maxRetainedBytes) {
413
+ return this.#core.readFtsPostings(tableId, columnId, upToVersion, maxRowIds, maxRetainedBytes);
129
414
  }
130
415
  async getTableByName(name) {
131
416
  const id = this.#core.getTableIdByName(name);
@@ -134,17 +419,20 @@ export class MemoryBlockStore {
134
419
  async listTables() {
135
420
  return this.#core.listTables();
136
421
  }
137
- async addSegment(record) {
138
- this.#core.addSegment(record);
139
- }
140
422
  async getSegment(id) {
141
423
  return this.#core.getSegment(id);
142
424
  }
143
- async listSegments(tableId) {
144
- return this.#core.listSegments(tableId);
425
+ async listSegmentPage(afterId, limit) {
426
+ return this.#core.listSegmentPage(afterId, limit);
145
427
  }
146
- async removeSegment(id) {
147
- 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));
148
436
  }
149
437
  async reserveRowIds(tableId, count) {
150
438
  return this.#core.reserveRowIds(tableId, count);
@@ -153,33 +441,69 @@ export class MemoryBlockStore {
153
441
  return this.#runAtomic(() => this.#core.reserveAutoIncrement(tableId, columnId, count, atLeast));
154
442
  }
155
443
  async getExistingUniqueKeys(tableId, keyTokens) {
444
+ assertStorageBulkReadItems(keyTokens, "Unique-key lookup batch");
156
445
  return this.#core.getExistingUniqueKeys(tableId, keyTokens);
157
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
+ }
158
465
  async getCurrentManifestVersion() {
159
466
  return this.#core.getCurrentManifestVersion();
160
467
  }
161
468
  async getCatalogProbe() {
162
469
  return this.#core.getCatalogProbe();
163
470
  }
164
- /**
165
- * Composed over this store's own methods rather than delegated to the core, so a subclass
166
- * that instruments `getTransactions` or `listSegments` observes these internal reads too —
167
- * the engine's read-count tests rely on that.
168
- */
169
- async getQueryCatalogState(tableNames) {
170
- const tables = await Promise.all(tableNames.map((name) => this.getTableByName(name)));
171
- const foundTableIds = new Set(tables.filter((table) => table !== undefined).map((table) => table.id));
172
- const segments = (await this.listSegments()).filter((record) => foundTableIds.has(record.tableId));
173
- const transactionIds = [...new Set(segments.map((segment) => segment.transactionId))];
174
- const transactions = (await this.getTransactions(transactionIds)).filter((record) => record !== undefined);
175
- const probe = this.#core.getCatalogProbe();
176
- return {
177
- manifestVersion: probe.manifestVersion,
178
- tables,
179
- segments,
180
- transactions,
181
- catalogEpoch: probe.catalogEpoch,
182
- };
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
+ });
183
507
  }
184
508
  async getCurrentManifest() {
185
509
  return this.#core.getCurrentManifest();
@@ -187,15 +511,15 @@ export class MemoryBlockStore {
187
511
  async getManifest(version) {
188
512
  return this.#core.getManifest(version);
189
513
  }
190
- async listManifests() {
191
- 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);
192
519
  }
193
520
  async listManifestPage(afterVersion, limit) {
194
521
  return this.#core.listManifestPage(afterVersion, limit);
195
522
  }
196
- async publishManifest(input) {
197
- return this.#runAtomic(() => this.#core.publishManifest(input));
198
- }
199
523
  async beginTransaction(input) {
200
524
  return this.#runAtomic(() => this.#core.beginTransaction(input));
201
525
  }
@@ -208,23 +532,35 @@ export class MemoryBlockStore {
208
532
  return this.#core.getTransaction(id);
209
533
  }
210
534
  async getTransactions(ids) {
535
+ assertStorageBulkReadItems(ids, "Transaction read batch");
211
536
  return this.#core.getTransactions(ids);
212
537
  }
213
- async listTransactions() {
214
- return this.#core.listTransactions();
215
- }
216
538
  async listTransactionPage(afterId, limit) {
217
539
  return this.#core.listTransactionPage(afterId, limit);
218
540
  }
219
541
  async updateTransaction(id, expectedRevision, update) {
220
542
  return this.#runAtomic(() => this.#core.updateTransaction(id, expectedRevision, update));
221
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
+ }
222
550
  async stageTransactionArtifacts(input) {
223
551
  return this.#runAtomic(() => {
224
552
  const updated = this.#core.stageTransactionArtifacts(input);
225
553
  // The record half validated everything; the bytes land in this same atomic step.
226
554
  for (const block of input.blocks)
227
- 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);
228
564
  return updated;
229
565
  });
230
566
  }
@@ -236,7 +572,7 @@ export class MemoryBlockStore {
236
572
  const summary = this.#core.writeTransaction(input);
237
573
  // The record half validated everything; the bytes land in this same atomic step.
238
574
  for (const block of input.blocks)
239
- this.#blocks.set(block.id, new Uint8Array(block.bytes));
575
+ this.#putBlock(block.id, block.bytes);
240
576
  return summary;
241
577
  });
242
578
  }
@@ -251,19 +587,20 @@ export class MemoryBlockStore {
251
587
  async listLeases() {
252
588
  return this.#core.listLeases();
253
589
  }
254
- async renewLease(id, expectedRevision, expiresAt) {
255
- 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));
256
595
  }
257
- async moveLease(id, expectedRevision, manifestVersion, expiresAt) {
258
- return this.#runAtomic(() => this.#core.moveLease(id, expectedRevision, manifestVersion, expiresAt));
596
+ async moveLease(input) {
597
+ return this.#runAtomic(() => this.#core.moveLease(input));
259
598
  }
260
599
  async removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff) {
261
600
  return this.#runAtomic(() => this.#core.removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff));
262
601
  }
263
- async removeLease(id) {
264
- return this.#runAtomic(() => {
265
- this.#core.removeLease(id);
266
- });
602
+ async removeLease(input) {
603
+ return this.#runAtomic(() => this.#core.removeLease(input));
267
604
  }
268
605
  async createCompactionJob(record) {
269
606
  this.#core.createCompactionJob(record);
@@ -284,71 +621,551 @@ export class MemoryBlockStore {
284
621
  return this.#runAtomic(() => this.#core.cancelCompactionJob(id, expectedRevision, cancelledAt));
285
622
  }
286
623
  async removeCompactionJob(id) {
287
- this.#core.removeCompactionJob(id);
624
+ return this.#core.removeCompactionJob(id);
288
625
  }
289
626
  async createGarbageCollectionJob(input) {
290
627
  return this.#runAtomic(() => this.#core.createGarbageCollectionJob(input));
291
628
  }
629
+ async updateGarbageCollectionPlanning(input) {
630
+ return this.#runAtomic(() => this.#core.updateGarbageCollectionPlanning(input));
631
+ }
292
632
  async getGarbageCollectionJob(id) {
293
633
  return this.#core.getGarbageCollectionJob(id);
294
634
  }
295
635
  async listGarbageCollectionJobs() {
296
636
  return this.#core.listGarbageCollectionJobs();
297
637
  }
638
+ async listGarbageCollectionJobPage(afterId, limit) {
639
+ return this.#core.listGarbageCollectionJobPage(afterId, limit);
640
+ }
298
641
  async runGarbageCollectionStep(input) {
299
642
  return this.#runAtomic(() => {
300
643
  const result = this.#core.runGarbageCollectionStep(input);
301
- result.reclaimedBlockIds.forEach((id) => this.#blocks.delete(id));
644
+ result.reclaimedBlockIds.forEach((id) => this.#deleteBlock(id));
302
645
  return result;
303
646
  });
304
647
  }
305
- async removePrunedManifestRecords() {
306
- return this.#runAtomic(() => this.#core.removePrunedManifestRecords().length);
648
+ async removePrunedManifestRecords(maxItems) {
649
+ return this.#runAtomic(() => this.#core.removePrunedManifestRecords(maxItems));
307
650
  }
308
651
  async removeGarbageCollectionJob(id) {
309
652
  return this.#runAtomic(() => {
310
653
  this.#core.removeGarbageCollectionJob(id);
311
654
  });
312
655
  }
313
- /**
314
- * Copies the current version out as a portable snapshot. Runs on the commit queue, so it sees
315
- * one consistent version rather than a commit in progress. What the snapshot carries and
316
- * drops is `RecordCore.exportSnapshot`'s contract.
317
- */
318
- exportSnapshot() {
319
- return this.#runAtomic(() => this.#core.exportSnapshot((id) => this.#blocks.get(id)));
320
- }
321
- /**
322
- * Loads a snapshot into this store, which must be empty — the same contract as the IndexedDB
323
- * store, so a caller holding a `BlockStore` can restore into either without asking which it
324
- * has. The load runs on the commit queue, so it cannot interleave with a commit.
325
- *
326
- * Progress is reported for parity with the IndexedDB store rather than because it is needed:
327
- * an in-memory load is a handful of map writes with no transaction to break it into, so it
328
- * reports the start and then the end.
329
- */
330
- async importSnapshot(snapshot, options = {}) {
331
- const totalBytes = snapshot.blocks.reduce((total, block) => total + block.bytes.byteLength, 0);
332
- options.onProgress?.({ phase: "blocks", writtenBytes: 0, totalBytes });
333
- await this.#runAtomic(() => {
334
- this.#core.importSnapshot(snapshot, (id, bytes) => this.#blocks.set(id, bytes.slice()));
335
- });
336
- options.onProgress?.({ phase: "done", writtenBytes: totalBytes, totalBytes });
337
- }
338
- /** Builds a store holding exactly what the snapshot captured. */
339
- static fromSnapshot(snapshot) {
340
- const store = new MemoryBlockStore();
341
- store.#core.loadSnapshot(snapshot, (id, bytes) => store.#blocks.set(id, bytes.slice()));
342
- 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
+ });
343
1118
  }
344
1119
  /** Block and spill bytes held; the record maps are negligible beside them. */
345
1120
  async getLogicalStorageBytes() {
346
- let total = 0;
347
- for (const bytes of this.#blocks.values())
348
- total += bytes.byteLength;
349
- for (const bytes of this.#tempRunPages.values())
350
- total += bytes.byteLength;
351
- return total;
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;
352
1169
  }
353
1170
  close() {
354
1171
  // The in-memory implementation owns no external resources.
@@ -362,6 +1179,10 @@ export class MemoryBlockStore {
362
1179
  });
363
1180
  this.#commitQueue = this.#commitQueue.then(() => {
364
1181
  try {
1182
+ if (this.#completedSnapshotFrameImport !== undefined &&
1183
+ this.#core.getCurrentManifestVersion() !== this.#completedSnapshotFrameImport.version) {
1184
+ this.#completedSnapshotFrameImport = undefined;
1185
+ }
365
1186
  resolveResult(operation());
366
1187
  }
367
1188
  catch (error) {
@@ -377,4 +1198,97 @@ function tempRunPageKey(ownerId, runId, pageIndex) {
377
1198
  function tempRunPagePrefix(ownerId, runId) {
378
1199
  return `${String(ownerId.length)}:${ownerId}:${String(runId.length)}:${runId}:`;
379
1200
  }
380
- //# 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
+ }