@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.
- package/README.md +4 -2
- package/dist/block-format/block.d.ts +32 -13
- package/dist/block-format/block.js +179 -55
- package/dist/block-format/checksum.d.ts +2 -1
- package/dist/block-format/checksum.js +5 -2
- package/dist/block-format/codecs.d.ts +7 -2
- package/dist/block-format/codecs.js +53 -12
- package/dist/block-format/column.d.ts +3 -2
- package/dist/block-format/column.js +96 -30
- package/dist/block-format/index.d.ts +2 -2
- package/dist/block-format/index.js +2 -2
- package/dist/block-format/physical.d.ts +8 -2
- package/dist/block-format/physical.js +14 -6
- package/dist/block-format/types.d.ts +7 -3
- package/dist/block-format/types.js +0 -1
- package/dist/block-format/unicode.d.ts +11 -0
- package/dist/block-format/unicode.js +46 -0
- package/dist/date-value.d.ts +15 -0
- package/dist/date-value.js +64 -0
- package/dist/engine/artifact-cache.d.ts +0 -1
- package/dist/engine/artifact-cache.js +0 -1
- package/dist/engine/batch.d.ts +5 -2
- package/dist/engine/batch.js +0 -1
- package/dist/engine/buffered-writer.d.ts +2 -1
- package/dist/engine/buffered-writer.js +34 -9
- package/dist/engine/cache-limits.d.ts +25 -0
- package/dist/engine/cache-limits.js +25 -0
- package/dist/engine/catalog.d.ts +5 -6
- package/dist/engine/catalog.js +5 -5
- package/dist/engine/client.d.ts +27 -7
- package/dist/engine/client.js +197 -31
- package/dist/engine/database.d.ts +161 -38
- package/dist/engine/database.js +4326 -1523
- package/dist/engine/defaults.d.ts +0 -1
- package/dist/engine/defaults.js +2 -4
- package/dist/engine/errors.d.ts +19 -1
- package/dist/engine/errors.js +32 -2
- package/dist/engine/fts.d.ts +0 -6
- package/dist/engine/fts.js +39 -14
- package/dist/engine/group-index.d.ts +0 -1
- package/dist/engine/group-index.js +6 -11
- package/dist/engine/index.d.ts +16 -11
- package/dist/engine/index.js +12 -10
- package/dist/engine/join-index.d.ts +0 -1
- package/dist/engine/join-index.js +2 -2
- package/dist/engine/keyed-live.d.ts +57 -0
- package/dist/engine/keyed-live.js +226 -0
- package/dist/engine/live-api.d.ts +4 -0
- package/dist/engine/live-api.js +4 -0
- package/dist/engine/live.d.ts +43 -26
- package/dist/engine/live.js +499 -129
- package/dist/engine/memory.d.ts +2 -1
- package/dist/engine/memory.js +3 -2
- package/dist/engine/optimizer.d.ts +0 -1
- package/dist/engine/optimizer.js +4 -4
- package/dist/engine/query-api.d.ts +3 -0
- package/dist/engine/query-api.js +3 -0
- package/dist/engine/query-cache.d.ts +0 -1
- package/dist/engine/query-cache.js +4 -4
- package/dist/engine/query.d.ts +4 -7
- package/dist/engine/query.js +348 -155
- package/dist/engine/result-wire.d.ts +0 -1
- package/dist/engine/result-wire.js +2 -2
- package/dist/engine/schema-wire.d.ts +1 -2
- package/dist/engine/schema-wire.js +0 -1
- package/dist/engine/schema.d.ts +3 -8
- package/dist/engine/schema.js +41 -24
- package/dist/engine/sort-keys.d.ts +0 -1
- package/dist/engine/sort-keys.js +2 -2
- package/dist/engine/sql-domains.d.ts +6 -1
- package/dist/engine/sql-domains.js +209 -45
- package/dist/engine/sql-driver.d.ts +2 -2
- package/dist/engine/sql-driver.js +0 -1
- package/dist/engine/sql-json.d.ts +0 -1
- package/dist/engine/sql-json.js +53 -7
- package/dist/engine/sql-semantics.d.ts +8 -5
- package/dist/engine/sql-semantics.js +427 -65
- package/dist/engine/typed-live.d.ts +67 -0
- package/dist/engine/typed-live.js +349 -0
- package/dist/engine/vector.d.ts +7 -1
- package/dist/engine/vector.js +202 -58
- package/dist/engine/worker-host.d.ts +8 -2
- package/dist/engine/worker-host.js +616 -152
- package/dist/engine/worker.d.ts +0 -1
- package/dist/engine/worker.js +3 -3
- package/dist/engine/write-block-planner.d.ts +18 -0
- package/dist/engine/write-block-planner.js +112 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/plan/index.d.ts +0 -1
- package/dist/plan/index.js +0 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/storage/indexeddb.d.ts +74 -65
- package/dist/storage/indexeddb.js +12301 -2076
- package/dist/storage/memory.d.ts +56 -66
- package/dist/storage/memory.js +1004 -139
- package/dist/storage/opfs/files.d.ts +18 -7
- package/dist/storage/opfs/files.js +115 -28
- package/dist/storage/opfs/index.d.ts +1 -1
- package/dist/storage/opfs/index.js +1 -1
- package/dist/storage/opfs/leader.d.ts +403 -91
- package/dist/storage/opfs/leader.js +4318 -602
- package/dist/storage/opfs/rpc.d.ts +15 -3
- package/dist/storage/opfs/rpc.js +223 -2
- package/dist/storage/opfs/snapshot-ledger.d.ts +40 -0
- package/dist/storage/opfs/snapshot-ledger.js +281 -0
- package/dist/storage/opfs/store.d.ts +26 -121
- package/dist/storage/opfs/store.js +436 -388
- package/dist/storage/persistence.d.ts +37 -0
- package/dist/storage/persistence.js +78 -0
- package/dist/storage/snapshot-stream.d.ts +24 -0
- package/dist/storage/snapshot-stream.js +897 -0
- package/dist/storage/snapshot.d.ts +9 -99
- package/dist/storage/snapshot.js +33 -309
- package/dist/storage/toolkit/extents.d.ts +41 -7
- package/dist/storage/toolkit/extents.js +402 -39
- package/dist/storage/toolkit/index.d.ts +3 -4
- package/dist/storage/toolkit/index.js +27 -3
- package/dist/storage/toolkit/record-core.d.ts +174 -61
- package/dist/storage/toolkit/record-core.js +5401 -849
- package/dist/storage/toolkit/sync-file.d.ts +10 -1
- package/dist/storage/toolkit/sync-file.js +50 -2
- package/dist/storage/toolkit/wal.d.ts +23 -5
- package/dist/storage/toolkit/wal.js +89 -27
- package/dist/storage/toolkit/wire.d.ts +4 -6
- package/dist/storage/toolkit/wire.js +80 -25
- package/dist/storage/types.d.ts +1271 -218
- package/dist/storage/types.js +1471 -138
- package/dist/testing/block-store-conformance.d.ts +0 -1
- package/dist/testing/block-store-conformance.js +877 -138
- package/dist/testing/index.d.ts +42 -31
- package/dist/testing/index.js +100 -68
- package/dist/testing/opfs-shim.d.ts +13 -3
- package/dist/testing/opfs-shim.js +40 -6
- package/dist/testing/simulator.d.ts +0 -1
- package/dist/testing/simulator.js +43 -12
- package/dist/testing/sqllogictest.d.ts +0 -1
- package/dist/testing/sqllogictest.js +0 -1
- package/dist/transactions/index.d.ts +81 -20
- package/dist/transactions/index.js +1050 -268
- package/dist/worker-protocol/index.d.ts +3 -2
- package/dist/worker-protocol/index.js +2 -5
- package/package.json +49 -1
- package/dist/block-format/block.d.ts.map +0 -1
- package/dist/block-format/block.js.map +0 -1
- package/dist/block-format/checksum.d.ts.map +0 -1
- package/dist/block-format/checksum.js.map +0 -1
- package/dist/block-format/codecs.d.ts.map +0 -1
- package/dist/block-format/codecs.js.map +0 -1
- package/dist/block-format/column.d.ts.map +0 -1
- package/dist/block-format/column.js.map +0 -1
- package/dist/block-format/index.d.ts.map +0 -1
- package/dist/block-format/index.js.map +0 -1
- package/dist/block-format/physical.d.ts.map +0 -1
- package/dist/block-format/physical.js.map +0 -1
- package/dist/block-format/types.d.ts.map +0 -1
- package/dist/block-format/types.js.map +0 -1
- package/dist/engine/artifact-cache.d.ts.map +0 -1
- package/dist/engine/artifact-cache.js.map +0 -1
- package/dist/engine/batch.d.ts.map +0 -1
- package/dist/engine/batch.js.map +0 -1
- package/dist/engine/buffered-writer.d.ts.map +0 -1
- package/dist/engine/buffered-writer.js.map +0 -1
- package/dist/engine/catalog.d.ts.map +0 -1
- package/dist/engine/catalog.js.map +0 -1
- package/dist/engine/client.d.ts.map +0 -1
- package/dist/engine/client.js.map +0 -1
- package/dist/engine/coordinator.d.ts +0 -17
- package/dist/engine/coordinator.d.ts.map +0 -1
- package/dist/engine/coordinator.js +0 -60
- package/dist/engine/coordinator.js.map +0 -1
- package/dist/engine/database.d.ts.map +0 -1
- package/dist/engine/database.js.map +0 -1
- package/dist/engine/defaults.d.ts.map +0 -1
- package/dist/engine/defaults.js.map +0 -1
- package/dist/engine/errors.d.ts.map +0 -1
- package/dist/engine/errors.js.map +0 -1
- package/dist/engine/fts.d.ts.map +0 -1
- package/dist/engine/fts.js.map +0 -1
- package/dist/engine/group-index.d.ts.map +0 -1
- package/dist/engine/group-index.js.map +0 -1
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js.map +0 -1
- package/dist/engine/join-index.d.ts.map +0 -1
- package/dist/engine/join-index.js.map +0 -1
- package/dist/engine/live.d.ts.map +0 -1
- package/dist/engine/live.js.map +0 -1
- package/dist/engine/memory.d.ts.map +0 -1
- package/dist/engine/memory.js.map +0 -1
- package/dist/engine/optimizer.d.ts.map +0 -1
- package/dist/engine/optimizer.js.map +0 -1
- package/dist/engine/query-cache.d.ts.map +0 -1
- package/dist/engine/query-cache.js.map +0 -1
- package/dist/engine/query.d.ts.map +0 -1
- package/dist/engine/query.js.map +0 -1
- package/dist/engine/result-wire.d.ts.map +0 -1
- package/dist/engine/result-wire.js.map +0 -1
- package/dist/engine/schema-wire.d.ts.map +0 -1
- package/dist/engine/schema-wire.js.map +0 -1
- package/dist/engine/schema.d.ts.map +0 -1
- package/dist/engine/schema.js.map +0 -1
- package/dist/engine/sort-keys.d.ts.map +0 -1
- package/dist/engine/sort-keys.js.map +0 -1
- package/dist/engine/sql-domains.d.ts.map +0 -1
- package/dist/engine/sql-domains.js.map +0 -1
- package/dist/engine/sql-driver.d.ts.map +0 -1
- package/dist/engine/sql-driver.js.map +0 -1
- package/dist/engine/sql-json.d.ts.map +0 -1
- package/dist/engine/sql-json.js.map +0 -1
- package/dist/engine/sql-semantics.d.ts.map +0 -1
- package/dist/engine/sql-semantics.js.map +0 -1
- package/dist/engine/vector.d.ts.map +0 -1
- package/dist/engine/vector.js.map +0 -1
- package/dist/engine/worker-host.d.ts.map +0 -1
- package/dist/engine/worker-host.js.map +0 -1
- package/dist/engine/worker.d.ts.map +0 -1
- package/dist/engine/worker.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/plan/index.d.ts.map +0 -1
- package/dist/plan/index.js.map +0 -1
- package/dist/storage/fixture-shape.d.ts +0 -42
- package/dist/storage/fixture-shape.d.ts.map +0 -1
- package/dist/storage/fixture-shape.js +0 -146
- package/dist/storage/fixture-shape.js.map +0 -1
- package/dist/storage/index.d.ts.map +0 -1
- package/dist/storage/index.js.map +0 -1
- package/dist/storage/indexeddb.d.ts.map +0 -1
- package/dist/storage/indexeddb.js.map +0 -1
- package/dist/storage/memory.d.ts.map +0 -1
- package/dist/storage/memory.js.map +0 -1
- package/dist/storage/opfs/files.d.ts.map +0 -1
- package/dist/storage/opfs/files.js.map +0 -1
- package/dist/storage/opfs/index.d.ts.map +0 -1
- package/dist/storage/opfs/index.js.map +0 -1
- package/dist/storage/opfs/leader.d.ts.map +0 -1
- package/dist/storage/opfs/leader.js.map +0 -1
- package/dist/storage/opfs/rpc.d.ts.map +0 -1
- package/dist/storage/opfs/rpc.js.map +0 -1
- package/dist/storage/opfs/store.d.ts.map +0 -1
- package/dist/storage/opfs/store.js.map +0 -1
- package/dist/storage/snapshot.d.ts.map +0 -1
- package/dist/storage/snapshot.js.map +0 -1
- package/dist/storage/toolkit/extents.d.ts.map +0 -1
- package/dist/storage/toolkit/extents.js.map +0 -1
- package/dist/storage/toolkit/index.d.ts.map +0 -1
- package/dist/storage/toolkit/index.js.map +0 -1
- package/dist/storage/toolkit/record-core.d.ts.map +0 -1
- package/dist/storage/toolkit/record-core.js.map +0 -1
- package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
- package/dist/storage/toolkit/sync-file.js.map +0 -1
- package/dist/storage/toolkit/wal.d.ts.map +0 -1
- package/dist/storage/toolkit/wal.js.map +0 -1
- package/dist/storage/toolkit/wire.d.ts.map +0 -1
- package/dist/storage/toolkit/wire.js.map +0 -1
- package/dist/storage/types.d.ts.map +0 -1
- package/dist/storage/types.js.map +0 -1
- package/dist/testing/block-store-conformance.d.ts.map +0 -1
- package/dist/testing/block-store-conformance.js.map +0 -1
- package/dist/testing/index.d.ts.map +0 -1
- package/dist/testing/index.js.map +0 -1
- package/dist/testing/opfs-shim.d.ts.map +0 -1
- package/dist/testing/opfs-shim.js.map +0 -1
- package/dist/testing/seeds.d.ts +0 -11
- package/dist/testing/seeds.d.ts.map +0 -1
- package/dist/testing/seeds.js +0 -50
- package/dist/testing/seeds.js.map +0 -1
- package/dist/testing/simulator.d.ts.map +0 -1
- package/dist/testing/simulator.js.map +0 -1
- package/dist/testing/sqllogictest.d.ts.map +0 -1
- package/dist/testing/sqllogictest.js.map +0 -1
- package/dist/transactions/index.d.ts.map +0 -1
- package/dist/transactions/index.js.map +0 -1
- package/dist/worker-protocol/index.d.ts.map +0 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
23
|
-
*
|
|
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
|
-
*
|
|
27
|
-
*
|
|
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 =
|
|
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
|
|
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
|
-
*
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
27
|
-
*
|
|
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
|
-
*
|
|
31
|
-
*
|
|
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 =
|
|
33
|
+
export const BLOCK_FORMAT_VERSION = 2;
|
|
35
34
|
const VERSION = BLOCK_FORMAT_VERSION;
|
|
36
|
-
const
|
|
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
|
|
44
|
-
// every column shape
|
|
45
|
-
//
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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:
|
|
159
|
+
column: decodeValidatedColumn(decoded.column),
|
|
120
160
|
};
|
|
121
161
|
}
|
|
122
162
|
/**
|
|
123
163
|
* Decompresses and checksum-verifies a block without materializing row values.
|
|
124
|
-
*
|
|
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 =
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 [,
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>;
|