@minnowdb/core 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/README.md +4 -2
  2. package/dist/block-format/block.d.ts +32 -13
  3. package/dist/block-format/block.js +179 -55
  4. package/dist/block-format/checksum.d.ts +2 -1
  5. package/dist/block-format/checksum.js +5 -2
  6. package/dist/block-format/codecs.d.ts +7 -2
  7. package/dist/block-format/codecs.js +53 -12
  8. package/dist/block-format/column.d.ts +3 -2
  9. package/dist/block-format/column.js +96 -30
  10. package/dist/block-format/index.d.ts +2 -2
  11. package/dist/block-format/index.js +2 -2
  12. package/dist/block-format/physical.d.ts +8 -2
  13. package/dist/block-format/physical.js +14 -6
  14. package/dist/block-format/types.d.ts +7 -3
  15. package/dist/block-format/types.js +0 -1
  16. package/dist/block-format/unicode.d.ts +11 -0
  17. package/dist/block-format/unicode.js +46 -0
  18. package/dist/date-value.d.ts +15 -0
  19. package/dist/date-value.js +64 -0
  20. package/dist/engine/artifact-cache.d.ts +0 -1
  21. package/dist/engine/artifact-cache.js +0 -1
  22. package/dist/engine/batch.d.ts +5 -2
  23. package/dist/engine/batch.js +0 -1
  24. package/dist/engine/buffered-writer.d.ts +2 -1
  25. package/dist/engine/buffered-writer.js +34 -9
  26. package/dist/engine/cache-limits.d.ts +25 -0
  27. package/dist/engine/cache-limits.js +25 -0
  28. package/dist/engine/catalog.d.ts +5 -6
  29. package/dist/engine/catalog.js +5 -5
  30. package/dist/engine/client.d.ts +27 -7
  31. package/dist/engine/client.js +197 -31
  32. package/dist/engine/database.d.ts +161 -38
  33. package/dist/engine/database.js +4326 -1523
  34. package/dist/engine/defaults.d.ts +0 -1
  35. package/dist/engine/defaults.js +2 -4
  36. package/dist/engine/errors.d.ts +19 -1
  37. package/dist/engine/errors.js +32 -2
  38. package/dist/engine/fts.d.ts +0 -6
  39. package/dist/engine/fts.js +39 -14
  40. package/dist/engine/group-index.d.ts +0 -1
  41. package/dist/engine/group-index.js +6 -11
  42. package/dist/engine/index.d.ts +16 -11
  43. package/dist/engine/index.js +12 -10
  44. package/dist/engine/join-index.d.ts +0 -1
  45. package/dist/engine/join-index.js +2 -2
  46. package/dist/engine/keyed-live.d.ts +57 -0
  47. package/dist/engine/keyed-live.js +226 -0
  48. package/dist/engine/live-api.d.ts +4 -0
  49. package/dist/engine/live-api.js +4 -0
  50. package/dist/engine/live.d.ts +43 -26
  51. package/dist/engine/live.js +499 -129
  52. package/dist/engine/memory.d.ts +2 -1
  53. package/dist/engine/memory.js +3 -2
  54. package/dist/engine/optimizer.d.ts +0 -1
  55. package/dist/engine/optimizer.js +4 -4
  56. package/dist/engine/query-api.d.ts +3 -0
  57. package/dist/engine/query-api.js +3 -0
  58. package/dist/engine/query-cache.d.ts +0 -1
  59. package/dist/engine/query-cache.js +4 -4
  60. package/dist/engine/query.d.ts +4 -7
  61. package/dist/engine/query.js +348 -155
  62. package/dist/engine/result-wire.d.ts +0 -1
  63. package/dist/engine/result-wire.js +2 -2
  64. package/dist/engine/schema-wire.d.ts +1 -2
  65. package/dist/engine/schema-wire.js +0 -1
  66. package/dist/engine/schema.d.ts +3 -8
  67. package/dist/engine/schema.js +41 -24
  68. package/dist/engine/sort-keys.d.ts +0 -1
  69. package/dist/engine/sort-keys.js +2 -2
  70. package/dist/engine/sql-domains.d.ts +6 -1
  71. package/dist/engine/sql-domains.js +209 -45
  72. package/dist/engine/sql-driver.d.ts +2 -2
  73. package/dist/engine/sql-driver.js +0 -1
  74. package/dist/engine/sql-json.d.ts +0 -1
  75. package/dist/engine/sql-json.js +53 -7
  76. package/dist/engine/sql-semantics.d.ts +8 -5
  77. package/dist/engine/sql-semantics.js +427 -65
  78. package/dist/engine/typed-live.d.ts +67 -0
  79. package/dist/engine/typed-live.js +349 -0
  80. package/dist/engine/vector.d.ts +7 -1
  81. package/dist/engine/vector.js +202 -58
  82. package/dist/engine/worker-host.d.ts +8 -2
  83. package/dist/engine/worker-host.js +616 -152
  84. package/dist/engine/worker.d.ts +0 -1
  85. package/dist/engine/worker.js +3 -3
  86. package/dist/engine/write-block-planner.d.ts +18 -0
  87. package/dist/engine/write-block-planner.js +112 -0
  88. package/dist/index.d.ts +0 -1
  89. package/dist/index.js +0 -1
  90. package/dist/plan/index.d.ts +0 -1
  91. package/dist/plan/index.js +0 -1
  92. package/dist/storage/index.d.ts +1 -1
  93. package/dist/storage/index.js +1 -1
  94. package/dist/storage/indexeddb.d.ts +74 -65
  95. package/dist/storage/indexeddb.js +12301 -2076
  96. package/dist/storage/memory.d.ts +56 -66
  97. package/dist/storage/memory.js +1004 -139
  98. package/dist/storage/opfs/files.d.ts +18 -7
  99. package/dist/storage/opfs/files.js +115 -28
  100. package/dist/storage/opfs/index.d.ts +1 -1
  101. package/dist/storage/opfs/index.js +1 -1
  102. package/dist/storage/opfs/leader.d.ts +403 -91
  103. package/dist/storage/opfs/leader.js +4318 -602
  104. package/dist/storage/opfs/rpc.d.ts +15 -3
  105. package/dist/storage/opfs/rpc.js +223 -2
  106. package/dist/storage/opfs/snapshot-ledger.d.ts +40 -0
  107. package/dist/storage/opfs/snapshot-ledger.js +281 -0
  108. package/dist/storage/opfs/store.d.ts +26 -121
  109. package/dist/storage/opfs/store.js +436 -388
  110. package/dist/storage/persistence.d.ts +37 -0
  111. package/dist/storage/persistence.js +78 -0
  112. package/dist/storage/snapshot-stream.d.ts +24 -0
  113. package/dist/storage/snapshot-stream.js +897 -0
  114. package/dist/storage/snapshot.d.ts +9 -99
  115. package/dist/storage/snapshot.js +33 -309
  116. package/dist/storage/toolkit/extents.d.ts +41 -7
  117. package/dist/storage/toolkit/extents.js +402 -39
  118. package/dist/storage/toolkit/index.d.ts +3 -4
  119. package/dist/storage/toolkit/index.js +27 -3
  120. package/dist/storage/toolkit/record-core.d.ts +174 -61
  121. package/dist/storage/toolkit/record-core.js +5401 -849
  122. package/dist/storage/toolkit/sync-file.d.ts +10 -1
  123. package/dist/storage/toolkit/sync-file.js +50 -2
  124. package/dist/storage/toolkit/wal.d.ts +23 -5
  125. package/dist/storage/toolkit/wal.js +89 -27
  126. package/dist/storage/toolkit/wire.d.ts +4 -6
  127. package/dist/storage/toolkit/wire.js +80 -25
  128. package/dist/storage/types.d.ts +1271 -218
  129. package/dist/storage/types.js +1471 -138
  130. package/dist/testing/block-store-conformance.d.ts +0 -1
  131. package/dist/testing/block-store-conformance.js +877 -138
  132. package/dist/testing/index.d.ts +42 -31
  133. package/dist/testing/index.js +100 -68
  134. package/dist/testing/opfs-shim.d.ts +13 -3
  135. package/dist/testing/opfs-shim.js +40 -6
  136. package/dist/testing/simulator.d.ts +0 -1
  137. package/dist/testing/simulator.js +43 -12
  138. package/dist/testing/sqllogictest.d.ts +0 -1
  139. package/dist/testing/sqllogictest.js +0 -1
  140. package/dist/transactions/index.d.ts +81 -20
  141. package/dist/transactions/index.js +1050 -268
  142. package/dist/worker-protocol/index.d.ts +3 -2
  143. package/dist/worker-protocol/index.js +2 -5
  144. package/package.json +49 -1
  145. package/dist/block-format/block.d.ts.map +0 -1
  146. package/dist/block-format/block.js.map +0 -1
  147. package/dist/block-format/checksum.d.ts.map +0 -1
  148. package/dist/block-format/checksum.js.map +0 -1
  149. package/dist/block-format/codecs.d.ts.map +0 -1
  150. package/dist/block-format/codecs.js.map +0 -1
  151. package/dist/block-format/column.d.ts.map +0 -1
  152. package/dist/block-format/column.js.map +0 -1
  153. package/dist/block-format/index.d.ts.map +0 -1
  154. package/dist/block-format/index.js.map +0 -1
  155. package/dist/block-format/physical.d.ts.map +0 -1
  156. package/dist/block-format/physical.js.map +0 -1
  157. package/dist/block-format/types.d.ts.map +0 -1
  158. package/dist/block-format/types.js.map +0 -1
  159. package/dist/engine/artifact-cache.d.ts.map +0 -1
  160. package/dist/engine/artifact-cache.js.map +0 -1
  161. package/dist/engine/batch.d.ts.map +0 -1
  162. package/dist/engine/batch.js.map +0 -1
  163. package/dist/engine/buffered-writer.d.ts.map +0 -1
  164. package/dist/engine/buffered-writer.js.map +0 -1
  165. package/dist/engine/catalog.d.ts.map +0 -1
  166. package/dist/engine/catalog.js.map +0 -1
  167. package/dist/engine/client.d.ts.map +0 -1
  168. package/dist/engine/client.js.map +0 -1
  169. package/dist/engine/coordinator.d.ts +0 -17
  170. package/dist/engine/coordinator.d.ts.map +0 -1
  171. package/dist/engine/coordinator.js +0 -60
  172. package/dist/engine/coordinator.js.map +0 -1
  173. package/dist/engine/database.d.ts.map +0 -1
  174. package/dist/engine/database.js.map +0 -1
  175. package/dist/engine/defaults.d.ts.map +0 -1
  176. package/dist/engine/defaults.js.map +0 -1
  177. package/dist/engine/errors.d.ts.map +0 -1
  178. package/dist/engine/errors.js.map +0 -1
  179. package/dist/engine/fts.d.ts.map +0 -1
  180. package/dist/engine/fts.js.map +0 -1
  181. package/dist/engine/group-index.d.ts.map +0 -1
  182. package/dist/engine/group-index.js.map +0 -1
  183. package/dist/engine/index.d.ts.map +0 -1
  184. package/dist/engine/index.js.map +0 -1
  185. package/dist/engine/join-index.d.ts.map +0 -1
  186. package/dist/engine/join-index.js.map +0 -1
  187. package/dist/engine/live.d.ts.map +0 -1
  188. package/dist/engine/live.js.map +0 -1
  189. package/dist/engine/memory.d.ts.map +0 -1
  190. package/dist/engine/memory.js.map +0 -1
  191. package/dist/engine/optimizer.d.ts.map +0 -1
  192. package/dist/engine/optimizer.js.map +0 -1
  193. package/dist/engine/query-cache.d.ts.map +0 -1
  194. package/dist/engine/query-cache.js.map +0 -1
  195. package/dist/engine/query.d.ts.map +0 -1
  196. package/dist/engine/query.js.map +0 -1
  197. package/dist/engine/result-wire.d.ts.map +0 -1
  198. package/dist/engine/result-wire.js.map +0 -1
  199. package/dist/engine/schema-wire.d.ts.map +0 -1
  200. package/dist/engine/schema-wire.js.map +0 -1
  201. package/dist/engine/schema.d.ts.map +0 -1
  202. package/dist/engine/schema.js.map +0 -1
  203. package/dist/engine/sort-keys.d.ts.map +0 -1
  204. package/dist/engine/sort-keys.js.map +0 -1
  205. package/dist/engine/sql-domains.d.ts.map +0 -1
  206. package/dist/engine/sql-domains.js.map +0 -1
  207. package/dist/engine/sql-driver.d.ts.map +0 -1
  208. package/dist/engine/sql-driver.js.map +0 -1
  209. package/dist/engine/sql-json.d.ts.map +0 -1
  210. package/dist/engine/sql-json.js.map +0 -1
  211. package/dist/engine/sql-semantics.d.ts.map +0 -1
  212. package/dist/engine/sql-semantics.js.map +0 -1
  213. package/dist/engine/vector.d.ts.map +0 -1
  214. package/dist/engine/vector.js.map +0 -1
  215. package/dist/engine/worker-host.d.ts.map +0 -1
  216. package/dist/engine/worker-host.js.map +0 -1
  217. package/dist/engine/worker.d.ts.map +0 -1
  218. package/dist/engine/worker.js.map +0 -1
  219. package/dist/index.d.ts.map +0 -1
  220. package/dist/index.js.map +0 -1
  221. package/dist/plan/index.d.ts.map +0 -1
  222. package/dist/plan/index.js.map +0 -1
  223. package/dist/storage/fixture-shape.d.ts +0 -42
  224. package/dist/storage/fixture-shape.d.ts.map +0 -1
  225. package/dist/storage/fixture-shape.js +0 -146
  226. package/dist/storage/fixture-shape.js.map +0 -1
  227. package/dist/storage/index.d.ts.map +0 -1
  228. package/dist/storage/index.js.map +0 -1
  229. package/dist/storage/indexeddb.d.ts.map +0 -1
  230. package/dist/storage/indexeddb.js.map +0 -1
  231. package/dist/storage/memory.d.ts.map +0 -1
  232. package/dist/storage/memory.js.map +0 -1
  233. package/dist/storage/opfs/files.d.ts.map +0 -1
  234. package/dist/storage/opfs/files.js.map +0 -1
  235. package/dist/storage/opfs/index.d.ts.map +0 -1
  236. package/dist/storage/opfs/index.js.map +0 -1
  237. package/dist/storage/opfs/leader.d.ts.map +0 -1
  238. package/dist/storage/opfs/leader.js.map +0 -1
  239. package/dist/storage/opfs/rpc.d.ts.map +0 -1
  240. package/dist/storage/opfs/rpc.js.map +0 -1
  241. package/dist/storage/opfs/store.d.ts.map +0 -1
  242. package/dist/storage/opfs/store.js.map +0 -1
  243. package/dist/storage/snapshot.d.ts.map +0 -1
  244. package/dist/storage/snapshot.js.map +0 -1
  245. package/dist/storage/toolkit/extents.d.ts.map +0 -1
  246. package/dist/storage/toolkit/extents.js.map +0 -1
  247. package/dist/storage/toolkit/index.d.ts.map +0 -1
  248. package/dist/storage/toolkit/index.js.map +0 -1
  249. package/dist/storage/toolkit/record-core.d.ts.map +0 -1
  250. package/dist/storage/toolkit/record-core.js.map +0 -1
  251. package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
  252. package/dist/storage/toolkit/sync-file.js.map +0 -1
  253. package/dist/storage/toolkit/wal.d.ts.map +0 -1
  254. package/dist/storage/toolkit/wal.js.map +0 -1
  255. package/dist/storage/toolkit/wire.d.ts.map +0 -1
  256. package/dist/storage/toolkit/wire.js.map +0 -1
  257. package/dist/storage/types.d.ts.map +0 -1
  258. package/dist/storage/types.js.map +0 -1
  259. package/dist/testing/block-store-conformance.d.ts.map +0 -1
  260. package/dist/testing/block-store-conformance.js.map +0 -1
  261. package/dist/testing/index.d.ts.map +0 -1
  262. package/dist/testing/index.js.map +0 -1
  263. package/dist/testing/opfs-shim.d.ts.map +0 -1
  264. package/dist/testing/opfs-shim.js.map +0 -1
  265. package/dist/testing/seeds.d.ts +0 -11
  266. package/dist/testing/seeds.d.ts.map +0 -1
  267. package/dist/testing/seeds.js +0 -50
  268. package/dist/testing/seeds.js.map +0 -1
  269. package/dist/testing/simulator.d.ts.map +0 -1
  270. package/dist/testing/simulator.js.map +0 -1
  271. package/dist/testing/sqllogictest.d.ts.map +0 -1
  272. package/dist/testing/sqllogictest.js.map +0 -1
  273. package/dist/transactions/index.d.ts.map +0 -1
  274. package/dist/transactions/index.js.map +0 -1
  275. package/dist/worker-protocol/index.d.ts.map +0 -1
  276. package/dist/worker-protocol/index.js.map +0 -1
