@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,108 +1,32 @@
1
- import type { FtsPosting, SegmentRecord, TableRecord, TransactionRecord } from "./types.js";
1
+ /** Public snapshot API over the bounded framed v1 container. */
2
+ export * from "./snapshot-stream.js";
2
3
  export declare const SNAPSHOT_FORMAT_VERSION = 1;
3
- /** One indexed column's full-text base, as it stood at the snapshot's version. */
4
- export interface SnapshotFtsIndex {
5
- columnId: string;
6
- coversVersion: number;
7
- totalTokens: number;
8
- /** Term-range partitioned, term-sorted within each chunk. */
9
- chunks: FtsPosting[][];
10
- }
11
- export interface SnapshotTable {
12
- record: TableRecord;
13
- /** The next row ID a write should reserve; below it, IDs may already be in use. */
14
- nextRowId: bigint;
15
- autoIncrement: Array<{
16
- columnId: string;
17
- next: bigint;
18
- }>;
19
- /** Complete unique-key membership, so a write after loading still conflicts correctly. */
20
- uniqueKeyTokens: string[];
21
- fts: SnapshotFtsIndex[];
22
- }
23
- export interface DatabaseSnapshot {
24
- /** The manifest version this snapshot captured, which the restored database keeps. */
25
- version: number;
26
- createdAt: string;
27
- tables: SnapshotTable[];
28
- segments: SegmentRecord[];
29
- /** Committed transactions only; segment visibility resolves through `committedVersion`. */
30
- transactions: TransactionRecord[];
31
- blocks: Array<{
32
- id: string;
33
- bytes: Uint8Array;
34
- }>;
35
- }
36
- /** Where a snapshot load has got to, for a progress bar over a multi-megabyte file. */
4
+ /** Export and worker-transport chunk ceiling; direct imports accept any non-empty source chunk. */
5
+ export declare const MAX_SNAPSHOT_STREAM_CHUNK_BYTES: number;
37
6
  export interface SnapshotLoadProgress {
38
7
  phase: "blocks" | "catalog" | "done";
39
8
  writtenBytes: number;
40
9
  totalBytes: number;
41
10
  }
42
- /**
43
- * Where a snapshot export has got to. `reading` covers copying the version out of the store and
44
- * encoding it, and reports no byte counts because the encoded length is not known until it is
45
- * finished. `transfer` happens only when the database lives in a worker and the bytes are handed
46
- * back a chunk at a time; an in-page database goes straight from `reading` to `done`.
47
- */
48
11
  export interface SnapshotExportProgress {
49
12
  phase: "reading" | "transfer" | "done";
50
13
  transferredBytes: number;
51
- /** Zero while reading; the encoded length once it is known. */
14
+ /** Zero while streaming; the exact encoded length is known by the byte-array wrapper. */
52
15
  totalBytes: number;
53
16
  }
54
- /** What a header-only read reports, without touching a single block payload. */
17
+ /** Header summary used by restore confirmation UIs without retaining database metadata. */
55
18
  export interface SnapshotSummary {
56
19
  formatVersion: number;
57
20
  version: number;
58
21
  createdAt: string;
59
22
  tableCount: number;
60
23
  blockCount: number;
61
- /** Total byte length of the block payloads, excluding the header. */
24
+ /** Stored body payload bytes, excluding frame envelopes, header, and footer. */
62
25
  payloadBytes: number;
63
26
  byteLength: number;
64
27
  }
65
28
  /**
66
- * Serializes a snapshot. Block bytes are copied verbatim they are already compressed and
67
- * checksummed, so the container never re-encodes or re-compresses them. The header is gzipped,
68
- * because it is JSON and its bulk is full-text posting lists and segment records, which compress
69
- * by roughly four to one.
70
- */
71
- export declare function encodeSnapshot(snapshot: DatabaseSnapshot): Promise<Uint8Array>;
72
- /**
73
- * Reads only the header. Cheap enough to run against a multi-hundred-megabyte file before
74
- * deciding whether to load it.
29
+ * Reads and validates the canonical header from a materialized snapshot. The decoder closes its
30
+ * source immediately after the header; body checks run during import, where frames are consumed.
75
31
  */
