@zakkster/lite-bake-stream 1.0.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 +457 -0
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/SPEC.md +364 -0
- package/llms.txt +81 -0
- package/package.json +117 -0
- package/src/FileIngest.js +104 -0
- package/src/MultiReader.js +160 -0
- package/src/PreserveReader.js +180 -0
- package/src/PreserveTokenizer.js +172 -0
- package/src/PreserveWriter.js +218 -0
- package/src/RangeReader.js +470 -0
- package/src/Reader.js +349 -0
- package/src/Split.js +359 -0
- package/src/StringTable.js +225 -0
- package/src/Tokenizer.js +691 -0
- package/src/Writer.js +713 -0
- package/src/index.js +193 -0
- package/types/FileIngest.d.ts +44 -0
- package/types/MultiReader.d.ts +36 -0
- package/types/PreserveReader.d.ts +44 -0
- package/types/PreserveTokenizer.d.ts +27 -0
- package/types/PreserveWriter.d.ts +35 -0
- package/types/RangeReader.d.ts +93 -0
- package/types/Reader.d.ts +85 -0
- package/types/Split.d.ts +66 -0
- package/types/StringTable.d.ts +23 -0
- package/types/Tokenizer.d.ts +42 -0
- package/types/Writer.d.ts +92 -0
- package/types/index.d.ts +58 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// @zakkster/lite-bake-stream / PreserveTokenizer
|
|
2
|
+
// NDJSON record-boundary scanner for preserve-mode. Copyright (c) 2026 Zahary Shinikchiev. MIT.
|
|
3
|
+
//
|
|
4
|
+
// The schema-mode Tokenizer walks the full JSON AST (numbers, strings, keys,
|
|
5
|
+
// object/array structure) so the Writer can pack values into typed lanes.
|
|
6
|
+
// Preserve-mode doesn't crack open records — it just needs to find where one
|
|
7
|
+
// record ends and the next begins, so the record's original bytes can be
|
|
8
|
+
// shoved into a shard intact.
|
|
9
|
+
//
|
|
10
|
+
// Contract:
|
|
11
|
+
// - Input: NDJSON bytes. One JSON value per line, delimited by 0x0A.
|
|
12
|
+
// - Output: sink.onRecord(bytes, from, to) for each complete record.
|
|
13
|
+
// The byte range excludes the trailing newline. CRLF endings are
|
|
14
|
+
// stripped (trailing \r trimmed). Empty and whitespace-only lines
|
|
15
|
+
// are skipped.
|
|
16
|
+
// - Chunk safety: records that span multiple feed() calls are buffered
|
|
17
|
+
// internally and emitted only when the closing newline is seen.
|
|
18
|
+
// - Depth aware: the scanner tracks brace/bracket depth AND whether it's
|
|
19
|
+
// inside a string (with backslash-escape awareness), so a literal
|
|
20
|
+
// 0x0A only counts as a boundary when it's outside any string and
|
|
21
|
+
// at depth 0. A well-formed NDJSON stream never has a raw 0x0A
|
|
22
|
+
// inside a string (must be escaped as \n = 0x5C 0x6E), but the
|
|
23
|
+
// scanner handles adversarial input safely.
|
|
24
|
+
//
|
|
25
|
+
// Errors (stable E_* codes):
|
|
26
|
+
// E_TRUNCATED - end of input reached mid-record (depth != 0 or in-string)
|
|
27
|
+
// E_RECORD_TOO_LARGE - single record exceeds maxRecordBytes (default: none)
|
|
28
|
+
|
|
29
|
+
export const VERSION = '1.0.0';
|
|
30
|
+
|
|
31
|
+
export class PreserveTokenizerError extends Error {
|
|
32
|
+
constructor(code, msg) { super(msg); this.code = code; this.name = 'PreserveTokenizerError'; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Byte values used by the scanner (named so the hot loop reads cleanly).
|
|
36
|
+
const BYTE_LF = 0x0A;
|
|
37
|
+
const BYTE_CR = 0x0D;
|
|
38
|
+
const BYTE_QUOTE = 0x22;
|
|
39
|
+
const BYTE_BSLASH = 0x5C;
|
|
40
|
+
const BYTE_LBRACE = 0x7B;
|
|
41
|
+
const BYTE_RBRACE = 0x7D;
|
|
42
|
+
const BYTE_LBRACK = 0x5B;
|
|
43
|
+
const BYTE_RBRACK = 0x5D;
|
|
44
|
+
const BYTE_SPACE = 0x20;
|
|
45
|
+
const BYTE_TAB = 0x09;
|
|
46
|
+
|
|
47
|
+
const INITIAL_BUF = 1 << 16; // 64 KiB — grows on demand for larger records
|
|
48
|
+
|
|
49
|
+
export class PreserveTokenizer {
|
|
50
|
+
constructor(sink, opts) {
|
|
51
|
+
if (!sink || typeof sink.onRecord !== 'function') {
|
|
52
|
+
throw new TypeError('PreserveTokenizer: sink must implement onRecord(bytes, from, to)');
|
|
53
|
+
}
|
|
54
|
+
opts = opts || {};
|
|
55
|
+
if (opts.framing && opts.framing !== 'ndjson') {
|
|
56
|
+
// 'array' framing (top-level `[a, b, c]`) is a future addition.
|
|
57
|
+
throw new PreserveTokenizerError('E_UNSUPPORTED_FRAMING',
|
|
58
|
+
'preserve-mode currently supports NDJSON only; got framing=' + opts.framing);
|
|
59
|
+
}
|
|
60
|
+
this._sink = sink;
|
|
61
|
+
this._maxRecordBytes = typeof opts.maxRecordBytes === 'number' ? opts.maxRecordBytes : 0; // 0 = unlimited
|
|
62
|
+
// Accumulator: bytes seen since the last emitted record's terminating LF.
|
|
63
|
+
this._buf = new Uint8Array(INITIAL_BUF);
|
|
64
|
+
this._bufLen = 0;
|
|
65
|
+
this._recordStart = 0;
|
|
66
|
+
// Scanner state
|
|
67
|
+
this._depth = 0;
|
|
68
|
+
this._inString = false;
|
|
69
|
+
this._escape = false;
|
|
70
|
+
this._absOffset = 0; // for error messages
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
feed(chunk) {
|
|
74
|
+
if (!(chunk instanceof Uint8Array)) {
|
|
75
|
+
throw new TypeError('PreserveTokenizer.feed: expected Uint8Array');
|
|
76
|
+
}
|
|
77
|
+
if (chunk.length === 0) return;
|
|
78
|
+
this._ensureCapacity(this._bufLen + chunk.length);
|
|
79
|
+
this._buf.set(chunk, this._bufLen);
|
|
80
|
+
const scanStart = this._bufLen;
|
|
81
|
+
this._bufLen += chunk.length;
|
|
82
|
+
this._scan(scanStart);
|
|
83
|
+
this._compact();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
end() {
|
|
87
|
+
if (this._bufLen > this._recordStart) {
|
|
88
|
+
// There's pending content that never terminated.
|
|
89
|
+
if (this._depth !== 0 || this._inString || this._escape) {
|
|
90
|
+
throw new PreserveTokenizerError('E_TRUNCATED',
|
|
91
|
+
'input ended mid-record at byte ' + (this._absOffset + this._bufLen) +
|
|
92
|
+
' (depth=' + this._depth + ', inString=' + this._inString + ')');
|
|
93
|
+
}
|
|
94
|
+
// Depth is balanced and we're not inside a string -- treat the tail as
|
|
95
|
+
// the last record even without a terminating LF.
|
|
96
|
+
this._emitRecord(this._recordStart, this._bufLen);
|
|
97
|
+
this._recordStart = this._bufLen;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_scan(from) {
|
|
102
|
+
const buf = this._buf;
|
|
103
|
+
const to = this._bufLen;
|
|
104
|
+
let depth = this._depth;
|
|
105
|
+
let inString = this._inString;
|
|
106
|
+
let escape = this._escape;
|
|
107
|
+
for (let i = from; i < to; i++) {
|
|
108
|
+
const b = buf[i];
|
|
109
|
+
if (escape) { escape = false; continue; }
|
|
110
|
+
if (inString) {
|
|
111
|
+
if (b === BYTE_BSLASH) escape = true;
|
|
112
|
+
else if (b === BYTE_QUOTE) inString = false;
|
|
113
|
+
} else {
|
|
114
|
+
if (b === BYTE_QUOTE) {
|
|
115
|
+
inString = true;
|
|
116
|
+
} else if (b === BYTE_LBRACE || b === BYTE_LBRACK) {
|
|
117
|
+
depth++;
|
|
118
|
+
} else if (b === BYTE_RBRACE || b === BYTE_RBRACK) {
|
|
119
|
+
depth--;
|
|
120
|
+
if (depth < 0) {
|
|
121
|
+
this._depth = depth; this._inString = inString; this._escape = escape;
|
|
122
|
+
throw new PreserveTokenizerError('E_UNBALANCED',
|
|
123
|
+
'closing bracket without matching open at byte ' + (this._absOffset + i));
|
|
124
|
+
}
|
|
125
|
+
} else if (b === BYTE_LF && depth === 0) {
|
|
126
|
+
// Record boundary. Flush [recordStart, i).
|
|
127
|
+
this._depth = depth; this._inString = inString; this._escape = escape;
|
|
128
|
+
this._emitRecord(this._recordStart, i);
|
|
129
|
+
this._recordStart = i + 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (this._maxRecordBytes > 0 && (i - this._recordStart) > this._maxRecordBytes) {
|
|
133
|
+
throw new PreserveTokenizerError('E_RECORD_TOO_LARGE',
|
|
134
|
+
'record starting at byte ' + (this._absOffset + this._recordStart) +
|
|
135
|
+
' exceeded maxRecordBytes=' + this._maxRecordBytes);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
this._depth = depth;
|
|
139
|
+
this._inString = inString;
|
|
140
|
+
this._escape = escape;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_emitRecord(from, to) {
|
|
144
|
+
// Trim trailing CR (CRLF ending).
|
|
145
|
+
while (to > from && this._buf[to - 1] === BYTE_CR) to--;
|
|
146
|
+
if (to === from) return;
|
|
147
|
+
// Skip whitespace-only records (blank lines between records).
|
|
148
|
+
let firstNonWs = from;
|
|
149
|
+
while (firstNonWs < to && (this._buf[firstNonWs] === BYTE_SPACE || this._buf[firstNonWs] === BYTE_TAB)) firstNonWs++;
|
|
150
|
+
if (firstNonWs === to) return;
|
|
151
|
+
this._sink.onRecord(this._buf, from, to);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
_compact() {
|
|
155
|
+
if (this._recordStart === 0) return;
|
|
156
|
+
// Move [recordStart, bufLen) to the front. copyWithin is in-place.
|
|
157
|
+
const remaining = this._bufLen - this._recordStart;
|
|
158
|
+
if (remaining > 0) this._buf.copyWithin(0, this._recordStart, this._bufLen);
|
|
159
|
+
this._absOffset += this._recordStart;
|
|
160
|
+
this._bufLen = remaining;
|
|
161
|
+
this._recordStart = 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_ensureCapacity(need) {
|
|
165
|
+
if (this._buf.length >= need) return;
|
|
166
|
+
let cap = this._buf.length;
|
|
167
|
+
while (cap < need) cap *= 2;
|
|
168
|
+
const next = new Uint8Array(cap);
|
|
169
|
+
next.set(this._buf.subarray(0, this._bufLen));
|
|
170
|
+
this._buf = next;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// @zakkster/lite-bake-stream / PreserveWriter
|
|
2
|
+
// Copyright (c) 2026 Zahary Shinikchiev. MIT.
|
|
3
|
+
//
|
|
4
|
+
// Preserve-mode sink: opaque record blobs packed into shards, with a trailing
|
|
5
|
+
// u32 offset table per shard. No schema, no lanes, no string table, no zone
|
|
6
|
+
// maps. Bytes in, same bytes out.
|
|
7
|
+
//
|
|
8
|
+
// Shard payload layout:
|
|
9
|
+
// [ blob0 | blob1 | ... | blob(N-1) | u32 offsets[N] ]
|
|
10
|
+
// ^payload_off ^offsetTableStart = payload_off + payload_len - N*4
|
|
11
|
+
//
|
|
12
|
+
// Container header uses the same 48-byte layout as schema-mode, but with:
|
|
13
|
+
// - flags byte at offset 7 has bit 0 set (preserve mode)
|
|
14
|
+
// - schema_block_off = 0
|
|
15
|
+
// - metadata_off = 0
|
|
16
|
+
// Reader dispatch happens via the flag bit; a schema-mode Reader refuses this
|
|
17
|
+
// container with R_WRONG_MODE.
|
|
18
|
+
|
|
19
|
+
export const VERSION = '1.0.0';
|
|
20
|
+
|
|
21
|
+
export class PreserveWriterError extends Error {
|
|
22
|
+
constructor(code, msg) { super(msg); this.code = code; this.name = 'PreserveWriterError'; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const CONTAINER_HEADER_BYTES = 48;
|
|
26
|
+
const SHARD_ENTRY_BYTES = 40;
|
|
27
|
+
const FOOTER_BYTES = 16;
|
|
28
|
+
const DEFAULT_TARGET_SHARD_BYTES = 8 * 1024 * 1024; // 8 MiB — smaller than schema mode
|
|
29
|
+
const INITIAL_OFFSETS_CAP = 4096;
|
|
30
|
+
|
|
31
|
+
export class PreserveWriter {
|
|
32
|
+
constructor(opts) {
|
|
33
|
+
opts = opts || {};
|
|
34
|
+
this._targetShardBytes = opts.targetShardBytes || DEFAULT_TARGET_SHARD_BYTES;
|
|
35
|
+
this._shards = [];
|
|
36
|
+
this._totalRows = 0;
|
|
37
|
+
this._finalized = false;
|
|
38
|
+
this._allocateShard();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_allocateShard() {
|
|
42
|
+
// Pre-allocate up to targetShardBytes for zero-GC record writes.
|
|
43
|
+
this._currentBlobs = new Uint8Array(this._targetShardBytes);
|
|
44
|
+
this._currentOffsets = new Uint32Array(INITIAL_OFFSETS_CAP);
|
|
45
|
+
this._currentBlobBytes = 0;
|
|
46
|
+
this._currentRowCount = 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Sink protocol for PreserveTokenizer. Bytes are copied immediately into
|
|
50
|
+
// the shard buffer, so the caller may reuse `bytes` on return.
|
|
51
|
+
onRecord(bytes, from, to) {
|
|
52
|
+
if (this._finalized) throw new PreserveWriterError('W_FINALIZED', 'writer already finalized');
|
|
53
|
+
const len = to - from;
|
|
54
|
+
if (len === 0) return;
|
|
55
|
+
|
|
56
|
+
// Would this record push us over the soft cap? Roll to a new shard first,
|
|
57
|
+
// unless the current shard is still empty (i.e. this is an oversized
|
|
58
|
+
// single record; keep it in its own shard).
|
|
59
|
+
if (this._currentBlobBytes + len > this._targetShardBytes && this._currentRowCount > 0) {
|
|
60
|
+
this._finalizeCurrentShard();
|
|
61
|
+
this._allocateShard();
|
|
62
|
+
}
|
|
63
|
+
// Grow shard buffer if this single record exceeds the pre-allocated size.
|
|
64
|
+
if (this._currentBlobBytes + len > this._currentBlobs.length) {
|
|
65
|
+
this._growBlobBuffer(this._currentBlobBytes + len);
|
|
66
|
+
}
|
|
67
|
+
// Grow offsets array if we've hit the pre-allocated ceiling.
|
|
68
|
+
if (this._currentRowCount >= this._currentOffsets.length) {
|
|
69
|
+
this._growOffsetsArray();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Copy record bytes into the shard buffer at the current cursor.
|
|
73
|
+
// Uint8Array.set is the fastest byte copy V8 offers; subarray on the RHS
|
|
74
|
+
// creates a small view (young-gen, collected trivially).
|
|
75
|
+
this._currentOffsets[this._currentRowCount] = this._currentBlobBytes;
|
|
76
|
+
if (from === 0 && to === bytes.length) {
|
|
77
|
+
this._currentBlobs.set(bytes, this._currentBlobBytes);
|
|
78
|
+
} else {
|
|
79
|
+
this._currentBlobs.set(bytes.subarray(from, to), this._currentBlobBytes);
|
|
80
|
+
}
|
|
81
|
+
this._currentBlobBytes += len;
|
|
82
|
+
this._currentRowCount++;
|
|
83
|
+
this._totalRows++;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Public API for callers that want to feed complete records directly.
|
|
87
|
+
writeRecord(bytes) {
|
|
88
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
89
|
+
throw new TypeError('PreserveWriter.writeRecord: expected Uint8Array');
|
|
90
|
+
}
|
|
91
|
+
this.onRecord(bytes, 0, bytes.length);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_growBlobBuffer(needed) {
|
|
95
|
+
let cap = this._currentBlobs.length;
|
|
96
|
+
while (cap < needed) cap *= 2;
|
|
97
|
+
const next = new Uint8Array(cap);
|
|
98
|
+
next.set(this._currentBlobs.subarray(0, this._currentBlobBytes));
|
|
99
|
+
this._currentBlobs = next;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_growOffsetsArray() {
|
|
103
|
+
const next = new Uint32Array(this._currentOffsets.length * 2);
|
|
104
|
+
next.set(this._currentOffsets);
|
|
105
|
+
this._currentOffsets = next;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_finalizeCurrentShard() {
|
|
109
|
+
if (this._currentRowCount === 0) return;
|
|
110
|
+
const rowCount = this._currentRowCount;
|
|
111
|
+
const blobLen = this._currentBlobBytes;
|
|
112
|
+
const offsetTableBytes = rowCount * 4;
|
|
113
|
+
const shardBytesLen = blobLen + offsetTableBytes;
|
|
114
|
+
|
|
115
|
+
const shardBytes = new Uint8Array(shardBytesLen);
|
|
116
|
+
shardBytes.set(this._currentBlobs.subarray(0, blobLen), 0);
|
|
117
|
+
// Trailing offset table: u32 LE per record.
|
|
118
|
+
const dv = new DataView(shardBytes.buffer, shardBytes.byteOffset, shardBytes.byteLength);
|
|
119
|
+
for (let i = 0; i < rowCount; i++) {
|
|
120
|
+
dv.setUint32(blobLen + i * 4, this._currentOffsets[i], true);
|
|
121
|
+
}
|
|
122
|
+
this._shards.push({
|
|
123
|
+
bytes: shardBytes,
|
|
124
|
+
rowCount,
|
|
125
|
+
blobLen,
|
|
126
|
+
});
|
|
127
|
+
// Zero out the working buffers so a stale ref can't leak old bytes into
|
|
128
|
+
// the next shard by accident. The buffers are reused via _allocateShard.
|
|
129
|
+
this._currentBlobs = null;
|
|
130
|
+
this._currentOffsets = null;
|
|
131
|
+
this._currentBlobBytes = 0;
|
|
132
|
+
this._currentRowCount = 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
finalize() {
|
|
136
|
+
if (this._finalized) throw new PreserveWriterError('W_FINALIZED', 'writer already finalized');
|
|
137
|
+
if (this._currentRowCount > 0) this._finalizeCurrentShard();
|
|
138
|
+
if (this._shards.length === 0) {
|
|
139
|
+
// Emit an empty container? Or error? Match schema-mode: throw.
|
|
140
|
+
throw new PreserveWriterError('W_EMPTY_INPUT', 'no records written; nothing to finalize');
|
|
141
|
+
}
|
|
142
|
+
this._finalized = true;
|
|
143
|
+
return this._assembleContainer();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get totalRows() { return this._totalRows; }
|
|
147
|
+
get shardCount() { return this._shards.length; }
|
|
148
|
+
|
|
149
|
+
_assembleContainer() {
|
|
150
|
+
const shardCount = this._shards.length;
|
|
151
|
+
const shardDirBytes = shardCount * SHARD_ENTRY_BYTES;
|
|
152
|
+
|
|
153
|
+
// Layout: header (48) | shard directory | shard payloads | footer (16)
|
|
154
|
+
// No schema block, no zone maps in preserve mode; schema_block_off = 0.
|
|
155
|
+
const shardDirOff = CONTAINER_HEADER_BYTES;
|
|
156
|
+
let cursor = shardDirOff + shardDirBytes;
|
|
157
|
+
const shardPayloadOffsets = new Array(shardCount);
|
|
158
|
+
for (let i = 0; i < shardCount; i++) {
|
|
159
|
+
shardPayloadOffsets[i] = cursor;
|
|
160
|
+
cursor += this._shards[i].bytes.length;
|
|
161
|
+
}
|
|
162
|
+
const totalBytes = cursor + FOOTER_BYTES;
|
|
163
|
+
|
|
164
|
+
const container = new ArrayBuffer(totalBytes);
|
|
165
|
+
const dv = new DataView(container);
|
|
166
|
+
const bytes = new Uint8Array(container);
|
|
167
|
+
|
|
168
|
+
// Header
|
|
169
|
+
bytes[0] = 0x4C; bytes[1] = 0x42; bytes[2] = 0x4B; bytes[3] = 0x31; // 'LBK1'
|
|
170
|
+
dv.setUint16(4, 1, true); // format_version
|
|
171
|
+
bytes[6] = 1; // endian LE
|
|
172
|
+
bytes[7] = 0x01; // flags: bit 0 = preserve mode
|
|
173
|
+
dv.setBigUint64(8, 0n, true); // schema_block_off = 0 (no schema)
|
|
174
|
+
dv.setBigUint64(16, 0n, true); // metadata_off = 0 (no zone maps)
|
|
175
|
+
dv.setBigUint64(24, BigInt(shardDirOff), true);
|
|
176
|
+
dv.setUint32(32, shardCount, true);
|
|
177
|
+
dv.setUint32(36, 0, true); // reserved1
|
|
178
|
+
dv.setBigUint64(40, BigInt(this._totalRows), true);
|
|
179
|
+
|
|
180
|
+
// Shard directory (40 bytes per entry, u64 payload_off, u32 payload_len,
|
|
181
|
+
// u32 row_count, u16 min_reader_version, u16 flags, u32 reserved,
|
|
182
|
+
// u64 local_string_off=0, u64 local_string_len=0)
|
|
183
|
+
for (let i = 0; i < shardCount; i++) {
|
|
184
|
+
const entryOff = shardDirOff + i * SHARD_ENTRY_BYTES;
|
|
185
|
+
const s = this._shards[i];
|
|
186
|
+
dv.setBigUint64(entryOff + 0, BigInt(shardPayloadOffsets[i]), true);
|
|
187
|
+
dv.setUint32(entryOff + 8, s.bytes.length, true);
|
|
188
|
+
dv.setUint32(entryOff + 12, s.rowCount, true);
|
|
189
|
+
dv.setUint16(entryOff + 16, 1, true); // min_reader_version
|
|
190
|
+
dv.setUint16(entryOff + 18, 0, true); // shard flags
|
|
191
|
+
dv.setUint32(entryOff + 20, 0, true); // reserved
|
|
192
|
+
dv.setBigUint64(entryOff + 24, 0n, true); // no string table
|
|
193
|
+
dv.setBigUint64(entryOff + 32, 0n, true);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Shard payloads
|
|
197
|
+
for (let i = 0; i < shardCount; i++) {
|
|
198
|
+
bytes.set(this._shards[i].bytes, shardPayloadOffsets[i]);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Footer
|
|
202
|
+
const footerOff = totalBytes - FOOTER_BYTES;
|
|
203
|
+
dv.setUint32(footerOff + 0, 0xFFFFFFFF, true); // CRC absent
|
|
204
|
+
dv.setUint32(footerOff + 4, 0, true);
|
|
205
|
+
bytes[footerOff + 8] = 0x31; // '1'
|
|
206
|
+
bytes[footerOff + 9] = 0x4B; // 'K'
|
|
207
|
+
bytes[footerOff + 10] = 0x42; // 'B'
|
|
208
|
+
bytes[footerOff + 11] = 0x4C; // 'L'
|
|
209
|
+
dv.setUint32(footerOff + 12, FOOTER_BYTES, true);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
buffer: container,
|
|
213
|
+
totalRows: this._totalRows,
|
|
214
|
+
shardCount,
|
|
215
|
+
mode: 'preserve',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|