@@ -1,122 +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
- /** Complete membership for every durable UNIQUE secondary index. */
22
- secondaryUniqueKeys?: Array<{
23
- indexId: string;
24
- keyTokens: string[];
25
- }>;
26
- fts: SnapshotFtsIndex[];
27
- }
28
- export interface DatabaseSnapshot {
29
- /** The manifest version this snapshot captured, which the restored database keeps. */
30
- version: number;
31
- createdAt: string;
32
- tables: SnapshotTable[];
33
- segments: SegmentRecord[];
34
- /** Committed transactions only; segment visibility resolves through `committedVersion`. */
35
- transactions: TransactionRecord[];
36
- blocks: Array<{
37
- id: string;
38
- bytes: Uint8Array;
39
- }>;
40
- }
41
- /** Validates catalog identities and complete uniqueness membership before a snapshot is loaded. */
42
- export declare function validateSnapshotCatalog(tables: ReadonlyArray<{
43
- record: TableRecord;
44
- uniqueKeyTokens: readonly string[];
45
- secondaryUniqueKeys?: ReadonlyArray<{
46
- indexId: string;
47
- keyTokens: readonly string[];
48
- }>;
49
- }>): void;
50
- /** 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;
51
6
  export interface SnapshotLoadProgress {
52
7
  phase: "blocks" | "catalog" | "done";
53
8
  writtenBytes: number;
54
9
  totalBytes: number;
55
10
  }
56
- /**
57
- * Where a snapshot export has got to. `reading` covers copying the version out of the store and
58
- * encoding it, and reports no byte counts because the encoded length is not known until it is
59
- * finished. `transfer` happens only when the database lives in a worker and the bytes are handed
60
- * back a chunk at a time; an in-page database goes straight from `reading` to `done`.
61
- */
62
11
  export interface SnapshotExportProgress {
63
12
  phase: "reading" | "transfer" | "done";
64
13
  transferredBytes: number;
65
- /** 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. */
66
15
  totalBytes: number;
67
16
  }