76
32
  export declare function readSnapshotSummary(bytes: Uint8Array): Promise<SnapshotSummary>;
77
- /**
78
- * Parses a snapshot and authenticates every block. `inspectBlock` verifies each payload's magic,
79
- * version, declared lengths, and envelope checksum without decompressing it, so a corrupt file
80
- * fails here rather than mid-query.
81
- */
82
- export declare function decodeSnapshot(bytes: Uint8Array): Promise<DatabaseSnapshot>;
83
- /**
84
- * Shared by both stores' exporters: given the manifest's live block set, decide which segments
85
- * and transactions the snapshot needs.
86
- *
87
- * A segment is live when every block it references survives in the manifest — compaction
88
- * supersedes a segment by dropping its blocks, so that test is exactly the reader's. A
89
- * transaction is needed when a live segment names it, and only committed transactions qualify:
90
- * an active one's artifacts are unpublished and would dangle. Their pending lists are cleared,
91
- * because after the restore the manifest is the only root anything needs.
92
- *
93
- * `committedVersion` is preserved exactly, and the restored database keeps the captured version
94
- * number rather than renumbering to 1. Segments sort by `logicalOrder ?? committedVersion`, so
95
- * flattening those versions would let an update fold ahead of the insert it patches; and change
96
- * detection compares committed versions against a snapshot version, which would misread any
97
- * transaction numbered above the manifest.
98
- */
99
- export declare function selectLiveRecords(input: {
100
- liveBlockIds: ReadonlySet<string>;
101
- segments: readonly SegmentRecord[];
102
- transactions: readonly TransactionRecord[];
103
- version: number;
104
- }): {
105
- segments: SegmentRecord[];
106
- transactions: TransactionRecord[];
107
- };
108
- //# sourceMappingURL=snapshot.d.ts.map
@@ -1,276 +1,44 @@
1
- /**
2
- * Database snapshots: one committed version of a database, copied out as a single portable
3
- * byte array and loaded back into any block store.
4
- *
5
- * A snapshot is a *published* view, not a backup of a store's internals. It carries the block
6
- * bytes the current manifest points at, the catalog needed to read them (tables, segments, and
7
- * the committed transactions segment visibility resolves through), the counters a later write
8
- * has to continue from, and the unique-key membership a later write has to conflict against.
9
- * It deliberately drops everything transient — leases, spill pages, garbage-collection and
10
- * compaction jobs, in-flight transactions — and it drops version history: the snapshot restores
11
- * as a single checkpoint, so a 10,000-commit database loads as one clean version.
12
- *
13
- * The container is a small JSON header followed by the block payloads laid end to end. Blocks
14
- * are already self-describing and doubly CRC-checked (see `../block-format/block.ts`), so the
15
- * container adds only a checksum over its own header and lets `inspectBlock` authenticate each
16
- * payload without decompressing any of them.
17
- */
18
- import { crc32, gzipCodec, inspectBlock } from "../block-format/index.js";
19
- /** ASCII `MNWSNAP1`, matching the block format's habit of a readable magic. */
20
- const MAGIC = Uint8Array.from([0x4d, 0x4e, 0x57, 0x53, 0x4e, 0x41, 0x50, 0x31]);
21
- /**
22
- * magic(8) + formatVersion(4) + storedHeaderLength(4) + headerChecksum(4) + headerLength(4)
23
- * + headerCodec(4). All little-endian, matching the block format.
24
- */
25
- const PREFIX_LENGTH = 28;
26
- const HEADER_CODEC_NONE = 0;
27
- const HEADER_CODEC_GZIP = 1;
1
+ /** Public snapshot API over the bounded framed v1 container. */
2
+ export * from "./snapshot-stream.js";
3
+ import { SNAPSHOT_FRAME_KINDS } from "./types.js";
4
+ import { decodeSnapshotFrameStream } from "./snapshot-stream.js";
28
5
  export const SNAPSHOT_FORMAT_VERSION = 1;
