@zakkster/lite-bake-stream 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +18 -1
- package/SPEC.md +3 -1
- package/llms.txt +10 -2
- package/package.json +1 -1
- package/src/Crc32c.js +96 -0
- package/src/FileIngest.js +1 -1
- package/src/MultiReader.js +1 -1
- package/src/PreserveReader.js +27 -4
- package/src/PreserveTokenizer.js +1 -1
- package/src/PreserveWriter.js +211 -60
- package/src/RangeReader.js +27 -2
- package/src/Reader.js +32 -4
- package/src/Split.js +15 -3
- package/src/StringTable.js +1 -1
- package/src/Tokenizer.js +1 -1
- package/src/Views.js +62 -0
- package/src/Writer.js +382 -143
- package/src/index.js +4 -4
- package/types/PreserveReader.d.ts +10 -2
- package/types/PreserveWriter.d.ts +30 -0
- package/types/RangeReader.d.ts +5 -0
- package/types/Reader.d.ts +10 -2
- package/types/Writer.d.ts +37 -0
- package/types/index.d.ts +2 -1
package/src/Writer.js
CHANGED
|
@@ -57,8 +57,10 @@
|
|
|
57
57
|
|
|
58
58
|
import { StringTable } from './StringTable.js';
|
|
59
59
|
import { checkOpts } from './Opts.js';
|
|
60
|
+
import { crc32cInit, crc32cUpdate, crc32cFinal, crc32cCombine } from './Crc32c.js';
|
|
61
|
+
import { validateSink, isThenable } from './Views.js';
|
|
60
62
|
|
|
61
|
-
export const VERSION = '1.
|
|
63
|
+
export const VERSION = '1.6.0';
|
|
62
64
|
|
|
63
65
|
const U32_MAX = 4294967295;
|
|
64
66
|
// Post-finalize sentinel for _recordDepth. Chosen = 2 so every post-finalize
|
|
@@ -71,7 +73,13 @@ const WRITER_OPTS = {
|
|
|
71
73
|
schema: { t: 'obj', nullable: true },
|
|
72
74
|
targetShardBytes: { t: 'int', min: 1, max: U32_MAX },
|
|
73
75
|
sampleBytes: { t: 'int', min: 0, max: U32_MAX },
|
|
76
|
+
crc: { t: 'bool' },
|
|
74
77
|
};
|
|
78
|
+
const FINALIZE_TO_SINK_OPTS = {
|
|
79
|
+
layout: { t: 'enum', values: ['prefix', 'stream'] },
|
|
80
|
+
crc: { t: 'bool' },
|
|
81
|
+
};
|
|
82
|
+
const CRC_ABSENT = 0xFFFFFFFF;
|
|
75
83
|
function raiseWriter(code, msg) { throw new WriterError(code, msg); }
|
|
76
84
|
|
|
77
85
|
const CONTAINER_HEADER_BYTES = 48;
|
|
@@ -91,6 +99,10 @@ const SHARD_FLAGS_NONE = 0;
|
|
|
91
99
|
// U32-lane fields). Read-only; never written into.
|
|
92
100
|
const EMPTY_STRING_TABLE_BYTES = new Uint8Array(0);
|
|
93
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
|
+
|
|
94
106
|
const FORMAT_VERSION = 1;
|
|
95
107
|
const ENDIAN_LE = 1;
|
|
96
108
|
|
|
@@ -225,6 +237,17 @@ export class Writer {
|
|
|
225
237
|
this._targetShardBytes = opts.targetShardBytes !== undefined ? opts.targetShardBytes : DEFAULT_SHARD_BYTES;
|
|
226
238
|
this._sampleBytes = opts.sampleBytes !== undefined ? opts.sampleBytes : this._targetShardBytes;
|
|
227
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)
|
|
228
251
|
|
|
229
252
|
// Frozen schema state
|
|
230
253
|
this._schema = null; // { fields: [{name, laneKind, offsetInRow}], rowStride }
|
|
@@ -633,17 +656,25 @@ export class Writer {
|
|
|
633
656
|
const shardMaxes = new Float64Array(T);
|
|
634
657
|
shardMins.set(this._currentShardMins);
|
|
635
658
|
shardMaxes.set(this._currentShardMaxes);
|
|
636
|
-
this.
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
//
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
+
}
|
|
647
678
|
this._currentShardBuffer = null;
|
|
648
679
|
this._currentShardBytes = null;
|
|
649
680
|
this._currentShardDv = null;
|
|
@@ -735,7 +766,12 @@ export class Writer {
|
|
|
735
766
|
// receive strings from post-drain records that fill subsequent shards.
|
|
736
767
|
}
|
|
737
768
|
|
|
738
|
-
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() {
|
|
739
775
|
if (this._finalized) throw new WriterError('W_FINALIZED', 'writer already finalized');
|
|
740
776
|
if (!this._schema) {
|
|
741
777
|
if (this._sample.rowCount === 0) throw new WriterError('W_EMPTY_INPUT', 'no records to write');
|
|
@@ -744,13 +780,94 @@ export class Writer {
|
|
|
744
780
|
}
|
|
745
781
|
if (this._currentShardRowCount > 0) this._finalizeCurrentShard();
|
|
746
782
|
if (this._shards.length === 0) throw new WriterError('W_EMPTY_INPUT', 'no records to write');
|
|
747
|
-
|
|
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);
|
|
748
791
|
this._finalized = true;
|
|
749
792
|
// Post-finalize sentinel: route every later sink event into a cold arm.
|
|
750
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
|
+
};
|
|
751
800
|
return this._container;
|
|
752
801
|
}
|
|
753
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
|
+
|
|
754
871
|
// Optional capability hook (BS-07): the Tokenizer calls this at construction
|
|
755
872
|
// to hand over the live tokenizer. The Writer then reads src.absOffset at
|
|
756
873
|
// record boundaries so the sample window is byte-true. A CALL only; the
|
|
@@ -762,100 +879,62 @@ export class Writer {
|
|
|
762
879
|
// dance is a documented no-op retained for source compatibility until 2.0.
|
|
763
880
|
setInputByteOffset() {}
|
|
764
881
|
|
|
765
|
-
// -------- container assembly --------
|
|
766
|
-
|
|
767
|
-
|
|
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() {
|
|
768
895
|
const enc = new TextEncoder();
|
|
769
|
-
const
|
|
896
|
+
const fields = this._schema.fields;
|
|
897
|
+
const nameBytes = new Array(fields.length);
|
|
770
898
|
let nameBlobLen = 0;
|
|
771
|
-
for (let i = 0; i <
|
|
772
|
-
nameBytes[i] = enc.encode(
|
|
899
|
+
for (let i = 0; i < fields.length; i++) {
|
|
900
|
+
nameBytes[i] = enc.encode(fields[i].name);
|
|
773
901
|
nameBlobLen += nameBytes[i].length;
|
|
774
902
|
}
|
|
775
|
-
|
|
776
|
-
const fieldCount = this._schema.fields.length;
|
|
777
|
-
const descriptorBytes = fieldCount * FIELD_DESCRIPTOR_BYTES;
|
|
903
|
+
const descriptorBytes = fields.length * FIELD_DESCRIPTOR_BYTES;
|
|
778
904
|
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;
|
|
905
|
+
schemaBlockBytes += (8 - (schemaBlockBytes & 7)) & 7;
|
|
906
|
+
return { nameBytes, nameBlobLen, descriptorBytes, schemaBlockBytes };
|
|
907
|
+
}
|
|
787
908
|
|
|
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;
|
|
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
|
+
}
|
|
813
914
|
|
|
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
|
-
}
|
|
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
|
+
}
|
|
833
927
|
|
|
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);
|
|
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);
|
|
854
933
|
let nameOff = 0;
|
|
855
934
|
for (let i = 0; i < fieldCount; i++) {
|
|
856
|
-
const descOff =
|
|
857
|
-
const f =
|
|
858
|
-
const nb = nameBytes[i];
|
|
935
|
+
const descOff = off + 8 + i * FIELD_DESCRIPTOR_BYTES;
|
|
936
|
+
const f = fields[i];
|
|
937
|
+
const nb = sb.nameBytes[i];
|
|
859
938
|
dv.setUint16(descOff + 0, nb.length, true);
|
|
860
939
|
dv.setUint16(descOff + 2, f.offsetInRow, true);
|
|
861
940
|
bytes[descOff + 4] = f.laneKind;
|
|
@@ -865,67 +944,227 @@ export class Writer {
|
|
|
865
944
|
dv.setBigUint64(descOff + 16, 0n, true);
|
|
866
945
|
nameOff += nb.length;
|
|
867
946
|
}
|
|
868
|
-
const nameBlobLenOff =
|
|
869
|
-
dv.setUint32(nameBlobLenOff, nameBlobLen, true);
|
|
947
|
+
const nameBlobLenOff = off + 8 + sb.descriptorBytes;
|
|
948
|
+
dv.setUint32(nameBlobLenOff, sb.nameBlobLen, true);
|
|
870
949
|
let namePosOff = nameBlobLenOff + 4;
|
|
871
950
|
for (let i = 0; i < fieldCount; i++) {
|
|
872
|
-
bytes.set(nameBytes[i], namePosOff);
|
|
873
|
-
namePosOff += nameBytes[i].length;
|
|
951
|
+
bytes.set(sb.nameBytes[i], namePosOff);
|
|
952
|
+
namePosOff += sb.nameBytes[i].length;
|
|
874
953
|
}
|
|
954
|
+
}
|
|
875
955
|
|
|
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
|
-
}
|
|
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
|
+
}
|
|
889
966
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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];
|
|
899
980
|
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
|
-
}
|
|
981
|
+
dv.setFloat64(minsOff + (s * T + t) * 8, sh.mins[t], true);
|
|
982
|
+
dv.setFloat64(maxesOff + (s * T + t) * 8, sh.maxes[t], true);
|
|
909
983
|
}
|
|
910
984
|
}
|
|
985
|
+
}
|
|
911
986
|
|
|
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);
|
|
987
|
+
_emitFooterInto(dv, bytes, footerOff, crcVal) {
|
|
988
|
+
dv.setUint32(footerOff + 0, crcVal >>> 0, true);
|
|
921
989
|
dv.setUint32(footerOff + 4, 0, true);
|
|
922
990
|
bytes[footerOff + 8] = 0x31;
|
|
923
991
|
bytes[footerOff + 9] = 0x4B;
|
|
924
992
|
bytes[footerOff + 10] = 0x42;
|
|
925
993
|
bytes[footerOff + 11] = 0x4C;
|
|
926
994
|
dv.setUint32(footerOff + 12, FOOTER_BYTES, true);
|
|
995
|
+
}
|
|
996
|
+
|
|
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
|
+
}
|
|
927
1052
|
|
|
928
|
-
|
|
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' };
|
|
929
1168
|
}
|
|
930
1169
|
|
|
931
1170
|
get schema() { return this._schema; }
|
package/src/index.js
CHANGED
|
@@ -28,7 +28,7 @@ export {
|
|
|
28
28
|
PreserveWriter, PreserveWriterError,
|
|
29
29
|
PreserveReader, PreserveReaderError,
|
|
30
30
|
};
|
|
31
|
-
export const VERSION = '1.
|
|
31
|
+
export const VERSION = '1.6.0';
|
|
32
32
|
|
|
33
33
|
const encoder = new TextEncoder();
|
|
34
34
|
|
|
@@ -188,12 +188,12 @@ async function _serializeAsyncIterablePreserve(iterable, opts) {
|
|
|
188
188
|
|
|
189
189
|
// Deserialize LBK1 bytes into the right Reader for the container's mode.
|
|
190
190
|
// Auto-detects preserve vs schema via header flag byte at offset 7 bit 0.
|
|
191
|
-
export function deserialize(bytes) {
|
|
191
|
+
export function deserialize(bytes, opts) {
|
|
192
192
|
const buffer = toContainerBuffer(bytes, 'deserialize');
|
|
193
193
|
if (buffer.byteLength < 8) {
|
|
194
194
|
throw new ReaderError('R_TRUNCATED', 'container too small to inspect header flags');
|
|
195
195
|
}
|
|
196
196
|
const flags = new Uint8Array(buffer, 7, 1)[0];
|
|
197
|
-
if (flags & 0x01) return new PreserveReader(buffer);
|
|
198
|
-
return new Reader(buffer);
|
|
197
|
+
if (flags & 0x01) return new PreserveReader(buffer, opts);
|
|
198
|
+
return new Reader(buffer, opts);
|
|
199
199
|
}
|
|
@@ -13,9 +13,17 @@ export interface PreserveShardHandle {
|
|
|
13
13
|
endRow: number;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export interface PreserveReaderOptions {
|
|
17
|
+
/** Verify the footer CRC-32C at open; throws R_BAD_CRC on mismatch, R_CRC_ABSENT if absent. */
|
|
18
|
+
verifyCrc?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
export class PreserveReader {
|
|
17
|
-
static fromBuffer(input: ArrayBuffer | Uint8Array): PreserveReader;
|
|
18
|
-
constructor(buffer: ArrayBuffer);
|
|
22
|
+
static fromBuffer(input: ArrayBuffer | Uint8Array, opts?: PreserveReaderOptions): PreserveReader;
|
|
23
|
+
constructor(buffer: ArrayBuffer, opts?: PreserveReaderOptions);
|
|
24
|
+
|
|
25
|
+
/** Recompute the footer CRC-32C over [0, footer_off): 'ok' | 'absent'; throws R_BAD_CRC on mismatch. */
|
|
26
|
+
verifyCrc(): 'ok' | 'absent';
|
|
19
27
|
|
|
20
28
|
readonly totalRows: number;
|
|
21
29
|
readonly shardCount: number;
|