68
- /** What a header-only read reports, without touching a single block payload. */
17
+ /** Header summary used by restore confirmation UIs without retaining database metadata. */
69
18
  export interface SnapshotSummary {
70
19
  formatVersion: number;
71
20
  version: number;
72
21
  createdAt: string;
73
22
  tableCount: number;
74
23
  blockCount: number;
75
- /** Total byte length of the block payloads, excluding the header. */
24
+ /** Stored body payload bytes, excluding frame envelopes, header, and footer. */
76
25
  payloadBytes: number;
77
26
  byteLength: number;
78
27
  }
79
28
  /**
80
- * Serializes a snapshot. Block bytes are copied verbatim they are already compressed and
81
- * checksummed, so the container never re-encodes or re-compresses them. The header is gzipped,
82
- * because it is JSON and its bulk is full-text posting lists and segment records, which compress
83
- * by roughly four to one.
84
- */
85
- export declare function encodeSnapshot(snapshot: DatabaseSnapshot): Promise<Uint8Array>;
86
- /**
87
- * Reads only the header. Cheap enough to run against a multi-hundred-megabyte file before
88
- * 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.
89
31
  */
90
32
  export declare function readSnapshotSummary(bytes: Uint8Array): Promise<SnapshotSummary>;
91
- /**
92
- * Parses a snapshot and authenticates every block. `inspectBlock` verifies each payload's magic,
93
- * version, declared lengths, and envelope checksum without decompressing it, so a corrupt file
94
- * fails here rather than mid-query.
95
- */
96
- export declare function decodeSnapshot(bytes: Uint8Array): Promise<DatabaseSnapshot>;
97
- /**
98
- * Shared by both stores' exporters: given the manifest's live block set, decide which segments
99
- * and transactions the snapshot needs.
100
- *
101
- * A segment is live when every block it references survives in the manifest — compaction
102
- * supersedes a segment by dropping its blocks, so that test is exactly the reader's. A
103
- * transaction is needed when a live segment names it, and only committed transactions qualify:
104
- * an active one's artifacts are unpublished and would dangle. Their pending lists are cleared,
105
- * because after the restore the manifest is the only root anything needs.
106
- *
107
- * `committedVersion` is preserved exactly, and the restored database keeps the captured version
108
- * number rather than renumbering to 1. Segments sort by `logicalOrder ?? committedVersion`, so
109
- * flattening those versions would let an update fold ahead of the insert it patches; and change
110
- * detection compares committed versions against a snapshot version, which would misread any
111
- * transaction numbered above the manifest.
112
- */
113
- export declare function selectLiveRecords(input: {
114
- liveBlockIds: ReadonlySet<string>;
115
- segments: readonly SegmentRecord[];
116
- transactions: readonly TransactionRecord[];
117
- version: number;
118
- }): {
119
- segments: SegmentRecord[];
120
- transactions: TransactionRecord[];
121
- };
122
- //# sourceMappingURL=snapshot.d.ts.map
@@ -1,320 +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
- import { validateSecondaryIndexes, validateTableColumns } from "./types.js";
20
- /** ASCII `MNWSNAP1`, matching the block format's habit of a readable magic. */
21
- const MAGIC = Uint8Array.from([0x4d, 0x4e, 0x57, 0x53, 0x4e, 0x41, 0x50, 0x31]);
22
- /**
23
- * magic(8) + formatVersion(4) + storedHeaderLength(4) + headerChecksum(4) + headerLength(4)
24
- * + headerCodec(4). All little-endian, matching the block format.
25
- */
26
- const PREFIX_LENGTH = 28;
27
- const HEADER_CODEC_NONE = 0;
28
- 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";
29
5
  export const SNAPSHOT_FORMAT_VERSION = 1;
