@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/PreserveWriter.js
CHANGED
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
// container with R_WRONG_MODE.
|
|
18
18
|
|
|
19
19
|
import { checkOpts } from './Opts.js';
|
|
20
|
+
import { crc32cInit, crc32cUpdate, crc32cFinal, crc32cCombine } from './Crc32c.js';
|
|
21
|
+
import { validateSink, isThenable } from './Views.js';
|
|
20
22
|
|
|
21
|
-
export const VERSION = '1.
|
|
23
|
+
export const VERSION = '1.6.0';
|
|
22
24
|
|
|
23
25
|
export class PreserveWriterError extends Error {
|
|
24
26
|
constructor(code, msg) { super(msg); this.code = code; this.name = 'PreserveWriterError'; }
|
|
@@ -28,6 +30,11 @@ function raisePreserveWriter(code, msg) { throw new PreserveWriterError(code, ms
|
|
|
28
30
|
const CONTAINER_HEADER_BYTES = 48;
|
|
29
31
|
const SHARD_ENTRY_BYTES = 40;
|
|
30
32
|
const FOOTER_BYTES = 16;
|
|
33
|
+
const CRC_ABSENT = 0xFFFFFFFF;
|
|
34
|
+
const FINALIZE_TO_SINK_OPTS = {
|
|
35
|
+
layout: { t: 'enum', values: ['prefix', 'stream'] },
|
|
36
|
+
crc: { t: 'bool' },
|
|
37
|
+
};
|
|
31
38
|
const DEFAULT_TARGET_SHARD_BYTES = 8 * 1024 * 1024; // 8 MiB — smaller than schema mode
|
|
32
39
|
const INITIAL_OFFSETS_CAP = 4096;
|
|
33
40
|
const U32_MAX = 4294967295;
|
|
@@ -38,6 +45,7 @@ const U32_MAX = 4294967295;
|
|
|
38
45
|
const PRESERVE_WRITER_OPTS = {
|
|
39
46
|
targetShardBytes: { t: 'int', min: 1, max: U32_MAX },
|
|
40
47
|
maxRecordBytes: { t: 'int', min: 0, max: U32_MAX },
|
|
48
|
+
crc: { t: 'bool' },
|
|
41
49
|
};
|
|
42
50
|
|
|
43
51
|
export class PreserveWriter {
|
|
@@ -45,16 +53,29 @@ export class PreserveWriter {
|
|
|
45
53
|
checkOpts('PreserveWriter', opts, PRESERVE_WRITER_OPTS, raisePreserveWriter);
|
|
46
54
|
opts = opts || {};
|
|
47
55
|
this._targetShardBytes = opts.targetShardBytes !== undefined ? opts.targetShardBytes : DEFAULT_TARGET_SHARD_BYTES;
|
|
56
|
+
this._crc = opts.crc === true;
|
|
48
57
|
this._shards = [];
|
|
49
58
|
this._totalRows = 0;
|
|
50
59
|
this._finalized = false;
|
|
60
|
+
// Streaming-emission state (M6); see Writer for the model. A bound sink makes
|
|
61
|
+
// _finalizeCurrentShard emit + drop each shard, retaining only a descriptor.
|
|
62
|
+
this._sink = null;
|
|
63
|
+
this._sinkCrcOn = false;
|
|
64
|
+
this._sinkPos = 0;
|
|
65
|
+
this._sinkCrc = 0;
|
|
66
|
+
this._currentBlobs = null; // null => never allocated; _allocateShard allocates once
|
|
51
67
|
this._allocateShard();
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
_allocateShard() {
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
// Allocate the working buffers ONCE (BS-27 floor). Every subsequent shard
|
|
72
|
+
// reuses them: _finalizeCurrentShard resets the cursors instead of nulling
|
|
73
|
+
// the buffers, so a shard roll allocates nothing. A grown buffer (oversized
|
|
74
|
+
// record) simply carries forward as a larger reusable buffer.
|
|
75
|
+
if (this._currentBlobs === null) {
|
|
76
|
+
this._currentBlobs = new Uint8Array(this._targetShardBytes);
|
|
77
|
+
this._currentOffsets = new Uint32Array(INITIAL_OFFSETS_CAP);
|
|
78
|
+
}
|
|
58
79
|
this._currentBlobBytes = 0;
|
|
59
80
|
this._currentRowCount = 0;
|
|
60
81
|
}
|
|
@@ -74,7 +95,19 @@ export class PreserveWriter {
|
|
|
74
95
|
this._allocateShard();
|
|
75
96
|
}
|
|
76
97
|
// Grow shard buffer if this single record exceeds the pre-allocated size.
|
|
98
|
+
// Fail closed (BS-31) on a BLOB-dimension crossing: this guard covers a
|
|
99
|
+
// record whose blob bytes push payload_len past the u32 ceiling (including
|
|
100
|
+
// the oversized-single-record path), and the _growBlobBuffer clamp keeps the
|
|
101
|
+
// buffer <= U32_MAX so such a write always re-enters this grow branch. The
|
|
102
|
+
// ORTHOGONAL crossing -- payload_len exceeding u32 via OFFSET-TABLE
|
|
103
|
+
// accumulation (many tiny records, blob under target, grow branch never
|
|
104
|
+
// firing) -- is caught in _finalizeCurrentShard. ST_BLOB_OVERFLOW is the
|
|
105
|
+
// shared cross-writer overflow code (R11).
|
|
77
106
|
if (this._currentBlobBytes + len > this._currentBlobs.length) {
|
|
107
|
+
if (this._currentBlobBytes + len + (this._currentRowCount + 1) * 4 > U32_MAX)
|
|
108
|
+
throw new PreserveWriterError('ST_BLOB_OVERFLOW',
|
|
109
|
+
'preserve shard payload ' + (this._currentBlobBytes + len) +
|
|
110
|
+
' bytes + offset table would exceed the u32 payload_len ceiling ' + U32_MAX);
|
|
78
111
|
this._growBlobBuffer(this._currentBlobBytes + len);
|
|
79
112
|
}
|
|
80
113
|
// Grow offsets array if we've hit the pre-allocated ceiling.
|
|
@@ -107,6 +140,9 @@ export class PreserveWriter {
|
|
|
107
140
|
_growBlobBuffer(needed) {
|
|
108
141
|
let cap = this._currentBlobs.length;
|
|
109
142
|
while (cap < needed) cap *= 2;
|
|
143
|
+
// Clamp so the buffer never exceeds the u32 payload ceiling; this makes the
|
|
144
|
+
// onRecord grow-branch guard provably complete (see onRecord).
|
|
145
|
+
if (cap > U32_MAX) cap = U32_MAX;
|
|
110
146
|
const next = new Uint8Array(cap);
|
|
111
147
|
next.set(this._currentBlobs.subarray(0, this._currentBlobBytes));
|
|
112
148
|
this._currentBlobs = next;
|
|
@@ -124,6 +160,15 @@ export class PreserveWriter {
|
|
|
124
160
|
const blobLen = this._currentBlobBytes;
|
|
125
161
|
const offsetTableBytes = rowCount * 4;
|
|
126
162
|
const shardBytesLen = blobLen + offsetTableBytes;
|
|
163
|
+
// Fail closed (BS-31, cold) on an OFFSET-TABLE-driven crossing: payload_len =
|
|
164
|
+
// blob bytes + rowCount*4 is written into the shard directory as a u32, and
|
|
165
|
+
// enough tiny records can push it past the ceiling without the onRecord grow
|
|
166
|
+
// branch ever firing. This is the door that branch cannot cover. Shared code
|
|
167
|
+
// ST_BLOB_OVERFLOW (R11).
|
|
168
|
+
if (shardBytesLen > U32_MAX)
|
|
169
|
+
throw new PreserveWriterError('ST_BLOB_OVERFLOW',
|
|
170
|
+
'preserve shard payload_len ' + shardBytesLen + ' (blob ' + blobLen + ' + offset table ' +
|
|
171
|
+
offsetTableBytes + ') exceeds the u32 ceiling ' + U32_MAX);
|
|
127
172
|
|
|
128
173
|
const shardBytes = new Uint8Array(shardBytesLen);
|
|
129
174
|
shardBytes.set(this._currentBlobs.subarray(0, blobLen), 0);
|
|
@@ -132,100 +177,236 @@ export class PreserveWriter {
|
|
|
132
177
|
for (let i = 0; i < rowCount; i++) {
|
|
133
178
|
dv.setUint32(blobLen + i * 4, this._currentOffsets[i], true);
|
|
134
179
|
}
|
|
135
|
-
this.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
180
|
+
if (this._sink !== null) {
|
|
181
|
+
this._streamEmitShard(shardBytes, rowCount);
|
|
182
|
+
} else {
|
|
183
|
+
this._shards.push({
|
|
184
|
+
bytes: shardBytes,
|
|
185
|
+
rowCount,
|
|
186
|
+
blobLen,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// Reuse the working buffers for the next shard: reset the cursors only, keep
|
|
190
|
+
// the allocations (BS-27). No stale bytes can leak -- every read into these
|
|
191
|
+
// buffers is bounded by _currentBlobBytes / _currentRowCount (both zeroed
|
|
192
|
+
// here), and the shard copy above took only subarray(0, blobLen).
|
|
144
193
|
this._currentBlobBytes = 0;
|
|
145
194
|
this._currentRowCount = 0;
|
|
146
195
|
}
|
|
147
196
|
|
|
148
|
-
|
|
197
|
+
_completeInput() {
|
|
149
198
|
if (this._finalized) throw new PreserveWriterError('W_FINALIZED', 'writer already finalized');
|
|
150
199
|
if (this._currentRowCount > 0) this._finalizeCurrentShard();
|
|
151
200
|
if (this._shards.length === 0) {
|
|
152
|
-
//
|
|
201
|
+
// Match schema-mode: an empty container is an error, not a silent no-op.
|
|
153
202
|
throw new PreserveWriterError('W_EMPTY_INPUT', 'no records written; nothing to finalize');
|
|
154
203
|
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
finalize() {
|
|
207
|
+
this._completeInput();
|
|
208
|
+
const bytes = this._buildPrefixBytes(this._crc);
|
|
155
209
|
this._finalized = true;
|
|
156
|
-
return this.
|
|
210
|
+
return { buffer: bytes.buffer, totalRows: this._totalRows, shardCount: this._shards.length, mode: 'preserve' };
|
|
157
211
|
}
|
|
158
212
|
|
|
159
213
|
get totalRows() { return this._totalRows; }
|
|
160
214
|
get shardCount() { return this._shards.length; }
|
|
161
215
|
|
|
162
|
-
|
|
216
|
+
// -------- container assembly (placement-free emitters) --------
|
|
217
|
+
// Preserve mode has no schema block and no zone maps: schema_block_off and
|
|
218
|
+
// metadata_off are 0. Classic prefix layout is header | directory | payloads |
|
|
219
|
+
// footer; the streaming layout (O9) is header | payloads | directory | footer.
|
|
220
|
+
|
|
221
|
+
_emitHeaderInto(dv, bytes, off, shardDirOff, shardCount, totalRows) {
|
|
222
|
+
bytes[off + 0] = 0x4C; bytes[off + 1] = 0x42; bytes[off + 2] = 0x4B; bytes[off + 3] = 0x31; // 'LBK1'
|
|
223
|
+
dv.setUint16(off + 4, 1, true); // format_version
|
|
224
|
+
bytes[off + 6] = 1; // endian LE
|
|
225
|
+
bytes[off + 7] = 0x01; // flags: bit 0 = preserve mode
|
|
226
|
+
dv.setBigUint64(off + 8, 0n, true); // schema_block_off = 0
|
|
227
|
+
dv.setBigUint64(off + 16, 0n, true); // metadata_off = 0
|
|
228
|
+
dv.setBigUint64(off + 24, BigInt(shardDirOff), true);
|
|
229
|
+
dv.setUint32(off + 32, shardCount, true);
|
|
230
|
+
dv.setUint32(off + 36, 0, true); // reserved1
|
|
231
|
+
dv.setBigUint64(off + 40, BigInt(totalRows), true);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
_emitDirEntryInto(dv, entryOff, payloadOff, payloadLen, rowCount) {
|
|
235
|
+
dv.setBigUint64(entryOff + 0, BigInt(payloadOff), true);
|
|
236
|
+
dv.setUint32(entryOff + 8, payloadLen, true);
|
|
237
|
+
dv.setUint32(entryOff + 12, rowCount, true);
|
|
238
|
+
dv.setUint16(entryOff + 16, 1, true); // min_reader_version
|
|
239
|
+
dv.setUint16(entryOff + 18, 0, true); // shard flags
|
|
240
|
+
dv.setUint32(entryOff + 20, 0, true); // reserved
|
|
241
|
+
dv.setBigUint64(entryOff + 24, 0n, true); // no string table
|
|
242
|
+
dv.setBigUint64(entryOff + 32, 0n, true);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
_emitFooterInto(dv, bytes, footerOff, crcVal) {
|
|
246
|
+
dv.setUint32(footerOff + 0, crcVal >>> 0, true);
|
|
247
|
+
dv.setUint32(footerOff + 4, 0, true);
|
|
248
|
+
bytes[footerOff + 8] = 0x31; // '1'
|
|
249
|
+
bytes[footerOff + 9] = 0x4B; // 'K'
|
|
250
|
+
bytes[footerOff + 10] = 0x42; // 'B'
|
|
251
|
+
bytes[footerOff + 11] = 0x4C; // 'L'
|
|
252
|
+
dv.setUint32(footerOff + 12, FOOTER_BYTES, true);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Classic prefix layout, byte-for-byte identical to the pre-M6 assembler when
|
|
256
|
+
// crc is off; the optional CRC-32C covers [0, footer_off).
|
|
257
|
+
_buildPrefixBytes(crcOn) {
|
|
163
258
|
const shardCount = this._shards.length;
|
|
164
259
|
const shardDirBytes = shardCount * SHARD_ENTRY_BYTES;
|
|
165
|
-
|
|
166
|
-
// Layout: header (48) | shard directory | shard payloads | footer (16)
|
|
167
|
-
// No schema block, no zone maps in preserve mode; schema_block_off = 0.
|
|
168
260
|
const shardDirOff = CONTAINER_HEADER_BYTES;
|
|
169
261
|
let cursor = shardDirOff + shardDirBytes;
|
|
170
|
-
const
|
|
262
|
+
const payloadOffs = new Array(shardCount);
|
|
171
263
|
for (let i = 0; i < shardCount; i++) {
|
|
172
|
-
|
|
264
|
+
payloadOffs[i] = cursor;
|
|
173
265
|
cursor += this._shards[i].bytes.length;
|
|
174
266
|
}
|
|
267
|
+
const footerOff = cursor;
|
|
175
268
|
const totalBytes = cursor + FOOTER_BYTES;
|
|
269
|
+
const buffer = new ArrayBuffer(totalBytes);
|
|
270
|
+
const dv = new DataView(buffer);
|
|
271
|
+
const bytes = new Uint8Array(buffer);
|
|
176
272
|
|
|
177
|
-
|
|
178
|
-
const dv = new DataView(container);
|
|
179
|
-
const bytes = new Uint8Array(container);
|
|
180
|
-
|
|
181
|
-
// Header
|
|
182
|
-
bytes[0] = 0x4C; bytes[1] = 0x42; bytes[2] = 0x4B; bytes[3] = 0x31; // 'LBK1'
|
|
183
|
-
dv.setUint16(4, 1, true); // format_version
|
|
184
|
-
bytes[6] = 1; // endian LE
|
|
185
|
-
bytes[7] = 0x01; // flags: bit 0 = preserve mode
|
|
186
|
-
dv.setBigUint64(8, 0n, true); // schema_block_off = 0 (no schema)
|
|
187
|
-
dv.setBigUint64(16, 0n, true); // metadata_off = 0 (no zone maps)
|
|
188
|
-
dv.setBigUint64(24, BigInt(shardDirOff), true);
|
|
189
|
-
dv.setUint32(32, shardCount, true);
|
|
190
|
-
dv.setUint32(36, 0, true); // reserved1
|
|
191
|
-
dv.setBigUint64(40, BigInt(this._totalRows), true);
|
|
192
|
-
|
|
193
|
-
// Shard directory (40 bytes per entry, u64 payload_off, u32 payload_len,
|
|
194
|
-
// u32 row_count, u16 min_reader_version, u16 flags, u32 reserved,
|
|
195
|
-
// u64 local_string_off=0, u64 local_string_len=0)
|
|
273
|
+
this._emitHeaderInto(dv, bytes, 0, shardDirOff, shardCount, this._totalRows);
|
|
196
274
|
for (let i = 0; i < shardCount; i++) {
|
|
197
|
-
const entryOff = shardDirOff + i * SHARD_ENTRY_BYTES;
|
|
198
275
|
const s = this._shards[i];
|
|
199
|
-
|
|
200
|
-
dv.setUint32(entryOff + 8, s.bytes.length, true);
|
|
201
|
-
dv.setUint32(entryOff + 12, s.rowCount, true);
|
|
202
|
-
dv.setUint16(entryOff + 16, 1, true); // min_reader_version
|
|
203
|
-
dv.setUint16(entryOff + 18, 0, true); // shard flags
|
|
204
|
-
dv.setUint32(entryOff + 20, 0, true); // reserved
|
|
205
|
-
dv.setBigUint64(entryOff + 24, 0n, true); // no string table
|
|
206
|
-
dv.setBigUint64(entryOff + 32, 0n, true);
|
|
276
|
+
this._emitDirEntryInto(dv, shardDirOff + i * SHARD_ENTRY_BYTES, payloadOffs[i], s.bytes.length, s.rowCount);
|
|
207
277
|
}
|
|
278
|
+
for (let i = 0; i < shardCount; i++) bytes.set(this._shards[i].bytes, payloadOffs[i]);
|
|
208
279
|
|
|
209
|
-
|
|
280
|
+
let crcVal = CRC_ABSENT;
|
|
281
|
+
if (crcOn) crcVal = crc32cFinal(crc32cUpdate(crc32cInit(), bytes, 0, footerOff));
|
|
282
|
+
this._emitFooterInto(dv, bytes, footerOff, crcVal);
|
|
283
|
+
return bytes;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// PUBLIC. Bind a sink and stream shards as they finalize (bounded RAM). Call
|
|
287
|
+
// before feeding, then finalizeToSink(sink, opts) writes the trailer. Calling
|
|
288
|
+
// finalizeToSink alone is BUFFERED mode (O(container) peak). opts.layout must
|
|
289
|
+
// be 'stream'; opts.crc defaults to the constructor crc.
|
|
290
|
+
beginStream(sink, opts) {
|
|
291
|
+
if (this._finalized) throw new PreserveWriterError('W_FINALIZED', 'writer already finalized');
|
|
292
|
+
if (this._sink !== null) throw new PreserveWriterError('W_FINALIZED', 'writer is already streaming to a sink');
|
|
293
|
+
if (this._shards.length > 0 || this._currentRowCount > 0)
|
|
294
|
+
throw new PreserveWriterError('W_FINALIZED', 'beginStream must be called before the first record');
|
|
295
|
+
checkOpts('PreserveWriter.beginStream', opts, FINALIZE_TO_SINK_OPTS, raisePreserveWriter);
|
|
296
|
+
opts = opts || {};
|
|
297
|
+
if (opts.layout !== undefined && opts.layout !== 'stream')
|
|
298
|
+
raisePreserveWriter('W_BAD_SINK', "beginStream requires layout:'stream'");
|
|
299
|
+
validateSink(sink, true, raisePreserveWriter);
|
|
300
|
+
this._sink = sink;
|
|
301
|
+
this._sinkCrcOn = opts.crc !== undefined ? opts.crc === true : this._crc;
|
|
302
|
+
this._sinkCrc = crc32cInit();
|
|
303
|
+
this._sinkWrite(new Uint8Array(CONTAINER_HEADER_BYTES), false);
|
|
304
|
+
this._sinkPos = CONTAINER_HEADER_BYTES;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
_sinkWrite(bytes, fold) {
|
|
308
|
+
if (fold && this._sinkCrcOn) this._sinkCrc = crc32cUpdate(this._sinkCrc, bytes, 0, bytes.length);
|
|
309
|
+
let ret;
|
|
310
|
+
// A throwing sink fails the writer closed and rethrows verbatim; retry -> W_FINALIZED.
|
|
311
|
+
try { ret = this._sink.write(bytes); }
|
|
312
|
+
catch (e) { this._finalized = true; throw e; }
|
|
313
|
+
if (isThenable(ret)) { this._finalized = true; raisePreserveWriter('W_BAD_SINK', 'sink.write returned a thenable; sinks must be synchronous'); }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Preserve payloads carry an internal offset table and are NOT 8-padded (the
|
|
317
|
+
// classic layout packs them back-to-back), so the streaming layout does the
|
|
318
|
+
// same: no inter-shard padding.
|
|
319
|
+
_streamEmitShard(shardBytes, rowCount) {
|
|
320
|
+
const payloadOff = this._sinkPos;
|
|
321
|
+
const payloadLen = shardBytes.length;
|
|
322
|
+
this._sinkWrite(shardBytes, true);
|
|
323
|
+
this._sinkPos += payloadLen;
|
|
324
|
+
this._shards.push({ rowCount, payloadOff, payloadLen });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// PUBLIC. Emit to a caller sink. Returns { totalRows, shardCount, mode,
|
|
328
|
+
// bytesWritten, layout }.
|
|
329
|
+
finalizeToSink(sink, opts) {
|
|
330
|
+
if (this._finalized) throw new PreserveWriterError('W_FINALIZED', 'writer already finalized');
|
|
331
|
+
checkOpts('PreserveWriter.finalizeToSink', opts, FINALIZE_TO_SINK_OPTS, raisePreserveWriter);
|
|
332
|
+
opts = opts || {};
|
|
333
|
+
|
|
334
|
+
if (this._sink !== null) {
|
|
335
|
+
if (sink !== this._sink) raisePreserveWriter('W_BAD_SINK', 'finalizeToSink sink differs from the streaming sink');
|
|
336
|
+
if (opts.layout !== undefined && opts.layout !== 'stream')
|
|
337
|
+
raisePreserveWriter('W_BAD_SINK', "a streaming writer must be finalized with layout:'stream'");
|
|
338
|
+
const crcOn = opts.crc !== undefined ? opts.crc === true : this._sinkCrcOn;
|
|
339
|
+
this._sinkCrcOn = crcOn;
|
|
340
|
+
this._completeInput();
|
|
341
|
+
return this._finishStream(sink, crcOn);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const layout = opts.layout !== undefined ? opts.layout : 'stream';
|
|
345
|
+
const crcOn = opts.crc !== undefined ? opts.crc === true : this._crc;
|
|
346
|
+
validateSink(sink, layout === 'stream', raisePreserveWriter);
|
|
347
|
+
this._completeInput();
|
|
348
|
+
|
|
349
|
+
if (layout === 'prefix') {
|
|
350
|
+
const bytes = this._buildPrefixBytes(crcOn);
|
|
351
|
+
let ret;
|
|
352
|
+
try { ret = sink.write(bytes); }
|
|
353
|
+
catch (e) { this._finalized = true; throw e; }
|
|
354
|
+
if (isThenable(ret)) { this._finalized = true; raisePreserveWriter('W_BAD_SINK', 'sink.write returned a thenable; sinks must be synchronous'); }
|
|
355
|
+
this._finalized = true;
|
|
356
|
+
return { totalRows: this._totalRows, shardCount: this._shards.length, mode: 'preserve', bytesWritten: bytes.length, layout: 'prefix' };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const buffered = this._shards;
|
|
360
|
+
this._shards = [];
|
|
361
|
+
this._sink = sink;
|
|
362
|
+
this._sinkCrcOn = crcOn;
|
|
363
|
+
this._sinkCrc = crc32cInit();
|
|
364
|
+
this._sinkWrite(new Uint8Array(CONTAINER_HEADER_BYTES), false);
|
|
365
|
+
this._sinkPos = CONTAINER_HEADER_BYTES;
|
|
366
|
+
for (let i = 0; i < buffered.length; i++) {
|
|
367
|
+
this._streamEmitShard(buffered[i].bytes, buffered[i].rowCount);
|
|
368
|
+
buffered[i] = null;
|
|
369
|
+
}
|
|
370
|
+
return this._finishStream(sink, crcOn);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
_finishStream(sink, crcOn) {
|
|
374
|
+
const shardCount = this._shards.length;
|
|
375
|
+
const shardDirBytes = shardCount * SHARD_ENTRY_BYTES;
|
|
376
|
+
const shardDirOff = this._sinkPos; // directory follows the payloads (O9)
|
|
377
|
+
const footerOff = shardDirOff + shardDirBytes;
|
|
378
|
+
|
|
379
|
+
const dir = new Uint8Array(shardDirBytes);
|
|
380
|
+
const ddv = new DataView(dir.buffer);
|
|
210
381
|
for (let i = 0; i < shardCount; i++) {
|
|
211
|
-
|
|
382
|
+
const d = this._shards[i];
|
|
383
|
+
this._emitDirEntryInto(ddv, i * SHARD_ENTRY_BYTES, d.payloadOff, d.payloadLen, d.rowCount);
|
|
212
384
|
}
|
|
385
|
+
this._sinkWrite(dir, true);
|
|
386
|
+
this._sinkPos += shardDirBytes;
|
|
213
387
|
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
dv.setUint32(footerOff + 4, 0, true);
|
|
218
|
-
bytes[footerOff + 8] = 0x31; // '1'
|
|
219
|
-
bytes[footerOff + 9] = 0x4B; // 'K'
|
|
220
|
-
bytes[footerOff + 10] = 0x42; // 'B'
|
|
221
|
-
bytes[footerOff + 11] = 0x4C; // 'L'
|
|
222
|
-
dv.setUint32(footerOff + 12, FOOTER_BYTES, true);
|
|
388
|
+
const header = new Uint8Array(CONTAINER_HEADER_BYTES);
|
|
389
|
+
const hdv = new DataView(header.buffer);
|
|
390
|
+
this._emitHeaderInto(hdv, header, 0, shardDirOff, shardCount, this._totalRows);
|
|
223
391
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
392
|
+
let crcVal = CRC_ABSENT;
|
|
393
|
+
if (crcOn) {
|
|
394
|
+
const headerCrc = crc32cFinal(crc32cUpdate(crc32cInit(), header, 0, CONTAINER_HEADER_BYTES));
|
|
395
|
+
const suffixCrc = crc32cFinal(this._sinkCrc);
|
|
396
|
+
crcVal = crc32cCombine(headerCrc, suffixCrc, footerOff - CONTAINER_HEADER_BYTES);
|
|
397
|
+
}
|
|
398
|
+
const footer = new Uint8Array(FOOTER_BYTES);
|
|
399
|
+
const fdv = new DataView(footer.buffer);
|
|
400
|
+
this._emitFooterInto(fdv, footer, 0, crcVal);
|
|
401
|
+
this._sinkWrite(footer, false);
|
|
402
|
+
this._sinkPos += FOOTER_BYTES;
|
|
403
|
+
|
|
404
|
+
let ret;
|
|
405
|
+
try { ret = sink.writeAt(header, 0); }
|
|
406
|
+
catch (e) { this._finalized = true; throw e; }
|
|
407
|
+
if (isThenable(ret)) { this._finalized = true; raisePreserveWriter('W_BAD_SINK', 'sink.writeAt returned a thenable; sinks must be synchronous'); }
|
|
408
|
+
|
|
409
|
+
this._finalized = true;
|
|
410
|
+
return { totalRows: this._totalRows, shardCount, mode: 'preserve', bytesWritten: this._sinkPos, layout: 'stream' };
|
|
230
411
|
}
|
|
231
412
|
}
|
package/src/RangeReader.js
CHANGED
|
@@ -30,8 +30,11 @@
|
|
|
30
30
|
// Error codes (stable, share prefix with Reader.js where semantics overlap):
|
|
31
31
|
// R_BAD_MAGIC / R_UNSUPPORTED_VERSION / R_UNSUPPORTED_ENDIAN / R_BAD_FIELD_FLAGS
|
|
32
32
|
// R_UNSUPPORTED_LANE / R_SHARD_VERSION_TOO_NEW / R_TRUNCATED / R_UNKNOWN_FIELD
|
|
33
|
-
// R_ADAPTER_SHORT_READ -- adapter returned fewer bytes than requested
|
|
34
|
-
//
|
|
33
|
+
// R_ADAPTER_SHORT_READ -- adapter returned fewer bytes than requested, OR a
|
|
34
|
+
// non-Uint8Array / wrong-length adapter return (T7c)
|
|
35
|
+
// R_ROW_OUT_OF_RANGE -- rowIdx negative, fractional, NaN, or >= totalRows (BS-32)
|
|
36
|
+
// R_OFFSET_TOO_LARGE -- a u64 header/schema/directory offset exceeds 2^53-1
|
|
37
|
+
// R_NOT_PREFETCHED -- syncRange over a shard that prefetchRange has not cached
|
|
35
38
|
// R_WRONG_MODE -- preserve-mode container fed to the schema RangeReader
|
|
36
39
|
// R_BAD_FLAGS -- unknown header flag bits set
|
|
37
40
|
// R_RESERVED_NONZERO -- a reserved header/descriptor/shard field is non-zero
|
|
@@ -41,8 +44,9 @@
|
|
|
41
44
|
|
|
42
45
|
import { StringTable } from './StringTable.js';
|
|
43
46
|
import { checkOpts } from './Opts.js';
|
|
47
|
+
import { crc32cInit, crc32cUpdate, crc32cFinal } from './Crc32c.js';
|
|
44
48
|
|
|
45
|
-
export const VERSION = '1.
|
|
49
|
+
export const VERSION = '1.6.0';
|
|
46
50
|
|
|
47
51
|
const CONTAINER_HEADER_BYTES = 48;
|
|
48
52
|
const SHARD_ENTRY_BYTES = 40;
|
|
@@ -53,10 +57,14 @@ const LANE_U32 = 3;
|
|
|
53
57
|
const READER_VERSION = 1;
|
|
54
58
|
const U32_MAX = 4294967295;
|
|
55
59
|
|
|
56
|
-
const RANGE_READER_OPTS = {
|
|
60
|
+
const RANGE_READER_OPTS = {
|
|
61
|
+
maxCachedShards: { t: 'int', min: 0, max: U32_MAX },
|
|
62
|
+
verifyCrc: { t: 'bool' },
|
|
63
|
+
};
|
|
57
64
|
const HTTP_ADAPTER_OPTS = { fetch: { t: 'fn' } };
|
|
58
65
|
|
|
59
66
|
const CONTAINER_FOOTER_BYTES = 16;
|
|
67
|
+
const CRC_ABSENT = 0xFFFFFFFF;
|
|
60
68
|
|
|
61
69
|
function laneBytesOf(k) { return k === LANE_F64 ? 8 : (k === LANE_U32 ? 4 : 0); }
|
|
62
70
|
|
|
@@ -65,6 +73,18 @@ export class RangeReaderError extends Error {
|
|
|
65
73
|
}
|
|
66
74
|
function raiseRange(code, msg) { throw new RangeReaderError(code, msg); }
|
|
67
75
|
|
|
76
|
+
// Read a u64 field from `dv` (the header, schema, or directory view -- three
|
|
77
|
+
// different DataViews here) and narrow it to a JS number, failing closed past
|
|
78
|
+
// Number.MAX_SAFE_INTEGER. Past 2^53-1 a Number() cast loses precision and every
|
|
79
|
+
// downstream bounds check reads a corrupted offset (BS-05). Cold: parse-only.
|
|
80
|
+
function u64(dv, off, what) {
|
|
81
|
+
const v = dv.getBigUint64(off, true);
|
|
82
|
+
if (v > 9007199254740991n)
|
|
83
|
+
throw new RangeReaderError('R_OFFSET_TOO_LARGE',
|
|
84
|
+
what + ' value ' + v + ' exceeds the safe-integer ceiling 9007199254740991');
|
|
85
|
+
return Number(v);
|
|
86
|
+
}
|
|
87
|
+
|
|
68
88
|
// Validate a local string table's shape before StringTable.parse casts a
|
|
69
89
|
// Uint32Array over it (T-1..T-4). `bytes` is the already-fetched shard slice;
|
|
70
90
|
// `off` and `len` locate the table within it.
|
|
@@ -186,6 +206,11 @@ export class RangeReader {
|
|
|
186
206
|
static async open(adapter, opts) {
|
|
187
207
|
const r = new RangeReader(adapter, opts);
|
|
188
208
|
await r._loadHeaderAndSchema();
|
|
209
|
+
if (opts && opts.verifyCrc === true) {
|
|
210
|
+
const status = await r.verifyCrc();
|
|
211
|
+
if (status === 'absent')
|
|
212
|
+
throw new RangeReaderError('R_CRC_ABSENT', 'verifyCrc:true but the container carries no CRC (footer CRC is absent, 0xFFFFFFFF)');
|
|
213
|
+
}
|
|
189
214
|
return r;
|
|
190
215
|
}
|
|
191
216
|
|
|
@@ -194,6 +219,7 @@ export class RangeReader {
|
|
|
194
219
|
opts = opts || {};
|
|
195
220
|
this.adapter = adapter;
|
|
196
221
|
this.maxCachedShards = opts.maxCachedShards !== undefined ? opts.maxCachedShards : 8;
|
|
222
|
+
this._footerCrc = CRC_ABSENT;
|
|
197
223
|
// shard cache: shardIdx -> { payloadBytes, payloadDv, stringTable, lastAccess }
|
|
198
224
|
this._shardCache = new Map();
|
|
199
225
|
this._accessCounter = 0;
|
|
@@ -205,7 +231,7 @@ export class RangeReader {
|
|
|
205
231
|
}
|
|
206
232
|
|
|
207
233
|
// Step 1: header (48 bytes) tells us schema + shard-dir offsets.
|
|
208
|
-
const headerBytes = await this.
|
|
234
|
+
const headerBytes = await this._fetchExact(0, CONTAINER_HEADER_BYTES);
|
|
209
235
|
const hdrDv = new DataView(headerBytes.buffer, headerBytes.byteOffset, headerBytes.byteLength);
|
|
210
236
|
if (headerBytes[0] !== 0x4C || headerBytes[1] !== 0x42 || headerBytes[2] !== 0x4B || headerBytes[3] !== 0x31) {
|
|
211
237
|
throw new RangeReaderError('R_BAD_MAGIC', 'header magic is not LBK1');
|
|
@@ -226,11 +252,11 @@ export class RangeReader {
|
|
|
226
252
|
if (reserved1 !== 0)
|
|
227
253
|
throw new RangeReaderError('R_RESERVED_NONZERO', 'header reserved1 at offset 36 must be 0, got ' + reserved1);
|
|
228
254
|
|
|
229
|
-
this._schemaBlockOff =
|
|
230
|
-
this._metadataOff =
|
|
231
|
-
this._shardDirOff =
|
|
255
|
+
this._schemaBlockOff = u64(hdrDv, 8, 'schema_block_off');
|
|
256
|
+
this._metadataOff = u64(hdrDv, 16, 'metadata_off');
|
|
257
|
+
this._shardDirOff = u64(hdrDv, 24, 'shard_directory_off');
|
|
232
258
|
this._shardCount = hdrDv.getUint32(32, true);
|
|
233
|
-
this._totalRows =
|
|
259
|
+
this._totalRows = u64(hdrDv, 40, 'total_rows');
|
|
234
260
|
|
|
235
261
|
const size = this.adapter.size;
|
|
236
262
|
if (this._schemaBlockOff < CONTAINER_HEADER_BYTES || this._schemaBlockOff >= size)
|
|
@@ -252,11 +278,11 @@ export class RangeReader {
|
|
|
252
278
|
if (this._shardDirOff <= this._schemaBlockOff)
|
|
253
279
|
throw new RangeReaderError('R_TRUNCATED', 'shard_directory_off ' + this._shardDirOff + ' not after schema_block_off ' + this._schemaBlockOff);
|
|
254
280
|
const schemaBlockLen = this._shardDirOff - this._schemaBlockOff;
|
|
255
|
-
const schemaBytes = await this.
|
|
281
|
+
const schemaBytes = await this._fetchExact(this._schemaBlockOff, schemaBlockLen);
|
|
256
282
|
this._parseSchema(schemaBytes);
|
|
257
283
|
|
|
258
284
|
// Step 3: shard directory (fixed size = shardCount * 40).
|
|
259
|
-
const dirBytes = await this.
|
|
285
|
+
const dirBytes = await this._fetchExact(this._shardDirOff, this._shardCount * SHARD_ENTRY_BYTES);
|
|
260
286
|
this._parseShardDirectory(dirBytes);
|
|
261
287
|
|
|
262
288
|
// Step 4 (M7): zone maps. Fetched ONCE at open, cached. Enables query
|
|
@@ -274,7 +300,7 @@ export class RangeReader {
|
|
|
274
300
|
|
|
275
301
|
async _loadFooter() {
|
|
276
302
|
const size = this.adapter.size;
|
|
277
|
-
const footer = await this.
|
|
303
|
+
const footer = await this._fetchExact(size - CONTAINER_FOOTER_BYTES, CONTAINER_FOOTER_BYTES);
|
|
278
304
|
if (footer[8] !== 0x31 || footer[9] !== 0x4B || footer[10] !== 0x42 || footer[11] !== 0x4C)
|
|
279
305
|
throw new RangeReaderError('R_BAD_FOOTER', 'footer magic_end is not 1KBL');
|
|
280
306
|
const fDv = new DataView(footer.buffer, footer.byteOffset, footer.byteLength);
|
|
@@ -283,6 +309,20 @@ export class RangeReader {
|
|
|
283
309
|
throw new RangeReaderError('R_BAD_FOOTER', 'footer_len ' + footerLen + ' is less than the minimum ' + CONTAINER_FOOTER_BYTES);
|
|
284
310
|
if (footerLen > size - CONTAINER_HEADER_BYTES)
|
|
285
311
|
throw new RangeReaderError('R_BAD_FOOTER', 'footer_len ' + footerLen + ' exceeds the container body size');
|
|
312
|
+
this._footerCrc = fDv.getUint32(0, true) >>> 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// SPEC 3.7 integrity over a ranged source: 'ok' | 'absent'; a mismatch throws
|
|
316
|
+
// R_BAD_CRC. Fetches the body [0, footer_off) in one range (a full-integrity
|
|
317
|
+
// check is inherently whole-body). The footer CRC was captured at open.
|
|
318
|
+
async verifyCrc() {
|
|
319
|
+
if (this._footerCrc === CRC_ABSENT) return 'absent';
|
|
320
|
+
const footerOff = this.adapter.size - CONTAINER_FOOTER_BYTES;
|
|
321
|
+
const body = await this._fetchExact(0, footerOff);
|
|
322
|
+
const actual = crc32cFinal(crc32cUpdate(crc32cInit(), body, 0, footerOff));
|
|
323
|
+
if (actual !== this._footerCrc)
|
|
324
|
+
throw new RangeReaderError('R_BAD_CRC', 'container CRC mismatch: stored 0x' + this._footerCrc.toString(16) + ' != computed 0x' + actual.toString(16));
|
|
325
|
+
return 'ok';
|
|
286
326
|
}
|
|
287
327
|
|
|
288
328
|
async _loadZoneMaps() {
|
|
@@ -291,7 +331,7 @@ export class RangeReader {
|
|
|
291
331
|
// corruption (D1), not absence.
|
|
292
332
|
if (this._metadataOff + 16 > this.adapter.size)
|
|
293
333
|
throw new RangeReaderError('R_BAD_METADATA', 'zone maps segment header runs past the container');
|
|
294
|
-
const hdr = await this.
|
|
334
|
+
const hdr = await this._fetchExact(this._metadataOff, 16);
|
|
295
335
|
if (hdr[0] !== 0x30 || hdr[1] !== 0x5A || hdr[2] !== 0x4D || hdr[3] !== 0x31)
|
|
296
336
|
throw new RangeReaderError('R_BAD_METADATA', 'zone maps magic is not ZM01');
|
|
297
337
|
const hdrDv = new DataView(hdr.buffer, hdr.byteOffset, hdr.byteLength);
|
|
@@ -309,7 +349,7 @@ export class RangeReader {
|
|
|
309
349
|
const restLen = fieldTableLen + fieldTablePad + shardCount * T * 8 * 2;
|
|
310
350
|
if (this._metadataOff + 16 + restLen > this.adapter.size)
|
|
311
351
|
throw new RangeReaderError('R_BAD_METADATA', 'zone maps segment runs past the container');
|
|
312
|
-
const rest = await this.
|
|
352
|
+
const rest = await this._fetchExact(this._metadataOff + 16, restLen);
|
|
313
353
|
const restDv = new DataView(rest.buffer, rest.byteOffset, rest.byteLength);
|
|
314
354
|
const tracked = new Array(T);
|
|
315
355
|
const fieldToPos = new Map();
|
|
@@ -357,7 +397,10 @@ export class RangeReader {
|
|
|
357
397
|
const laneKind = bytes[off + 4];
|
|
358
398
|
const flags = bytes[off + 5];
|
|
359
399
|
const reserved2 = dv.getUint16(off + 6, true);
|
|
360
|
-
const nameStrOff =
|
|
400
|
+
const nameStrOff = u64(dv, off + 8, 'field ' + i + ' name_str_off');
|
|
401
|
+
// reserved3 is BigInt-compared vs 0n below, never Number()-cast, so it
|
|
402
|
+
// needs no narrowing guard -- the reserved-must-be-zero check catches any
|
|
403
|
+
// out-of-range value (BS-05).
|
|
361
404
|
const reserved3 = dv.getBigUint64(off + 16, true);
|
|
362
405
|
if (flags !== 0) throw new RangeReaderError('R_BAD_FIELD_FLAGS', 'field ' + i + ' has non-zero flags');
|
|
363
406
|
if (reserved2 !== 0) throw new RangeReaderError('R_RESERVED_NONZERO', 'field ' + i + ' reserved2 must be 0, got ' + reserved2);
|
|
@@ -383,14 +426,14 @@ export class RangeReader {
|
|
|
383
426
|
let cumulativeRow = 0;
|
|
384
427
|
for (let i = 0; i < this._shardCount; i++) {
|
|
385
428
|
const off = i * SHARD_ENTRY_BYTES;
|
|
386
|
-
const payloadOff =
|
|
429
|
+
const payloadOff = u64(dv, off + 0, 'shard ' + i + ' payload_off');
|
|
387
430
|
const payloadLen = dv.getUint32(off + 8, true);
|
|
388
431
|
const rowCount = dv.getUint32(off + 12, true);
|
|
389
432
|
const minReaderVer = dv.getUint16(off + 16, true);
|
|
390
433
|
const shardFlags = dv.getUint16(off + 18, true);
|
|
391
434
|
const shardReserved = dv.getUint32(off + 20, true);
|
|
392
|
-
const localStrOff =
|
|
393
|
-
const localStrLen =
|
|
435
|
+
const localStrOff = u64(dv, off + 24, 'shard ' + i + ' local_string_off');
|
|
436
|
+
const localStrLen = u64(dv, off + 32, 'shard ' + i + ' local_string_len');
|
|
394
437
|
if (minReaderVer > READER_VERSION)
|
|
395
438
|
throw new RangeReaderError('R_SHARD_VERSION_TOO_NEW',
|
|
396
439
|
'shard ' + i + ' requires reader version ' + minReaderVer);
|
|
@@ -433,7 +476,10 @@ export class RangeReader {
|
|
|
433
476
|
|
|
434
477
|
// Locate the shard containing rowIdx via binary search.
|
|
435
478
|
_findShardIndex(rowIdx) {
|
|
436
|
-
|
|
479
|
+
// BS-32: Number.isInteger rejects fractional/NaN as well as out-of-range in
|
|
480
|
+
// one guard. This ONE site covers get(), prefetchRange, syncRange's bounds
|
|
481
|
+
// walk and the syncRange view's inner get.
|
|
482
|
+
if (!Number.isInteger(rowIdx) || rowIdx < 0 || rowIdx >= this._totalRows) {
|
|
437
483
|
throw new RangeReaderError('R_ROW_OUT_OF_RANGE',
|
|
438
484
|
'rowIdx ' + rowIdx + ' out of range [0, ' + this._totalRows + ')');
|
|
439
485
|
}
|
|
@@ -447,6 +493,21 @@ export class RangeReader {
|
|
|
447
493
|
return -1; // unreachable given the range check above
|
|
448
494
|
}
|
|
449
495
|
|
|
496
|
+
// Every adapter read funnels through here (T7c). The IOAdapter contract says
|
|
497
|
+
// fetch() MUST return exactly byteLength bytes; a short, over-long, or
|
|
498
|
+
// non-Uint8Array return would otherwise seed a DataView over the wrong extent
|
|
499
|
+
// and decode silent garbage. Fail closed with R_ADAPTER_SHORT_READ instead.
|
|
500
|
+
async _fetchExact(byteOffset, byteLength) {
|
|
501
|
+
const buf = await this.adapter.fetch(byteOffset, byteLength);
|
|
502
|
+
if (!(buf instanceof Uint8Array) || buf.byteLength !== byteLength) {
|
|
503
|
+
throw new RangeReaderError('R_ADAPTER_SHORT_READ',
|
|
504
|
+
'adapter returned ' +
|
|
505
|
+
(buf instanceof Uint8Array ? buf.byteLength + ' bytes' : Object.prototype.toString.call(buf)) +
|
|
506
|
+
' for a ' + byteLength + '-byte request at offset ' + byteOffset);
|
|
507
|
+
}
|
|
508
|
+
return buf;
|
|
509
|
+
}
|
|
510
|
+
|
|
450
511
|
// Load a shard's payload + local string table with ONE range request.
|
|
451
512
|
// They are contiguous in the container by SPEC 3.4, so a single fetch covers
|
|
452
513
|
// both. Returns the cached shard record.
|
|
@@ -464,7 +525,7 @@ export class RangeReader {
|
|
|
464
525
|
throw new RangeReaderError('R_TRUNCATED',
|
|
465
526
|
'shard ' + shardIdx + ' string table not contiguous with payload');
|
|
466
527
|
}
|
|
467
|
-
const combined = await this.
|
|
528
|
+
const combined = await this._fetchExact(combinedOff, combinedLen);
|
|
468
529
|
const payloadBytes = combined.subarray(0, s.payloadLen);
|
|
469
530
|
// DataView over the payload's underlying ArrayBuffer window.
|
|
470
531
|
const payloadDv = new DataView(payloadBytes.buffer, payloadBytes.byteOffset, payloadBytes.byteLength);
|
|
@@ -532,7 +593,7 @@ export class RangeReader {
|
|
|
532
593
|
const lastShard = this._findShardIndex(lastRow - 1);
|
|
533
594
|
for (let s = firstShard; s <= lastShard; s++) {
|
|
534
595
|
if (!this._shardCache.has(s)) {
|
|
535
|
-
throw new RangeReaderError('
|
|
596
|
+
throw new RangeReaderError('R_NOT_PREFETCHED',
|
|
536
597
|
'syncRange requires shard ' + s + ' to be prefetched (call prefetchRange first)');
|
|
537
598
|
}
|
|
538
599
|
}
|