@zakkster/lite-bake-stream 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +156 -128
- package/README.md +294 -95
- package/SPEC.md +32 -30
- package/llms.txt +18 -13
- package/package.json +2 -2
- package/src/Crc32c.js +96 -0
- package/src/FileIngest.js +6 -2
- package/src/MultiReader.js +1 -1
- package/src/Opts.js +4 -0
- package/src/PreserveReader.js +29 -4
- package/src/PreserveTokenizer.js +12 -3
- package/src/PreserveWriter.js +218 -61
- package/src/RangeReader.js +29 -2
- package/src/Reader.js +34 -4
- package/src/Split.js +22 -6
- package/src/StringTable.js +7 -4
- package/src/Tokenizer.js +3 -2
- package/src/Views.js +65 -0
- package/src/Writer.js +387 -146
- package/src/index.js +8 -4
- package/types/MultiReader.d.ts +1 -1
- package/types/PreserveReader.d.ts +10 -2
- package/types/PreserveWriter.d.ts +30 -0
- package/types/RangeReader.d.ts +6 -1
- package/types/Reader.d.ts +11 -3
- package/types/Split.d.ts +1 -1
- package/types/Tokenizer.d.ts +1 -1
- package/types/Writer.d.ts +39 -2
- package/types/index.d.ts +2 -1
package/src/Writer.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// setInputSource; hand-driven Writers with no source sample by record
|
|
12
12
|
// count) buffered in a columnar staging area, per-field lane kind inferred
|
|
13
13
|
// from observed value types (number->F64, string->U32-into-string-table).
|
|
14
|
+
// See decisions/0011-sample-drain-reintern.md.
|
|
14
15
|
// null is lane-neutral (BS-20): it marks the field but sets no kind, so
|
|
15
16
|
// null+string infers U32 and null+number infers F64. A field that saw both
|
|
16
17
|
// a real number and a real string raises W_MIXED_LANE_TYPES at freeze.
|
|
@@ -52,13 +53,16 @@
|
|
|
52
53
|
// W_SCHEMA_TOO_WIDE - a field offset exceeds the u16 offset_in_row ceiling
|
|
53
54
|
// W_FIELD_NAME_INVALID - field name empty, non-string, or > 255 UTF-8 bytes
|
|
54
55
|
// W_FINALIZED - a sink event or finalize() after finalize()
|
|
56
|
+
// W_BAD_SINK - finalizeToSink given a sink missing write/writeAt or async
|
|
55
57
|
// E_UNKNOWN_OPTION - unknown constructor option key
|
|
56
58
|
// E_OPTION_VALUE - constructor option value out of domain
|
|
57
59
|
|
|
58
60
|
import { StringTable } from './StringTable.js';
|
|
59
61
|
import { checkOpts } from './Opts.js';
|
|
62
|
+
import { crc32cInit, crc32cUpdate, crc32cFinal, crc32cCombine } from './Crc32c.js';
|
|
63
|
+
import { validateSink, isThenable } from './Views.js';
|
|
60
64
|
|
|
61
|
-
export const VERSION = '1.
|
|
65
|
+
export const VERSION = '1.6.1';
|
|
62
66
|
|
|
63
67
|
const U32_MAX = 4294967295;
|
|
64
68
|
// Post-finalize sentinel for _recordDepth. Chosen = 2 so every post-finalize
|
|
@@ -71,7 +75,13 @@ const WRITER_OPTS = {
|
|
|
71
75
|
schema: { t: 'obj', nullable: true },
|
|
72
76
|
targetShardBytes: { t: 'int', min: 1, max: U32_MAX },
|
|
73
77
|
sampleBytes: { t: 'int', min: 0, max: U32_MAX },
|
|
78
|
+
crc: { t: 'bool' },
|
|
74
79
|
};
|
|
80
|
+
const FINALIZE_TO_SINK_OPTS = {
|
|
81
|
+
layout: { t: 'enum', values: ['prefix', 'stream'] },
|
|
82
|
+
crc: { t: 'bool' },
|
|
83
|
+
};
|
|
84
|
+
const CRC_ABSENT = 0xFFFFFFFF;
|
|
75
85
|
function raiseWriter(code, msg) { throw new WriterError(code, msg); }
|
|
76
86
|
|
|
77
87
|
const CONTAINER_HEADER_BYTES = 48;
|
|
@@ -91,6 +101,10 @@ const SHARD_FLAGS_NONE = 0;
|
|
|
91
101
|
// U32-lane fields). Read-only; never written into.
|
|
92
102
|
const EMPTY_STRING_TABLE_BYTES = new Uint8Array(0);
|
|
93
103
|
|
|
104
|
+
// Shared read-only zero span for 8-byte shard-payload alignment padding in the
|
|
105
|
+
// streaming path. Never written into.
|
|
106
|
+
const ZERO_PAD = new Uint8Array(8);
|
|
107
|
+
|
|
94
108
|
const FORMAT_VERSION = 1;
|
|
95
109
|
const ENDIAN_LE = 1;
|
|
96
110
|
|
|
@@ -225,13 +239,24 @@ export class Writer {
|
|
|
225
239
|
this._targetShardBytes = opts.targetShardBytes !== undefined ? opts.targetShardBytes : DEFAULT_SHARD_BYTES;
|
|
226
240
|
this._sampleBytes = opts.sampleBytes !== undefined ? opts.sampleBytes : this._targetShardBytes;
|
|
227
241
|
this._explicitSchema = opts.schema !== undefined ? opts.schema : null;
|
|
242
|
+
this._crc = opts.crc === true;
|
|
243
|
+
|
|
244
|
+
// Streaming-emission state (M6). Null sink => classic buffered writer:
|
|
245
|
+
// shards accumulate in _shards until finalize()/finalizeToSink drains them.
|
|
246
|
+
// A bound sink (beginStream) switches _finalizeCurrentShard to emit each
|
|
247
|
+
// shard as it finalizes and retain only a scalar directory descriptor, so
|
|
248
|
+
// peak RAM stays O(targetShardBytes + directory) instead of O(container).
|
|
249
|
+
this._sink = null;
|
|
250
|
+
this._sinkCrcOn = false;
|
|
251
|
+
this._sinkPos = 0; // current absolute write position in the sink
|
|
252
|
+
this._sinkCrc = 0; // running int32 CRC of the body suffix [48, pos)
|
|
228
253
|
|
|
229
254
|
// Frozen schema state
|
|
230
255
|
this._schema = null; // { fields: [{name, laneKind, offsetInRow}], rowStride }
|
|
231
256
|
this._fieldNamesUtf8 = null;
|
|
232
257
|
this._fieldNameHashes = null;
|
|
233
|
-
this._fieldLaneKinds = null; // Uint8Array
|
|
234
|
-
this._fieldOffsets = null; // Uint16Array
|
|
258
|
+
this._fieldLaneKinds = null; // Uint8Array -- per-field LANE_* (fast dispatch)
|
|
259
|
+
this._fieldOffsets = null; // Uint16Array -- per-field byte offset in row
|
|
235
260
|
this._rowValueSlotsF64 = null; // Float64Array<fieldCount> scratch
|
|
236
261
|
this._rowValueSlotsU32 = null; // Uint32Array<fieldCount> scratch
|
|
237
262
|
|
|
@@ -313,7 +338,7 @@ export class Writer {
|
|
|
313
338
|
} else {
|
|
314
339
|
// Sample window: decode to a JS string NOW. The tokenizer will reuse this
|
|
315
340
|
// buffer for the value bytes before onNumber/onString fires, so we cannot
|
|
316
|
-
// defer decoding. String allocation here is expected
|
|
341
|
+
// defer decoding. String allocation here is expected -- bounded to the
|
|
317
342
|
// sample window; steady-state post-freeze remains zero-alloc.
|
|
318
343
|
this._currentKeyName = KEY_DECODER.decode(bytes.subarray(from, to));
|
|
319
344
|
}
|
|
@@ -633,17 +658,25 @@ export class Writer {
|
|
|
633
658
|
const shardMaxes = new Float64Array(T);
|
|
634
659
|
shardMins.set(this._currentShardMins);
|
|
635
660
|
shardMaxes.set(this._currentShardMaxes);
|
|
636
|
-
this.
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
//
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
661
|
+
if (this._sink !== null) {
|
|
662
|
+
// Streaming mode: emit this shard to the bound sink now and retain only a
|
|
663
|
+
// scalar directory descriptor (payload/string offsets, row count, zone
|
|
664
|
+
// bounds). The heavy payload + string-table byte arrays are dropped, so
|
|
665
|
+
// peak RAM does not grow with shard count.
|
|
666
|
+
this._streamEmitShard(copy, stBytes, this._currentShardRowCount, shardMins, shardMaxes);
|
|
667
|
+
} else {
|
|
668
|
+
this._shards.push({
|
|
669
|
+
bytes: copy,
|
|
670
|
+
rowCount: this._currentShardRowCount,
|
|
671
|
+
stringTableBytes: stBytes,
|
|
672
|
+
// D2 audit: the incrementally-tracked budget at this finalize. MUST equal
|
|
673
|
+
// stBytes.length (the emitted directory local_string_len). Cross-checked by
|
|
674
|
+
// Ceilings.test.js via __stringTableBudgetAudit; not part of the public API.
|
|
675
|
+
budgetTracked: this._stringTableBytes,
|
|
676
|
+
mins: shardMins,
|
|
677
|
+
maxes: shardMaxes,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
647
680
|
this._currentShardBuffer = null;
|
|
648
681
|
this._currentShardBytes = null;
|
|
649
682
|
this._currentShardDv = null;
|
|
@@ -735,7 +768,12 @@ export class Writer {
|
|
|
735
768
|
// receive strings from post-drain records that fill subsequent shards.
|
|
736
769
|
}
|
|
737
770
|
|
|
738
|
-
finalize()
|
|
771
|
+
// Shared input-completion prologue for finalize() and finalizeToSink(): freeze
|
|
772
|
+
// an inferred schema, drain the sample, finalize the trailing partial shard,
|
|
773
|
+
// and fail closed on empty input. After this returns, _shards holds one entry
|
|
774
|
+
// per shard (a buffered {bytes,...} in the classic path, or a scalar directory
|
|
775
|
+
// descriptor when a sink was bound via beginStream).
|
|
776
|
+
_completeInput() {
|
|
739
777
|
if (this._finalized) throw new WriterError('W_FINALIZED', 'writer already finalized');
|
|
740
778
|
if (!this._schema) {
|
|
741
779
|
if (this._sample.rowCount === 0) throw new WriterError('W_EMPTY_INPUT', 'no records to write');
|
|
@@ -744,13 +782,94 @@ export class Writer {
|
|
|
744
782
|
}
|
|
745
783
|
if (this._currentShardRowCount > 0) this._finalizeCurrentShard();
|
|
746
784
|
if (this._shards.length === 0) throw new WriterError('W_EMPTY_INPUT', 'no records to write');
|
|
747
|
-
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
finalize() {
|
|
788
|
+
this._completeInput();
|
|
789
|
+
// finalize() IS layout:'prefix' over an implicit in-memory buffer: it builds
|
|
790
|
+
// the identical classic container the pre-M6 assembler produced (byte-for-
|
|
791
|
+
// byte when crc is off) and returns the frozen struct.
|
|
792
|
+
const bytes = this._buildPrefixBytes(this._crc);
|
|
748
793
|
this._finalized = true;
|
|
749
794
|
// Post-finalize sentinel: route every later sink event into a cold arm.
|
|
750
795
|
this._recordDepth = FINALIZED_DEPTH;
|
|
796
|
+
this._container = {
|
|
797
|
+
buffer: bytes.buffer,
|
|
798
|
+
totalRows: this._totalRows,
|
|
799
|
+
shardCount: this._shards.length,
|
|
800
|
+
schema: this._schema,
|
|
801
|
+
};
|
|
751
802
|
return this._container;
|
|
752
803
|
}
|
|
753
804
|
|
|
805
|
+
// PUBLIC. Bind a sink and switch to streaming emission BEFORE the first record
|
|
806
|
+
// is fed (the two-step bounded-RAM contract): call beginStream(sink, opts),
|
|
807
|
+
// then feed the tokenizer -- each shard is written to the sink as it finalizes
|
|
808
|
+
// and its bytes are dropped, so peak RAM is O(targetShardBytes + directory) --
|
|
809
|
+
// then call finalizeToSink(sink, opts), which appends the schema/directory/
|
|
810
|
+
// zone-map trailer and footer and performs the single header backpatch. Calling
|
|
811
|
+
// finalizeToSink alone (without beginStream) is BUFFERED mode: correct, but
|
|
812
|
+
// peak RAM is O(container). opts.layout must be 'stream'; opts.crc (default:
|
|
813
|
+
// the constructor crc) toggles CRC-32C.
|
|
814
|
+
beginStream(sink, opts) {
|
|
815
|
+
if (this._finalized) throw new WriterError('W_FINALIZED', 'writer already finalized');
|
|
816
|
+
if (this._sink !== null) throw new WriterError('W_FINALIZED', 'writer is already streaming to a sink');
|
|
817
|
+
if (this._shards.length > 0 || this._currentShardRowCount > 0)
|
|
818
|
+
throw new WriterError('W_FINALIZED', 'beginStream must be called before the first record');
|
|
819
|
+
checkOpts('Writer.beginStream', opts, FINALIZE_TO_SINK_OPTS, raiseWriter);
|
|
820
|
+
opts = opts || {};
|
|
821
|
+
if (opts.layout !== undefined && opts.layout !== 'stream')
|
|
822
|
+
raiseWriter('W_BAD_SINK', "beginStream requires layout:'stream'");
|
|
823
|
+
validateSink(sink, true, raiseWriter);
|
|
824
|
+
this._sink = sink;
|
|
825
|
+
this._sinkCrcOn = opts.crc !== undefined ? opts.crc === true : this._crc;
|
|
826
|
+
this._sinkPos = 0;
|
|
827
|
+
this._sinkCrc = crc32cInit();
|
|
828
|
+
// Reserve the 48-byte header up front; it is backpatched by ONE writeAt at
|
|
829
|
+
// the end once shardCount and the trailer offsets are known. Excluded from
|
|
830
|
+
// the running suffix CRC (folded in via crc32cCombine at the end).
|
|
831
|
+
const placeholder = new Uint8Array(CONTAINER_HEADER_BYTES);
|
|
832
|
+
this._sinkWrite(placeholder, false);
|
|
833
|
+
this._sinkPos = CONTAINER_HEADER_BYTES;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Write bytes to the bound sink, optionally folding them into the running body
|
|
837
|
+
// CRC. A thenable return means an async sink -- the contract is synchronous, so
|
|
838
|
+
// that is a malformed sink (W_BAD_SINK). NOTE: _sinkPos is advanced by callers.
|
|
839
|
+
_sinkWrite(bytes, fold) {
|
|
840
|
+
if (fold && this._sinkCrcOn) this._sinkCrc = crc32cUpdate(this._sinkCrc, bytes, 0, bytes.length);
|
|
841
|
+
let ret;
|
|
842
|
+
// A sink that throws mid-emission fails the writer closed and its own error
|
|
843
|
+
// is rethrown verbatim -- never laundered into a package code (the FileIngest
|
|
844
|
+
// mid-stream precedent). A retry then hits W_FINALIZED.
|
|
845
|
+
try { ret = this._sink.write(bytes); }
|
|
846
|
+
catch (e) { this._finalized = true; throw e; }
|
|
847
|
+
if (isThenable(ret)) { this._finalized = true; raiseWriter('W_BAD_SINK', 'sink.write returned a thenable; sinks must be synchronous'); }
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Streaming emit of one finalized shard: payload, 8-alignment pad, local string
|
|
851
|
+
// table -- each folded into the body CRC. Retains only a scalar descriptor.
|
|
852
|
+
_streamEmitShard(payloadBytes, stBytes, rowCount, mins, maxes) {
|
|
853
|
+
const payloadOff = this._sinkPos;
|
|
854
|
+
const payloadLen = payloadBytes.length;
|
|
855
|
+
this._sinkWrite(payloadBytes, true);
|
|
856
|
+
this._sinkPos += payloadLen;
|
|
857
|
+
const pad = (8 - (payloadLen & 7)) & 7;
|
|
858
|
+
if (pad > 0) { this._sinkWrite(ZERO_PAD.subarray(0, pad), true); this._sinkPos += pad; }
|
|
859
|
+
const stOff = this._sinkPos;
|
|
860
|
+
const stLen = stBytes.length;
|
|
861
|
+
if (stLen > 0) { this._sinkWrite(stBytes, true); this._sinkPos += stLen; }
|
|
862
|
+
this._shards.push({
|
|
863
|
+
rowCount,
|
|
864
|
+
payloadOff,
|
|
865
|
+
payloadLen,
|
|
866
|
+
stringOff: stLen > 0 ? stOff : 0,
|
|
867
|
+
stringLen: stLen,
|
|
868
|
+
mins,
|
|
869
|
+
maxes,
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
|
|
754
873
|
// Optional capability hook (BS-07): the Tokenizer calls this at construction
|
|
755
874
|
// to hand over the live tokenizer. The Writer then reads src.absOffset at
|
|
756
875
|
// record boundaries so the sample window is byte-true. A CALL only; the
|
|
@@ -762,100 +881,62 @@ export class Writer {
|
|
|
762
881
|
// dance is a documented no-op retained for source compatibility until 2.0.
|
|
763
882
|
setInputByteOffset() {}
|
|
764
883
|
|
|
765
|
-
// -------- container assembly --------
|
|
766
|
-
|
|
767
|
-
|
|
884
|
+
// -------- container assembly (placement-free emitters) --------
|
|
885
|
+
//
|
|
886
|
+
// Both finalize() (layout:'prefix') and finalizeToSink(layout:'stream') drive
|
|
887
|
+
// the SAME byte-producing emitters below. The classic prefix layout builds one
|
|
888
|
+
// contiguous buffer; the streaming layout writes payloads first, accumulates a
|
|
889
|
+
// scalar directory, and emits the schema/directory/zone-map trailer + footer at
|
|
890
|
+
// the end with a single header backpatch. The emitters take an explicit target
|
|
891
|
+
// offset so they are reusable across both a full-container buffer and a small
|
|
892
|
+
// trailer buffer.
|
|
893
|
+
|
|
894
|
+
// Encode field names once and size the schema block (8-byte padded). Shared by
|
|
895
|
+
// every layout.
|
|
896
|
+
_computeSchemaBlock() {
|
|
768
897
|
const enc = new TextEncoder();
|
|
769
|
-
const
|
|
898
|
+
const fields = this._schema.fields;
|
|
899
|
+
const nameBytes = new Array(fields.length);
|
|
770
900
|
let nameBlobLen = 0;
|
|
771
|
-
for (let i = 0; i <
|
|
772
|
-
nameBytes[i] = enc.encode(
|
|
901
|
+
for (let i = 0; i < fields.length; i++) {
|
|
902
|
+
nameBytes[i] = enc.encode(fields[i].name);
|
|
773
903
|
nameBlobLen += nameBytes[i].length;
|
|
774
904
|
}
|
|
775
|
-
|
|
776
|
-
const fieldCount = this._schema.fields.length;
|
|
777
|
-
const descriptorBytes = fieldCount * FIELD_DESCRIPTOR_BYTES;
|
|
905
|
+
const descriptorBytes = fields.length * FIELD_DESCRIPTOR_BYTES;
|
|
778
906
|
let schemaBlockBytes = 8 + descriptorBytes + 4 + nameBlobLen;
|
|
779
|
-
|
|
780
|
-
schemaBlockBytes
|
|
781
|
-
|
|
782
|
-
const shardCount = this._shards.length;
|
|
783
|
-
const shardDirBytes = shardCount * SHARD_ENTRY_BYTES;
|
|
784
|
-
|
|
785
|
-
const schemaBlockOff = CONTAINER_HEADER_BYTES;
|
|
786
|
-
const shardDirOff = schemaBlockOff + schemaBlockBytes;
|
|
907
|
+
schemaBlockBytes += (8 - (schemaBlockBytes & 7)) & 7;
|
|
908
|
+
return { nameBytes, nameBlobLen, descriptorBytes, schemaBlockBytes };
|
|
909
|
+
}
|
|
787
910
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
let zoneMapsOff = 0;
|
|
794
|
-
let zoneMapsBytes = 0;
|
|
795
|
-
let zoneMapsFieldTableOff = 0;
|
|
796
|
-
let zoneMapsMinsOff = 0;
|
|
797
|
-
let zoneMapsMaxesOff = 0;
|
|
798
|
-
if (zoneMapsEnabled) {
|
|
799
|
-
zoneMapsOff = shardDirOff + shardDirBytes;
|
|
800
|
-
// Layout: 16-byte header + T*u16 field indices, padded to 8, then
|
|
801
|
-
// shardCount*T*8 mins, then shardCount*T*8 maxes.
|
|
802
|
-
const headerLen = 16;
|
|
803
|
-
const fieldTableLen = T * 2;
|
|
804
|
-
const fieldTablePad = (8 - (fieldTableLen & 7)) & 7;
|
|
805
|
-
zoneMapsFieldTableOff = zoneMapsOff + headerLen;
|
|
806
|
-
const minsRel = headerLen + fieldTableLen + fieldTablePad;
|
|
807
|
-
zoneMapsMinsOff = zoneMapsOff + minsRel;
|
|
808
|
-
const minsMaxesLen = shardCount * T * 8 * 2;
|
|
809
|
-
zoneMapsMaxesOff = zoneMapsMinsOff + shardCount * T * 8;
|
|
810
|
-
zoneMapsBytes = minsRel + minsMaxesLen;
|
|
811
|
-
}
|
|
812
|
-
const firstShardOff = shardDirOff + shardDirBytes + zoneMapsBytes;
|
|
911
|
+
_zoneMapsByteLen(T, shardCount) {
|
|
912
|
+
const fieldTableLen = T * 2;
|
|
913
|
+
const fieldTablePad = (8 - (fieldTableLen & 7)) & 7;
|
|
914
|
+
return 16 + fieldTableLen + fieldTablePad + shardCount * T * 8 * 2;
|
|
915
|
+
}
|
|
813
916
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
cursor += shardPayloadPadded[i];
|
|
827
|
-
// String table immediately follows the shard payload
|
|
828
|
-
shardStringTableOffsets[i] = cursor;
|
|
829
|
-
const stLen = s.stringTableBytes.length;
|
|
830
|
-
shardStringTableLens[i] = stLen;
|
|
831
|
-
cursor += stLen; // stringTable.serialize() already 8-byte padded
|
|
832
|
-
}
|
|
917
|
+
_emitHeaderInto(dv, bytes, off, h) {
|
|
918
|
+
bytes[off + 0] = 0x4C; bytes[off + 1] = 0x42; bytes[off + 2] = 0x4B; bytes[off + 3] = 0x31;
|
|
919
|
+
dv.setUint16(off + 4, FORMAT_VERSION, true);
|
|
920
|
+
bytes[off + 6] = ENDIAN_LE;
|
|
921
|
+
bytes[off + 7] = 0;
|
|
922
|
+
dv.setBigUint64(off + 8, BigInt(h.schemaBlockOff), true);
|
|
923
|
+
dv.setBigUint64(off + 16, BigInt(h.metadataOff), true); // metadata_off -- 0 iff no zone maps
|
|
924
|
+
dv.setBigUint64(off + 24, BigInt(h.shardDirOff), true);
|
|
925
|
+
dv.setUint32(off + 32, h.shardCount, true);
|
|
926
|
+
dv.setUint32(off + 36, 0, true); // reserved1
|
|
927
|
+
dv.setBigUint64(off + 40, BigInt(h.totalRows), true);
|
|
928
|
+
}
|
|
833
929
|
|
|
834
|
-
|
|
835
|
-
const
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
// Header
|
|
840
|
-
bytes[0] = 0x4C; bytes[1] = 0x42; bytes[2] = 0x4B; bytes[3] = 0x31;
|
|
841
|
-
dv.setUint16(4, FORMAT_VERSION, true);
|
|
842
|
-
bytes[6] = ENDIAN_LE;
|
|
843
|
-
bytes[7] = 0;
|
|
844
|
-
dv.setBigUint64(8, BigInt(schemaBlockOff), true);
|
|
845
|
-
dv.setBigUint64(16, BigInt(zoneMapsOff), true); // metadata_off — 0 iff no zone maps
|
|
846
|
-
dv.setBigUint64(24, BigInt(shardDirOff), true);
|
|
847
|
-
dv.setUint32(32, shardCount, true);
|
|
848
|
-
dv.setUint32(36, 0, true); // reserved1
|
|
849
|
-
dv.setBigUint64(40, BigInt(this._totalRows), true);
|
|
850
|
-
|
|
851
|
-
// Schema block
|
|
852
|
-
dv.setUint32(schemaBlockOff, fieldCount, true);
|
|
853
|
-
dv.setUint32(schemaBlockOff + 4, this._schema.rowStride, true);
|
|
930
|
+
_emitSchemaInto(dv, bytes, off, sb) {
|
|
931
|
+
const fields = this._schema.fields;
|
|
932
|
+
const fieldCount = fields.length;
|
|
933
|
+
dv.setUint32(off, fieldCount, true);
|
|
934
|
+
dv.setUint32(off + 4, this._schema.rowStride, true);
|
|
854
935
|
let nameOff = 0;
|
|
855
936
|
for (let i = 0; i < fieldCount; i++) {
|
|
856
|
-
const descOff =
|
|
857
|
-
const f =
|
|
858
|
-
const nb = nameBytes[i];
|
|
937
|
+
const descOff = off + 8 + i * FIELD_DESCRIPTOR_BYTES;
|
|
938
|
+
const f = fields[i];
|
|
939
|
+
const nb = sb.nameBytes[i];
|
|
859
940
|
dv.setUint16(descOff + 0, nb.length, true);
|
|
860
941
|
dv.setUint16(descOff + 2, f.offsetInRow, true);
|
|
861
942
|
bytes[descOff + 4] = f.laneKind;
|
|
@@ -865,67 +946,227 @@ export class Writer {
|
|
|
865
946
|
dv.setBigUint64(descOff + 16, 0n, true);
|
|
866
947
|
nameOff += nb.length;
|
|
867
948
|
}
|
|
868
|
-
const nameBlobLenOff =
|
|
869
|
-
dv.setUint32(nameBlobLenOff, nameBlobLen, true);
|
|
949
|
+
const nameBlobLenOff = off + 8 + sb.descriptorBytes;
|
|
950
|
+
dv.setUint32(nameBlobLenOff, sb.nameBlobLen, true);
|
|
870
951
|
let namePosOff = nameBlobLenOff + 4;
|
|
871
952
|
for (let i = 0; i < fieldCount; i++) {
|
|
872
|
-
bytes.set(nameBytes[i], namePosOff);
|
|
873
|
-
namePosOff += nameBytes[i].length;
|
|
953
|
+
bytes.set(sb.nameBytes[i], namePosOff);
|
|
954
|
+
namePosOff += sb.nameBytes[i].length;
|
|
874
955
|
}
|
|
956
|
+
}
|
|
875
957
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
dv.setBigUint64(entryOff + 24, BigInt(shardStringTableLens[i] > 0 ? shardStringTableOffsets[i] : 0), true);
|
|
887
|
-
dv.setBigUint64(entryOff + 32, BigInt(shardStringTableLens[i]), true);
|
|
888
|
-
}
|
|
958
|
+
_emitDirEntryInto(dv, entryOff, payloadOff, payloadLen, rowCount, stOff, stLen) {
|
|
959
|
+
dv.setBigUint64(entryOff + 0, BigInt(payloadOff), true);
|
|
960
|
+
dv.setUint32(entryOff + 8, payloadLen, true);
|
|
961
|
+
dv.setUint32(entryOff + 12, rowCount, true);
|
|
962
|
+
dv.setUint16(entryOff + 16, SHARD_MIN_READER_VERSION, true);
|
|
963
|
+
dv.setUint16(entryOff + 18, SHARD_FLAGS_NONE, true);
|
|
964
|
+
dv.setUint32(entryOff + 20, 0, true); // reserved
|
|
965
|
+
dv.setBigUint64(entryOff + 24, BigInt(stLen > 0 ? stOff : 0), true);
|
|
966
|
+
dv.setBigUint64(entryOff + 32, BigInt(stLen), true);
|
|
967
|
+
}
|
|
889
968
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
969
|
+
_emitZoneMapsInto(dv, bytes, off, T, shardCount, src) {
|
|
970
|
+
bytes[off + 0] = 0x30; bytes[off + 1] = 0x5A; bytes[off + 2] = 0x4D; bytes[off + 3] = 0x31; // 'ZM01'
|
|
971
|
+
dv.setUint32(off + 4, shardCount, true);
|
|
972
|
+
dv.setUint32(off + 8, T, true);
|
|
973
|
+
dv.setUint32(off + 12, 0, true);
|
|
974
|
+
const fieldTableOff = off + 16;
|
|
975
|
+
for (let t = 0; t < T; t++) dv.setUint16(fieldTableOff + t * 2, this._trackedFieldIndices[t], true);
|
|
976
|
+
const fieldTableLen = T * 2;
|
|
977
|
+
const fieldTablePad = (8 - (fieldTableLen & 7)) & 7;
|
|
978
|
+
const minsOff = fieldTableOff + fieldTableLen + fieldTablePad;
|
|
979
|
+
const maxesOff = minsOff + shardCount * T * 8;
|
|
980
|
+
for (let s = 0; s < shardCount; s++) {
|
|
981
|
+
const sh = src[s];
|
|
899
982
|
for (let t = 0; t < T; t++) {
|
|
900
|
-
dv.
|
|
901
|
-
|
|
902
|
-
// Mins / maxes: row-major, [shard * T + tracked]
|
|
903
|
-
for (let s = 0; s < shardCount; s++) {
|
|
904
|
-
const sh = this._shards[s];
|
|
905
|
-
for (let t = 0; t < T; t++) {
|
|
906
|
-
dv.setFloat64(zoneMapsMinsOff + (s * T + t) * 8, sh.mins[t], true);
|
|
907
|
-
dv.setFloat64(zoneMapsMaxesOff + (s * T + t) * 8, sh.maxes[t], true);
|
|
908
|
-
}
|
|
983
|
+
dv.setFloat64(minsOff + (s * T + t) * 8, sh.mins[t], true);
|
|
984
|
+
dv.setFloat64(maxesOff + (s * T + t) * 8, sh.maxes[t], true);
|
|
909
985
|
}
|
|
910
986
|
}
|
|
987
|
+
}
|
|
911
988
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
bytes.set(this._shards[i].bytes, shardPayloadOffsets[i]);
|
|
915
|
-
bytes.set(this._shards[i].stringTableBytes, shardStringTableOffsets[i]);
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
// Footer
|
|
919
|
-
const footerOff = totalBytes - FOOTER_BYTES;
|
|
920
|
-
dv.setUint32(footerOff + 0, 0xFFFFFFFF, true);
|
|
989
|
+
_emitFooterInto(dv, bytes, footerOff, crcVal) {
|
|
990
|
+
dv.setUint32(footerOff + 0, crcVal >>> 0, true);
|
|
921
991
|
dv.setUint32(footerOff + 4, 0, true);
|
|
922
992
|
bytes[footerOff + 8] = 0x31;
|
|
923
993
|
bytes[footerOff + 9] = 0x4B;
|
|
924
994
|
bytes[footerOff + 10] = 0x42;
|
|
925
995
|
bytes[footerOff + 11] = 0x4C;
|
|
926
996
|
dv.setUint32(footerOff + 12, FOOTER_BYTES, true);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Classic prefix layout: header | schema | directory | zone maps | payloads |
|
|
1000
|
+
// footer, in ONE contiguous buffer. Byte-for-byte identical to the pre-M6
|
|
1001
|
+
// assembler when crc is off; the optional CRC-32C covers [0, footer_off).
|
|
1002
|
+
_buildPrefixBytes(crcOn) {
|
|
1003
|
+
const sb = this._computeSchemaBlock();
|
|
1004
|
+
const shardCount = this._shards.length;
|
|
1005
|
+
const shardDirBytes = shardCount * SHARD_ENTRY_BYTES;
|
|
1006
|
+
const schemaBlockOff = CONTAINER_HEADER_BYTES;
|
|
1007
|
+
const shardDirOff = schemaBlockOff + sb.schemaBlockBytes;
|
|
1008
|
+
const T = this._trackedFieldIndices.length;
|
|
1009
|
+
const zoneMapsEnabled = T > 0 && shardCount > 0;
|
|
1010
|
+
let zoneMapsOff = 0, zoneMapsBytes = 0;
|
|
1011
|
+
if (zoneMapsEnabled) {
|
|
1012
|
+
zoneMapsOff = shardDirOff + shardDirBytes;
|
|
1013
|
+
zoneMapsBytes = this._zoneMapsByteLen(T, shardCount);
|
|
1014
|
+
}
|
|
1015
|
+
const firstShardOff = shardDirOff + shardDirBytes + zoneMapsBytes;
|
|
1016
|
+
let cursor = firstShardOff;
|
|
1017
|
+
const payloadOffs = new Array(shardCount);
|
|
1018
|
+
const stOffs = new Array(shardCount);
|
|
1019
|
+
for (let i = 0; i < shardCount; i++) {
|
|
1020
|
+
const s = this._shards[i];
|
|
1021
|
+
payloadOffs[i] = cursor;
|
|
1022
|
+
const payloadLen = s.bytes.length;
|
|
1023
|
+
const payloadPad = (8 - (payloadLen & 7)) & 7;
|
|
1024
|
+
cursor += payloadLen + payloadPad;
|
|
1025
|
+
stOffs[i] = cursor;
|
|
1026
|
+
cursor += s.stringTableBytes.length; // stringTable.serialize() is already 8-padded
|
|
1027
|
+
}
|
|
1028
|
+
const footerOff = cursor;
|
|
1029
|
+
const totalBytes = cursor + FOOTER_BYTES;
|
|
1030
|
+
const buffer = new ArrayBuffer(totalBytes);
|
|
1031
|
+
const dv = new DataView(buffer);
|
|
1032
|
+
const bytes = new Uint8Array(buffer);
|
|
1033
|
+
|
|
1034
|
+
this._emitHeaderInto(dv, bytes, 0, {
|
|
1035
|
+
schemaBlockOff, metadataOff: zoneMapsEnabled ? zoneMapsOff : 0,
|
|
1036
|
+
shardDirOff, shardCount, totalRows: this._totalRows,
|
|
1037
|
+
});
|
|
1038
|
+
this._emitSchemaInto(dv, bytes, schemaBlockOff, sb);
|
|
1039
|
+
for (let i = 0; i < shardCount; i++) {
|
|
1040
|
+
const s = this._shards[i];
|
|
1041
|
+
this._emitDirEntryInto(dv, shardDirOff + i * SHARD_ENTRY_BYTES,
|
|
1042
|
+
payloadOffs[i], s.bytes.length, s.rowCount, stOffs[i], s.stringTableBytes.length);
|
|
1043
|
+
}
|
|
1044
|
+
if (zoneMapsEnabled) this._emitZoneMapsInto(dv, bytes, zoneMapsOff, T, shardCount, this._shards);
|
|
1045
|
+
for (let i = 0; i < shardCount; i++) {
|
|
1046
|
+
bytes.set(this._shards[i].bytes, payloadOffs[i]);
|
|
1047
|
+
bytes.set(this._shards[i].stringTableBytes, stOffs[i]);
|
|
1048
|
+
}
|
|
1049
|
+
let crcVal = CRC_ABSENT;
|
|
1050
|
+
if (crcOn) crcVal = crc32cFinal(crc32cUpdate(crc32cInit(), bytes, 0, footerOff));
|
|
1051
|
+
this._emitFooterInto(dv, bytes, footerOff, crcVal);
|
|
1052
|
+
return bytes;
|
|
1053
|
+
}
|
|
927
1054
|
|
|
928
|
-
|
|
1055
|
+
// PUBLIC. Emit the container to a caller sink instead of returning a buffer.
|
|
1056
|
+
// layout:'prefix' -- the classic layout, written in one sink.write().
|
|
1057
|
+
// layout:'stream' -- payloads stream first, then the trailer + footer, with
|
|
1058
|
+
// one sink.writeAt(header, 0) backpatch (needs writeAt).
|
|
1059
|
+
// Returns { totalRows, shardCount, schema|mode, bytesWritten, layout }. When
|
|
1060
|
+
// the writer was pre-bound to this sink via beginStream (bounded-RAM ingest),
|
|
1061
|
+
// the shards are already emitted and this only writes the trailer.
|
|
1062
|
+
finalizeToSink(sink, opts) {
|
|
1063
|
+
if (this._finalized) throw new WriterError('W_FINALIZED', 'writer already finalized');
|
|
1064
|
+
checkOpts('Writer.finalizeToSink', opts, FINALIZE_TO_SINK_OPTS, raiseWriter);
|
|
1065
|
+
opts = opts || {};
|
|
1066
|
+
|
|
1067
|
+
if (this._sink !== null) {
|
|
1068
|
+
// Pre-bound streaming: shards were emitted during ingest. Finish the input
|
|
1069
|
+
// (streams the trailing shard), then write the trailer.
|
|
1070
|
+
if (sink !== this._sink) raiseWriter('W_BAD_SINK', 'finalizeToSink sink differs from the streaming sink');
|
|
1071
|
+
if (opts.layout !== undefined && opts.layout !== 'stream')
|
|
1072
|
+
raiseWriter('W_BAD_SINK', "a streaming writer must be finalized with layout:'stream'");
|
|
1073
|
+
const crcOn = opts.crc !== undefined ? opts.crc === true : this._sinkCrcOn;
|
|
1074
|
+
this._sinkCrcOn = crcOn;
|
|
1075
|
+
this._completeInput();
|
|
1076
|
+
return this._finishStream(sink, crcOn);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const layout = opts.layout !== undefined ? opts.layout : 'stream';
|
|
1080
|
+
const crcOn = opts.crc !== undefined ? opts.crc === true : this._crc;
|
|
1081
|
+
validateSink(sink, layout === 'stream', raiseWriter);
|
|
1082
|
+
this._completeInput();
|
|
1083
|
+
|
|
1084
|
+
if (layout === 'prefix') {
|
|
1085
|
+
const bytes = this._buildPrefixBytes(crcOn);
|
|
1086
|
+
let ret;
|
|
1087
|
+
try { ret = sink.write(bytes); }
|
|
1088
|
+
catch (e) { this._finalized = true; throw e; }
|
|
1089
|
+
if (isThenable(ret)) { this._finalized = true; raiseWriter('W_BAD_SINK', 'sink.write returned a thenable; sinks must be synchronous'); }
|
|
1090
|
+
this._finalized = true;
|
|
1091
|
+
this._recordDepth = FINALIZED_DEPTH;
|
|
1092
|
+
return { totalRows: this._totalRows, shardCount: this._shards.length, schema: this._schema, bytesWritten: bytes.length, layout: 'prefix' };
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// layout 'stream', buffered shards: stream them out now, dropping each.
|
|
1096
|
+
const buffered = this._shards;
|
|
1097
|
+
this._shards = [];
|
|
1098
|
+
this._sink = sink;
|
|
1099
|
+
this._sinkCrcOn = crcOn;
|
|
1100
|
+
this._sinkPos = 0;
|
|
1101
|
+
this._sinkCrc = crc32cInit();
|
|
1102
|
+
this._sinkWrite(new Uint8Array(CONTAINER_HEADER_BYTES), false);
|
|
1103
|
+
this._sinkPos = CONTAINER_HEADER_BYTES;
|
|
1104
|
+
for (let i = 0; i < buffered.length; i++) {
|
|
1105
|
+
const s = buffered[i];
|
|
1106
|
+
this._streamEmitShard(s.bytes, s.stringTableBytes, s.rowCount, s.mins, s.maxes);
|
|
1107
|
+
buffered[i] = null;
|
|
1108
|
+
}
|
|
1109
|
+
return this._finishStream(sink, crcOn);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Write the schema/directory/zone-map trailer + footer and backpatch the header.
|
|
1113
|
+
// _shards holds one scalar descriptor per emitted shard; _sinkPos/_sinkCrc are
|
|
1114
|
+
// the current sink position and running body CRC over [48, pos).
|
|
1115
|
+
_finishStream(sink, crcOn) {
|
|
1116
|
+
const shardCount = this._shards.length;
|
|
1117
|
+
const sb = this._computeSchemaBlock();
|
|
1118
|
+
const schemaBlockOff = this._sinkPos; // 8-aligned: every payload region is 8-padded
|
|
1119
|
+
const shardDirBytes = shardCount * SHARD_ENTRY_BYTES;
|
|
1120
|
+
const shardDirOff = schemaBlockOff + sb.schemaBlockBytes;
|
|
1121
|
+
const T = this._trackedFieldIndices.length;
|
|
1122
|
+
const zoneMapsEnabled = T > 0 && shardCount > 0;
|
|
1123
|
+
let zoneMapsOff = 0, zoneMapsBytes = 0;
|
|
1124
|
+
if (zoneMapsEnabled) {
|
|
1125
|
+
zoneMapsOff = shardDirOff + shardDirBytes;
|
|
1126
|
+
zoneMapsBytes = this._zoneMapsByteLen(T, shardCount);
|
|
1127
|
+
}
|
|
1128
|
+
const trailerLen = sb.schemaBlockBytes + shardDirBytes + zoneMapsBytes;
|
|
1129
|
+
const footerOff = schemaBlockOff + trailerLen;
|
|
1130
|
+
|
|
1131
|
+
const trailer = new Uint8Array(trailerLen);
|
|
1132
|
+
const tdv = new DataView(trailer.buffer);
|
|
1133
|
+
this._emitSchemaInto(tdv, trailer, 0, sb);
|
|
1134
|
+
for (let i = 0; i < shardCount; i++) {
|
|
1135
|
+
const d = this._shards[i];
|
|
1136
|
+
this._emitDirEntryInto(tdv, sb.schemaBlockBytes + i * SHARD_ENTRY_BYTES,
|
|
1137
|
+
d.payloadOff, d.payloadLen, d.rowCount, d.stringOff, d.stringLen);
|
|
1138
|
+
}
|
|
1139
|
+
if (zoneMapsEnabled) this._emitZoneMapsInto(tdv, trailer, sb.schemaBlockBytes + shardDirBytes, T, shardCount, this._shards);
|
|
1140
|
+
this._sinkWrite(trailer, true);
|
|
1141
|
+
this._sinkPos += trailerLen;
|
|
1142
|
+
|
|
1143
|
+
const header = new Uint8Array(CONTAINER_HEADER_BYTES);
|
|
1144
|
+
const hdv = new DataView(header.buffer);
|
|
1145
|
+
this._emitHeaderInto(hdv, header, 0, {
|
|
1146
|
+
schemaBlockOff, metadataOff: zoneMapsEnabled ? zoneMapsOff : 0,
|
|
1147
|
+
shardDirOff, shardCount, totalRows: this._totalRows,
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
let crcVal = CRC_ABSENT;
|
|
1151
|
+
if (crcOn) {
|
|
1152
|
+
const headerCrc = crc32cFinal(crc32cUpdate(crc32cInit(), header, 0, CONTAINER_HEADER_BYTES));
|
|
1153
|
+
const suffixCrc = crc32cFinal(this._sinkCrc);
|
|
1154
|
+
crcVal = crc32cCombine(headerCrc, suffixCrc, footerOff - CONTAINER_HEADER_BYTES);
|
|
1155
|
+
}
|
|
1156
|
+
const footer = new Uint8Array(FOOTER_BYTES);
|
|
1157
|
+
const fdv = new DataView(footer.buffer);
|
|
1158
|
+
this._emitFooterInto(fdv, footer, 0, crcVal);
|
|
1159
|
+
this._sinkWrite(footer, false);
|
|
1160
|
+
this._sinkPos += FOOTER_BYTES;
|
|
1161
|
+
|
|
1162
|
+
let ret;
|
|
1163
|
+
try { ret = sink.writeAt(header, 0); }
|
|
1164
|
+
catch (e) { this._finalized = true; throw e; }
|
|
1165
|
+
if (isThenable(ret)) { this._finalized = true; raiseWriter('W_BAD_SINK', 'sink.writeAt returned a thenable; sinks must be synchronous'); }
|
|
1166
|
+
|
|
1167
|
+
this._finalized = true;
|
|
1168
|
+
this._recordDepth = FINALIZED_DEPTH;
|
|
1169
|
+
return { totalRows: this._totalRows, shardCount, schema: this._schema, bytesWritten: this._sinkPos, layout: 'stream' };
|
|
929
1170
|
}
|
|
930
1171
|
|
|
931
1172
|
get schema() { return this._schema; }
|