30
- /** Validates catalog identities and complete uniqueness membership before a snapshot is loaded. */
31
- export function validateSnapshotCatalog(tables) {
32
- const indexNames = new Set();
33
- for (const table of tables) {
34
- validateTableColumns(table.record.columns);
35
- validateSecondaryIndexes(table.record);
36
- if (new Set(table.uniqueKeyTokens).size !== table.uniqueKeyTokens.length) {
37
- throw new Error(`Snapshot repeats a table unique key: ${table.record.name}`);
38
- }
39
- if (table.record.uniqueKeyColumnId === undefined && table.uniqueKeyTokens.length > 0) {
40
- throw new Error(`Snapshot has orphaned table unique keys: ${table.record.name}`);
41
- }
42
- const memberships = new Set();
43
- for (const entry of table.secondaryUniqueKeys ?? []) {
44
- if (memberships.has(entry.indexId)) {
45
- throw new Error(`Snapshot repeats UNIQUE-index membership: ${entry.indexId}`);
46
- }
47
- memberships.add(entry.indexId);
48
- const index = table.record.secondaryIndexes?.[entry.indexId];
49
- if (index?.unique !== true || index.uniqueEnforced !== true) {
50
- throw new Error(`Snapshot has orphaned UNIQUE-index membership: ${entry.indexId}`);
51
- }
52
- if (new Set(entry.keyTokens).size !== entry.keyTokens.length) {
53
- throw new Error(`Snapshot repeats a UNIQUE-index key: ${index.name}`);
54
- }
55
- }
56
- for (const [indexId, index] of Object.entries(table.record.secondaryIndexes ?? {})) {
57
- if (indexNames.has(index.name))
58
- throw new TypeError(`Index already exists: ${index.name}`);
59
- indexNames.add(index.name);
60
- if (index.uniqueEnforced === true && !memberships.has(indexId)) {
61
- throw new Error(`Snapshot is missing UNIQUE-index membership: ${index.name}`);
62
- }
63
- }
64
- }
65
- }
66
- function encodeSegment(segment) {
67
- const { rowIdStart, rowIdEndExclusive, rowIdSpans, ...rest } = segment;
68
- return {
69
- ...rest,
70
- rowIdStart: rowIdStart.toString(),
71
- rowIdEndExclusive: rowIdEndExclusive.toString(),
72
- ...(rowIdSpans === undefined
73
- ? {}
74
- : {
75
- rowIdSpans: rowIdSpans.map((span) => ({
76
- rowStart: span.rowStart,
77
- rowCount: span.rowCount,
78
- rowIdStart: span.rowIdStart.toString(),
79
- })),
80
- }),
81
- };
82
- }
83
- function decodeSegment(segment) {
84
- const { rowIdStart, rowIdEndExclusive, rowIdSpans, ...rest } = segment;
85
- const spans = rowIdSpans?.map((span) => ({
86
- rowStart: span.rowStart,
87
- rowCount: span.rowCount,
88
- rowIdStart: parseBigInt(span.rowIdStart, "segment row-ID span"),
89
- }));
90
- return {
91
- ...rest,
92
- rowIdStart: parseBigInt(rowIdStart, "segment row-ID start"),
93
- rowIdEndExclusive: parseBigInt(rowIdEndExclusive, "segment row-ID end"),
94
- ...(spans === undefined ? {} : { rowIdSpans: spans }),
95
- };
96
- }
97
- function encodePosting(posting) {
98
- const [first, ...rest] = posting.rowIds;
99
- if (first === undefined)
100
- return { term: posting.term, first: "0", deltas: [], tf: posting.tf };
101
- const deltas = [];
102
- let previous = first;
103
- for (const rowId of rest) {
104
- const gap = rowId - previous;
105
- if (gap > BigInt(Number.MAX_SAFE_INTEGER)) {
106
- throw new RangeError("Full-text row-ID gap is too large to snapshot");
107
- }
108
- deltas.push(Number(gap));
109
- previous = rowId;
110
- }
111
- return { term: posting.term, first: first.toString(), deltas, tf: posting.tf };
112
- }
113
- function decodePosting(posting) {
114
- const rowIds = [];
115
- if (posting.tf.length > 0) {
116
- let current = parseBigInt(posting.first, "full-text row ID");
117
- rowIds.push(current);
118
- for (const delta of posting.deltas) {
119
- current += BigInt(delta);
120
- rowIds.push(current);
121
- }
122
- }
123
- return { term: posting.term, rowIds, tf: posting.tf };
124
- }
125
- function parseBigInt(value, context) {
126
- if (!/^-?\d+$/.test(value))
127
- throw new Error(`Snapshot has an invalid ${context}: ${value}`);
128
- return BigInt(value);
129
- }
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;
130
8
  /**
131
- * Serializes a snapshot. Block bytes are copied verbatim they are already compressed and
132
- * checksummed, so the container never re-encodes or re-compresses them. The header is gzipped,
133
- * because it is JSON and its bulk is full-text posting lists and segment records, which compress
134
- * by roughly four to one.
135
- */
136
- export async function encodeSnapshot(snapshot) {
137
- const header = {
138
- formatVersion: SNAPSHOT_FORMAT_VERSION,
139
- version: snapshot.version,
140
- createdAt: snapshot.createdAt,
141
- blocks: snapshot.blocks.map((block) => ({ id: block.id, length: block.bytes.byteLength })),
142
- tables: snapshot.tables.map((table) => ({
143
- record: table.record,
144
- nextRowId: table.nextRowId.toString(),
145
- autoIncrement: table.autoIncrement.map((entry) => ({
146
- columnId: entry.columnId,
147
- next: entry.next.toString(),
148
- })),
149
- uniqueKeyTokens: table.uniqueKeyTokens,
150
- ...(table.secondaryUniqueKeys === undefined
151
- ? {}
152
- : { secondaryUniqueKeys: table.secondaryUniqueKeys }),
153
- fts: table.fts.map((index) => ({
154
- columnId: index.columnId,
155
- coversVersion: index.coversVersion,
156
- totalTokens: index.totalTokens,
157
- chunks: index.chunks.map((chunk) => chunk.map(encodePosting)),
158
- })),
159
- })),
160
- segments: snapshot.segments.map(encodeSegment),
161
- transactions: snapshot.transactions,
162
- };
163
- const json = new TextEncoder().encode(JSON.stringify(header));
164
- const stored = await gzipCodec.compress(json);
165
- const payloadBytes = snapshot.blocks.reduce((total, block) => total + block.bytes.byteLength, 0);
166
- const bytes = new Uint8Array(PREFIX_LENGTH + stored.byteLength + payloadBytes);
167
- bytes.set(MAGIC, 0);
168
- const view = new DataView(bytes.buffer);
169
- view.setUint32(8, SNAPSHOT_FORMAT_VERSION, true);
170
- view.setUint32(12, stored.byteLength, true);
171
- view.setUint32(16, crc32(stored), true);
172
- view.setUint32(20, json.byteLength, true);
173
- view.setUint32(24, HEADER_CODEC_GZIP, true);
174
- bytes.set(stored, PREFIX_LENGTH);
175
- let offset = PREFIX_LENGTH + stored.byteLength;
176
- for (const block of snapshot.blocks) {
177
- bytes.set(block.bytes, offset);
178
- offset += block.bytes.byteLength;
179
- }
180
- return bytes;
181
- }
182
- async function readHeader(bytes) {
183
- if (bytes.byteLength < PREFIX_LENGTH)
184
- throw new Error("Truncated snapshot header");
185
- if (!MAGIC.every((byte, index) => bytes[index] === byte)) {
186
- throw new Error("Not a Minnow snapshot");
187
- }
188
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
189
- const formatVersion = view.getUint32(8, true);
190
- if (formatVersion !== SNAPSHOT_FORMAT_VERSION) {
191
- throw new Error(`Unsupported snapshot version ${String(formatVersion)}`);
192
- }
193
- const storedLength = view.getUint32(12, true);
194
- const payloadOffset = PREFIX_LENGTH + storedLength;
195
- if (payloadOffset > bytes.byteLength)
196
- throw new Error("Truncated snapshot header");
197
- const stored = bytes.subarray(PREFIX_LENGTH, payloadOffset);
198
- // Checked before the header is decompressed, let alone parsed: a damaged header must fail
199
- // as a checksum mismatch rather than as whatever gzip or JSON happens to say about it.
200
- if (crc32(stored) !== view.getUint32(16, true)) {
201
- throw new Error("Snapshot header checksum mismatch");
202
- }
203
- const codec = view.getUint32(24, true);
204
- if (codec !== HEADER_CODEC_NONE && codec !== HEADER_CODEC_GZIP) {
205
- throw new Error(`Unsupported snapshot header codec ${String(codec)}`);
206
- }
207
- const json = codec === HEADER_CODEC_GZIP
208
- ? await gzipCodec.decompress(stored, view.getUint32(20, true))
209
- : stored;
210
- const header = JSON.parse(new TextDecoder().decode(json));
211
- return { header, payloadOffset };
212
- }
213
- /**
214
- * Reads only the header. Cheap enough to run against a multi-hundred-megabyte file before
215
- * 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.
216
11
  */
