@zakkster/lite-bake-stream 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +18 -1
- package/SPEC.md +3 -1
- package/llms.txt +10 -2
- package/package.json +1 -1
- package/src/Crc32c.js +96 -0
- package/src/FileIngest.js +1 -1
- package/src/MultiReader.js +1 -1
- package/src/PreserveReader.js +27 -4
- package/src/PreserveTokenizer.js +1 -1
- package/src/PreserveWriter.js +211 -60
- package/src/RangeReader.js +27 -2
- package/src/Reader.js +32 -4
- package/src/Split.js +15 -3
- package/src/StringTable.js +1 -1
- package/src/Tokenizer.js +1 -1
- package/src/Views.js +62 -0
- package/src/Writer.js +382 -143
- package/src/index.js +4 -4
- package/types/PreserveReader.d.ts +10 -2
- package/types/PreserveWriter.d.ts +30 -0
- package/types/RangeReader.d.ts +5 -0
- package/types/Reader.d.ts +10 -2
- package/types/Writer.d.ts +37 -0
- package/types/index.d.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
4
|
|
|
5
|
+
## [1.6.0] -- 2026-09-02
|
|
6
|
+
|
|
7
|
+
M6 -- streaming emission + optional integrity. Public `beginStream(sink, { layout: 'stream', crc? })` binds a sink BEFORE feeding, so each shard payload streams to the sink as it finalizes and is dropped; `finalizeToSink(sink, { layout })` then writes the schema/directory/zone-map trailer + footer with one `writeAt(header, 0)` backpatch and returns `{ totalRows, shardCount, schema | mode, bytesWritten, layout }`. Called without a prior `beginStream`, `finalizeToSink` is the buffered convenience mode (`O(container)` peak, stated in the memory-model docs). `layout: 'prefix'` is byte-identical to `finalize()`. With the two-step contract, peak memory drops from `O(container)` to `O(targetShardBytes + directory)`. A default-emitted stream container is a legal v1 container (format_version stays 1) accepted by every shipped reader, `checkContainer`, and `mergeContainers`. `finalize()` is re-based over the same emitters, so its output is byte-for-byte identical to 1.5.0 (frozen sha256 goldens hold).
|
|
8
|
+
|
|
9
|
+
Optional CRC-32C (Castagnoli, table-driven, zero deps, `src/Crc32c.js`): writers opt in with `{ crc: true }`; readers get `verifyCrc()` -> `'ok' | 'absent'` (mismatch throws `R_BAD_CRC`) and an open option `{ verifyCrc: true }` that fails closed on mismatch (`R_BAD_CRC`) and absence (`R_CRC_ABSENT`). `mergeContainers` recomputes the CRC iff every input carried one, else absent. KATs: `CRC32C("")=0x00000000`, `CRC32C("123456789")=0xE3069283`; the stream layout closes the ring with a split-buffer combine (`crc32cCombine`, KAT-pinned and covered end-to-end by a streaming `beginStream` + flipped-byte case). New sink guard `W_BAD_SINK`. Three new thrown codes (`W_BAD_SINK`, `R_BAD_CRC`, `R_CRC_ABSENT`); inventory 57 -> 60 / 0 unpinned.
|
|
10
|
+
|
|
11
|
+
Measured peak RSS (full-tier gate, 500 MB streamed in 8 MiB chunks into a counting sink, container never materialized): `peakRss - baselineRss` = 32.1 MiB against the bound `4*targetShardBytes + 64*shardCount + 64 MiB` = 96.0 MiB at 41 shards -- versus the buffered `finalize()` path at ~704 MiB (baseline `soak --mb=500`: baselineRss 667 MiB, peakRss 1371 MiB). Suite: 479 -> 515 tests / 515 pass / 0 fail / 0 todo. Torture: 44/44 fast, 47/47 full, arrayBuffers growth 0.00 MB both tiers, t7 sink-release witness (retained shard slots <= 1; independently armed during qa -- retained shards fail 364/364, clean run 0/0). Measurement note: t7's final arrayBuffers read now settles and collects twice before reading (the gc-profiler contract) -- t5-fuzz's large-ArrayBuffer churn was polluting the single-gc read via V8's buffer pool; the 8 MB budget and all RULES numbers are unchanged, and the M6 paths retain ~0 in isolation. Pack 32 -> 33 (new `src/Crc32c.js`). See `decisions/0009-streaming-emission.md`.
|
|
12
|
+
|
|
5
13
|
## [1.5.0] -- 2026-09-02
|
|
6
14
|
|
|
7
15
|
M5 -- every ceiling gets a door; the shard budget counts all the bytes it ships; the floors are measured. BS-26 closed; BS-27's floor items closed (the writer-holds-all-shards ~2x container peak moves to M6's streaming emission); BS-32/BS-33/BS-34 closed. Suite: 461 -> 479 tests / 479 pass / 0 fail / 0 todo. Torture: 44/44 fast (~322 ms tier sum), 47/47 full (~5.6 s) incl. a 2100-shard directory-scale scenario (SPEC checker green, RangeReader LRU cap held at 8, `_maybeEvict` allocation-flat over 20000 ops); arrayBuffers growth 0.00 MB both tiers; t7 soak tracker size 0. Inventory gate: 54 -> 57 thrown codes / 0 unpinned. Falsifiability, measured item-by-item with baseline src stashed: all 15 new door/behavior assertions fail on baseline and pass on this tree, every stay-green net holds on both, and an all-F64-lane container is byte-identical to the 1.4.1 output (same sha256 and length). BREAK matrix {1, t6, l, m, j} all exit non-zero.
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
> Streaming byte-level JSON compiler for [`@zakkster/lite-bake`](https://github.com/PeshoVurtoleta/lite-bake). Zero-GC, tree-shakeable, gigabyte-scale.
|
|
15
15
|
|
|
16
|
-
**Status:** v1.
|
|
16
|
+
**Status:** v1.6.0. LBK1 format frozen at `format_version: 1`. Qualified on an 8 GB soak with zero GC and byte-exact preservation across 590 million cells.
|
|
17
17
|
|
|
18
18
|
## Two modes, one API
|
|
19
19
|
|
|
@@ -123,6 +123,23 @@ All of these fit the format's existing forward-compat seams — `format_version`
|
|
|
123
123
|
|
|
124
124
|
Two release gates. Zero major GC AND every declared cell round-trips exactly. If either fails, no publish.
|
|
125
125
|
|
|
126
|
+
## Streaming emission and integrity
|
|
127
|
+
|
|
128
|
+
`finalize()` returns the whole container as one `ArrayBuffer` -- peak memory is `O(container)`. For bounded-memory output, `writer.finalizeToSink(sink, { layout })` emits to a caller sink and returns `{ totalRows, shardCount, schema | mode, bytesWritten, layout }` (no buffer). A sink is any object with a synchronous `write(bytes)`; `layout: 'stream'` also needs `writeAt(bytes, position)` for one header backpatch -- satisfiable by `fs.write(fd, buf, 0, len, pos)`, the FS Access API's `createWritable().write({ type: 'write', position })`, and the in-memory `MemorySink`.
|
|
129
|
+
|
|
130
|
+
There are two ways to drive a stream, and they deliver different peaks:
|
|
131
|
+
|
|
132
|
+
- **Two-step, bounded RAM** -- call `writer.beginStream(sink, { layout: 'stream' })` BEFORE feeding, feed the tokenizer, then `writer.finalizeToSink(sink, { layout: 'stream' })`. Each shard is written to the sink as it finalizes and its bytes are dropped, so peak memory is `peak(stream) = 2*targetShardBytes + shardCount*(40 + 16*T) + schemaBlockBytes` -- `O(targetShardBytes + directory)`, never `O(container)`. This is the mode the 500 MB gate proves.
|
|
133
|
+
- **One-shot, buffered** -- call `writer.finalizeToSink(sink, opts)` alone (no `beginStream`). Correct and simplest, but the shards are buffered first, so peak is `peak(prefix) = containerBytes + sum(shard bytes)` -- `O(container)`, the same as `finalize()`.
|
|
134
|
+
|
|
135
|
+
`layout: 'prefix'` is the classic layout, **byte-identical** to `finalize()`. `layout: 'stream'` places the schema/directory/zone-map trailer and footer after the payloads with one header backpatch; a default-emitted stream container is a legal v1 container (format_version stays 1) that every shipped reader, `checkContainer`, and `mergeContainers` accept.
|
|
136
|
+
|
|
137
|
+
A malformed sink (non-object, missing `write`, missing `writeAt` for stream, or an async/thenable return) throws `W_BAD_SINK`; a sink that throws mid-emission fails the writer closed and rethrows the source error verbatim (a retry then hits `W_FINALIZED`).
|
|
138
|
+
|
|
139
|
+
**Memory model.** `peak(prefix) = containerBytes + sum(shard bytes)`; `peak(stream) = 2*targetShardBytes + shardCount*(40 + 16*T) + schemaBlockBytes`. The full-tier gate drives the two-step bounded path, streaming 500 MB in 8 MiB chunks into a counting sink, and asserts `peakRss - baselineRss <= 4*targetShardBytes + 64*shardCount + 64 MiB` (measured 32.1 MiB against the 96.0 MiB bound at 41 shards; the buffered path is ~704 MiB at 500 MB).
|
|
140
|
+
|
|
141
|
+
**Optional CRC-32C** (Castagnoli, table-driven, zero deps). Opt in with `{ crc: true }` on the writer (or serialize `writer` opts); coverage is `[0, footer_off)`, folded per emitted chunk. Readers expose `verifyCrc()` -> `'ok' | 'absent'` (a mismatch throws `R_BAD_CRC`) on `Reader`/`PreserveReader` (sync) and `RangeReader` (async). The open option `{ verifyCrc: true }` (also `deserialize(bytes, opts)`) fails closed on both mismatch (`R_BAD_CRC`) and absence (`R_CRC_ABSENT`) -- `null` is not zero. `0xFFFFFFFF` means absent and stays legal. `mergeContainers` recomputes the CRC iff every input carried one, else emits absent. See `decisions/0009-streaming-emission.md`.
|
|
142
|
+
|
|
126
143
|
## Release testing tiers
|
|
127
144
|
|
|
128
145
|
The gates run at four scales, chosen to fit different hardware and time budgets:
|
package/SPEC.md
CHANGED
|
@@ -165,7 +165,9 @@ reader.findShards(fieldName, {min, max}) → shardIdx[] // shards that MA
|
|
|
165
165
|
| 8 | 4 | `magic_end` | ASCII `1 K B L` = `0x31 0x4B 0x42 0x4C`. |
|
|
166
166
|
| 12 | 4 | `footer_len`| u32. Currently `16`. Lets future versions grow the footer without breaking magic detection. |
|
|
167
167
|
|
|
168
|
-
CRC is optional
|
|
168
|
+
CRC is optional; producers that omit it write `0xFFFFFFFF` (absent). Producers opt in with the writer option `{ crc: true }`. The CRC-32C uses the Castagnoli polynomial (bit-reflected `0x82F63B78`); check value `CRC32C("123456789") = 0xE3069283`. Readers verify with `verifyCrc()` (returns `'ok' | 'absent'`, throws `R_BAD_CRC` on mismatch) or the open option `{ verifyCrc: true }`, which additionally throws `R_CRC_ABSENT` when the caller demanded verification but the CRC is absent.
|
|
169
|
+
|
|
170
|
+
Note (SPEC 3 / 3.6): container offsets are ABSOLUTE, so two conformant orderings exist for a v1 container. The classic PREFIX ordering places the schema block, shard directory, and zone maps before the shard payloads. The STREAMING ordering (`layout: 'stream'`) places the shard payloads first, then the schema block, shard directory, and zone maps as a trailer, then the footer; the schema block still immediately precedes the shard directory (adjacency preserved). Both are valid v1 containers; format_version is unchanged.
|
|
169
171
|
|
|
170
172
|
## 4. Schema and field descriptors
|
|
171
173
|
|
package/llms.txt
CHANGED
|
@@ -8,13 +8,13 @@ Ingest gigabyte-scale JSON (top-level array or NDJSON) into the `lite-bake` LBK1
|
|
|
8
8
|
|
|
9
9
|
## Status
|
|
10
10
|
|
|
11
|
-
v1.
|
|
11
|
+
v1.6.0 — stable. LBK1 format frozen at `format_version: 1`. Qualified on an 8 GB soak (M1 MacBook Pro): 98.37M rows, 4.89 GB container, zero major GC, zero minor GC, 499 KB total heap allocation, 590.21M cells verified byte-exact, zero mismatches. Tokenizer benches at 222-237 MB/s (~55% of JSON.parse, with no object graph allocated).
|
|
12
12
|
|
|
13
13
|
Public API follows semver from 1.0.0. Future additions (I64 lane, columnar payload mode, container-level string table) land via the format's forward-compat seams -- `min_reader_version` on ShardEntry, reserved FieldDescriptor flags, the `metadata_off` block wrapper -- without a format_version bump.
|
|
14
14
|
|
|
15
15
|
See SPEC.md for the LBK1 container format, section 3.6 for zone maps, section 4.3 for the reserved field flags.
|
|
16
16
|
|
|
17
|
-
## Public API (v1.
|
|
17
|
+
## Public API (v1.6.0)
|
|
18
18
|
|
|
19
19
|
Two ingest modes share one top-level API:
|
|
20
20
|
|
|
@@ -87,6 +87,14 @@ F64 only. Values exceeding IEEE 754 double range are rejected as `E_NUMBER_OVERF
|
|
|
87
87
|
|
|
88
88
|
Subpath entries per SPEC section 6. `sideEffects: false`. Consumers import only the path they need; the browser reader never pulls the writer.
|
|
89
89
|
|
|
90
|
+
## Streaming emission and integrity (M6)
|
|
91
|
+
|
|
92
|
+
`finalize()` returns the whole container as one ArrayBuffer (peak memory O(container)). `writer.finalizeToSink(sink, { layout })` emits to a caller sink instead and returns `{ totalRows, shardCount, schema|mode, bytesWritten, layout }` (no buffer). A sink is any object with a synchronous `write(bytes)`; `layout: 'stream'` also needs `writeAt(bytes, position)` for one header backpatch. Two ways to drive a stream with different peaks: (1) TWO-STEP bounded RAM -- `writer.beginStream(sink, { layout: 'stream' })` BEFORE feeding, feed the tokenizer, then `writer.finalizeToSink(sink, { layout: 'stream' })`; each shard streams to the sink as it finalizes and its bytes drop, so peak is `peak(stream) = 2*targetShardBytes + shardCount*(40 + 16*T) + schemaBlockBytes` = O(targetShardBytes + directory). (2) ONE-SHOT buffered -- `finalizeToSink(sink, opts)` alone (no beginStream); correct but the shards are buffered first, so peak is `peak(prefix) = containerBytes + sum(shard bytes)` = O(container), same as `finalize()`. `layout: 'prefix'` is the classic layout, byte-identical to `finalize()`. A default-emitted stream container is a legal v1 container (format_version stays 1) that every shipped reader, `checkContainer`, and `mergeContainers` accept. A malformed sink (non-object, missing `write`, missing `writeAt` for stream, or an async/thenable return) throws `W_BAD_SINK`; a sink that throws mid-emission fails the writer closed and rethrows the source error verbatim (a retry then hits `W_FINALIZED`).
|
|
93
|
+
|
|
94
|
+
Memory model. `peak(prefix) = containerBytes + sum(shard bytes)`; `peak(stream) = 2*targetShardBytes + shardCount*(40 + 16*T) + schemaBlockBytes`. The full-tier gate streams 500 MB in 8 MiB chunks into a counting sink and asserts `peakRss - baselineRss <= 4*targetShardBytes + 64*shardCount + 64 MiB` (measured 32.1 MiB vs the 96.0 MiB bound at 41 shards; the buffered prefix path is ~704 MiB at 500 MB).
|
|
95
|
+
|
|
96
|
+
Optional CRC-32C (Castagnoli, table-driven, zero deps). Writers opt in with `{ crc: true }` (constructor or serialize `writer` opts); coverage is [0, footer_off), folded per emitted chunk. Readers expose `verifyCrc()` -> `'ok' | 'absent'` (a mismatch throws `R_BAD_CRC`) on `Reader`/`PreserveReader` (sync) and `RangeReader` (async). The open option `{ verifyCrc: true }` (also `deserialize(bytes, opts)`) fails closed on both mismatch (`R_BAD_CRC`) and absence (`R_CRC_ABSENT`). `0xFFFFFFFF` means absent and stays legal (SPEC 3.7). `mergeContainers` recomputes the CRC iff every input carried one, else emits absent. KAT: `CRC32C("123456789") = 0xE3069283`. See decisions/0009-streaming-emission.md.
|
|
97
|
+
|
|
90
98
|
## Non-goals
|
|
91
99
|
|
|
92
100
|
JSON5, JSONC, comments, trailing commas, streaming field updates, compression, native (non-JS) producers.
|
package/package.json
CHANGED
package/src/Crc32c.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// @zakkster/lite-bake-stream / Crc32c (internal)
|
|
2
|
+
// Copyright (c) 2026 Zahary Shinikchiev. MIT.
|
|
3
|
+
//
|
|
4
|
+
// CRC-32C (Castagnoli, bit-reflected polynomial 0x82F63B78) over container
|
|
5
|
+
// bodies. Backs the optional footer integrity field (SPEC 3.7). The 256-entry
|
|
6
|
+
// lookup table is built ONCE at import (cold); the update loop is scalar and
|
|
7
|
+
// allocation-free, folding one byte per iteration:
|
|
8
|
+
// crc = (crc >>> 8) ^ TABLE[(crc ^ b) & 0xFF]
|
|
9
|
+
// It is called per emitted CHUNK, never per record.
|
|
10
|
+
//
|
|
11
|
+
// The running CRC is carried as a signed int32 (init = 0xFFFFFFFF, i.e. -1);
|
|
12
|
+
// crc32cFinal applies the trailing one-complement and returns an unsigned u32.
|
|
13
|
+
//
|
|
14
|
+
// crc32cCombine folds the finalized CRC of a suffix onto the finalized CRC of a
|
|
15
|
+
// prefix (GF(2) matrix method, as in zlib's crc32_combine but with the CRC-32C
|
|
16
|
+
// polynomial). The streaming writer uses it ONCE at end-of-input to prepend the
|
|
17
|
+
// 48-byte header CRC onto the running body CRC without re-reading the dropped
|
|
18
|
+
// shard payloads. Cold path; not on any per-chunk loop.
|
|
19
|
+
//
|
|
20
|
+
// Not a public export. No subpath. Internal to the package.
|
|
21
|
+
|
|
22
|
+
const POLY = 0x82F63B78; // CRC-32C, bit-reflected
|
|
23
|
+
const TABLE = buildTable();
|
|
24
|
+
|
|
25
|
+
function buildTable() {
|
|
26
|
+
const t = new Int32Array(256);
|
|
27
|
+
for (let n = 0; n < 256; n++) {
|
|
28
|
+
let c = n;
|
|
29
|
+
for (let k = 0; k < 8; k++) {
|
|
30
|
+
c = (c & 1) ? (POLY ^ (c >>> 1)) : (c >>> 1);
|
|
31
|
+
}
|
|
32
|
+
t[n] = c | 0;
|
|
33
|
+
}
|
|
34
|
+
return t;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function crc32cInit() { return 0xFFFFFFFF | 0; }
|
|
38
|
+
|
|
39
|
+
export function crc32cUpdate(crc, bytes, off, len) {
|
|
40
|
+
let c = crc | 0;
|
|
41
|
+
const end = off + len;
|
|
42
|
+
for (let i = off; i < end; i++) {
|
|
43
|
+
c = (c >>> 8) ^ TABLE[(c ^ bytes[i]) & 0xFF];
|
|
44
|
+
}
|
|
45
|
+
return c | 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function crc32cFinal(crc) {
|
|
49
|
+
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---- GF(2) matrix helpers for crc32cCombine (cold) --------------------------
|
|
53
|
+
|
|
54
|
+
const _even = new Int32Array(32);
|
|
55
|
+
const _odd = new Int32Array(32);
|
|
56
|
+
|
|
57
|
+
function gf2Times(mat, vec) {
|
|
58
|
+
let sum = 0;
|
|
59
|
+
let v = vec >>> 0;
|
|
60
|
+
let i = 0;
|
|
61
|
+
while (v !== 0) {
|
|
62
|
+
if (v & 1) sum ^= mat[i];
|
|
63
|
+
v >>>= 1;
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
66
|
+
return sum | 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function gf2Square(square, mat) {
|
|
70
|
+
for (let n = 0; n < 32; n++) square[n] = gf2Times(mat, mat[n]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// crcA = finalized CRC of prefix A; crcB = finalized CRC of suffix B; len2 =
|
|
74
|
+
// byte length of B. Returns the finalized CRC of A || B. len2 may exceed 2^31,
|
|
75
|
+
// so it is halved by division and tested by remainder.
|
|
76
|
+
export function crc32cCombine(crcA, crcB, len2) {
|
|
77
|
+
if (len2 === 0) return crcA >>> 0;
|
|
78
|
+
const odd = _odd, even = _even;
|
|
79
|
+
odd[0] = POLY | 0; // operator for a single zero bit
|
|
80
|
+
let row = 1;
|
|
81
|
+
for (let n = 1; n < 32; n++) { odd[n] = row; row = (row << 1) | 0; }
|
|
82
|
+
gf2Square(even, odd); // 2 zero bits
|
|
83
|
+
gf2Square(odd, even); // 4 zero bits
|
|
84
|
+
let crc = crcA >>> 0;
|
|
85
|
+
let len = len2;
|
|
86
|
+
do {
|
|
87
|
+
gf2Square(even, odd);
|
|
88
|
+
if (len % 2) crc = gf2Times(even, crc) >>> 0;
|
|
89
|
+
len = Math.floor(len / 2);
|
|
90
|
+
if (len === 0) break;
|
|
91
|
+
gf2Square(odd, even);
|
|
92
|
+
if (len % 2) crc = gf2Times(odd, crc) >>> 0;
|
|
93
|
+
len = Math.floor(len / 2);
|
|
94
|
+
} while (len !== 0);
|
|
95
|
+
return (crc ^ (crcB >>> 0)) >>> 0;
|
|
96
|
+
}
|
package/src/FileIngest.js
CHANGED
|
@@ -27,7 +27,7 @@ import { PreserveWriter } from './PreserveWriter.js';
|
|
|
27
27
|
import { PreserveReader } from './PreserveReader.js';
|
|
28
28
|
import { checkOpts } from './Opts.js';
|
|
29
29
|
|
|
30
|
-
export const VERSION = '1.
|
|
30
|
+
export const VERSION = '1.6.0';
|
|
31
31
|
|
|
32
32
|
const U32_MAX = 4294967295;
|
|
33
33
|
const INGEST_OPTS = {
|
package/src/MultiReader.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// M_ROW_OUT_OF_RANGE - rowIdx >= totalRows
|
|
26
26
|
// M_TOO_MANY_ROWS - cumulative row count exceeds Number.MAX_SAFE_INTEGER
|
|
27
27
|
|
|
28
|
-
export const VERSION = '1.
|
|
28
|
+
export const VERSION = '1.6.0';
|
|
29
29
|
|
|
30
30
|
export class MultiReaderError extends Error {
|
|
31
31
|
constructor(code, msg) { super(msg); this.code = code; this.name = 'MultiReaderError'; }
|
package/src/PreserveReader.js
CHANGED
|
@@ -32,12 +32,18 @@
|
|
|
32
32
|
// R_OFFSET_TOO_LARGE - a u64 header/directory offset exceeds 2^53-1
|
|
33
33
|
|
|
34
34
|
import { toContainerBuffer } from './Views.js';
|
|
35
|
+
import { checkOpts } from './Opts.js';
|
|
36
|
+
import { crc32cInit, crc32cUpdate, crc32cFinal } from './Crc32c.js';
|
|
35
37
|
|
|
36
|
-
export const VERSION = '1.
|
|
38
|
+
export const VERSION = '1.6.0';
|
|
37
39
|
|
|
38
40
|
const CONTAINER_HEADER_BYTES = 48;
|
|
39
41
|
const SHARD_ENTRY_BYTES = 40;
|
|
40
42
|
const FOOTER_BYTES = 16;
|
|
43
|
+
const CRC_ABSENT = 0xFFFFFFFF;
|
|
44
|
+
|
|
45
|
+
const PRESERVE_READER_OPTS = { verifyCrc: { t: 'bool' } };
|
|
46
|
+
function raisePreserveReaderOpt(code, msg) { throw new PreserveReaderError(code, msg); }
|
|
41
47
|
|
|
42
48
|
export class PreserveReaderError extends Error {
|
|
43
49
|
constructor(code, msg) { super(msg); this.code = code; this.name = 'PreserveReaderError'; }
|
|
@@ -55,14 +61,15 @@ function u64(dv, off, what) {
|
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
export class PreserveReader {
|
|
58
|
-
static fromBuffer(input) {
|
|
59
|
-
return new PreserveReader(toContainerBuffer(input, 'PreserveReader.fromBuffer'));
|
|
64
|
+
static fromBuffer(input, opts) {
|
|
65
|
+
return new PreserveReader(toContainerBuffer(input, 'PreserveReader.fromBuffer'), opts);
|
|
60
66
|
}
|
|
61
67
|
|
|
62
|
-
constructor(buffer) {
|
|
68
|
+
constructor(buffer, opts) {
|
|
63
69
|
if (!(buffer instanceof ArrayBuffer)) {
|
|
64
70
|
throw new TypeError('PreserveReader: expected ArrayBuffer');
|
|
65
71
|
}
|
|
72
|
+
checkOpts('PreserveReader', opts, PRESERVE_READER_OPTS, raisePreserveReaderOpt);
|
|
66
73
|
this._buffer = buffer;
|
|
67
74
|
this._dv = new DataView(buffer);
|
|
68
75
|
this._bytes = new Uint8Array(buffer);
|
|
@@ -70,6 +77,22 @@ export class PreserveReader {
|
|
|
70
77
|
this._parseHeader();
|
|
71
78
|
this._parseFooter();
|
|
72
79
|
this._parseShardDirectory();
|
|
80
|
+
if (opts && opts.verifyCrc === true) {
|
|
81
|
+
const status = this.verifyCrc();
|
|
82
|
+
if (status === 'absent')
|
|
83
|
+
throw new PreserveReaderError('R_CRC_ABSENT', 'verifyCrc:true but the container carries no CRC (footer CRC is absent, 0xFFFFFFFF)');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// SPEC 3.7 integrity: 'ok' | 'absent'; a mismatch throws R_BAD_CRC.
|
|
88
|
+
verifyCrc() {
|
|
89
|
+
const footerOff = this._buffer.byteLength - FOOTER_BYTES;
|
|
90
|
+
const stored = this._dv.getUint32(footerOff, true) >>> 0;
|
|
91
|
+
if (stored === CRC_ABSENT) return 'absent';
|
|
92
|
+
const actual = crc32cFinal(crc32cUpdate(crc32cInit(), this._bytes, 0, footerOff));
|
|
93
|
+
if (actual !== stored)
|
|
94
|
+
throw new PreserveReaderError('R_BAD_CRC', 'container CRC mismatch: stored 0x' + stored.toString(16) + ' != computed 0x' + actual.toString(16));
|
|
95
|
+
return 'ok';
|
|
73
96
|
}
|
|
74
97
|
|
|
75
98
|
_parseHeader() {
|
package/src/PreserveTokenizer.js
CHANGED
package/src/PreserveWriter.js
CHANGED
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
// container with R_WRONG_MODE.
|
|
18
18
|
|
|
19
19
|
import { checkOpts } from './Opts.js';
|
|
20
|
+
import { crc32cInit, crc32cUpdate, crc32cFinal, crc32cCombine } from './Crc32c.js';
|
|
21
|
+
import { validateSink, isThenable } from './Views.js';
|
|
20
22
|
|
|
21
|
-
export const VERSION = '1.
|
|
23
|
+
export const VERSION = '1.6.0';
|
|
22
24
|
|
|
23
25
|
export class PreserveWriterError extends Error {
|
|
24
26
|
constructor(code, msg) { super(msg); this.code = code; this.name = 'PreserveWriterError'; }
|
|
@@ -28,6 +30,11 @@ function raisePreserveWriter(code, msg) { throw new PreserveWriterError(code, ms
|
|
|
28
30
|
const CONTAINER_HEADER_BYTES = 48;
|
|
29
31
|
const SHARD_ENTRY_BYTES = 40;
|
|
30
32
|
const FOOTER_BYTES = 16;
|
|
33
|
+
const CRC_ABSENT = 0xFFFFFFFF;
|
|
34
|
+
const FINALIZE_TO_SINK_OPTS = {
|
|
35
|
+
layout: { t: 'enum', values: ['prefix', 'stream'] },
|
|
36
|
+
crc: { t: 'bool' },
|
|
37
|
+
};
|
|
31
38
|
const DEFAULT_TARGET_SHARD_BYTES = 8 * 1024 * 1024; // 8 MiB — smaller than schema mode
|
|
32
39
|
const INITIAL_OFFSETS_CAP = 4096;
|
|
33
40
|
const U32_MAX = 4294967295;
|
|
@@ -38,6 +45,7 @@ const U32_MAX = 4294967295;
|
|
|
38
45
|
const PRESERVE_WRITER_OPTS = {
|
|
39
46
|
targetShardBytes: { t: 'int', min: 1, max: U32_MAX },
|
|
40
47
|
maxRecordBytes: { t: 'int', min: 0, max: U32_MAX },
|
|
48
|
+
crc: { t: 'bool' },
|
|
41
49
|
};
|
|
42
50
|
|
|
43
51
|
export class PreserveWriter {
|
|
@@ -45,9 +53,16 @@ export class PreserveWriter {
|
|
|
45
53
|
checkOpts('PreserveWriter', opts, PRESERVE_WRITER_OPTS, raisePreserveWriter);
|
|
46
54
|
opts = opts || {};
|
|
47
55
|
this._targetShardBytes = opts.targetShardBytes !== undefined ? opts.targetShardBytes : DEFAULT_TARGET_SHARD_BYTES;
|
|
56
|
+
this._crc = opts.crc === true;
|
|
48
57
|
this._shards = [];
|
|
49
58
|
this._totalRows = 0;
|
|
50
59
|
this._finalized = false;
|
|
60
|
+
// Streaming-emission state (M6); see Writer for the model. A bound sink makes
|
|
61
|
+
// _finalizeCurrentShard emit + drop each shard, retaining only a descriptor.
|
|
62
|
+
this._sink = null;
|
|
63
|
+
this._sinkCrcOn = false;
|
|
64
|
+
this._sinkPos = 0;
|
|
65
|
+
this._sinkCrc = 0;
|
|
51
66
|
this._currentBlobs = null; // null => never allocated; _allocateShard allocates once
|
|
52
67
|
this._allocateShard();
|
|
53
68
|
}
|
|
@@ -162,11 +177,15 @@ export class PreserveWriter {
|
|
|
162
177
|
for (let i = 0; i < rowCount; i++) {
|
|
163
178
|
dv.setUint32(blobLen + i * 4, this._currentOffsets[i], true);
|
|
164
179
|
}
|
|
165
|
-
this.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
180
|
+
if (this._sink !== null) {
|
|
181
|
+
this._streamEmitShard(shardBytes, rowCount);
|
|
182
|
+
} else {
|
|
183
|
+
this._shards.push({
|
|
184
|
+
bytes: shardBytes,
|
|
185
|
+
rowCount,
|
|
186
|
+
blobLen,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
170
189
|
// Reuse the working buffers for the next shard: reset the cursors only, keep
|
|
171
190
|
// the allocations (BS-27). No stale bytes can leak -- every read into these
|
|
172
191
|
// buffers is bounded by _currentBlobBytes / _currentRowCount (both zeroed
|
|
@@ -175,87 +194,219 @@ export class PreserveWriter {
|
|
|
175
194
|
this._currentRowCount = 0;
|
|
176
195
|
}
|
|
177
196
|
|
|
178
|
-
|
|
197
|
+
_completeInput() {
|
|
179
198
|
if (this._finalized) throw new PreserveWriterError('W_FINALIZED', 'writer already finalized');
|
|
180
199
|
if (this._currentRowCount > 0) this._finalizeCurrentShard();
|
|
181
200
|
if (this._shards.length === 0) {
|
|
182
|
-
//
|
|
201
|
+
// Match schema-mode: an empty container is an error, not a silent no-op.
|
|
183
202
|
throw new PreserveWriterError('W_EMPTY_INPUT', 'no records written; nothing to finalize');
|
|
184
203
|
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
finalize() {
|
|
207
|
+
this._completeInput();
|
|
208
|
+
const bytes = this._buildPrefixBytes(this._crc);
|
|
185
209
|
this._finalized = true;
|
|
186
|
-
return this.
|
|
210
|
+
return { buffer: bytes.buffer, totalRows: this._totalRows, shardCount: this._shards.length, mode: 'preserve' };
|
|
187
211
|
}
|
|
188
212
|
|
|
189
213
|
get totalRows() { return this._totalRows; }
|
|
190
214
|
get shardCount() { return this._shards.length; }
|
|
191
215
|
|
|
192
|
-
|
|
216
|
+
// -------- container assembly (placement-free emitters) --------
|
|
217
|
+
// Preserve mode has no schema block and no zone maps: schema_block_off and
|
|
218
|
+
// metadata_off are 0. Classic prefix layout is header | directory | payloads |
|
|
219
|
+
// footer; the streaming layout (O9) is header | payloads | directory | footer.
|
|
220
|
+
|
|
221
|
+
_emitHeaderInto(dv, bytes, off, shardDirOff, shardCount, totalRows) {
|
|
222
|
+
bytes[off + 0] = 0x4C; bytes[off + 1] = 0x42; bytes[off + 2] = 0x4B; bytes[off + 3] = 0x31; // 'LBK1'
|
|
223
|
+
dv.setUint16(off + 4, 1, true); // format_version
|
|
224
|
+
bytes[off + 6] = 1; // endian LE
|
|
225
|
+
bytes[off + 7] = 0x01; // flags: bit 0 = preserve mode
|
|
226
|
+
dv.setBigUint64(off + 8, 0n, true); // schema_block_off = 0
|
|
227
|
+
dv.setBigUint64(off + 16, 0n, true); // metadata_off = 0
|
|
228
|
+
dv.setBigUint64(off + 24, BigInt(shardDirOff), true);
|
|
229
|
+
dv.setUint32(off + 32, shardCount, true);
|
|
230
|
+
dv.setUint32(off + 36, 0, true); // reserved1
|
|
231
|
+
dv.setBigUint64(off + 40, BigInt(totalRows), true);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
_emitDirEntryInto(dv, entryOff, payloadOff, payloadLen, rowCount) {
|
|
235
|
+
dv.setBigUint64(entryOff + 0, BigInt(payloadOff), true);
|
|
236
|
+
dv.setUint32(entryOff + 8, payloadLen, true);
|
|
237
|
+
dv.setUint32(entryOff + 12, rowCount, true);
|
|
238
|
+
dv.setUint16(entryOff + 16, 1, true); // min_reader_version
|
|
239
|
+
dv.setUint16(entryOff + 18, 0, true); // shard flags
|
|
240
|
+
dv.setUint32(entryOff + 20, 0, true); // reserved
|
|
241
|
+
dv.setBigUint64(entryOff + 24, 0n, true); // no string table
|
|
242
|
+
dv.setBigUint64(entryOff + 32, 0n, true);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
_emitFooterInto(dv, bytes, footerOff, crcVal) {
|
|
246
|
+
dv.setUint32(footerOff + 0, crcVal >>> 0, true);
|
|
247
|
+
dv.setUint32(footerOff + 4, 0, true);
|
|
248
|
+
bytes[footerOff + 8] = 0x31; // '1'
|
|
249
|
+
bytes[footerOff + 9] = 0x4B; // 'K'
|
|
250
|
+
bytes[footerOff + 10] = 0x42; // 'B'
|
|
251
|
+
bytes[footerOff + 11] = 0x4C; // 'L'
|
|
252
|
+
dv.setUint32(footerOff + 12, FOOTER_BYTES, true);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Classic prefix layout, byte-for-byte identical to the pre-M6 assembler when
|
|
256
|
+
// crc is off; the optional CRC-32C covers [0, footer_off).
|
|
257
|
+
_buildPrefixBytes(crcOn) {
|
|
193
258
|
const shardCount = this._shards.length;
|
|
194
259
|
const shardDirBytes = shardCount * SHARD_ENTRY_BYTES;
|
|
195
|
-
|
|
196
|
-
// Layout: header (48) | shard directory | shard payloads | footer (16)
|
|
197
|
-
// No schema block, no zone maps in preserve mode; schema_block_off = 0.
|
|
198
260
|
const shardDirOff = CONTAINER_HEADER_BYTES;
|
|
199
261
|
let cursor = shardDirOff + shardDirBytes;
|
|
200
|
-
const
|
|
262
|
+
const payloadOffs = new Array(shardCount);
|
|
201
263
|
for (let i = 0; i < shardCount; i++) {
|
|
202
|
-
|
|
264
|
+
payloadOffs[i] = cursor;
|
|
203
265
|
cursor += this._shards[i].bytes.length;
|
|
204
266
|
}
|
|
267
|
+
const footerOff = cursor;
|
|
205
268
|
const totalBytes = cursor + FOOTER_BYTES;
|
|
269
|
+
const buffer = new ArrayBuffer(totalBytes);
|
|
270
|
+
const dv = new DataView(buffer);
|
|
271
|
+
const bytes = new Uint8Array(buffer);
|
|
206
272
|
|
|
207
|
-
|
|
208
|
-
const dv = new DataView(container);
|
|
209
|
-
const bytes = new Uint8Array(container);
|
|
210
|
-
|
|
211
|
-
// Header
|
|
212
|
-
bytes[0] = 0x4C; bytes[1] = 0x42; bytes[2] = 0x4B; bytes[3] = 0x31; // 'LBK1'
|
|
213
|
-
dv.setUint16(4, 1, true); // format_version
|
|
214
|
-
bytes[6] = 1; // endian LE
|
|
215
|
-
bytes[7] = 0x01; // flags: bit 0 = preserve mode
|
|
216
|
-
dv.setBigUint64(8, 0n, true); // schema_block_off = 0 (no schema)
|
|
217
|
-
dv.setBigUint64(16, 0n, true); // metadata_off = 0 (no zone maps)
|
|
218
|
-
dv.setBigUint64(24, BigInt(shardDirOff), true);
|
|
219
|
-
dv.setUint32(32, shardCount, true);
|
|
220
|
-
dv.setUint32(36, 0, true); // reserved1
|
|
221
|
-
dv.setBigUint64(40, BigInt(this._totalRows), true);
|
|
222
|
-
|
|
223
|
-
// Shard directory (40 bytes per entry, u64 payload_off, u32 payload_len,
|
|
224
|
-
// u32 row_count, u16 min_reader_version, u16 flags, u32 reserved,
|
|
225
|
-
// u64 local_string_off=0, u64 local_string_len=0)
|
|
273
|
+
this._emitHeaderInto(dv, bytes, 0, shardDirOff, shardCount, this._totalRows);
|
|
226
274
|
for (let i = 0; i < shardCount; i++) {
|
|
227
|
-
const entryOff = shardDirOff + i * SHARD_ENTRY_BYTES;
|
|
228
275
|
const s = this._shards[i];
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
276
|
+
this._emitDirEntryInto(dv, shardDirOff + i * SHARD_ENTRY_BYTES, payloadOffs[i], s.bytes.length, s.rowCount);
|
|
277
|
+
}
|
|
278
|
+
for (let i = 0; i < shardCount; i++) bytes.set(this._shards[i].bytes, payloadOffs[i]);
|
|
279
|
+
|
|
280
|
+
let crcVal = CRC_ABSENT;
|
|
281
|
+
if (crcOn) crcVal = crc32cFinal(crc32cUpdate(crc32cInit(), bytes, 0, footerOff));
|
|
282
|
+
this._emitFooterInto(dv, bytes, footerOff, crcVal);
|
|
283
|
+
return bytes;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// PUBLIC. Bind a sink and stream shards as they finalize (bounded RAM). Call
|
|
287
|
+
// before feeding, then finalizeToSink(sink, opts) writes the trailer. Calling
|
|
288
|
+
// finalizeToSink alone is BUFFERED mode (O(container) peak). opts.layout must
|
|
289
|
+
// be 'stream'; opts.crc defaults to the constructor crc.
|
|
290
|
+
beginStream(sink, opts) {
|
|
291
|
+
if (this._finalized) throw new PreserveWriterError('W_FINALIZED', 'writer already finalized');
|
|
292
|
+
if (this._sink !== null) throw new PreserveWriterError('W_FINALIZED', 'writer is already streaming to a sink');
|
|
293
|
+
if (this._shards.length > 0 || this._currentRowCount > 0)
|
|
294
|
+
throw new PreserveWriterError('W_FINALIZED', 'beginStream must be called before the first record');
|
|
295
|
+
checkOpts('PreserveWriter.beginStream', opts, FINALIZE_TO_SINK_OPTS, raisePreserveWriter);
|
|
296
|
+
opts = opts || {};
|
|
297
|
+
if (opts.layout !== undefined && opts.layout !== 'stream')
|
|
298
|
+
raisePreserveWriter('W_BAD_SINK', "beginStream requires layout:'stream'");
|
|
299
|
+
validateSink(sink, true, raisePreserveWriter);
|
|
300
|
+
this._sink = sink;
|
|
301
|
+
this._sinkCrcOn = opts.crc !== undefined ? opts.crc === true : this._crc;
|
|
302
|
+
this._sinkCrc = crc32cInit();
|
|
303
|
+
this._sinkWrite(new Uint8Array(CONTAINER_HEADER_BYTES), false);
|
|
304
|
+
this._sinkPos = CONTAINER_HEADER_BYTES;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
_sinkWrite(bytes, fold) {
|
|
308
|
+
if (fold && this._sinkCrcOn) this._sinkCrc = crc32cUpdate(this._sinkCrc, bytes, 0, bytes.length);
|
|
309
|
+
let ret;
|
|
310
|
+
// A throwing sink fails the writer closed and rethrows verbatim; retry -> W_FINALIZED.
|
|
311
|
+
try { ret = this._sink.write(bytes); }
|
|
312
|
+
catch (e) { this._finalized = true; throw e; }
|
|
313
|
+
if (isThenable(ret)) { this._finalized = true; raisePreserveWriter('W_BAD_SINK', 'sink.write returned a thenable; sinks must be synchronous'); }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Preserve payloads carry an internal offset table and are NOT 8-padded (the
|
|
317
|
+
// classic layout packs them back-to-back), so the streaming layout does the
|
|
318
|
+
// same: no inter-shard padding.
|
|
319
|
+
_streamEmitShard(shardBytes, rowCount) {
|
|
320
|
+
const payloadOff = this._sinkPos;
|
|
321
|
+
const payloadLen = shardBytes.length;
|
|
322
|
+
this._sinkWrite(shardBytes, true);
|
|
323
|
+
this._sinkPos += payloadLen;
|
|
324
|
+
this._shards.push({ rowCount, payloadOff, payloadLen });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// PUBLIC. Emit to a caller sink. Returns { totalRows, shardCount, mode,
|
|
328
|
+
// bytesWritten, layout }.
|
|
329
|
+
finalizeToSink(sink, opts) {
|
|
330
|
+
if (this._finalized) throw new PreserveWriterError('W_FINALIZED', 'writer already finalized');
|
|
331
|
+
checkOpts('PreserveWriter.finalizeToSink', opts, FINALIZE_TO_SINK_OPTS, raisePreserveWriter);
|
|
332
|
+
opts = opts || {};
|
|
333
|
+
|
|
334
|
+
if (this._sink !== null) {
|
|
335
|
+
if (sink !== this._sink) raisePreserveWriter('W_BAD_SINK', 'finalizeToSink sink differs from the streaming sink');
|
|
336
|
+
if (opts.layout !== undefined && opts.layout !== 'stream')
|
|
337
|
+
raisePreserveWriter('W_BAD_SINK', "a streaming writer must be finalized with layout:'stream'");
|
|
338
|
+
const crcOn = opts.crc !== undefined ? opts.crc === true : this._sinkCrcOn;
|
|
339
|
+
this._sinkCrcOn = crcOn;
|
|
340
|
+
this._completeInput();
|
|
341
|
+
return this._finishStream(sink, crcOn);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const layout = opts.layout !== undefined ? opts.layout : 'stream';
|
|
345
|
+
const crcOn = opts.crc !== undefined ? opts.crc === true : this._crc;
|
|
346
|
+
validateSink(sink, layout === 'stream', raisePreserveWriter);
|
|
347
|
+
this._completeInput();
|
|
348
|
+
|
|
349
|
+
if (layout === 'prefix') {
|
|
350
|
+
const bytes = this._buildPrefixBytes(crcOn);
|
|
351
|
+
let ret;
|
|
352
|
+
try { ret = sink.write(bytes); }
|
|
353
|
+
catch (e) { this._finalized = true; throw e; }
|
|
354
|
+
if (isThenable(ret)) { this._finalized = true; raisePreserveWriter('W_BAD_SINK', 'sink.write returned a thenable; sinks must be synchronous'); }
|
|
355
|
+
this._finalized = true;
|
|
356
|
+
return { totalRows: this._totalRows, shardCount: this._shards.length, mode: 'preserve', bytesWritten: bytes.length, layout: 'prefix' };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const buffered = this._shards;
|
|
360
|
+
this._shards = [];
|
|
361
|
+
this._sink = sink;
|
|
362
|
+
this._sinkCrcOn = crcOn;
|
|
363
|
+
this._sinkCrc = crc32cInit();
|
|
364
|
+
this._sinkWrite(new Uint8Array(CONTAINER_HEADER_BYTES), false);
|
|
365
|
+
this._sinkPos = CONTAINER_HEADER_BYTES;
|
|
366
|
+
for (let i = 0; i < buffered.length; i++) {
|
|
367
|
+
this._streamEmitShard(buffered[i].bytes, buffered[i].rowCount);
|
|
368
|
+
buffered[i] = null;
|
|
237
369
|
}
|
|
370
|
+
return this._finishStream(sink, crcOn);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
_finishStream(sink, crcOn) {
|
|
374
|
+
const shardCount = this._shards.length;
|
|
375
|
+
const shardDirBytes = shardCount * SHARD_ENTRY_BYTES;
|
|
376
|
+
const shardDirOff = this._sinkPos; // directory follows the payloads (O9)
|
|
377
|
+
const footerOff = shardDirOff + shardDirBytes;
|
|
238
378
|
|
|
239
|
-
|
|
379
|
+
const dir = new Uint8Array(shardDirBytes);
|
|
380
|
+
const ddv = new DataView(dir.buffer);
|
|
240
381
|
for (let i = 0; i < shardCount; i++) {
|
|
241
|
-
|
|
382
|
+
const d = this._shards[i];
|
|
383
|
+
this._emitDirEntryInto(ddv, i * SHARD_ENTRY_BYTES, d.payloadOff, d.payloadLen, d.rowCount);
|
|
242
384
|
}
|
|
385
|
+
this._sinkWrite(dir, true);
|
|
386
|
+
this._sinkPos += shardDirBytes;
|
|
243
387
|
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
dv.setUint32(footerOff + 4, 0, true);
|
|
248
|
-
bytes[footerOff + 8] = 0x31; // '1'
|
|
249
|
-
bytes[footerOff + 9] = 0x4B; // 'K'
|
|
250
|
-
bytes[footerOff + 10] = 0x42; // 'B'
|
|
251
|
-
bytes[footerOff + 11] = 0x4C; // 'L'
|
|
252
|
-
dv.setUint32(footerOff + 12, FOOTER_BYTES, true);
|
|
388
|
+
const header = new Uint8Array(CONTAINER_HEADER_BYTES);
|
|
389
|
+
const hdv = new DataView(header.buffer);
|
|
390
|
+
this._emitHeaderInto(hdv, header, 0, shardDirOff, shardCount, this._totalRows);
|
|
253
391
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
392
|
+
let crcVal = CRC_ABSENT;
|
|
393
|
+
if (crcOn) {
|
|
394
|
+
const headerCrc = crc32cFinal(crc32cUpdate(crc32cInit(), header, 0, CONTAINER_HEADER_BYTES));
|
|
395
|
+
const suffixCrc = crc32cFinal(this._sinkCrc);
|
|
396
|
+
crcVal = crc32cCombine(headerCrc, suffixCrc, footerOff - CONTAINER_HEADER_BYTES);
|
|
397
|
+
}
|
|
398
|
+
const footer = new Uint8Array(FOOTER_BYTES);
|
|
399
|
+
const fdv = new DataView(footer.buffer);
|
|
400
|
+
this._emitFooterInto(fdv, footer, 0, crcVal);
|
|
401
|
+
this._sinkWrite(footer, false);
|
|
402
|
+
this._sinkPos += FOOTER_BYTES;
|
|
403
|
+
|
|
404
|
+
let ret;
|
|
405
|
+
try { ret = sink.writeAt(header, 0); }
|
|
406
|
+
catch (e) { this._finalized = true; throw e; }
|
|
407
|
+
if (isThenable(ret)) { this._finalized = true; raisePreserveWriter('W_BAD_SINK', 'sink.writeAt returned a thenable; sinks must be synchronous'); }
|
|
408
|
+
|
|
409
|
+
this._finalized = true;
|
|
410
|
+
return { totalRows: this._totalRows, shardCount, mode: 'preserve', bytesWritten: this._sinkPos, layout: 'stream' };
|
|
260
411
|
}
|
|
261
412
|
}
|