@zakkster/lite-bake-stream 1.1.0 → 1.3.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 +58 -0
- package/README.md +1 -1
- package/llms.txt +2 -2
- package/package.json +5 -1
- package/src/FileIngest.js +25 -7
- package/src/MultiReader.js +20 -4
- package/src/Opts.js +189 -0
- package/src/PreserveReader.js +69 -13
- package/src/PreserveTokenizer.js +35 -4
- package/src/PreserveWriter.js +15 -2
- package/src/RangeReader.js +146 -16
- package/src/Reader.js +125 -25
- package/src/Split.js +40 -8
- package/src/StringTable.js +1 -1
- package/src/Tokenizer.js +68 -30
- package/src/Views.js +25 -0
- package/src/Writer.js +87 -15
- package/src/index.js +30 -24
package/src/Writer.js
CHANGED
|
@@ -26,10 +26,31 @@
|
|
|
26
26
|
// W_MIXED_LANE_TYPES - a field saw both number and string values
|
|
27
27
|
// W_LANE_MISMATCH - post-freeze: value type doesn't match schema lane
|
|
28
28
|
// W_UNKNOWN_LANE_KIND - explicit schema declares an unknown laneKind
|
|
29
|
+
// W_DUPLICATE_FIELD - schema declares the same field name twice
|
|
30
|
+
// W_SCHEMA_TOO_WIDE - a field offset exceeds the u16 offset_in_row ceiling
|
|
31
|
+
// W_FIELD_NAME_INVALID - field name empty, non-string, or > 255 UTF-8 bytes
|
|
32
|
+
// W_FINALIZED - a sink event or finalize() after finalize()
|
|
33
|
+
// E_UNKNOWN_OPTION - unknown constructor option key
|
|
34
|
+
// E_OPTION_VALUE - constructor option value out of domain
|
|
29
35
|
|
|
30
36
|
import { StringTable } from './StringTable.js';
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
import { checkOpts } from './Opts.js';
|
|
38
|
+
|
|
39
|
+
export const VERSION = '1.3.0';
|
|
40
|
+
|
|
41
|
+
const U32_MAX = 4294967295;
|
|
42
|
+
// Post-finalize sentinel for _recordDepth. Chosen = 2 so every post-finalize
|
|
43
|
+
// sink event lands in an existing cold arm (onNumber/onString depth != 1,
|
|
44
|
+
// onStartObject depth > 1) without adding a per-record compare on the hot path.
|
|
45
|
+
// It is NOT a real nesting depth: onEndObject is unreachable post-finalize
|
|
46
|
+
// because onStartObject throws W_FINALIZED first.
|
|
47
|
+
const FINALIZED_DEPTH = 2;
|
|
48
|
+
const WRITER_OPTS = {
|
|
49
|
+
schema: { t: 'obj', nullable: true },
|
|
50
|
+
targetShardBytes: { t: 'int', min: 1, max: U32_MAX },
|
|
51
|
+
sampleBytes: { t: 'int', min: 0, max: U32_MAX },
|
|
52
|
+
};
|
|
53
|
+
function raiseWriter(code, msg) { throw new WriterError(code, msg); }
|
|
33
54
|
|
|
34
55
|
const CONTAINER_HEADER_BYTES = 48;
|
|
35
56
|
const SHARD_ENTRY_BYTES = 40;
|
|
@@ -43,6 +64,11 @@ const FIELD_FLAGS_NONE = 0;
|
|
|
43
64
|
const SHARD_MIN_READER_VERSION = 1;
|
|
44
65
|
const SHARD_FLAGS_NONE = 0;
|
|
45
66
|
|
|
67
|
+
// Shared empty view for F64-only shards, which emit NO local string table
|
|
68
|
+
// (SPEC 3.4: local_string_off/local_string_len are 0 when a shard has no
|
|
69
|
+
// U32-lane fields). Read-only; never written into.
|
|
70
|
+
const EMPTY_STRING_TABLE_BYTES = new Uint8Array(0);
|
|
71
|
+
|
|
46
72
|
const FORMAT_VERSION = 1;
|
|
47
73
|
const ENDIAN_LE = 1;
|
|
48
74
|
|
|
@@ -157,10 +183,11 @@ class SampleBuffer {
|
|
|
157
183
|
|
|
158
184
|
export class Writer {
|
|
159
185
|
constructor(opts) {
|
|
186
|
+
checkOpts('Writer', opts, WRITER_OPTS, raiseWriter);
|
|
160
187
|
opts = opts || {};
|
|
161
|
-
this._targetShardBytes = opts.targetShardBytes
|
|
162
|
-
this._sampleBytes = opts.sampleBytes
|
|
163
|
-
this._explicitSchema = opts.schema
|
|
188
|
+
this._targetShardBytes = opts.targetShardBytes !== undefined ? opts.targetShardBytes : DEFAULT_SHARD_BYTES;
|
|
189
|
+
this._sampleBytes = opts.sampleBytes !== undefined ? opts.sampleBytes : this._targetShardBytes;
|
|
190
|
+
this._explicitSchema = opts.schema !== undefined ? opts.schema : null;
|
|
164
191
|
|
|
165
192
|
// Frozen schema state
|
|
166
193
|
this._schema = null; // { fields: [{name, laneKind, offsetInRow}], rowStride }
|
|
@@ -201,7 +228,10 @@ export class Writer {
|
|
|
201
228
|
|
|
202
229
|
onStartObject() {
|
|
203
230
|
this._recordDepth++;
|
|
204
|
-
if (this._recordDepth > 1)
|
|
231
|
+
if (this._recordDepth > 1) {
|
|
232
|
+
if (this._finalized) throw new WriterError('W_FINALIZED', 'writer already finalized');
|
|
233
|
+
throw new WriterError('W_NESTED_UNSUPPORTED', 'nested object inside record');
|
|
234
|
+
}
|
|
205
235
|
if (this._schema) {
|
|
206
236
|
// clear scratch slots
|
|
207
237
|
const f = this._rowValueSlotsF64, u = this._rowValueSlotsU32;
|
|
@@ -216,7 +246,8 @@ export class Writer {
|
|
|
216
246
|
}
|
|
217
247
|
|
|
218
248
|
onStartArray() {
|
|
219
|
-
if (this.
|
|
249
|
+
if (this._finalized) throw new WriterError('W_FINALIZED', 'writer already finalized');
|
|
250
|
+
if (this._recordDepth === 0) throw new WriterError('W_TOP_LEVEL_NOT_OBJECT', 'top-level array is not an object; LBK1 schema mode stores objects only (SPEC 5.1)');
|
|
220
251
|
throw new WriterError('W_NESTED_UNSUPPORTED', 'nested array inside record');
|
|
221
252
|
}
|
|
222
253
|
|
|
@@ -236,7 +267,10 @@ export class Writer {
|
|
|
236
267
|
}
|
|
237
268
|
|
|
238
269
|
onNumber(v) {
|
|
239
|
-
if (this._recordDepth !== 1)
|
|
270
|
+
if (this._recordDepth !== 1) {
|
|
271
|
+
if (this._finalized) throw new WriterError('W_FINALIZED', 'writer already finalized');
|
|
272
|
+
throw new WriterError('W_TOP_LEVEL_NOT_OBJECT', 'top-level number is not an object; LBK1 schema mode stores objects only (SPEC 5.1)');
|
|
273
|
+
}
|
|
240
274
|
if (this._schema) {
|
|
241
275
|
const idx = this._currentFieldIdx;
|
|
242
276
|
if (idx < 0) return;
|
|
@@ -255,7 +289,10 @@ export class Writer {
|
|
|
255
289
|
onNull() { this.onNumber(0); }
|
|
256
290
|
|
|
257
291
|
onString(bytes, from, to) {
|
|
258
|
-
if (this._recordDepth
|
|
292
|
+
if (this._recordDepth !== 1) {
|
|
293
|
+
if (this._finalized) throw new WriterError('W_FINALIZED', 'writer already finalized');
|
|
294
|
+
throw new WriterError('W_TOP_LEVEL_NOT_OBJECT', 'top-level string is not an object; LBK1 schema mode stores objects only (SPEC 5.1)');
|
|
295
|
+
}
|
|
259
296
|
if (this._schema) {
|
|
260
297
|
const idx = this._currentFieldIdx;
|
|
261
298
|
if (idx < 0) return;
|
|
@@ -327,14 +364,45 @@ export class Writer {
|
|
|
327
364
|
|
|
328
365
|
_finalizeSchema(fields, rowStride) {
|
|
329
366
|
this._schema = { fields, rowStride };
|
|
367
|
+
// BS-15: a shard with no U32-lane field emits NO local string table.
|
|
368
|
+
// Computed once at freeze, consulted per shard at finalize / assembly.
|
|
369
|
+
this._hasU32 = false;
|
|
370
|
+
for (let i = 0; i < fields.length; i++) {
|
|
371
|
+
if (fields[i].laneKind === LANE_U32) { this._hasU32 = true; break; }
|
|
372
|
+
}
|
|
330
373
|
|
|
331
374
|
const enc = new TextEncoder();
|
|
332
375
|
this._fieldNamesUtf8 = new Array(fields.length);
|
|
333
376
|
this._fieldNameHashes = new Uint32Array(fields.length);
|
|
334
377
|
this._fieldLaneKinds = new Uint8Array(fields.length);
|
|
335
378
|
this._fieldOffsets = new Uint16Array(fields.length);
|
|
379
|
+
// Freeze-time schema validation (BS-02/BS-03): runs once per schema on both
|
|
380
|
+
// freeze paths (explicit + sample-drain funnel here), never per record.
|
|
381
|
+
const seenNames = new Map();
|
|
336
382
|
for (let i = 0; i < fields.length; i++) {
|
|
337
|
-
const
|
|
383
|
+
const name = fields[i].name;
|
|
384
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
385
|
+
throw new WriterError('W_FIELD_NAME_INVALID',
|
|
386
|
+
'field name at index ' + i + ' must be a non-empty string of at most 255 UTF-8 bytes; got ' +
|
|
387
|
+
(typeof name === 'string' ? '0 bytes' : typeof name));
|
|
388
|
+
}
|
|
389
|
+
const b = enc.encode(name);
|
|
390
|
+
if (b.length > 255) {
|
|
391
|
+
throw new WriterError('W_FIELD_NAME_INVALID',
|
|
392
|
+
'field name at index ' + i + ' must be a non-empty string of at most 255 UTF-8 bytes; got ' +
|
|
393
|
+
b.length + ' bytes');
|
|
394
|
+
}
|
|
395
|
+
const first = seenNames.get(name);
|
|
396
|
+
if (first !== undefined) {
|
|
397
|
+
throw new WriterError('W_DUPLICATE_FIELD',
|
|
398
|
+
"duplicate field name '" + name + "' at index " + i + ' (first declared at index ' + first + ')');
|
|
399
|
+
}
|
|
400
|
+
seenNames.set(name, i);
|
|
401
|
+
if (fields[i].offsetInRow > 65535) {
|
|
402
|
+
throw new WriterError('W_SCHEMA_TOO_WIDE',
|
|
403
|
+
"field '" + name + "' at offset " + fields[i].offsetInRow +
|
|
404
|
+
' exceeds the u16 offset_in_row ceiling (max 65535)');
|
|
405
|
+
}
|
|
338
406
|
this._fieldNamesUtf8[i] = b;
|
|
339
407
|
this._fieldNameHashes[i] = hashBytes(b, 0, b.length);
|
|
340
408
|
this._fieldLaneKinds[i] = fields[i].laneKind;
|
|
@@ -435,9 +503,11 @@ export class Writer {
|
|
|
435
503
|
const usedBytes = this._currentShardRowCount * this._schema.rowStride;
|
|
436
504
|
const copy = new Uint8Array(usedBytes);
|
|
437
505
|
copy.set(this._currentShardBytes.subarray(0, usedBytes));
|
|
438
|
-
// Serialize the current shard's string table
|
|
506
|
+
// Serialize the current shard's string table. A shard whose schema has no
|
|
507
|
+
// U32 lane emits NO table at all (BS-15 / SPEC 3.4): zero bytes here means
|
|
508
|
+
// the ShardEntry carries local_string_off = 0 / local_string_len = 0.
|
|
439
509
|
const st = this._perShardStringTable || this._stringTable;
|
|
440
|
-
const
|
|
510
|
+
const stBytes = this._hasU32 ? st.serialize().bytes : EMPTY_STRING_TABLE_BYTES;
|
|
441
511
|
// Snapshot zone-map bounds for this shard. Copy so subsequent shards can
|
|
442
512
|
// reuse _currentShardMins/Maxes without clobbering the pushed record.
|
|
443
513
|
const T = this._trackedFieldIndices.length;
|
|
@@ -448,7 +518,7 @@ export class Writer {
|
|
|
448
518
|
this._shards.push({
|
|
449
519
|
bytes: copy,
|
|
450
520
|
rowCount: this._currentShardRowCount,
|
|
451
|
-
stringTableBytes:
|
|
521
|
+
stringTableBytes: stBytes,
|
|
452
522
|
mins: shardMins,
|
|
453
523
|
maxes: shardMaxes,
|
|
454
524
|
});
|
|
@@ -526,7 +596,7 @@ export class Writer {
|
|
|
526
596
|
}
|
|
527
597
|
|
|
528
598
|
finalize() {
|
|
529
|
-
if (this._finalized)
|
|
599
|
+
if (this._finalized) throw new WriterError('W_FINALIZED', 'writer already finalized');
|
|
530
600
|
if (!this._schema) {
|
|
531
601
|
if (this._sample.rowCount === 0) throw new WriterError('W_EMPTY_INPUT', 'no records to write');
|
|
532
602
|
this._freezeSchemaFromSample();
|
|
@@ -536,6 +606,8 @@ export class Writer {
|
|
|
536
606
|
if (this._shards.length === 0) throw new WriterError('W_EMPTY_INPUT', 'no records to write');
|
|
537
607
|
this._container = this._assembleContainer();
|
|
538
608
|
this._finalized = true;
|
|
609
|
+
// Post-finalize sentinel: route every later sink event into a cold arm.
|
|
610
|
+
this._recordDepth = FINALIZED_DEPTH;
|
|
539
611
|
return this._container;
|
|
540
612
|
}
|
|
541
613
|
|
|
@@ -662,7 +734,7 @@ export class Writer {
|
|
|
662
734
|
dv.setUint16(entryOff + 16, SHARD_MIN_READER_VERSION, true);
|
|
663
735
|
dv.setUint16(entryOff + 18, SHARD_FLAGS_NONE, true);
|
|
664
736
|
dv.setUint32(entryOff + 20, 0, true); // reserved
|
|
665
|
-
dv.setBigUint64(entryOff + 24, BigInt(shardStringTableOffsets[i]), true);
|
|
737
|
+
dv.setBigUint64(entryOff + 24, BigInt(shardStringTableLens[i] > 0 ? shardStringTableOffsets[i] : 0), true);
|
|
666
738
|
dv.setBigUint64(entryOff + 32, BigInt(shardStringTableLens[i]), true);
|
|
667
739
|
}
|
|
668
740
|
|
package/src/index.js
CHANGED
|
@@ -16,6 +16,8 @@ import { StringTable } from './StringTable.js';
|
|
|
16
16
|
import { PreserveTokenizer, PreserveTokenizerError } from './PreserveTokenizer.js';
|
|
17
17
|
import { PreserveWriter, PreserveWriterError } from './PreserveWriter.js';
|
|
18
18
|
import { PreserveReader, PreserveReaderError } from './PreserveReader.js';
|
|
19
|
+
import { checkOpts } from './Opts.js';
|
|
20
|
+
import { toContainerBuffer } from './Views.js';
|
|
19
21
|
|
|
20
22
|
export {
|
|
21
23
|
Tokenizer, TokenizerError,
|
|
@@ -26,10 +28,17 @@ export {
|
|
|
26
28
|
PreserveWriter, PreserveWriterError,
|
|
27
29
|
PreserveReader, PreserveReaderError,
|
|
28
30
|
};
|
|
29
|
-
export const VERSION = '1.
|
|
31
|
+
export const VERSION = '1.3.0';
|
|
30
32
|
|
|
31
33
|
const encoder = new TextEncoder();
|
|
32
34
|
|
|
35
|
+
const SERIALIZE_OPTS = {
|
|
36
|
+
preserve: { t: 'bool' },
|
|
37
|
+
framing: { t: 'enum', values: ['auto', 'array', 'ndjson'] },
|
|
38
|
+
writer: { t: 'obj' },
|
|
39
|
+
};
|
|
40
|
+
function raiseSerialize(code, msg) { throw new WriterError(code, msg); }
|
|
41
|
+
|
|
33
42
|
// Serialize input data into an LBK1 container. Input may be:
|
|
34
43
|
// - Uint8Array (raw NDJSON bytes)
|
|
35
44
|
// - string (NDJSON text)
|
|
@@ -43,22 +52,32 @@ const encoder = new TextEncoder();
|
|
|
43
52
|
// writer: writer options. Schema mode: { schema, targetShardBytes, sampleBytes }.
|
|
44
53
|
// Preserve mode: { targetShardBytes, maxRecordBytes }.
|
|
45
54
|
export function serialize(input, opts) {
|
|
55
|
+
// Validate synchronously in the prologue so a typo throws before any async
|
|
56
|
+
// dispatch (R7): E_UNKNOWN_OPTION must surface as a throw, never a rejected
|
|
57
|
+
// promise, for ReadableStream / AsyncIterable inputs too.
|
|
58
|
+
checkOpts('serialize', opts, SERIALIZE_OPTS, raiseSerialize);
|
|
46
59
|
opts = opts || {};
|
|
47
60
|
const preserve = opts.preserve === true;
|
|
61
|
+
const framing = opts.framing !== undefined ? opts.framing : 'ndjson';
|
|
62
|
+
const writerOpts = opts.writer !== undefined ? opts.writer : {};
|
|
63
|
+
if (preserve && opts.framing !== undefined && opts.framing !== 'ndjson') {
|
|
64
|
+
throw new WriterError('E_OPTION_CONFLICT',
|
|
65
|
+
"serialize: preserve mode is NDJSON-only; remove framing:'" + opts.framing + "' or set preserve:false");
|
|
66
|
+
}
|
|
48
67
|
|
|
49
68
|
if (input && typeof input.getReader === 'function') {
|
|
50
69
|
return preserve
|
|
51
70
|
? _serializeReadableStreamPreserve(input, opts)
|
|
52
|
-
: _serializeReadableStream(input,
|
|
71
|
+
: _serializeReadableStream(input, writerOpts, framing);
|
|
53
72
|
}
|
|
54
73
|
if (input && typeof input[Symbol.asyncIterator] === 'function') {
|
|
55
74
|
return preserve
|
|
56
75
|
? _serializeAsyncIterablePreserve(input, opts)
|
|
57
|
-
: _serializeAsyncIterable(input,
|
|
76
|
+
: _serializeAsyncIterable(input, writerOpts, framing);
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
if (preserve) return _serializeSyncPreserve(input, opts);
|
|
61
|
-
return _serializeSyncSchema(input,
|
|
80
|
+
return _serializeSyncSchema(input, writerOpts, framing);
|
|
62
81
|
}
|
|
63
82
|
|
|
64
83
|
function _serializeSyncSchema(input, writerOpts, framing) {
|
|
@@ -83,10 +102,10 @@ function _serializeSyncSchema(input, writerOpts, framing) {
|
|
|
83
102
|
}
|
|
84
103
|
|
|
85
104
|
function _serializeSyncPreserve(input, opts) {
|
|
86
|
-
const w = new PreserveWriter(opts.writer
|
|
105
|
+
const w = new PreserveWriter(opts.writer !== undefined ? opts.writer : {});
|
|
87
106
|
const t = new PreserveTokenizer(w, {
|
|
88
107
|
framing: 'ndjson',
|
|
89
|
-
maxRecordBytes: (opts.writer && opts.writer.maxRecordBytes)
|
|
108
|
+
maxRecordBytes: (opts.writer !== undefined && opts.writer.maxRecordBytes !== undefined) ? opts.writer.maxRecordBytes : 0,
|
|
90
109
|
});
|
|
91
110
|
if (input instanceof Uint8Array) {
|
|
92
111
|
t.feed(input);
|
|
@@ -124,10 +143,10 @@ async function _serializeReadableStream(stream, writerOpts, framing) {
|
|
|
124
143
|
}
|
|
125
144
|
|
|
126
145
|
async function _serializeReadableStreamPreserve(stream, opts) {
|
|
127
|
-
const w = new PreserveWriter(opts.writer
|
|
146
|
+
const w = new PreserveWriter(opts.writer !== undefined ? opts.writer : {});
|
|
128
147
|
const t = new PreserveTokenizer(w, {
|
|
129
148
|
framing: 'ndjson',
|
|
130
|
-
maxRecordBytes: (opts.writer && opts.writer.maxRecordBytes)
|
|
149
|
+
maxRecordBytes: (opts.writer !== undefined && opts.writer.maxRecordBytes !== undefined) ? opts.writer.maxRecordBytes : 0,
|
|
131
150
|
});
|
|
132
151
|
const reader = stream.getReader();
|
|
133
152
|
try {
|
|
@@ -154,10 +173,10 @@ async function _serializeAsyncIterable(iterable, writerOpts, framing) {
|
|
|
154
173
|
}
|
|
155
174
|
|
|
156
175
|
async function _serializeAsyncIterablePreserve(iterable, opts) {
|
|
157
|
-
const w = new PreserveWriter(opts.writer
|
|
176
|
+
const w = new PreserveWriter(opts.writer !== undefined ? opts.writer : {});
|
|
158
177
|
const t = new PreserveTokenizer(w, {
|
|
159
178
|
framing: 'ndjson',
|
|
160
|
-
maxRecordBytes: (opts.writer && opts.writer.maxRecordBytes)
|
|
179
|
+
maxRecordBytes: (opts.writer !== undefined && opts.writer.maxRecordBytes !== undefined) ? opts.writer.maxRecordBytes : 0,
|
|
161
180
|
});
|
|
162
181
|
for await (const chunk of iterable) {
|
|
163
182
|
if (!(chunk instanceof Uint8Array)) throw new TypeError('serialize: async iterable must yield Uint8Array');
|
|
@@ -170,20 +189,7 @@ async function _serializeAsyncIterablePreserve(iterable, opts) {
|
|
|
170
189
|
// Deserialize LBK1 bytes into the right Reader for the container's mode.
|
|
171
190
|
// Auto-detects preserve vs schema via header flag byte at offset 7 bit 0.
|
|
172
191
|
export function deserialize(bytes) {
|
|
173
|
-
|
|
174
|
-
if (bytes instanceof ArrayBuffer) {
|
|
175
|
-
buffer = bytes;
|
|
176
|
-
} else if (bytes instanceof Uint8Array) {
|
|
177
|
-
if (bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) {
|
|
178
|
-
buffer = bytes.buffer;
|
|
179
|
-
} else {
|
|
180
|
-
const copy = new Uint8Array(bytes.byteLength);
|
|
181
|
-
copy.set(bytes);
|
|
182
|
-
buffer = copy.buffer;
|
|
183
|
-
}
|
|
184
|
-
} else {
|
|
185
|
-
throw new TypeError('deserialize: expected Uint8Array or ArrayBuffer, got ' + typeof bytes);
|
|
186
|
-
}
|
|
192
|
+
const buffer = toContainerBuffer(bytes, 'deserialize');
|
|
187
193
|
if (buffer.byteLength < 8) {
|
|
188
194
|
throw new ReaderError('R_TRUNCATED', 'container too small to inspect header flags');
|
|
189
195
|
}
|