217
12
  export async function readSnapshotSummary(bytes) {
218
- const { header, payloadOffset } = await readHeader(bytes);
219
- return {
220
- formatVersion: header.formatVersion,
221
- version: header.version,
222
- createdAt: header.createdAt,
223
- tableCount: header.tables.length,
224
- blockCount: header.blocks.length,
225
- payloadBytes: bytes.byteLength - payloadOffset,
226
- byteLength: bytes.byteLength,
227
- };
228
- }
229
- /**
230
- * Parses a snapshot and authenticates every block. `inspectBlock` verifies each payload's magic,
231
- * version, declared lengths, and envelope checksum without decompressing it, so a corrupt file
232
- * fails here rather than mid-query.
233
- */
234
- export async function decodeSnapshot(bytes) {
235
- const { header, payloadOffset } = await readHeader(bytes);
236
- const declared = header.blocks.reduce((total, block) => total + block.length, 0);
237
- if (payloadOffset + declared !== bytes.byteLength) {
238
- throw new Error("Snapshot length mismatch");
13
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength === 0) {
14
+ throw new TypeError("Snapshot bytes must be a non-empty Uint8Array");
239
15
  }
240
- const blocks = [];
241
- let offset = payloadOffset;
242
- for (const block of header.blocks) {
243
- const payload = bytes.subarray(offset, offset + block.length);
244
- inspectBlock(payload);
245
- blocks.push({ id: block.id, bytes: payload });
246
- offset += block.length;
247
- }
248
- const present = new Set(blocks.map((block) => block.id));
249
- validateSnapshotCatalog(header.tables);
250
- const segments = header.segments.map(decodeSegment);
251
- for (const segment of segments) {
252
- for (const ids of Object.values(segment.columnBlockIds)) {
253
- for (const id of ids) {
254
- if (!present.has(id))
255
- throw new Error(`Snapshot segment references a missing block: ${id}`);
256
- }
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);
257
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;
258
34
  }
259
35
  return {
260
- version: header.version,
36
+ formatVersion: header.formatVersion,
37
+ version: header.databaseVersion,
261
38
  createdAt: header.createdAt,
262
- tables: header.tables.map((table) => ({
263
- record: table.record,
264
- nextRowId: parseBigInt(table.nextRowId, "row-ID counter"),
265
- autoIncrement: table.autoIncrement.map((entry) => ({
266
- columnId: entry.columnId,
267
- next: parseBigInt(entry.next, "auto-increment counter"),
268
- })),
269
- uniqueKeyTokens: table.uniqueKeyTokens,
270
- ...(table.secondaryUniqueKeys === undefined
271
- ? {}
272
- : { secondaryUniqueKeys: table.secondaryUniqueKeys }),
273
- fts: table.fts.map((index) => ({
274
- columnId: index.columnId,
275
- coversVersion: index.coversVersion,
276
- totalTokens: index.totalTokens,
277
- chunks: index.chunks.map((chunk) => chunk.map(decodePosting)),
278
- })),
279
- })),
280
- segments,
281
- transactions: header.transactions,
282
- blocks,
39
+ tableCount: header.kinds["catalog-page"].itemCount,
40
+ blockCount: header.kinds.block.itemCount,
41
+ payloadBytes,
42
+ byteLength,
283
43
  };
284
44
  }
