@zakkster/lite-bake-stream 1.4.1 → 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 +70 -0
- package/README.md +18 -1
- package/SPEC.md +3 -1
- package/llms.txt +21 -2
- package/package.json +13 -1
- package/src/Crc32c.js +96 -0
- package/src/FileIngest.js +1 -1
- package/src/MultiReader.js +5 -2
- package/src/PreserveReader.js +51 -13
- package/src/PreserveTokenizer.js +1 -1
- package/src/PreserveWriter.js +248 -67
- package/src/RangeReader.js +82 -21
- package/src/Reader.js +77 -13
- package/src/Split.js +24 -4
- package/src/StringTable.js +62 -6
- package/src/Tokenizer.js +1 -1
- package/src/Views.js +62 -0
- package/src/Writer.js +496 -149
- package/src/index.js +6 -6
- 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/StringTable.d.ts +5 -0
- package/types/Writer.d.ts +37 -0
- package/types/index.d.ts +3 -2
package/src/Writer.js
CHANGED
|
@@ -23,6 +23,21 @@
|
|
|
23
23
|
// String fields intern via the per-shard StringTable which allocates only on
|
|
24
24
|
// unique inserts (bounded by cardinality, not row count).
|
|
25
25
|
//
|
|
26
|
+
// Shard-byte budget (D2): a shard rolls when its row count reaches the maxRows
|
|
27
|
+
// ceiling OR when payload_bytes + string_table_bytes reaches targetShardBytes.
|
|
28
|
+
// The byte term is CHUNK-INDEPENDENT: it reads only _currentShardRowCount,
|
|
29
|
+
// rowStride, _targetShardBytes and _stringTableBytes -- the first three are
|
|
30
|
+
// chunk-invariant and _stringTableBytes is a pure function of the unique
|
|
31
|
+
// interned byte ranges in arrival order (= logical record order, which the
|
|
32
|
+
// Tokenizer delivers identically for any input chunking). No chunk-local state
|
|
33
|
+
// (absOffset, buffer boundaries) enters, so re-chunked input yields byte-
|
|
34
|
+
// identical containers (the t0 law). A single row or string that alone exceeds
|
|
35
|
+
// targetShardBytes still gets its own oversized shard (maxRows has a
|
|
36
|
+
// Math.max(1, ...) floor and the budget runs post-write); payload_len stays
|
|
37
|
+
// u32-honest via the targetShardBytes constructor guard. A schema with no U32
|
|
38
|
+
// lane keeps a zero string budget and rolls on rows only -- byte-identical to
|
|
39
|
+
// the pre-D2 baseline.
|
|
40
|
+
//
|
|
26
41
|
// Error codes (stable):
|
|
27
42
|
// W_TOP_LEVEL_NOT_OBJECT - top-level value is not an object
|
|
28
43
|
// W_NESTED_UNSUPPORTED - nested object/array inside a record
|
|
@@ -42,8 +57,10 @@
|
|
|
42
57
|
|
|
43
58
|
import { StringTable } from './StringTable.js';
|
|
44
59
|
import { checkOpts } from './Opts.js';
|
|
60
|
+
import { crc32cInit, crc32cUpdate, crc32cFinal, crc32cCombine } from './Crc32c.js';
|
|
61
|
+
import { validateSink, isThenable } from './Views.js';
|
|
45
62
|
|
|
46
|
-
export const VERSION = '1.
|
|
63
|
+
export const VERSION = '1.6.0';
|
|
47
64
|
|
|
48
65
|
const U32_MAX = 4294967295;
|
|
49
66
|
// Post-finalize sentinel for _recordDepth. Chosen = 2 so every post-finalize
|
|
@@ -56,7 +73,13 @@ const WRITER_OPTS = {
|
|
|
56
73
|
schema: { t: 'obj', nullable: true },
|
|
57
74
|
targetShardBytes: { t: 'int', min: 1, max: U32_MAX },
|
|
58
75
|
sampleBytes: { t: 'int', min: 0, max: U32_MAX },
|
|
76
|
+
crc: { t: 'bool' },
|
|
77
|
+
};
|
|
78
|
+
const FINALIZE_TO_SINK_OPTS = {
|
|
79
|
+
layout: { t: 'enum', values: ['prefix', 'stream'] },
|
|
80
|
+
crc: { t: 'bool' },
|
|
59
81
|
};
|
|
82
|
+
const CRC_ABSENT = 0xFFFFFFFF;
|
|
60
83
|
function raiseWriter(code, msg) { throw new WriterError(code, msg); }
|
|
61
84
|
|
|
62
85
|
const CONTAINER_HEADER_BYTES = 48;
|
|
@@ -76,6 +99,10 @@ const SHARD_FLAGS_NONE = 0;
|
|
|
76
99
|
// U32-lane fields). Read-only; never written into.
|
|
77
100
|
const EMPTY_STRING_TABLE_BYTES = new Uint8Array(0);
|
|
78
101
|
|
|
102
|
+
// Shared read-only zero span for 8-byte shard-payload alignment padding in the
|
|
103
|
+
// streaming path. Never written into.
|
|
104
|
+
const ZERO_PAD = new Uint8Array(8);
|
|
105
|
+
|
|
79
106
|
const FORMAT_VERSION = 1;
|
|
80
107
|
const ENDIAN_LE = 1;
|
|
81
108
|
|
|
@@ -105,6 +132,12 @@ export class WriterError extends Error {
|
|
|
105
132
|
}
|
|
106
133
|
}
|
|
107
134
|
|
|
135
|
+
// Shared decoder for sample-window key names (BS-27 floor). onKey fires once per
|
|
136
|
+
// key per sample record; a fresh TextDecoder each time is pure garbage. One
|
|
137
|
+
// module-scope instance, reused -- decode() is stateless across calls. The other
|
|
138
|
+
// TextDecoders in the codebase are per-open/lazy and stay as they are.
|
|
139
|
+
const KEY_DECODER = new TextDecoder();
|
|
140
|
+
|
|
108
141
|
function hashBytes(bytes, from, to) {
|
|
109
142
|
let h = 0x811c9dc5 | 0;
|
|
110
143
|
for (let i = from; i < to; i++) {
|
|
@@ -204,6 +237,17 @@ export class Writer {
|
|
|
204
237
|
this._targetShardBytes = opts.targetShardBytes !== undefined ? opts.targetShardBytes : DEFAULT_SHARD_BYTES;
|
|
205
238
|
this._sampleBytes = opts.sampleBytes !== undefined ? opts.sampleBytes : this._targetShardBytes;
|
|
206
239
|
this._explicitSchema = opts.schema !== undefined ? opts.schema : null;
|
|
240
|
+
this._crc = opts.crc === true;
|
|
241
|
+
|
|
242
|
+
// Streaming-emission state (M6). Null sink => classic buffered writer:
|
|
243
|
+
// shards accumulate in _shards until finalize()/finalizeToSink drains them.
|
|
244
|
+
// A bound sink (beginStream) switches _finalizeCurrentShard to emit each
|
|
245
|
+
// shard as it finalizes and retain only a scalar directory descriptor, so
|
|
246
|
+
// peak RAM stays O(targetShardBytes + directory) instead of O(container).
|
|
247
|
+
this._sink = null;
|
|
248
|
+
this._sinkCrcOn = false;
|
|
249
|
+
this._sinkPos = 0; // current absolute write position in the sink
|
|
250
|
+
this._sinkCrc = 0; // running int32 CRC of the body suffix [48, pos)
|
|
207
251
|
|
|
208
252
|
// Frozen schema state
|
|
209
253
|
this._schema = null; // { fields: [{name, laneKind, offsetInRow}], rowStride }
|
|
@@ -226,6 +270,15 @@ export class Writer {
|
|
|
226
270
|
this._shards = []; // { bytes, rowCount, stringTableBytes }
|
|
227
271
|
this._totalRows = 0;
|
|
228
272
|
|
|
273
|
+
// D2 shard-byte budget (zero-alloc, chunk-independent). _stringTableBytes is
|
|
274
|
+
// the serialized byte length the CURRENT shard's string table would emit RIGHT
|
|
275
|
+
// NOW; _maxInternedIdx is the highest interned index seen this shard, so a new
|
|
276
|
+
// unique arrival (strIdx > _maxInternedIdx) is detected in one compare. Both
|
|
277
|
+
// recomputed only in the cold unique-string arm (bounded by cardinality, not
|
|
278
|
+
// rows) and reset at each shard finalize. Real values are set at freeze.
|
|
279
|
+
this._stringTableBytes = 0;
|
|
280
|
+
this._maxInternedIdx = 0;
|
|
281
|
+
|
|
229
282
|
// Per-record parse state
|
|
230
283
|
this._recordDepth = 0;
|
|
231
284
|
this._currentKeyBytes = null;
|
|
@@ -285,7 +338,7 @@ export class Writer {
|
|
|
285
338
|
// buffer for the value bytes before onNumber/onString fires, so we cannot
|
|
286
339
|
// defer decoding. String allocation here is expected — bounded to the
|
|
287
340
|
// sample window; steady-state post-freeze remains zero-alloc.
|
|
288
|
-
this._currentKeyName =
|
|
341
|
+
this._currentKeyName = KEY_DECODER.decode(bytes.subarray(from, to));
|
|
289
342
|
}
|
|
290
343
|
}
|
|
291
344
|
|
|
@@ -343,8 +396,17 @@ export class Writer {
|
|
|
343
396
|
const lane = this._fieldLaneKinds[idx];
|
|
344
397
|
if (lane !== LANE_U32) throw new WriterError('W_LANE_MISMATCH',
|
|
345
398
|
'field ' + this._schema.fields[idx].name + ' is not a U32 lane but got a string');
|
|
346
|
-
const
|
|
399
|
+
const st = this._currentShardStringTable();
|
|
400
|
+
const strIdx = st.intern(bytes, from, to);
|
|
347
401
|
this._rowValueSlotsU32[idx] = strIdx;
|
|
402
|
+
// D2 (cold arm, unique arrivals only): recompute the serialized string-table
|
|
403
|
+
// byte length. serialize() emits (8 + 4*(count+1) + blobLen), 8-padded, and
|
|
404
|
+
// count = strIdx + 1 for a fresh entry, so the padded form is
|
|
405
|
+
// (16 + 4*strIdx + blobLen) rounded up to 8 -- exact vs StringTable.serialize.
|
|
406
|
+
if (strIdx > this._maxInternedIdx) {
|
|
407
|
+
this._maxInternedIdx = strIdx;
|
|
408
|
+
this._stringTableBytes = (16 + 4 * strIdx + st.blobLen + 7) & ~7;
|
|
409
|
+
}
|
|
348
410
|
} else {
|
|
349
411
|
if (!this._currentKeyName) return;
|
|
350
412
|
this._sample.setString(this._currentKeyName, bytes, from, to);
|
|
@@ -452,6 +514,19 @@ export class Writer {
|
|
|
452
514
|
this._fieldLaneKinds[i] = fields[i].laneKind;
|
|
453
515
|
this._fieldOffsets[i] = fields[i].offsetInRow;
|
|
454
516
|
}
|
|
517
|
+
// R7 floor: O(1) field-name resolution. hash -> field index, or an array of
|
|
518
|
+
// indices when two names collide (Array.isArray in the cold arm). Built once
|
|
519
|
+
// at freeze; _lookupFieldIdx replaces its O(F) scan with one Map.get plus the
|
|
520
|
+
// mandatory byte-equal confirm, so a hash collision still resolves correctly.
|
|
521
|
+
const fieldHashMap = new Map();
|
|
522
|
+
for (let i = 0; i < fields.length; i++) {
|
|
523
|
+
const h = this._fieldNameHashes[i];
|
|
524
|
+
const existing = fieldHashMap.get(h);
|
|
525
|
+
if (existing === undefined) fieldHashMap.set(h, i);
|
|
526
|
+
else if (Array.isArray(existing)) existing.push(i);
|
|
527
|
+
else fieldHashMap.set(h, [existing, i]);
|
|
528
|
+
}
|
|
529
|
+
this._fieldHashMap = fieldHashMap;
|
|
455
530
|
this._rowValueSlotsF64 = new Float64Array(fields.length);
|
|
456
531
|
this._rowValueSlotsU32 = new Uint32Array(fields.length);
|
|
457
532
|
this._currentShardMaxRows = Math.max(1, Math.floor(this._targetShardBytes / rowStride));
|
|
@@ -467,14 +542,27 @@ export class Writer {
|
|
|
467
542
|
this._trackedFieldToPos = new Int32Array(fields.length);
|
|
468
543
|
for (let i = 0; i < fields.length; i++) this._trackedFieldToPos[i] = -1;
|
|
469
544
|
for (let t = 0; t < tracked.length; t++) this._trackedFieldToPos[tracked[t]] = t;
|
|
545
|
+
|
|
546
|
+
// D2 budget baseline. A fresh string table always carries reserved entry 0,
|
|
547
|
+
// so a U32-lane shard with no real strings still serializes to 16 bytes; a
|
|
548
|
+
// schema with no U32 lane emits NO table (0 bytes, EMPTY_STRING_TABLE_BYTES).
|
|
549
|
+
// Seeding _stringTableBytes to 0 for the F64-only case keeps such containers
|
|
550
|
+
// byte-identical to the pre-D2 baseline (the roll then fires on rows only via
|
|
551
|
+
// the maxRows term, which is <= the byte term for a zero string budget).
|
|
552
|
+
this._maxInternedIdx = 0;
|
|
553
|
+
this._stringTableBytes = this._hasU32 ? 16 : 0;
|
|
470
554
|
}
|
|
471
555
|
|
|
472
556
|
_lookupFieldIdx(bytes, from, to) {
|
|
473
557
|
const h = hashBytes(bytes, from, to);
|
|
474
|
-
const
|
|
558
|
+
const hit = this._fieldHashMap.get(h);
|
|
559
|
+
if (hit === undefined) return -1;
|
|
475
560
|
const names = this._fieldNamesUtf8;
|
|
476
|
-
|
|
477
|
-
|
|
561
|
+
// Hit path (the common case): a single index. Byte-confirm and return.
|
|
562
|
+
if (typeof hit === 'number') return bytesEqual(bytes, from, to, names[hit]) ? hit : -1;
|
|
563
|
+
// Cold: >= 2 field names share this hash; byte-confirm each candidate.
|
|
564
|
+
for (let k = 0; k < hit.length; k++) {
|
|
565
|
+
if (bytesEqual(bytes, from, to, names[hit[k]])) return hit[k];
|
|
478
566
|
}
|
|
479
567
|
return -1;
|
|
480
568
|
}
|
|
@@ -511,7 +599,15 @@ export class Writer {
|
|
|
511
599
|
}
|
|
512
600
|
this._currentShardRowCount++;
|
|
513
601
|
this._totalRows++;
|
|
514
|
-
|
|
602
|
+
// D2 roll: fire on the row ceiling OR when payload + string-table bytes
|
|
603
|
+
// reach the target. The row-ceiling term fires first for any F64-only shard
|
|
604
|
+
// (string budget 0, maxRows = floor(target/stride) <= target/stride), so
|
|
605
|
+
// such containers stay byte-identical to baseline; string shards can roll
|
|
606
|
+
// earlier. Zero allocation -- one add + compare per row.
|
|
607
|
+
const n = this._currentShardRowCount;
|
|
608
|
+
if (n >= this._currentShardMaxRows ||
|
|
609
|
+
n * this._schema.rowStride + this._stringTableBytes >= this._targetShardBytes)
|
|
610
|
+
this._finalizeCurrentShard();
|
|
515
611
|
} else {
|
|
516
612
|
const endOff = this._source !== null ? this._source.absOffset : this._absOffset;
|
|
517
613
|
const consumed = Math.max(1, endOff - this._recordStartByteOffset);
|
|
@@ -560,13 +656,25 @@ export class Writer {
|
|
|
560
656
|
const shardMaxes = new Float64Array(T);
|
|
561
657
|
shardMins.set(this._currentShardMins);
|
|
562
658
|
shardMaxes.set(this._currentShardMaxes);
|
|
563
|
-
this.
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}
|
|
659
|
+
if (this._sink !== null) {
|
|
660
|
+
// Streaming mode: emit this shard to the bound sink now and retain only a
|
|
661
|
+
// scalar directory descriptor (payload/string offsets, row count, zone
|
|
662
|
+
// bounds). The heavy payload + string-table byte arrays are dropped, so
|
|
663
|
+
// peak RAM does not grow with shard count.
|
|
664
|
+
this._streamEmitShard(copy, stBytes, this._currentShardRowCount, shardMins, shardMaxes);
|
|
665
|
+
} else {
|
|
666
|
+
this._shards.push({
|
|
667
|
+
bytes: copy,
|
|
668
|
+
rowCount: this._currentShardRowCount,
|
|
669
|
+
stringTableBytes: stBytes,
|
|
670
|
+
// D2 audit: the incrementally-tracked budget at this finalize. MUST equal
|
|
671
|
+
// stBytes.length (the emitted directory local_string_len). Cross-checked by
|
|
672
|
+
// Ceilings.test.js via __stringTableBudgetAudit; not part of the public API.
|
|
673
|
+
budgetTracked: this._stringTableBytes,
|
|
674
|
+
mins: shardMins,
|
|
675
|
+
maxes: shardMaxes,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
570
678
|
this._currentShardBuffer = null;
|
|
571
679
|
this._currentShardBytes = null;
|
|
572
680
|
this._currentShardDv = null;
|
|
@@ -574,6 +682,9 @@ export class Writer {
|
|
|
574
682
|
// Reset string table for next shard (per-shard independence)
|
|
575
683
|
st.reset();
|
|
576
684
|
this._perShardStringTable = st;
|
|
685
|
+
// Reset the D2 budget trackers alongside st.reset() (per-shard independence).
|
|
686
|
+
this._maxInternedIdx = 0;
|
|
687
|
+
this._stringTableBytes = this._hasU32 ? 16 : 0;
|
|
577
688
|
}
|
|
578
689
|
|
|
579
690
|
_drainSampleToShards() {
|
|
@@ -607,6 +718,11 @@ export class Writer {
|
|
|
607
718
|
const trackedIdx = this._trackedFieldToPos;
|
|
608
719
|
const mins = this._currentShardMins;
|
|
609
720
|
const maxes = this._currentShardMaxes;
|
|
721
|
+
// D2: honour the same string-aware byte budget the post-freeze hot path
|
|
722
|
+
// uses, so a sample-drained container and a post-freeze one agree on shard
|
|
723
|
+
// boundaries for identical logical input. writtenRows may finish below
|
|
724
|
+
// chunkRows when the string table pushes the shard over target.
|
|
725
|
+
let writtenRows = chunkRows;
|
|
610
726
|
for (let r = 0; r < chunkRows; r++) {
|
|
611
727
|
const rowOff = r * stride;
|
|
612
728
|
for (let f = 0; f < fields.length; f++) {
|
|
@@ -625,14 +741,24 @@ export class Writer {
|
|
|
625
741
|
const strBytes = sampleTable.bytesAt(sampleIdx);
|
|
626
742
|
const newIdx = strBytes ? dstTable.intern(strBytes, 0, strBytes.length) : 0;
|
|
627
743
|
dv.setUint32(fo, newIdx, true);
|
|
744
|
+
if (newIdx > this._maxInternedIdx) {
|
|
745
|
+
this._maxInternedIdx = newIdx;
|
|
746
|
+
this._stringTableBytes = (16 + 4 * newIdx + dstTable.blobLen + 7) & ~7;
|
|
747
|
+
}
|
|
628
748
|
}
|
|
629
749
|
}
|
|
750
|
+
const done = r + 1;
|
|
751
|
+
if (done < chunkRows &&
|
|
752
|
+
done * stride + this._stringTableBytes >= this._targetShardBytes) {
|
|
753
|
+
writtenRows = done;
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
630
756
|
}
|
|
631
|
-
this._currentShardRowCount =
|
|
632
|
-
this._totalRows +=
|
|
757
|
+
this._currentShardRowCount = writtenRows;
|
|
758
|
+
this._totalRows += writtenRows;
|
|
633
759
|
this._finalizeCurrentShard(); // serializes dstTable, resets in-place for the next iter
|
|
634
|
-
srcRow +=
|
|
635
|
-
rowsRemaining -=
|
|
760
|
+
srcRow += writtenRows;
|
|
761
|
+
rowsRemaining -= writtenRows;
|
|
636
762
|
}
|
|
637
763
|
|
|
638
764
|
this._sample = null;
|
|
@@ -640,7 +766,12 @@ export class Writer {
|
|
|
640
766
|
// receive strings from post-drain records that fill subsequent shards.
|
|
641
767
|
}
|
|
642
768
|
|
|
643
|
-
finalize()
|
|
769
|
+
// Shared input-completion prologue for finalize() and finalizeToSink(): freeze
|
|
770
|
+
// an inferred schema, drain the sample, finalize the trailing partial shard,
|
|
771
|
+
// and fail closed on empty input. After this returns, _shards holds one entry
|
|
772
|
+
// per shard (a buffered {bytes,...} in the classic path, or a scalar directory
|
|
773
|
+
// descriptor when a sink was bound via beginStream).
|
|
774
|
+
_completeInput() {
|
|
644
775
|
if (this._finalized) throw new WriterError('W_FINALIZED', 'writer already finalized');
|
|
645
776
|
if (!this._schema) {
|
|
646
777
|
if (this._sample.rowCount === 0) throw new WriterError('W_EMPTY_INPUT', 'no records to write');
|
|
@@ -649,13 +780,94 @@ export class Writer {
|
|
|
649
780
|
}
|
|
650
781
|
if (this._currentShardRowCount > 0) this._finalizeCurrentShard();
|
|
651
782
|
if (this._shards.length === 0) throw new WriterError('W_EMPTY_INPUT', 'no records to write');
|
|
652
|
-
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
finalize() {
|
|
786
|
+
this._completeInput();
|
|
787
|
+
// finalize() IS layout:'prefix' over an implicit in-memory buffer: it builds
|
|
788
|
+
// the identical classic container the pre-M6 assembler produced (byte-for-
|
|
789
|
+
// byte when crc is off) and returns the frozen struct.
|
|
790
|
+
const bytes = this._buildPrefixBytes(this._crc);
|
|
653
791
|
this._finalized = true;
|
|
654
792
|
// Post-finalize sentinel: route every later sink event into a cold arm.
|
|
655
793
|
this._recordDepth = FINALIZED_DEPTH;
|
|
794
|
+
this._container = {
|
|
795
|
+
buffer: bytes.buffer,
|
|
796
|
+
totalRows: this._totalRows,
|
|
797
|
+
shardCount: this._shards.length,
|
|
798
|
+
schema: this._schema,
|
|
799
|
+
};
|
|
656
800
|
return this._container;
|
|
657
801
|
}
|
|
658
802
|
|
|
803
|
+
// PUBLIC. Bind a sink and switch to streaming emission BEFORE the first record
|
|
804
|
+
// is fed (the two-step bounded-RAM contract): call beginStream(sink, opts),
|
|
805
|
+
// then feed the tokenizer -- each shard is written to the sink as it finalizes
|
|
806
|
+
// and its bytes are dropped, so peak RAM is O(targetShardBytes + directory) --
|
|
807
|
+
// then call finalizeToSink(sink, opts), which appends the schema/directory/
|
|
808
|
+
// zone-map trailer and footer and performs the single header backpatch. Calling
|
|
809
|
+
// finalizeToSink alone (without beginStream) is BUFFERED mode: correct, but
|
|
810
|
+
// peak RAM is O(container). opts.layout must be 'stream'; opts.crc (default:
|
|
811
|
+
// the constructor crc) toggles CRC-32C.
|
|
812
|
+
beginStream(sink, opts) {
|
|
813
|
+
if (this._finalized) throw new WriterError('W_FINALIZED', 'writer already finalized');
|
|
814
|
+
if (this._sink !== null) throw new WriterError('W_FINALIZED', 'writer is already streaming to a sink');
|
|
815
|
+
if (this._shards.length > 0 || this._currentShardRowCount > 0)
|
|
816
|
+
throw new WriterError('W_FINALIZED', 'beginStream must be called before the first record');
|
|
817
|
+
checkOpts('Writer.beginStream', opts, FINALIZE_TO_SINK_OPTS, raiseWriter);
|
|
818
|
+
opts = opts || {};
|
|
819
|
+
if (opts.layout !== undefined && opts.layout !== 'stream')
|
|
820
|
+
raiseWriter('W_BAD_SINK', "beginStream requires layout:'stream'");
|
|
821
|
+
validateSink(sink, true, raiseWriter);
|
|
822
|
+
this._sink = sink;
|
|
823
|
+
this._sinkCrcOn = opts.crc !== undefined ? opts.crc === true : this._crc;
|
|
824
|
+
this._sinkPos = 0;
|
|
825
|
+
this._sinkCrc = crc32cInit();
|
|
826
|
+
// Reserve the 48-byte header up front; it is backpatched by ONE writeAt at
|
|
827
|
+
// the end once shardCount and the trailer offsets are known. Excluded from
|
|
828
|
+
// the running suffix CRC (folded in via crc32cCombine at the end).
|
|
829
|
+
const placeholder = new Uint8Array(CONTAINER_HEADER_BYTES);
|
|
830
|
+
this._sinkWrite(placeholder, false);
|
|
831
|
+
this._sinkPos = CONTAINER_HEADER_BYTES;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Write bytes to the bound sink, optionally folding them into the running body
|
|
835
|
+
// CRC. A thenable return means an async sink -- the contract is synchronous, so
|
|
836
|
+
// that is a malformed sink (W_BAD_SINK). NOTE: _sinkPos is advanced by callers.
|
|
837
|
+
_sinkWrite(bytes, fold) {
|
|
838
|
+
if (fold && this._sinkCrcOn) this._sinkCrc = crc32cUpdate(this._sinkCrc, bytes, 0, bytes.length);
|
|
839
|
+
let ret;
|
|
840
|
+
// A sink that throws mid-emission fails the writer closed and its own error
|
|
841
|
+
// is rethrown verbatim -- never laundered into a package code (the FileIngest
|
|
842
|
+
// mid-stream precedent). A retry then hits W_FINALIZED.
|
|
843
|
+
try { ret = this._sink.write(bytes); }
|
|
844
|
+
catch (e) { this._finalized = true; throw e; }
|
|
845
|
+
if (isThenable(ret)) { this._finalized = true; raiseWriter('W_BAD_SINK', 'sink.write returned a thenable; sinks must be synchronous'); }
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Streaming emit of one finalized shard: payload, 8-alignment pad, local string
|
|
849
|
+
// table -- each folded into the body CRC. Retains only a scalar descriptor.
|
|
850
|
+
_streamEmitShard(payloadBytes, stBytes, rowCount, mins, maxes) {
|
|
851
|
+
const payloadOff = this._sinkPos;
|
|
852
|
+
const payloadLen = payloadBytes.length;
|
|
853
|
+
this._sinkWrite(payloadBytes, true);
|
|
854
|
+
this._sinkPos += payloadLen;
|
|
855
|
+
const pad = (8 - (payloadLen & 7)) & 7;
|
|
856
|
+
if (pad > 0) { this._sinkWrite(ZERO_PAD.subarray(0, pad), true); this._sinkPos += pad; }
|
|
857
|
+
const stOff = this._sinkPos;
|
|
858
|
+
const stLen = stBytes.length;
|
|
859
|
+
if (stLen > 0) { this._sinkWrite(stBytes, true); this._sinkPos += stLen; }
|
|
860
|
+
this._shards.push({
|
|
861
|
+
rowCount,
|
|
862
|
+
payloadOff,
|
|
863
|
+
payloadLen,
|
|
864
|
+
stringOff: stLen > 0 ? stOff : 0,
|
|
865
|
+
stringLen: stLen,
|
|
866
|
+
mins,
|
|
867
|
+
maxes,
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
659
871
|
// Optional capability hook (BS-07): the Tokenizer calls this at construction
|
|
660
872
|
// to hand over the live tokenizer. The Writer then reads src.absOffset at
|
|
661
873
|
// record boundaries so the sample window is byte-true. A CALL only; the
|
|
@@ -667,100 +879,62 @@ export class Writer {
|
|
|
667
879
|
// dance is a documented no-op retained for source compatibility until 2.0.
|
|
668
880
|
setInputByteOffset() {}
|
|
669
881
|
|
|
670
|
-
// -------- container assembly --------
|
|
671
|
-
|
|
672
|
-
|
|
882
|
+
// -------- container assembly (placement-free emitters) --------
|
|
883
|
+
//
|
|
884
|
+
// Both finalize() (layout:'prefix') and finalizeToSink(layout:'stream') drive
|
|
885
|
+
// the SAME byte-producing emitters below. The classic prefix layout builds one
|
|
886
|
+
// contiguous buffer; the streaming layout writes payloads first, accumulates a
|
|
887
|
+
// scalar directory, and emits the schema/directory/zone-map trailer + footer at
|
|
888
|
+
// the end with a single header backpatch. The emitters take an explicit target
|
|
889
|
+
// offset so they are reusable across both a full-container buffer and a small
|
|
890
|
+
// trailer buffer.
|
|
891
|
+
|
|
892
|
+
// Encode field names once and size the schema block (8-byte padded). Shared by
|
|
893
|
+
// every layout.
|
|
894
|
+
_computeSchemaBlock() {
|
|
673
895
|
const enc = new TextEncoder();
|
|
674
|
-
const
|
|
896
|
+
const fields = this._schema.fields;
|
|
897
|
+
const nameBytes = new Array(fields.length);
|
|
675
898
|
let nameBlobLen = 0;
|
|
676
|
-
for (let i = 0; i <
|
|
677
|
-
nameBytes[i] = enc.encode(
|
|
899
|
+
for (let i = 0; i < fields.length; i++) {
|
|
900
|
+
nameBytes[i] = enc.encode(fields[i].name);
|
|
678
901
|
nameBlobLen += nameBytes[i].length;
|
|
679
902
|
}
|
|
680
|
-
|
|
681
|
-
const fieldCount = this._schema.fields.length;
|
|
682
|
-
const descriptorBytes = fieldCount * FIELD_DESCRIPTOR_BYTES;
|
|
903
|
+
const descriptorBytes = fields.length * FIELD_DESCRIPTOR_BYTES;
|
|
683
904
|
let schemaBlockBytes = 8 + descriptorBytes + 4 + nameBlobLen;
|
|
684
|
-
|
|
685
|
-
schemaBlockBytes
|
|
686
|
-
|
|
687
|
-
const shardCount = this._shards.length;
|
|
688
|
-
const shardDirBytes = shardCount * SHARD_ENTRY_BYTES;
|
|
689
|
-
|
|
690
|
-
const schemaBlockOff = CONTAINER_HEADER_BYTES;
|
|
691
|
-
const shardDirOff = schemaBlockOff + schemaBlockBytes;
|
|
905
|
+
schemaBlockBytes += (8 - (schemaBlockBytes & 7)) & 7;
|
|
906
|
+
return { nameBytes, nameBlobLen, descriptorBytes, schemaBlockBytes };
|
|
907
|
+
}
|
|
692
908
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
let zoneMapsOff = 0;
|
|
699
|
-
let zoneMapsBytes = 0;
|
|
700
|
-
let zoneMapsFieldTableOff = 0;
|
|
701
|
-
let zoneMapsMinsOff = 0;
|
|
702
|
-
let zoneMapsMaxesOff = 0;
|
|
703
|
-
if (zoneMapsEnabled) {
|
|
704
|
-
zoneMapsOff = shardDirOff + shardDirBytes;
|
|
705
|
-
// Layout: 16-byte header + T*u16 field indices, padded to 8, then
|
|
706
|
-
// shardCount*T*8 mins, then shardCount*T*8 maxes.
|
|
707
|
-
const headerLen = 16;
|
|
708
|
-
const fieldTableLen = T * 2;
|
|
709
|
-
const fieldTablePad = (8 - (fieldTableLen & 7)) & 7;
|
|
710
|
-
zoneMapsFieldTableOff = zoneMapsOff + headerLen;
|
|
711
|
-
const minsRel = headerLen + fieldTableLen + fieldTablePad;
|
|
712
|
-
zoneMapsMinsOff = zoneMapsOff + minsRel;
|
|
713
|
-
const minsMaxesLen = shardCount * T * 8 * 2;
|
|
714
|
-
zoneMapsMaxesOff = zoneMapsMinsOff + shardCount * T * 8;
|
|
715
|
-
zoneMapsBytes = minsRel + minsMaxesLen;
|
|
716
|
-
}
|
|
717
|
-
const firstShardOff = shardDirOff + shardDirBytes + zoneMapsBytes;
|
|
909
|
+
_zoneMapsByteLen(T, shardCount) {
|
|
910
|
+
const fieldTableLen = T * 2;
|
|
911
|
+
const fieldTablePad = (8 - (fieldTableLen & 7)) & 7;
|
|
912
|
+
return 16 + fieldTableLen + fieldTablePad + shardCount * T * 8 * 2;
|
|
913
|
+
}
|
|
718
914
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
cursor += shardPayloadPadded[i];
|
|
732
|
-
// String table immediately follows the shard payload
|
|
733
|
-
shardStringTableOffsets[i] = cursor;
|
|
734
|
-
const stLen = s.stringTableBytes.length;
|
|
735
|
-
shardStringTableLens[i] = stLen;
|
|
736
|
-
cursor += stLen; // stringTable.serialize() already 8-byte padded
|
|
737
|
-
}
|
|
915
|
+
_emitHeaderInto(dv, bytes, off, h) {
|
|
916
|
+
bytes[off + 0] = 0x4C; bytes[off + 1] = 0x42; bytes[off + 2] = 0x4B; bytes[off + 3] = 0x31;
|
|
917
|
+
dv.setUint16(off + 4, FORMAT_VERSION, true);
|
|
918
|
+
bytes[off + 6] = ENDIAN_LE;
|
|
919
|
+
bytes[off + 7] = 0;
|
|
920
|
+
dv.setBigUint64(off + 8, BigInt(h.schemaBlockOff), true);
|
|
921
|
+
dv.setBigUint64(off + 16, BigInt(h.metadataOff), true); // metadata_off -- 0 iff no zone maps
|
|
922
|
+
dv.setBigUint64(off + 24, BigInt(h.shardDirOff), true);
|
|
923
|
+
dv.setUint32(off + 32, h.shardCount, true);
|
|
924
|
+
dv.setUint32(off + 36, 0, true); // reserved1
|
|
925
|
+
dv.setBigUint64(off + 40, BigInt(h.totalRows), true);
|
|
926
|
+
}
|
|
738
927
|
|
|
739
|
-
|
|
740
|
-
const
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
// Header
|
|
745
|
-
bytes[0] = 0x4C; bytes[1] = 0x42; bytes[2] = 0x4B; bytes[3] = 0x31;
|
|
746
|
-
dv.setUint16(4, FORMAT_VERSION, true);
|
|
747
|
-
bytes[6] = ENDIAN_LE;
|
|
748
|
-
bytes[7] = 0;
|
|
749
|
-
dv.setBigUint64(8, BigInt(schemaBlockOff), true);
|
|
750
|
-
dv.setBigUint64(16, BigInt(zoneMapsOff), true); // metadata_off — 0 iff no zone maps
|
|
751
|
-
dv.setBigUint64(24, BigInt(shardDirOff), true);
|
|
752
|
-
dv.setUint32(32, shardCount, true);
|
|
753
|
-
dv.setUint32(36, 0, true); // reserved1
|
|
754
|
-
dv.setBigUint64(40, BigInt(this._totalRows), true);
|
|
755
|
-
|
|
756
|
-
// Schema block
|
|
757
|
-
dv.setUint32(schemaBlockOff, fieldCount, true);
|
|
758
|
-
dv.setUint32(schemaBlockOff + 4, this._schema.rowStride, true);
|
|
928
|
+
_emitSchemaInto(dv, bytes, off, sb) {
|
|
929
|
+
const fields = this._schema.fields;
|
|
930
|
+
const fieldCount = fields.length;
|
|
931
|
+
dv.setUint32(off, fieldCount, true);
|
|
932
|
+
dv.setUint32(off + 4, this._schema.rowStride, true);
|
|
759
933
|
let nameOff = 0;
|
|
760
934
|
for (let i = 0; i < fieldCount; i++) {
|
|
761
|
-
const descOff =
|
|
762
|
-
const f =
|
|
763
|
-
const nb = nameBytes[i];
|
|
935
|
+
const descOff = off + 8 + i * FIELD_DESCRIPTOR_BYTES;
|
|
936
|
+
const f = fields[i];
|
|
937
|
+
const nb = sb.nameBytes[i];
|
|
764
938
|
dv.setUint16(descOff + 0, nb.length, true);
|
|
765
939
|
dv.setUint16(descOff + 2, f.offsetInRow, true);
|
|
766
940
|
bytes[descOff + 4] = f.laneKind;
|
|
@@ -770,70 +944,243 @@ export class Writer {
|
|
|
770
944
|
dv.setBigUint64(descOff + 16, 0n, true);
|
|
771
945
|
nameOff += nb.length;
|
|
772
946
|
}
|
|
773
|
-
const nameBlobLenOff =
|
|
774
|
-
dv.setUint32(nameBlobLenOff, nameBlobLen, true);
|
|
947
|
+
const nameBlobLenOff = off + 8 + sb.descriptorBytes;
|
|
948
|
+
dv.setUint32(nameBlobLenOff, sb.nameBlobLen, true);
|
|
775
949
|
let namePosOff = nameBlobLenOff + 4;
|
|
776
950
|
for (let i = 0; i < fieldCount; i++) {
|
|
777
|
-
bytes.set(nameBytes[i], namePosOff);
|
|
778
|
-
namePosOff += nameBytes[i].length;
|
|
951
|
+
bytes.set(sb.nameBytes[i], namePosOff);
|
|
952
|
+
namePosOff += sb.nameBytes[i].length;
|
|
779
953
|
}
|
|
954
|
+
}
|
|
780
955
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
dv.setBigUint64(entryOff + 24, BigInt(shardStringTableLens[i] > 0 ? shardStringTableOffsets[i] : 0), true);
|
|
792
|
-
dv.setBigUint64(entryOff + 32, BigInt(shardStringTableLens[i]), true);
|
|
793
|
-
}
|
|
956
|
+
_emitDirEntryInto(dv, entryOff, payloadOff, payloadLen, rowCount, stOff, stLen) {
|
|
957
|
+
dv.setBigUint64(entryOff + 0, BigInt(payloadOff), true);
|
|
958
|
+
dv.setUint32(entryOff + 8, payloadLen, true);
|
|
959
|
+
dv.setUint32(entryOff + 12, rowCount, true);
|
|
960
|
+
dv.setUint16(entryOff + 16, SHARD_MIN_READER_VERSION, true);
|
|
961
|
+
dv.setUint16(entryOff + 18, SHARD_FLAGS_NONE, true);
|
|
962
|
+
dv.setUint32(entryOff + 20, 0, true); // reserved
|
|
963
|
+
dv.setBigUint64(entryOff + 24, BigInt(stLen > 0 ? stOff : 0), true);
|
|
964
|
+
dv.setBigUint64(entryOff + 32, BigInt(stLen), true);
|
|
965
|
+
}
|
|
794
966
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
967
|
+
_emitZoneMapsInto(dv, bytes, off, T, shardCount, src) {
|
|
968
|
+
bytes[off + 0] = 0x30; bytes[off + 1] = 0x5A; bytes[off + 2] = 0x4D; bytes[off + 3] = 0x31; // 'ZM01'
|
|
969
|
+
dv.setUint32(off + 4, shardCount, true);
|
|
970
|
+
dv.setUint32(off + 8, T, true);
|
|
971
|
+
dv.setUint32(off + 12, 0, true);
|
|
972
|
+
const fieldTableOff = off + 16;
|
|
973
|
+
for (let t = 0; t < T; t++) dv.setUint16(fieldTableOff + t * 2, this._trackedFieldIndices[t], true);
|
|
974
|
+
const fieldTableLen = T * 2;
|
|
975
|
+
const fieldTablePad = (8 - (fieldTableLen & 7)) & 7;
|
|
976
|
+
const minsOff = fieldTableOff + fieldTableLen + fieldTablePad;
|
|
977
|
+
const maxesOff = minsOff + shardCount * T * 8;
|
|
978
|
+
for (let s = 0; s < shardCount; s++) {
|
|
979
|
+
const sh = src[s];
|
|
804
980
|
for (let t = 0; t < T; t++) {
|
|
805
|
-
dv.
|
|
981
|
+
dv.setFloat64(minsOff + (s * T + t) * 8, sh.mins[t], true);
|
|
982
|
+
dv.setFloat64(maxesOff + (s * T + t) * 8, sh.maxes[t], true);
|
|
806
983
|
}
|
|
807
|
-
// Mins / maxes: row-major, [shard * T + tracked]
|
|
808
|
-
for (let s = 0; s < shardCount; s++) {
|
|
809
|
-
const sh = this._shards[s];
|
|
810
|
-
for (let t = 0; t < T; t++) {
|
|
811
|
-
dv.setFloat64(zoneMapsMinsOff + (s * T + t) * 8, sh.mins[t], true);
|
|
812
|
-
dv.setFloat64(zoneMapsMaxesOff + (s * T + t) * 8, sh.maxes[t], true);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
// Payloads + local string tables
|
|
818
|
-
for (let i = 0; i < shardCount; i++) {
|
|
819
|
-
bytes.set(this._shards[i].bytes, shardPayloadOffsets[i]);
|
|
820
|
-
bytes.set(this._shards[i].stringTableBytes, shardStringTableOffsets[i]);
|
|
821
984
|
}
|
|
985
|
+
}
|
|
822
986
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
dv.setUint32(footerOff + 0, 0xFFFFFFFF, true);
|
|
987
|
+
_emitFooterInto(dv, bytes, footerOff, crcVal) {
|
|
988
|
+
dv.setUint32(footerOff + 0, crcVal >>> 0, true);
|
|
826
989
|
dv.setUint32(footerOff + 4, 0, true);
|
|
827
990
|
bytes[footerOff + 8] = 0x31;
|
|
828
991
|
bytes[footerOff + 9] = 0x4B;
|
|
829
992
|
bytes[footerOff + 10] = 0x42;
|
|
830
993
|
bytes[footerOff + 11] = 0x4C;
|
|
831
994
|
dv.setUint32(footerOff + 12, FOOTER_BYTES, true);
|
|
995
|
+
}
|
|
832
996
|
|
|
833
|
-
|
|
997
|
+
// Classic prefix layout: header | schema | directory | zone maps | payloads |
|
|
998
|
+
// footer, in ONE contiguous buffer. Byte-for-byte identical to the pre-M6
|
|
999
|
+
// assembler when crc is off; the optional CRC-32C covers [0, footer_off).
|
|
1000
|
+
_buildPrefixBytes(crcOn) {
|
|
1001
|
+
const sb = this._computeSchemaBlock();
|
|
1002
|
+
const shardCount = this._shards.length;
|
|
1003
|
+
const shardDirBytes = shardCount * SHARD_ENTRY_BYTES;
|
|
1004
|
+
const schemaBlockOff = CONTAINER_HEADER_BYTES;
|
|
1005
|
+
const shardDirOff = schemaBlockOff + sb.schemaBlockBytes;
|
|
1006
|
+
const T = this._trackedFieldIndices.length;
|
|
1007
|
+
const zoneMapsEnabled = T > 0 && shardCount > 0;
|
|
1008
|
+
let zoneMapsOff = 0, zoneMapsBytes = 0;
|
|
1009
|
+
if (zoneMapsEnabled) {
|
|
1010
|
+
zoneMapsOff = shardDirOff + shardDirBytes;
|
|
1011
|
+
zoneMapsBytes = this._zoneMapsByteLen(T, shardCount);
|
|
1012
|
+
}
|
|
1013
|
+
const firstShardOff = shardDirOff + shardDirBytes + zoneMapsBytes;
|
|
1014
|
+
let cursor = firstShardOff;
|
|
1015
|
+
const payloadOffs = new Array(shardCount);
|
|
1016
|
+
const stOffs = new Array(shardCount);
|
|
1017
|
+
for (let i = 0; i < shardCount; i++) {
|
|
1018
|
+
const s = this._shards[i];
|
|
1019
|
+
payloadOffs[i] = cursor;
|
|
1020
|
+
const payloadLen = s.bytes.length;
|
|
1021
|
+
const payloadPad = (8 - (payloadLen & 7)) & 7;
|
|
1022
|
+
cursor += payloadLen + payloadPad;
|
|
1023
|
+
stOffs[i] = cursor;
|
|
1024
|
+
cursor += s.stringTableBytes.length; // stringTable.serialize() is already 8-padded
|
|
1025
|
+
}
|
|
1026
|
+
const footerOff = cursor;
|
|
1027
|
+
const totalBytes = cursor + FOOTER_BYTES;
|
|
1028
|
+
const buffer = new ArrayBuffer(totalBytes);
|
|
1029
|
+
const dv = new DataView(buffer);
|
|
1030
|
+
const bytes = new Uint8Array(buffer);
|
|
1031
|
+
|
|
1032
|
+
this._emitHeaderInto(dv, bytes, 0, {
|
|
1033
|
+
schemaBlockOff, metadataOff: zoneMapsEnabled ? zoneMapsOff : 0,
|
|
1034
|
+
shardDirOff, shardCount, totalRows: this._totalRows,
|
|
1035
|
+
});
|
|
1036
|
+
this._emitSchemaInto(dv, bytes, schemaBlockOff, sb);
|
|
1037
|
+
for (let i = 0; i < shardCount; i++) {
|
|
1038
|
+
const s = this._shards[i];
|
|
1039
|
+
this._emitDirEntryInto(dv, shardDirOff + i * SHARD_ENTRY_BYTES,
|
|
1040
|
+
payloadOffs[i], s.bytes.length, s.rowCount, stOffs[i], s.stringTableBytes.length);
|
|
1041
|
+
}
|
|
1042
|
+
if (zoneMapsEnabled) this._emitZoneMapsInto(dv, bytes, zoneMapsOff, T, shardCount, this._shards);
|
|
1043
|
+
for (let i = 0; i < shardCount; i++) {
|
|
1044
|
+
bytes.set(this._shards[i].bytes, payloadOffs[i]);
|
|
1045
|
+
bytes.set(this._shards[i].stringTableBytes, stOffs[i]);
|
|
1046
|
+
}
|
|
1047
|
+
let crcVal = CRC_ABSENT;
|
|
1048
|
+
if (crcOn) crcVal = crc32cFinal(crc32cUpdate(crc32cInit(), bytes, 0, footerOff));
|
|
1049
|
+
this._emitFooterInto(dv, bytes, footerOff, crcVal);
|
|
1050
|
+
return bytes;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// PUBLIC. Emit the container to a caller sink instead of returning a buffer.
|
|
1054
|
+
// layout:'prefix' -- the classic layout, written in one sink.write().
|
|
1055
|
+
// layout:'stream' -- payloads stream first, then the trailer + footer, with
|
|
1056
|
+
// one sink.writeAt(header, 0) backpatch (needs writeAt).
|
|
1057
|
+
// Returns { totalRows, shardCount, schema|mode, bytesWritten, layout }. When
|
|
1058
|
+
// the writer was pre-bound to this sink via beginStream (bounded-RAM ingest),
|
|
1059
|
+
// the shards are already emitted and this only writes the trailer.
|
|
1060
|
+
finalizeToSink(sink, opts) {
|
|
1061
|
+
if (this._finalized) throw new WriterError('W_FINALIZED', 'writer already finalized');
|
|
1062
|
+
checkOpts('Writer.finalizeToSink', opts, FINALIZE_TO_SINK_OPTS, raiseWriter);
|
|
1063
|
+
opts = opts || {};
|
|
1064
|
+
|
|
1065
|
+
if (this._sink !== null) {
|
|
1066
|
+
// Pre-bound streaming: shards were emitted during ingest. Finish the input
|
|
1067
|
+
// (streams the trailing shard), then write the trailer.
|
|
1068
|
+
if (sink !== this._sink) raiseWriter('W_BAD_SINK', 'finalizeToSink sink differs from the streaming sink');
|
|
1069
|
+
if (opts.layout !== undefined && opts.layout !== 'stream')
|
|
1070
|
+
raiseWriter('W_BAD_SINK', "a streaming writer must be finalized with layout:'stream'");
|
|
1071
|
+
const crcOn = opts.crc !== undefined ? opts.crc === true : this._sinkCrcOn;
|
|
1072
|
+
this._sinkCrcOn = crcOn;
|
|
1073
|
+
this._completeInput();
|
|
1074
|
+
return this._finishStream(sink, crcOn);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const layout = opts.layout !== undefined ? opts.layout : 'stream';
|
|
1078
|
+
const crcOn = opts.crc !== undefined ? opts.crc === true : this._crc;
|
|
1079
|
+
validateSink(sink, layout === 'stream', raiseWriter);
|
|
1080
|
+
this._completeInput();
|
|
1081
|
+
|
|
1082
|
+
if (layout === 'prefix') {
|
|
1083
|
+
const bytes = this._buildPrefixBytes(crcOn);
|
|
1084
|
+
let ret;
|
|
1085
|
+
try { ret = sink.write(bytes); }
|
|
1086
|
+
catch (e) { this._finalized = true; throw e; }
|
|
1087
|
+
if (isThenable(ret)) { this._finalized = true; raiseWriter('W_BAD_SINK', 'sink.write returned a thenable; sinks must be synchronous'); }
|
|
1088
|
+
this._finalized = true;
|
|
1089
|
+
this._recordDepth = FINALIZED_DEPTH;
|
|
1090
|
+
return { totalRows: this._totalRows, shardCount: this._shards.length, schema: this._schema, bytesWritten: bytes.length, layout: 'prefix' };
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// layout 'stream', buffered shards: stream them out now, dropping each.
|
|
1094
|
+
const buffered = this._shards;
|
|
1095
|
+
this._shards = [];
|
|
1096
|
+
this._sink = sink;
|
|
1097
|
+
this._sinkCrcOn = crcOn;
|
|
1098
|
+
this._sinkPos = 0;
|
|
1099
|
+
this._sinkCrc = crc32cInit();
|
|
1100
|
+
this._sinkWrite(new Uint8Array(CONTAINER_HEADER_BYTES), false);
|
|
1101
|
+
this._sinkPos = CONTAINER_HEADER_BYTES;
|
|
1102
|
+
for (let i = 0; i < buffered.length; i++) {
|
|
1103
|
+
const s = buffered[i];
|
|
1104
|
+
this._streamEmitShard(s.bytes, s.stringTableBytes, s.rowCount, s.mins, s.maxes);
|
|
1105
|
+
buffered[i] = null;
|
|
1106
|
+
}
|
|
1107
|
+
return this._finishStream(sink, crcOn);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Write the schema/directory/zone-map trailer + footer and backpatch the header.
|
|
1111
|
+
// _shards holds one scalar descriptor per emitted shard; _sinkPos/_sinkCrc are
|
|
1112
|
+
// the current sink position and running body CRC over [48, pos).
|
|
1113
|
+
_finishStream(sink, crcOn) {
|
|
1114
|
+
const shardCount = this._shards.length;
|
|
1115
|
+
const sb = this._computeSchemaBlock();
|
|
1116
|
+
const schemaBlockOff = this._sinkPos; // 8-aligned: every payload region is 8-padded
|
|
1117
|
+
const shardDirBytes = shardCount * SHARD_ENTRY_BYTES;
|
|
1118
|
+
const shardDirOff = schemaBlockOff + sb.schemaBlockBytes;
|
|
1119
|
+
const T = this._trackedFieldIndices.length;
|
|
1120
|
+
const zoneMapsEnabled = T > 0 && shardCount > 0;
|
|
1121
|
+
let zoneMapsOff = 0, zoneMapsBytes = 0;
|
|
1122
|
+
if (zoneMapsEnabled) {
|
|
1123
|
+
zoneMapsOff = shardDirOff + shardDirBytes;
|
|
1124
|
+
zoneMapsBytes = this._zoneMapsByteLen(T, shardCount);
|
|
1125
|
+
}
|
|
1126
|
+
const trailerLen = sb.schemaBlockBytes + shardDirBytes + zoneMapsBytes;
|
|
1127
|
+
const footerOff = schemaBlockOff + trailerLen;
|
|
1128
|
+
|
|
1129
|
+
const trailer = new Uint8Array(trailerLen);
|
|
1130
|
+
const tdv = new DataView(trailer.buffer);
|
|
1131
|
+
this._emitSchemaInto(tdv, trailer, 0, sb);
|
|
1132
|
+
for (let i = 0; i < shardCount; i++) {
|
|
1133
|
+
const d = this._shards[i];
|
|
1134
|
+
this._emitDirEntryInto(tdv, sb.schemaBlockBytes + i * SHARD_ENTRY_BYTES,
|
|
1135
|
+
d.payloadOff, d.payloadLen, d.rowCount, d.stringOff, d.stringLen);
|
|
1136
|
+
}
|
|
1137
|
+
if (zoneMapsEnabled) this._emitZoneMapsInto(tdv, trailer, sb.schemaBlockBytes + shardDirBytes, T, shardCount, this._shards);
|
|
1138
|
+
this._sinkWrite(trailer, true);
|
|
1139
|
+
this._sinkPos += trailerLen;
|
|
1140
|
+
|
|
1141
|
+
const header = new Uint8Array(CONTAINER_HEADER_BYTES);
|
|
1142
|
+
const hdv = new DataView(header.buffer);
|
|
1143
|
+
this._emitHeaderInto(hdv, header, 0, {
|
|
1144
|
+
schemaBlockOff, metadataOff: zoneMapsEnabled ? zoneMapsOff : 0,
|
|
1145
|
+
shardDirOff, shardCount, totalRows: this._totalRows,
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
let crcVal = CRC_ABSENT;
|
|
1149
|
+
if (crcOn) {
|
|
1150
|
+
const headerCrc = crc32cFinal(crc32cUpdate(crc32cInit(), header, 0, CONTAINER_HEADER_BYTES));
|
|
1151
|
+
const suffixCrc = crc32cFinal(this._sinkCrc);
|
|
1152
|
+
crcVal = crc32cCombine(headerCrc, suffixCrc, footerOff - CONTAINER_HEADER_BYTES);
|
|
1153
|
+
}
|
|
1154
|
+
const footer = new Uint8Array(FOOTER_BYTES);
|
|
1155
|
+
const fdv = new DataView(footer.buffer);
|
|
1156
|
+
this._emitFooterInto(fdv, footer, 0, crcVal);
|
|
1157
|
+
this._sinkWrite(footer, false);
|
|
1158
|
+
this._sinkPos += FOOTER_BYTES;
|
|
1159
|
+
|
|
1160
|
+
let ret;
|
|
1161
|
+
try { ret = sink.writeAt(header, 0); }
|
|
1162
|
+
catch (e) { this._finalized = true; throw e; }
|
|
1163
|
+
if (isThenable(ret)) { this._finalized = true; raiseWriter('W_BAD_SINK', 'sink.writeAt returned a thenable; sinks must be synchronous'); }
|
|
1164
|
+
|
|
1165
|
+
this._finalized = true;
|
|
1166
|
+
this._recordDepth = FINALIZED_DEPTH;
|
|
1167
|
+
return { totalRows: this._totalRows, shardCount, schema: this._schema, bytesWritten: this._sinkPos, layout: 'stream' };
|
|
834
1168
|
}
|
|
835
1169
|
|
|
836
1170
|
get schema() { return this._schema; }
|
|
837
1171
|
get totalRows() { return this._totalRows; }
|
|
838
1172
|
get shardCount() { return this._shards.length + (this._currentShardRowCount > 0 ? 1 : 0); }
|
|
1173
|
+
|
|
1174
|
+
// @internal TEST-ONLY (D2): per-shard [{ tracked, emitted }] string-table byte
|
|
1175
|
+
// lengths. tracked is the incrementally-maintained budget at each finalize;
|
|
1176
|
+
// emitted is the serialized local_string_len that reached the container. They
|
|
1177
|
+
// MUST be equal at every shard -- Ceilings.test.js proves it, closing the
|
|
1178
|
+
// formula-drift bug class. No semver guarantee; absent from the public API.
|
|
1179
|
+
__stringTableBudgetAudit() {
|
|
1180
|
+
const out = new Array(this._shards.length);
|
|
1181
|
+
for (let i = 0; i < this._shards.length; i++) {
|
|
1182
|
+
out[i] = { tracked: this._shards[i].budgetTracked, emitted: this._shards[i].stringTableBytes.length };
|
|
1183
|
+
}
|
|
1184
|
+
return out;
|
|
1185
|
+
}
|
|
839
1186
|
}
|