29
- function encodeSegment(segment) {
30
- const { rowIdStart, rowIdEndExclusive, rowIdSpans, ...rest } = segment;
31
- return {
32
- ...rest,
33
- rowIdStart: rowIdStart.toString(),
34
- rowIdEndExclusive: rowIdEndExclusive.toString(),
35
- ...(rowIdSpans === undefined
36
- ? {}
37
- : {
38
- rowIdSpans: rowIdSpans.map((span) => ({
39
- rowStart: span.rowStart,
40
- rowCount: span.rowCount,
41
- rowIdStart: span.rowIdStart.toString(),
42
- })),
43
- }),
44
- };
45
- }
46
- function decodeSegment(segment) {
47
- const { rowIdStart, rowIdEndExclusive, rowIdSpans, ...rest } = segment;
48
- const spans = rowIdSpans?.map((span) => ({
49
- rowStart: span.rowStart,
50
- rowCount: span.rowCount,
51
- rowIdStart: parseBigInt(span.rowIdStart, "segment row-ID span"),
52
- }));
53
- return {
54
- ...rest,
55
- rowIdStart: parseBigInt(rowIdStart, "segment row-ID start"),
56
- rowIdEndExclusive: parseBigInt(rowIdEndExclusive, "segment row-ID end"),
57
- ...(spans === undefined ? {} : { rowIdSpans: spans }),
58
- };
59
- }
60
- function encodePosting(posting) {
61
- const [first, ...rest] = posting.rowIds;
62
- if (first === undefined)
63
- return { term: posting.term, first: "0", deltas: [], tf: posting.tf };
64
- const deltas = [];
65
- let previous = first;
66
- for (const rowId of rest) {
67
- const gap = rowId - previous;
68
- if (gap > BigInt(Number.MAX_SAFE_INTEGER)) {
69
- throw new RangeError("Full-text row-ID gap is too large to snapshot");
70
- }
71
- deltas.push(Number(gap));
72
- previous = rowId;
73
- }
74
- return { term: posting.term, first: first.toString(), deltas, tf: posting.tf };
75
- }
76
- function decodePosting(posting) {
77
- const rowIds = [];
78
- if (posting.tf.length > 0) {
79
- let current = parseBigInt(posting.first, "full-text row ID");
80
- rowIds.push(current);
81
- for (const delta of posting.deltas) {
82
- current += BigInt(delta);
83
- rowIds.push(current);
84
- }
85
- }
86
- return { term: posting.term, rowIds, tf: posting.tf };
87
- }
88
- function parseBigInt(value, context) {
89
- if (!/^-?\d+$/.test(value))
90
- throw new Error(`Snapshot has an invalid ${context}: ${value}`);
91
- return BigInt(value);
92
- }
6
+ /** Export and worker-transport chunk ceiling; direct imports accept any non-empty source chunk. */
7
+ export const MAX_SNAPSHOT_STREAM_CHUNK_BYTES = 1024 * 1024;
93
8
  /**
94
- * Serializes a snapshot. Block bytes are copied verbatim they are already compressed and
95
- * checksummed, so the container never re-encodes or re-compresses them. The header is gzipped,
96
- * because it is JSON and its bulk is full-text posting lists and segment records, which compress
97
- * by roughly four to one.
98
- */
99
- export async function encodeSnapshot(snapshot) {
100
- const header = {
101
- formatVersion: SNAPSHOT_FORMAT_VERSION,
102
- version: snapshot.version,
103
- createdAt: snapshot.createdAt,
104
- blocks: snapshot.blocks.map((block) => ({ id: block.id, length: block.bytes.byteLength })),
105
- tables: snapshot.tables.map((table) => ({
106
- record: table.record,
107
- nextRowId: table.nextRowId.toString(),
108
- autoIncrement: table.autoIncrement.map((entry) => ({
109
- columnId: entry.columnId,
110
- next: entry.next.toString(),
111
- })),
112
- uniqueKeyTokens: table.uniqueKeyTokens,
113
- fts: table.fts.map((index) => ({
114
- columnId: index.columnId,
115
- coversVersion: index.coversVersion,
116
- totalTokens: index.totalTokens,
117
- chunks: index.chunks.map((chunk) => chunk.map(encodePosting)),
118
- })),
119
- })),
120
- segments: snapshot.segments.map(encodeSegment),
121
- transactions: snapshot.transactions,
122
- };
123
- const json = new TextEncoder().encode(JSON.stringify(header));
124
- const stored = await gzipCodec.compress(json);
125
- const payloadBytes = snapshot.blocks.reduce((total, block) => total + block.bytes.byteLength, 0);
126
- const bytes = new Uint8Array(PREFIX_LENGTH + stored.byteLength + payloadBytes);
127
- bytes.set(MAGIC, 0);
128
- const view = new DataView(bytes.buffer);
129
- view.setUint32(8, SNAPSHOT_FORMAT_VERSION, true);
130
- view.setUint32(12, stored.byteLength, true);
131
- view.setUint32(16, crc32(stored), true);
132
- view.setUint32(20, json.byteLength, true);
133
- view.setUint32(24, HEADER_CODEC_GZIP, true);
134
- bytes.set(stored, PREFIX_LENGTH);
135
- let offset = PREFIX_LENGTH + stored.byteLength;
136
- for (const block of snapshot.blocks) {
137
- bytes.set(block.bytes, offset);
138
- offset += block.bytes.byteLength;
139
- }
140
- return bytes;
141
- }
142
- async function readHeader(bytes) {
143
- if (bytes.byteLength < PREFIX_LENGTH)
144
- throw new Error("Truncated snapshot header");
145
- if (!MAGIC.every((byte, index) => bytes[index] === byte)) {
146
- throw new Error("Not a Minnow snapshot");
147
- }
148
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
149
- const formatVersion = view.getUint32(8, true);
150
- if (formatVersion !== SNAPSHOT_FORMAT_VERSION) {
151
- throw new Error(`Unsupported snapshot version ${String(formatVersion)}`);
152
- }
153
- const storedLength = view.getUint32(12, true);
154
- const payloadOffset = PREFIX_LENGTH + storedLength;
155
- if (payloadOffset > bytes.byteLength)
156
- throw new Error("Truncated snapshot header");
157
- const stored = bytes.subarray(PREFIX_LENGTH, payloadOffset);
158
- // Checked before the header is decompressed, let alone parsed: a damaged header must fail
159
- // as a checksum mismatch rather than as whatever gzip or JSON happens to say about it.
160
- if (crc32(stored) !== view.getUint32(16, true)) {
161
- throw new Error("Snapshot header checksum mismatch");
162
- }
163
- const codec = view.getUint32(24, true);
164
- if (codec !== HEADER_CODEC_NONE && codec !== HEADER_CODEC_GZIP) {
165
- throw new Error(`Unsupported snapshot header codec ${String(codec)}`);
166
- }
167
- const json = codec === HEADER_CODEC_GZIP
168
- ? await gzipCodec.decompress(stored, view.getUint32(20, true))
169
- : stored;
170
- const header = JSON.parse(new TextDecoder().decode(json));
171
- return { header, payloadOffset };
172
- }
173
- /**
174
- * Reads only the header. Cheap enough to run against a multi-hundred-megabyte file before
175
- * deciding whether to load it.
9
+ * Reads and validates the canonical header from a materialized snapshot. The decoder closes its
10
+ * source immediately after the header; body checks run during import, where frames are consumed.
176
11
  */