285
- /**
286
- * Shared by both stores' exporters: given the manifest's live block set, decide which segments
287
- * and transactions the snapshot needs.
288
- *
289
- * A segment is live when every block it references survives in the manifest — compaction
290
- * supersedes a segment by dropping its blocks, so that test is exactly the reader's. A
291
- * transaction is needed when a live segment names it, and only committed transactions qualify:
292
- * an active one's artifacts are unpublished and would dangle. Their pending lists are cleared,
293
- * because after the restore the manifest is the only root anything needs.
294
- *
295
- * `committedVersion` is preserved exactly, and the restored database keeps the captured version
296
- * number rather than renumbering to 1. Segments sort by `logicalOrder ?? committedVersion`, so
297
- * flattening those versions would let an update fold ahead of the insert it patches; and change
298
- * detection compares committed versions against a snapshot version, which would misread any
299
- * transaction numbered above the manifest.
300
- */
301
- export function selectLiveRecords(input) {
302
- const committed = new Map(input.transactions
303
- .filter((record) => record.status === "committed" &&
304
- record.committedVersion !== null &&
305
- record.committedVersion <= input.version)
306
- .map((record) => [record.id, record]));
307
- const segments = input.segments.filter((segment) => {
308
- if (!committed.has(segment.transactionId))
309
- return false;
310
- return Object.values(segment.columnBlockIds)
311
- .flat()
312
- .every((id) => input.liveBlockIds.has(id));
313
- });
314
- const usedTransactionIds = new Set(segments.map((segment) => segment.transactionId));
315
- const transactions = [...committed.values()]
316
- .filter((record) => usedTransactionIds.has(record.id))
317
- .map((record) => ({ ...record, pendingBlockIds: [], pendingSegmentIds: [] }));
318
- return { segments, transactions };
319
- }
320
- //# 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;