@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
package/README.md CHANGED
@@ -11,11 +11,13 @@ npm install @minnowdb/core
11
11
  - Joins, CTEs, window functions, grouping sets, upserts, `RETURNING`, triggers, exact decimals,
12
12
  JSON/JSONB, arrays, enums, sequences, and savepoints.
13
13
  - Compressed column storage, secondary indexes, full-text search, and snapshot reads.
14
- - Atomic writes across tabs through IndexedDB or OPFS.
14
+ - Atomic writes across tabs through IndexedDB or OPFS, strict durability by default, and explicit
15
+ origin-eviction persistence policy.
15
16
  - A ready-made worker client with the same everyday database API.
16
17
  - TypeScript schema declarations and metadata-only migrations, including SQL domains and
17
18
  composite primary/foreign keys.
18
- - Batch writes, live queries, snapshots, compaction, and configurable query memory.
19
+ - Batch writes, pull-driven query cursors, live queries, snapshots, compaction, and configurable
20
+ query memory.
19
21
 
20
22
  Use [the PostgreSQL compatibility page](https://minnowdb.com/docs/sql/feature-matrix/) for the
21
23
  exact SQL surface. Use [the documentation](https://minnowdb.com/docs/) for installation, storage,
@@ -1,6 +1,6 @@
1
1
  import type { BlockDescription, BlockMetadata, ColumnInput, Compression, DecodedBlock, DecodedPhysicalBlock, PhysicalColumnPayload } from "./types.js";
2
2
  /**
3
- * Version 1 layout (all integers little-endian):
3
+ * Version 2 layout (all integers little-endian):
4
4
  *
5
5
  * 0 magic "BRDB"
6
6
  * 4 envelope checksum: crc32 over bytes [8, headerLength + metadataLength)
@@ -12,35 +12,54 @@ import type { BlockDescription, BlockMetadata, ColumnInput, Compression, Decoded
12
12
  * 24 metadata length (u32)
13
13
  * 28 encoded length (u32)
14
14
  * 32 stored length (u32)
15
- * 36 payload checksum: crc32 over the uncompressed encoded payload
15
+ * 36 logical checksum: crc32 over the uncompressed encoded payload
16
+ * 40 stored checksum: crc32 over the stored payload, before decompression
16
17
  *
17
- * The envelope checksum authenticates every header field and the metadata JSON, so header-only
18
+ * The envelope checksum integrity-checks every header field and the metadata JSON, so header-only
18
19
  * reads (zone-map pruning, block inventories) can trust derived statistics without paying the
19
20
  * decompress-and-revalidate cost of the payload.
20
21
  */
21
22
  /**
22
- * The block envelope version. Every persisted block carries it, and `readBlock` refuses any
23
- * value it does not recognize -- so this number is a compatibility contract with every database
24
- * already sitting in a user's browser, not an internal detail.
23
+ * The first frozen block envelope version. Every persisted block carries it, and readers dispatch
24
+ * by this value, so the number is a compatibility contract rather than an internal detail.
25
25
  *
26
- * Changing it means older blocks stop being readable unless a read path for them is kept. Before
27
- * changing it, run `npm run fixture:format` on the *current* build to freeze what this version
28
- * writes; format-compatibility.test.ts then holds the new build to still reading it.
26
+ * Never change v2 decoding in place. A future writer gets a new number and keeps the v2 read path;
27
+ * format-vectors.test.ts permanently freezes Minnow-owned v2 bytes.
29
28
  */
30
- export declare const BLOCK_FORMAT_VERSION = 1;
29
+ export declare const BLOCK_FORMAT_VERSION = 2;
30
+ export declare const BLOCK_HEADER_LENGTH = 44;
31
+ export declare const MAX_BLOCK_METADATA_BYTE_LENGTH = 1024;
32
+ /** Absolute persisted value ceiling: envelope + metadata + maximum stored payload. */
33
+ export declare const MAX_STORED_BLOCK_BYTE_LENGTH: number;
34
+ /** A codec expanded an otherwise valid physical column beyond the stored block hard limit. */
35
+ export declare class StoredBlockPayloadTooLargeError extends RangeError {
36
+ readonly byteLength: number;
37
+ constructor(byteLength: number);
38
+ }
39
+ /** Validates a codec result length without allocating the payload. */
40
+ export declare function assertStoredBlockPayloadByteLength(byteLength: number): void;
31
41
  /**
32
42
  * Returns a conservative bound for the complete persisted block value: the fixed header, the
33
43
  * exact UTF-8 JSON metadata, and the selected codec's maximum stored payload.
34
44
  */
35
45
  export declare function maximumPhysicalBlockByteLength(encodedByteLength: number, metadata: BlockMetadata, compression: Compression): number;
36
46
  export declare function encodeBlock(input: ColumnInput, compression?: Compression): Promise<Uint8Array>;
37
- /** Encodes physical column bytes after validating their complete version-zero layout. */
47
+ /** Encodes physical column bytes after validating their complete canonical v2 layout. */
38
48
  export declare function encodePhysicalBlock(input: PhysicalColumnPayload, compression?: Compression): Promise<Uint8Array>;
39
49
  export declare function decodeBlock(bytes: Uint8Array): Promise<DecodedBlock>;
40
50
  /**
41
51
  * Decompresses and checksum-verifies a block without materializing row values.
42
- * The returned physical byte array is owned by the result.
52
+ *
53
+ * For a raw block, `column.bytes` is a zero-copy view into `bytes`. Both views must be treated as
54
+ * immutable for the lifetime of the result. Gzip results own a new decompressed allocation.
43
55
  */
44
56
  export declare function decodePhysicalBlock(bytes: Uint8Array): Promise<DecodedPhysicalBlock>;
57
+ /**
58
+ * Verifies every stored byte without decompressing or copying the payload.
59
+ *
60
+ * Snapshot import and OPFS recovery use this stronger operation before accepting a block.
61
+ * Planning and pruning should continue to use `inspectBlock`, which deliberately reads only the
62
+ * fixed header and metadata envelope.
63
+ */
64
+ export declare function verifyStoredBlock(bytes: Uint8Array): BlockDescription;
45
65
  export declare function inspectBlock(bytes: Uint8Array): BlockDescription;
46
- //# sourceMappingURL=block.d.ts.map
@@ -1,10 +1,10 @@
1
1
  import { crc32 } from "./checksum.js";
2
- import { decodeColumn, encodeColumn } from "./column.js";
3
- import { getCodec, getCompressionMemoryBound } from "./codecs.js";
4
- import { MAX_PHYSICAL_COLUMN_BYTE_LENGTH, validatePhysicalColumn } from "./physical.js";
2
+ import { decodeValidatedColumn, encodeColumn } from "./column.js";
3
+ import { CompressionOutputLimitError, getCodec, getCompressionMemoryBound } from "./codecs.js";
4
+ import { assertBlockRowCount, MAX_PHYSICAL_COLUMN_BYTE_LENGTH, validatePhysicalColumn, } from "./physical.js";
5
5
  const MAGIC = Uint8Array.of(0x42, 0x52, 0x44, 0x42);
6
6
  /**
7
- * Version 1 layout (all integers little-endian):
7
+ * Version 2 layout (all integers little-endian):
8
8
  *
9
9
  * 0 magic "BRDB"
10
10
  * 4 envelope checksum: crc32 over bytes [8, headerLength + metadataLength)
@@ -16,53 +16,64 @@ const MAGIC = Uint8Array.of(0x42, 0x52, 0x44, 0x42);
16
16
  * 24 metadata length (u32)
17
17
  * 28 encoded length (u32)
18
18
  * 32 stored length (u32)
19
- * 36 payload checksum: crc32 over the uncompressed encoded payload
19
+ * 36 logical checksum: crc32 over the uncompressed encoded payload
20
+ * 40 stored checksum: crc32 over the stored payload, before decompression
20
21
  *
21
- * The envelope checksum authenticates every header field and the metadata JSON, so header-only
22
+ * The envelope checksum integrity-checks every header field and the metadata JSON, so header-only
22
23
  * reads (zone-map pruning, block inventories) can trust derived statistics without paying the
23
24
  * decompress-and-revalidate cost of the payload.
24
25
  */
25
26
  /**
26
- * The block envelope version. Every persisted block carries it, and `readBlock` refuses any
27
- * value it does not recognize -- so this number is a compatibility contract with every database
28
- * already sitting in a user's browser, not an internal detail.
27
+ * The first frozen block envelope version. Every persisted block carries it, and readers dispatch
28
+ * by this value, so the number is a compatibility contract rather than an internal detail.
29
29
  *
30
- * Changing it means older blocks stop being readable unless a read path for them is kept. Before
31
- * changing it, run `npm run fixture:format` on the *current* build to freeze what this version
32
- * writes; format-compatibility.test.ts then holds the new build to still reading it.
30
+ * Never change v2 decoding in place. A future writer gets a new number and keeps the v2 read path;
31
+ * format-vectors.test.ts permanently freezes Minnow-owned v2 bytes.
33
32
  */
34
- export const BLOCK_FORMAT_VERSION = 1;
33
+ export const BLOCK_FORMAT_VERSION = 2;
35
34
  const VERSION = BLOCK_FORMAT_VERSION;
36
- const HEADER_LENGTH = 40;
35
+ export const BLOCK_HEADER_LENGTH = 44;
36
+ const HEADER_LENGTH = BLOCK_HEADER_LENGTH;
37
37
  const ENVELOPE_CHECKSUM_START = 8;
38
38
  const MAX_BLOCK_LENGTH = MAX_PHYSICAL_COLUMN_BYTE_LENGTH;
39
+ export const MAX_BLOCK_METADATA_BYTE_LENGTH = 1_024;
40
+ const MAX_METADATA_LENGTH = MAX_BLOCK_METADATA_BYTE_LENGTH;
41
+ /** Absolute persisted value ceiling: envelope + metadata + maximum stored payload. */
42
+ export const MAX_STORED_BLOCK_BYTE_LENGTH = BLOCK_HEADER_LENGTH + MAX_METADATA_LENGTH + MAX_PHYSICAL_COLUMN_BYTE_LENGTH;
39
43
  const textEncoder = new TextEncoder();
40
44
  const textDecoder = new TextDecoder("utf-8", { fatal: true });
41
45
  const typeIds = { boolean: 1, number: 2, string: 3, datetime: 4 };
42
46
  const typesById = new Map(Object.entries(typeIds).map(([type, id]) => [id, type]));
43
- // Codec 1 was run-length encoding, removed because it was both slower and larger than raw on
44
- // every column shape measured. The ids of the survivors do not move: renumbering would make an
45
- // old block decode as the wrong codec instead of failing.
47
+ // Codec 1 was a pre-freeze run-length prototype, removed because it was both slower and larger
48
+ // than raw on every measured column shape. Keep the id permanently reserved so it can never be
49
+ // confused with a different codec in diagnostic or development data.
46
50
  const codecIds = { raw: 0, gzip: 2 };
47
51
  const codecsById = new Map(Object.entries(codecIds).map(([codec, id]) => [id, codec]));
52
+ /** A codec expanded an otherwise valid physical column beyond the stored block hard limit. */
53
+ export class StoredBlockPayloadTooLargeError extends RangeError {
54
+ byteLength;
55
+ constructor(byteLength) {
56
+ super("Stored block payload exceeds maximum byte length");
57
+ this.byteLength = byteLength;
58
+ this.name = "StoredBlockPayloadTooLargeError";
59
+ }
60
+ }
61
+ /** Validates a codec result length without allocating the payload. */
62
+ export function assertStoredBlockPayloadByteLength(byteLength) {
63
+ if (!Number.isSafeInteger(byteLength) || byteLength < 0) {
64
+ throw new RangeError("Invalid stored payload length");
65
+ }
66
+ if (byteLength > MAX_BLOCK_LENGTH)
67
+ throw new StoredBlockPayloadTooLargeError(byteLength);
68
+ }
48
69
  /**
49
70
  * Returns a conservative bound for the complete persisted block value: the fixed header, the
50
71
  * exact UTF-8 JSON metadata, and the selected codec's maximum stored payload.
51
72
  */
52
73
  export function maximumPhysicalBlockByteLength(encodedByteLength, metadata, compression) {
53
74
  assertLength(encodedByteLength, "encoded payload");
54
- let serializedMetadata;
55
- try {
56
- serializedMetadata = JSON.stringify(metadata);
57
- }
58
- catch {
59
- throw new TypeError("Block metadata must be JSON serializable");
60
- }
61
- if (typeof serializedMetadata !== "string") {
62
- throw new TypeError("Block metadata must be JSON serializable");
63
- }
64
- const metadataByteLength = textEncoder.encode(serializedMetadata).byteLength;
65
- assertLength(metadataByteLength, "metadata");
75
+ const metadataByteLength = encodeCanonicalMetadata(metadata).byteLength;
76
+ assertMetadataLength(metadataByteLength);
66
77
  const maximumStoredPayloadBytes = getCompressionMemoryBound(compression, encodedByteLength).maximumOutputBytes;
67
78
  const total = HEADER_LENGTH + metadataByteLength + maximumStoredPayloadBytes;
68
79
  if (!Number.isSafeInteger(total)) {
@@ -71,8 +82,7 @@ export function maximumPhysicalBlockByteLength(encodedByteLength, metadata, comp
71
82
  return total;
72
83
  }
73
84
  export async function encodeBlock(input, compression = "raw") {
74
- if (input.values.length > 0xffffffff)
75
- throw new RangeError("Too many rows in one block");
85
+ assertBlockRowCount(input.values.length);
76
86
  const encoded = encodeColumn(input);
77
87
  return encodeValidatedPhysicalBlock({
78
88
  type: input.type,
@@ -82,16 +92,39 @@ export async function encodeBlock(input, compression = "raw") {
82
92
  metadata: encoded.metadata,
83
93
  }, compression);
84
94
  }
85
- /** Encodes physical column bytes after validating their complete version-zero layout. */
95
+ /** Encodes physical column bytes after validating their complete canonical v2 layout. */
86
96
  export async function encodePhysicalBlock(input, compression = "raw") {
87
97
  return encodeValidatedPhysicalBlock(validatePhysicalColumn(input), compression);
88
98
  }
89
99
  async function encodeValidatedPhysicalBlock(input, compression) {
100
+ // validatePhysicalColumn constructs this exact closed metadata shape. Avoid serializing,
101
+ // parsing, and serializing it again on every write; the public sizing helper validates its
102
+ // independently supplied metadata because it has no such provenance.
90
103
  const metadata = textEncoder.encode(JSON.stringify(input.metadata));
91
- const stored = await getCodec(compression).compress(input.bytes);
92
- assertLength(metadata.byteLength, "metadata");
104
+ // Checksum before the first possible async yield. The gzip codec also takes its input copy
105
+ // synchronously when called, so caller mutation after encodePhysicalBlock returns cannot make
106
+ // the checksum and compressed bytes observe different physical payloads.
107
+ const logicalChecksum = crc32(input.bytes);
108
+ // Avoid an async yield on raw blocks: validation and the copy into the immutable envelope
109
+ // complete before a caller can mutate a physical input buffer. The gzip codec takes its own
110
+ // input copy before yielding to the native stream.
111
+ let stored;
112
+ try {
113
+ stored =
114
+ compression === "raw"
115
+ ? input.bytes
116
+ : await getCodec(compression).compress(input.bytes, MAX_BLOCK_LENGTH);
117
+ }
118
+ catch (error) {
119
+ if (error instanceof CompressionOutputLimitError) {
120
+ throw new StoredBlockPayloadTooLargeError(error.byteLength);
121
+ }
122
+ throw error;
123
+ }
124
+ assertMetadataLength(metadata.byteLength);
93
125
  assertLength(input.bytes.byteLength, "encoded payload");
94
- assertLength(stored.byteLength, "stored payload");
126
+ assertStoredBlockPayloadByteLength(stored.byteLength);
127
+ const storedChecksum = compression === "raw" ? logicalChecksum : crc32(stored);
95
128
  const output = new Uint8Array(HEADER_LENGTH + metadata.byteLength + stored.byteLength);
96
129
  output.set(MAGIC);
97
130
  const view = new DataView(output.buffer);
@@ -106,30 +139,51 @@ async function encodeValidatedPhysicalBlock(input, compression) {
106
139
  view.setUint32(24, metadata.byteLength, true);
107
140
  view.setUint32(28, input.bytes.byteLength, true);
108
141
  view.setUint32(32, stored.byteLength, true);
109
- view.setUint32(36, crc32(input.bytes), true);
142
+ view.setUint32(36, logicalChecksum, true);
143
+ view.setUint32(40, storedChecksum, true);
110
144
  output.set(metadata, HEADER_LENGTH);
111
145
  output.set(stored, HEADER_LENGTH + metadata.byteLength);
112
146
  view.setUint32(4, crc32(output.subarray(ENVELOPE_CHECKSUM_START, HEADER_LENGTH + metadata.byteLength)), true);
113
147
  return output;
114
148
  }
115
149
  export async function decodeBlock(bytes) {
116
- const decoded = await decodePhysicalBlock(bytes);
150
+ // Materialize raw values before the first async yield. Otherwise a caller could mutate the
151
+ // input after its checksum passes but before decodeColumn reads it. The gzip codec takes an
152
+ // input copy synchronously, so its awaited path has no equivalent gap.
153
+ const description = verifyStoredBlock(bytes);
154
+ const decoded = description.compression === "raw"
155
+ ? validateDecodedPhysicalBlock(bytes.subarray(description.headerLength + description.metadataLength), description)
156
+ : await decodeVerifiedPhysicalBlock(bytes, description);
117
157
  return {
118
158
  description: decoded.description,
119
- column: decodeColumn(decoded.description.type, decoded.column.bytes, decoded.description.rowCount),
159
+ column: decodeValidatedColumn(decoded.column),
120
160
  };
121
161
  }
122
162
  /**
123
163
  * Decompresses and checksum-verifies a block without materializing row values.
124
- * The returned physical byte array is owned by the result.
164
+ *
165
+ * For a raw block, `column.bytes` is a zero-copy view into `bytes`. Both views must be treated as
166
+ * immutable for the lifetime of the result. Gzip results own a new decompressed allocation.
125
167
  */
126
168
  export async function decodePhysicalBlock(bytes) {
127
- const description = inspectBlock(bytes);
128
- const metadataLength = bytes.byteLength - HEADER_LENGTH - description.storedLength;
129
- const stored = bytes.subarray(HEADER_LENGTH + metadataLength);
169
+ const description = verifyStoredBlock(bytes);
170
+ if (description.compression === "raw") {
171
+ return validateDecodedPhysicalBlock(bytes.subarray(description.headerLength + description.metadataLength), description);
172
+ }
173
+ return decodeVerifiedPhysicalBlock(bytes, description);
174
+ }
175
+ async function decodeVerifiedPhysicalBlock(bytes, description) {
176
+ const stored = bytes.subarray(description.headerLength + description.metadataLength);
130
177
  const encoded = await getCodec(description.compression).decompress(stored, description.encodedLength);
131
- if (crc32(encoded) !== description.checksum)
132
- throw new Error("Block checksum mismatch");
178
+ // For raw blocks the stored and logical bytes are identical, and inspectBlock requires both
179
+ // declared checksums to agree. verifyStoredBlock therefore performed the logical check too,
180
+ // avoiding a second full scan on the default codec.
181
+ if (description.compression !== "raw" && crc32(encoded) !== description.checksum) {
182
+ throw new Error("Block logical checksum mismatch");
183
+ }
184
+ return validateDecodedPhysicalBlock(encoded, description);
185
+ }
186
+ function validateDecodedPhysicalBlock(encoded, description) {
133
187
  const column = validatePhysicalColumn({
134
188
  type: description.type,
135
189
  rowCount: description.rowCount,
@@ -145,22 +199,49 @@ export async function decodePhysicalBlock(bytes) {
145
199
  column,
146
200
  };
147
201
  }
202
+ /**
203
+ * Verifies every stored byte without decompressing or copying the payload.
204
+ *
205
+ * Snapshot import and OPFS recovery use this stronger operation before accepting a block.
206
+ * Planning and pruning should continue to use `inspectBlock`, which deliberately reads only the
207
+ * fixed header and metadata envelope.
208
+ */
209
+ export function verifyStoredBlock(bytes) {
210
+ const description = inspectBlock(bytes);
211
+ const storedStart = bytes.byteLength - description.storedLength;
212
+ if (crc32(bytes.subarray(storedStart)) !== description.storedChecksum) {
213
+ throw new Error("Block stored payload checksum mismatch");
214
+ }
215
+ return description;
216
+ }
148
217
  export function inspectBlock(bytes) {
149
- if (bytes.byteLength < HEADER_LENGTH)
218
+ // Only magic and version are common across versions. Dispatch before interpreting any later
219
+ // offsets, so future envelopes can grow or replace the v2 header without weakening its reader.
220
+ if (bytes.byteLength < 10)
150
221
  throw new Error("Truncated block header");
151
222
  if (!MAGIC.every((byte, index) => bytes[index] === byte))
152
223
  throw new Error("Invalid block magic");
153
224
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
154
225
  const formatVersion = view.getUint16(8, true);
155
- if (formatVersion !== VERSION) {
156
- throw new Error(`Unsupported block version ${String(formatVersion)}`);
226
+ switch (formatVersion) {
227
+ case 2:
228
+ return inspectV2Block(bytes);
229
+ default:
230
+ throw new Error(`Unsupported block version ${String(formatVersion)}`);
157
231
  }
232
+ }
233
+ /** Permanent reader for the frozen v2 envelope. */
234
+ function inspectV2Block(bytes) {
235
+ if (bytes.byteLength < HEADER_LENGTH)
236
+ throw new Error("Truncated block header");
237
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
238
+ const formatVersion = view.getUint16(8, true);
158
239
  if (view.getUint16(10, true) !== HEADER_LENGTH)
159
240
  throw new Error("Invalid block header length");
160
241
  const metadataLength = view.getUint32(24, true);
161
242
  const encodedLength = view.getUint32(28, true);
162
243
  const storedLength = view.getUint32(32, true);
163
- assertLength(metadataLength, "metadata");
244
+ assertMetadataLength(metadataLength);
164
245
  assertLength(encodedLength, "encoded payload");
165
246
  assertLength(storedLength, "stored payload");
166
247
  if (HEADER_LENGTH + metadataLength + storedLength !== bytes.byteLength) {
@@ -172,6 +253,8 @@ export function inspectBlock(bytes) {
172
253
  if (envelope !== view.getUint32(4, true)) {
173
254
  throw new Error("Block envelope checksum mismatch");
174
255
  }
256
+ const rowCount = view.getUint32(16, true);
257
+ assertBlockRowCount(rowCount);
175
258
  const type = typesById.get(view.getUint8(12));
176
259
  if (type === undefined)
177
260
  throw new Error("Unknown logical type");
@@ -180,41 +263,83 @@ export function inspectBlock(bytes) {
180
263
  const codecId = view.getUint8(14);
181
264
  const compression = codecsById.get(codecId);
182
265
  if (compression === undefined) {
183
- throw new Error(codecId === 1
184
- ? "Block uses the removed RLE codec; rewrite it as raw or gzip with an older build"
185
- : "Unknown compression codec");
266
+ throw new Error(codecId === 1 ? "Block uses reserved compression codec id 1" : "Unknown compression codec");
186
267
  }
187
268
  if (view.getUint8(15) !== 0)
188
269
  throw new Error("Unsupported mandatory block flags");
189
- const rowCount = view.getUint32(16, true);
270
+ const checksum = view.getUint32(36, true);
271
+ const storedChecksum = view.getUint32(40, true);
272
+ if (compression === "raw" && checksum !== storedChecksum) {
273
+ throw new Error("Raw block checksums do not match");
274
+ }
275
+ if (compression === "raw" && encodedLength !== storedLength) {
276
+ throw new Error("Raw block lengths do not match");
277
+ }
190
278
  const nullCount = view.getUint32(20, true);
191
279
  if (nullCount > rowCount)
192
280
  throw new Error("Null count exceeds row count");
193
281
  let parsedMetadata;
282
+ let metadataJson;
194
283
  try {
195
- parsedMetadata = JSON.parse(textDecoder.decode(bytes.subarray(HEADER_LENGTH, HEADER_LENGTH + metadataLength)));
284
+ metadataJson = textDecoder.decode(bytes.subarray(HEADER_LENGTH, HEADER_LENGTH + metadataLength));
285
+ parsedMetadata = JSON.parse(metadataJson);
196
286
  }
197
287
  catch {
198
288
  throw new Error("Invalid block metadata");
199
289
  }
200
290
  const metadata = parseMetadata(parsedMetadata);
291
+ // There is one canonical metadata representation. Rejecting alternate key order, duplicate
292
+ // keys, unknown fields, and insignificant whitespace keeps byte fixtures meaningful and avoids
293
+ // multiple envelopes describing the same block.
294
+ if (JSON.stringify(metadata) !== metadataJson)
295
+ throw new Error("Non-canonical block metadata");
201
296
  return {
202
297
  formatVersion,
298
+ headerLength: HEADER_LENGTH,
299
+ metadataLength,
203
300
  type,
204
301
  compression,
205
302
  rowCount,
206
303
  nullCount,
207
304
  encodedLength,
208
305
  storedLength,
209
- checksum: view.getUint32(36, true),
306
+ checksum,
307
+ storedChecksum,
210
308
  metadata,
211
309
  };
212
310
  }
311
+ function assertMetadataLength(length) {
312
+ if (!Number.isSafeInteger(length) || length < 0 || length > MAX_METADATA_LENGTH) {
313
+ throw new RangeError("Invalid metadata length");
314
+ }
315
+ }
213
316
  function assertLength(length, name) {
214
317
  if (!Number.isSafeInteger(length) || length < 0 || length > MAX_BLOCK_LENGTH) {
215
318
  throw new RangeError(`Invalid ${name} length`);
216
319
  }
217
320
  }
321
+ function encodeCanonicalMetadata(metadata) {
322
+ let source;
323
+ try {
324
+ source = JSON.stringify(metadata);
325
+ }
326
+ catch {
327
+ throw new TypeError("Block metadata must be canonical JSON");
328
+ }
329
+ if (typeof source !== "string")
330
+ throw new TypeError("Block metadata must be canonical JSON");
331
+ let canonical;
332
+ try {
333
+ canonical = parseMetadata(JSON.parse(source));
334
+ }
335
+ catch {
336
+ throw new TypeError("Block metadata must match the supported canonical shape");
337
+ }
338
+ if (JSON.stringify(canonical) !== source) {
339
+ throw new TypeError("Block metadata must match the supported canonical shape");
340
+ }
341
+ return textEncoder.encode(source);
342
+ }
218
343
  function parseMetadata(value) {
219
344
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
220
345
  throw new Error("Invalid block metadata shape");
@@ -241,4 +366,3 @@ function metadataEquals(left, right) {
241
366
  }
242
367
  return left.zoneMap.min === right.zoneMap.min && left.zoneMap.max === right.zoneMap.max;
243
368
  }
244
- //# sourceMappingURL=block.js.map
@@ -4,4 +4,5 @@
4
4
  * times over, and the tail falls back to single-byte steps.
5
5
  */
6
6
  export declare function crc32(bytes: Uint8Array): number;
7
- //# sourceMappingURL=checksum.d.ts.map
7
+ /** Extends an already-finalized CRC-32 without concatenating or retaining the preceding bytes. */
8
+ export declare function crc32Continue(previous: number, bytes: Uint8Array): number;
@@ -20,7 +20,11 @@ for (let slice = 1; slice < 8; slice += 1) {
20
20
  * times over, and the tail falls back to single-byte steps.
21
21
  */
22
22
  export function crc32(bytes) {
23
- let checksum = 0xffffffff;
23
+ return crc32Continue(0, bytes);
24
+ }
25
+ /** Extends an already-finalized CRC-32 without concatenating or retaining the preceding bytes. */
26
+ export function crc32Continue(previous, bytes) {
27
+ let checksum = (previous ^ 0xffffffff) >>> 0;
24
28
  const length = bytes.byteLength;
25
29
  const wordEnd = length - (length % 8);
26
30
  let index = 0;
@@ -53,4 +57,3 @@ export function crc32(bytes) {
53
57
  }
54
58
  return (checksum ^ 0xffffffff) >>> 0;
55
59
  }
56
- //# sourceMappingURL=checksum.js.map
@@ -1,9 +1,15 @@
1
1
  import type { Compression } from "./types.js";
2
2
  export interface CompressionCodec {
3
3
  readonly id: Compression;
4
- compress(bytes: Uint8Array): Promise<Uint8Array>;
4
+ compress(bytes: Uint8Array, maximumOutputLength?: number): Promise<Uint8Array>;
5
5
  decompress(bytes: Uint8Array, expectedLength: number): Promise<Uint8Array>;
6
6
  }
7
+ /** A caller's explicit compressed-output ceiling was reached before an output join/allocation. */
8
+ export declare class CompressionOutputLimitError extends RangeError {
9
+ readonly byteLength: number;
10
+ readonly maximumOutputLength: number;
11
+ constructor(byteLength: number, maximumOutputLength: number);
12
+ }
7
13
  export interface CompressionMemoryBound {
8
14
  /** Largest returned buffer owned by the caller. */
9
15
  readonly maximumOutputBytes: number;
@@ -21,4 +27,3 @@ export declare const rawCodec: CompressionCodec;
21
27
  export declare function getCompressionMemoryBound(compression: Compression, inputLength: number): CompressionMemoryBound;
22
28
  export declare const gzipCodec: CompressionCodec;
23
29
  export declare function getCodec(id: Compression): CompressionCodec;
24
- //# sourceMappingURL=codecs.d.ts.map
@@ -1,3 +1,14 @@
1
+ /** A caller's explicit compressed-output ceiling was reached before an output join/allocation. */
2
+ export class CompressionOutputLimitError extends RangeError {
3
+ byteLength;
4
+ maximumOutputLength;
5
+ constructor(byteLength, maximumOutputLength) {
6
+ super("Compressed payload exceeds caller output ceiling");
7
+ this.byteLength = byteLength;
8
+ this.maximumOutputLength = maximumOutputLength;
9
+ this.name = "CompressionOutputLimitError";
10
+ }
11
+ }
1
12
  /**
2
13
  * The raw codec returns its input view unchanged in both directions: callers treat compressed
3
14
  * and decompressed payloads as read-only (encode copies the payload into the block envelope,
@@ -7,8 +18,14 @@
7
18
  */
8
19
  export const rawCodec = {
9
20
  id: "raw",
10
- compress(bytes) {
11
- return Promise.resolve(bytes);
21
+ async compress(bytes, maximumOutputLength) {
22
+ if (maximumOutputLength !== undefined) {
23
+ assertCallerOutputLength(maximumOutputLength);
24
+ if (bytes.byteLength > maximumOutputLength) {
25
+ throw new CompressionOutputLimitError(bytes.byteLength, maximumOutputLength);
26
+ }
27
+ }
28
+ return bytes;
12
29
  },
13
30
  decompress(bytes, expectedLength) {
14
31
  if (bytes.byteLength !== expectedLength)
@@ -43,19 +60,25 @@ export function getCompressionMemoryBound(compression, inputLength) {
43
60
  }
44
61
  }
45
62
  }
46
- async function transform(bytes, stream, maximumOutputLength = Number.POSITIVE_INFINITY, boundErrorMessage = "Decompressed payload exceeds declared length") {
63
+ async function transform(bytes, stream, maximumOutputLength = Number.POSITIVE_INFINITY, boundErrorMessage = "Decompressed payload exceeds declared length", outputLimitError) {
47
64
  const write = (async () => {
48
65
  const writer = stream.writable.getWriter();
49
66
  await writer.write(new Uint8Array(bytes));
50
67
  await writer.close();
51
68
  })();
52
- const [, result] = await Promise.all([
69
+ const [writeResult, readResult] = await Promise.allSettled([
53
70
  write,
54
- readBounded(stream.readable, maximumOutputLength, boundErrorMessage),
71
+ readBounded(stream.readable, maximumOutputLength, boundErrorMessage, outputLimitError),
55
72
  ]);
56
- return result;
73
+ // A reader-side bound failure intentionally cancels the writer. Preserve that precise error
74
+ // instead of racing it against the writer rejection caused by the cancellation.
75
+ if (readResult.status === "rejected")
76
+ throw readResult.reason;
77
+ if (writeResult.status === "rejected")
78
+ throw writeResult.reason;
79
+ return readResult.value;
57
80
  }
58
- async function readBounded(readable, maximumLength, boundErrorMessage) {
81
+ async function readBounded(readable, maximumLength, boundErrorMessage, outputLimitError) {
59
82
  const reader = readable.getReader();
60
83
  const chunks = [];
61
84
  let length = 0;
@@ -65,8 +88,14 @@ async function readBounded(readable, maximumLength, boundErrorMessage) {
65
88
  break;
66
89
  length += value.byteLength;
67
90
  if (length > maximumLength) {
68
- await reader.cancel(boundErrorMessage);
69
- throw new Error(boundErrorMessage);
91
+ const error = outputLimitError?.(length) ?? new Error(boundErrorMessage);
92
+ try {
93
+ await reader.cancel(error);
94
+ }
95
+ catch {
96
+ // Cancellation is cleanup. The deterministic bound error remains the operation result.
97
+ }
98
+ throw error;
70
99
  }
71
100
  chunks.push(value);
72
101
  }
@@ -80,9 +109,17 @@ async function readBounded(readable, maximumLength, boundErrorMessage) {
80
109
  }
81
110
  export const gzipCodec = {
82
111
  id: "gzip",
83
- async compress(bytes) {
112
+ async compress(bytes, maximumOutputLength) {
84
113
  const { maximumOutputBytes } = getCompressionMemoryBound("gzip", bytes.byteLength);
85
- return transform(bytes, new CompressionStream("gzip"), maximumOutputBytes, "Gzip payload exceeds compression bound");
114
+ if (maximumOutputLength !== undefined)
115
+ assertCallerOutputLength(maximumOutputLength);
116
+ const outputLimit = Math.min(maximumOutputBytes, maximumOutputLength ?? maximumOutputBytes);
117
+ const callerLimited = outputLimit < maximumOutputBytes;
118
+ return transform(bytes, new CompressionStream("gzip"), outputLimit, callerLimited
119
+ ? "Compressed payload exceeds caller output ceiling"
120
+ : "Gzip payload exceeds compression bound", callerLimited
121
+ ? (byteLength) => new CompressionOutputLimitError(byteLength, outputLimit)
122
+ : undefined);
86
123
  },
87
124
  async decompress(bytes, expectedLength) {
88
125
  const output = await transform(bytes, new DecompressionStream("gzip"), expectedLength);
@@ -91,6 +128,11 @@ export const gzipCodec = {
91
128
  return output;
92
129
  },
93
130
  };
131
+ function assertCallerOutputLength(value) {
132
+ if (!Number.isSafeInteger(value) || value < 0) {
133
+ throw new RangeError("Invalid compression output ceiling");
134
+ }
135
+ }
94
136
  export function getCodec(id) {
95
137
  switch (id) {
96
138
  case "raw":
@@ -99,4 +141,3 @@ export function getCodec(id) {
99
141
  return gzipCodec;
100
142
  }
101
143
  }
102
- //# sourceMappingURL=codecs.js.map
@@ -1,8 +1,9 @@
1
- import type { BlockMetadata, ColumnInput, DecodedColumn, LogicalType } from "./types.js";
1
+ import type { BlockMetadata, ColumnInput, DecodedColumn, LogicalType, ValidatedPhysicalColumn } from "./types.js";
2
2
  export declare function encodeColumn(input: ColumnInput): {
3
3
  bytes: Uint8Array;
4
4
  metadata: BlockMetadata;
5
5
  nullCount: number;
6
6
  };
7
7
  export declare function decodeColumn<T extends LogicalType>(type: T, bytes: Uint8Array, rowCount: number): DecodedColumn<T>;
8
- //# sourceMappingURL=column.d.ts.map
8
+ /** Package-internal row materialization after the physical payload was already validated. */
9
+ export declare function decodeValidatedColumn<T extends LogicalType>(column: ValidatedPhysicalColumn<T>): DecodedColumn<T>;