177
12
  export async function readSnapshotSummary(bytes) {
178
- const { header, payloadOffset } = await readHeader(bytes);
179
- return {
180
- formatVersion: header.formatVersion,
181
- version: header.version,
182
- createdAt: header.createdAt,
183
- tableCount: header.tables.length,
184
- blockCount: header.blocks.length,
185
- payloadBytes: bytes.byteLength - payloadOffset,
186
- byteLength: bytes.byteLength,
187
- };
188
- }
189
- /**
190
- * Parses a snapshot and authenticates every block. `inspectBlock` verifies each payload's magic,
191
- * version, declared lengths, and envelope checksum without decompressing it, so a corrupt file
192
- * fails here rather than mid-query.
193
- */
194
- export async function decodeSnapshot(bytes) {
195
- const { header, payloadOffset } = await readHeader(bytes);
196
- const declared = header.blocks.reduce((total, block) => total + block.length, 0);
197
- if (payloadOffset + declared !== bytes.byteLength) {
198
- throw new Error("Snapshot length mismatch");
199
- }
200
- const blocks = [];
201
- let offset = payloadOffset;
202
- for (const block of header.blocks) {
203
- const payload = bytes.subarray(offset, offset + block.length);
204
- inspectBlock(payload);
205
- blocks.push({ id: block.id, bytes: payload });
206
- offset += block.length;
13
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength === 0) {
14
+ throw new TypeError("Snapshot bytes must be a non-empty Uint8Array");
207
15
  }
208
- const present = new Set(blocks.map((block) => block.id));
209
- const segments = header.segments.map(decodeSegment);
210
- for (const segment of segments) {
211
- for (const ids of Object.values(segment.columnBlockIds)) {
212
- for (const id of ids) {
213
- if (!present.has(id))
214
- throw new Error(`Snapshot segment references a missing block: ${id}`);
215
- }
16
+ const entries = decodeSnapshotFrameStream((async function* () {
17
+ for (let offset = 0; offset < bytes.byteLength; offset += MAX_SNAPSHOT_STREAM_CHUNK_BYTES) {
18
+ yield bytes.subarray(offset, offset + MAX_SNAPSHOT_STREAM_CHUNK_BYTES);
216
19
  }
20
+ })());
21
+ const first = await entries.next();
22
+ await entries.return(undefined);
23
+ if (first.done || first.value.type !== "header")
24
+ throw new Error("Snapshot header is missing");
25
+ return summaryFromHeader(first.value.header, bytes.byteLength);
26
+ }
27
+ function summaryFromHeader(header, byteLength) {
28
+ let payloadBytes = 0;
29
+ for (const kind of SNAPSHOT_FRAME_KINDS) {
30
+ const next = payloadBytes + header.kinds[kind].storedBytes;
31
+ if (!Number.isSafeInteger(next))
32
+ throw new RangeError("Snapshot payload is too large");
33
+ payloadBytes = next;
217
34
  }
218
35
  return {
219
- version: header.version,
36
+ formatVersion: header.formatVersion,
37
+ version: header.databaseVersion,
220
38
  createdAt: header.createdAt,
221
- tables: header.tables.map((table) => ({
222
- record: table.record,
223
- nextRowId: parseBigInt(table.nextRowId, "row-ID counter"),
224
- autoIncrement: table.autoIncrement.map((entry) => ({
225
- columnId: entry.columnId,
226
- next: parseBigInt(entry.next, "auto-increment counter"),
227
- })),
228
- uniqueKeyTokens: table.uniqueKeyTokens,
229
- fts: table.fts.map((index) => ({
230
- columnId: index.columnId,
231
- coversVersion: index.coversVersion,
232
- totalTokens: index.totalTokens,
233
- chunks: index.chunks.map((chunk) => chunk.map(decodePosting)),
234
- })),
235
- })),
236
- segments,
237
- transactions: header.transactions,
238
- blocks,
39
+ tableCount: header.kinds["catalog-page"].itemCount,
40
+ blockCount: header.kinds.block.itemCount,
41
+ payloadBytes,
42
+ byteLength,
239
43
  };
240
44
  }
241
- /**
242
- * Shared by both stores' exporters: given the manifest's live block set, decide which segments
243
- * and transactions the snapshot needs.
244
- *
245
- * A segment is live when every block it references survives in the manifest — compaction
246
- * supersedes a segment by dropping its blocks, so that test is exactly the reader's. A
247
- * transaction is needed when a live segment names it, and only committed transactions qualify:
248
- * an active one's artifacts are unpublished and would dangle. Their pending lists are cleared,
249
- * because after the restore the manifest is the only root anything needs.
250
- *
251
- * `committedVersion` is preserved exactly, and the restored database keeps the captured version
252
- * number rather than renumbering to 1. Segments sort by `logicalOrder ?? committedVersion`, so
253
- * flattening those versions would let an update fold ahead of the insert it patches; and change
254
- * detection compares committed versions against a snapshot version, which would misread any
255
- * transaction numbered above the manifest.
256
- */
257
- export function selectLiveRecords(input) {
258
- const committed = new Map(input.transactions
259
- .filter((record) => record.status === "committed" &&
260
- record.committedVersion !== null &&
261
- record.committedVersion <= input.version)
262
- .map((record) => [record.id, record]));
263
- const segments = input.segments.filter((segment) => {
264
- if (!committed.has(segment.transactionId))
265
- return false;
266
- return Object.values(segment.columnBlockIds)
267
- .flat()
268
- .every((id) => input.liveBlockIds.has(id));
269
- });
270
- const usedTransactionIds = new Set(segments.map((segment) => segment.transactionId));
271
- const transactions = [...committed.values()]
272
- .filter((record) => usedTransactionIds.has(record.id))
273
- .map((record) => ({ ...record, pendingBlockIds: [], pendingSegmentIds: [] }));
274
- return { segments, transactions };
275
- }
276
- //# sourceMappingURL=snapshot.js.map
@@ -1,13 +1,13 @@
1
- import type { SyncFileHandle } from "./sync-file.js";
1
+ import { type SyncFileHandle } from "./sync-file.js";
2
2
  /**
3
3
  * Packed, append-only extent files for bulk bytes — blocks and full-text base chunks.
4
4
  *
5
5
  * The leader appends into one tail extent through a handle it keeps open (microseconds per
6
6
  * write) and seals it at a size threshold; sealed extents are immutable. Every payload's home
7
- * is a placement — extent id, offset, length — recorded by the WAL entry that publishes it,
8
- * so recovery re-derives the tail position from the replayed placements and any bytes beyond
9
- * it (a crash between an extent write and its WAL frame) are dead space, overwritten by the
10
- * next append.
7
+ * is a placement — extent id, offset, length, and whole-payload checksum — recorded by the WAL
8
+ * entry that publishes it. Recovery verifies each new placement before applying its entry,
9
+ * re-derives the tail position from the consistent prefix, and overwrites any unpublished bytes
10
+ * beyond it on the next append.
11
11
  *
12
12
  * Extents assume a single writer (the OPFS adapter's leader), so holding handles — the tail's
13
13
  * permanently, sealed ones' in a small read cache — never contends with anyone.
@@ -22,10 +22,16 @@ export interface ExtentFiles {
22
22
  }): Promise<SyncFileHandle>;
23
23
  deleteFile(path: readonly string[]): Promise<boolean>;
24
24
  }
25
+ export interface ExtentPoolOptions {
26
+ /** @internal Deterministic checksum instrumentation for performance/correctness tests. */
27
+ _checksumForTests?: (bytes: Uint8Array) => number;
28
+ }
25
29
  export interface Placement {
26
30
  extent: number;
27
31
  offset: number;
28
32
  length: number;
33
+ /** CRC32 of every stored payload byte, independent of the payload's own format. */
34
+ checksum: number;
29
35
  }
30
36
  /** Persisted in checkpoints; placements in replayed WAL entries refine `tailOffset`. */
31
37
  export interface ExtentMeta {
@@ -35,11 +41,31 @@ export interface ExtentMeta {
35
41
  /** Live payload bytes per extent id; an extent at zero that is not the tail is deletable. */
36
42
  liveBytes: Array<readonly [number, number]>;
37
43
  }
44
+ /** Opaque O(1) rollback point for bytes not yet published by a WAL frame. */
45
+ export declare class ExtentBatchMark {
46
+ readonly tailExtentId: number;
47
+ readonly nextExtentId: number;
48
+ readonly tailOffset: number;
49
+ /** @internal Original counters are captured lazily only for extents the batch touches. */
50
+ readonly _liveBytesBefore: Map<number, number | undefined>;
51
+ constructor(tailExtentId: number, nextExtentId: number, tailOffset: number);
52
+ }
38
53
  export declare class ExtentPool {
39
54
  #private;
40
55
  private constructor();
41
56
  /** Opens (or creates) the tail extent. `meta` comes from the checkpoint, if any. */
42
- static open(tree: ExtentFiles, meta: ExtentMeta | undefined): Promise<ExtentPool>;
57
+ static open(tree: ExtentFiles, meta: ExtentMeta | undefined, options?: ExtentPoolOptions): Promise<ExtentPool>;
58
+ /** Captures the exact append/accounting state before an unpublished multi-payload batch. */
59
+ markBatch(): ExtentBatchMark;
60
+ /** Publishes a successful batch; constant-time and allocation-free. */
61
+ commitBatch(mark: ExtentBatchMark): void;
62
+ /**
63
+ * Rolls back every append and accounting mutation since `mark`. This is deliberately an
64
+ * async slow path; successful appends retain the allocation-free hot path.
65
+ */
66
+ rollbackBatch(mark: ExtentBatchMark): Promise<void>;
67
+ /** Removes an unpublished physical suffix left by a crash before its WAL publication. */
68
+ truncateTailToPublishedOffset(): void;
43
69
  /**
44
70
  * Replay hook: a WAL entry recorded this placement after the checkpoint was taken. Advances
45
71
  * the tail position and live-byte accounting to match what the writer had done.
@@ -51,12 +77,18 @@ export declare class ExtentPool {
51
77
  /** Appends synchronously; seals and rolls the tail first when it is full (the async part). */
52
78
  append(bytes: Uint8Array, flush: boolean): Promise<Placement>;
53
79
  read(placement: Placement): Promise<Uint8Array>;
80
+ /** Reads and verifies one complete opaque payload with exactly one checksum scan. */
81
+ readVerified(placement: Placement): Promise<Uint8Array>;
82
+ /** True when the complete placement currently exists; used to bound crash replay. */
83
+ contains(placement: Placement): Promise<boolean>;
54
84
  /**
55
85
  * Returns the sealed extent with the most dead space once less than half its physical bytes
56
86
  * remain live. Relocating that extent keeps sealed-file bytes below twice live payload bytes;
57
87
  * the append tail contributes only one bounded extent beyond that ratio.
58
88
  */
59
89
  fragmentedExtentId(): Promise<number | undefined>;
90
+ /** Actual bytes of every checkpoint-known extent, including the held tail handle. */
91
+ physicalByteLengths(): Promise<Map<number, number>>;
60
92
  /** Live-byte accounting for reclaimed payloads; reports extents now safe to delete. */
61
93
  release(placements: Iterable<Placement>): number[];
62
94
  /** Removes a drained extent's file and forgets it. Idempotent. */
@@ -66,4 +98,6 @@ export declare class ExtentPool {
66
98
  close(): void;
67
99
  }
68
100
  export declare function extentPath(id: number): string[];
69
- //# sourceMappingURL=extents.d.ts.map
101
+ export declare function validPlacement(placement: Placement): boolean;
102
+ export declare function assertValidPlacement(placement: Placement): void;
103
+ export declare function assertValidExtentMeta(meta: ExtentMeta): void;