@elaraai/east 1.0.55 → 1.0.57
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/dist/src/serialization/beast2/index.d.ts +5 -3
- package/dist/src/serialization/beast2/index.d.ts.map +1 -1
- package/dist/src/serialization/beast2/index.js +1 -1
- package/dist/src/serialization/beast2/index.js.map +1 -1
- package/dist/src/serialization/beast2/v5/codec.d.ts +8 -1
- package/dist/src/serialization/beast2/v5/codec.d.ts.map +1 -1
- package/dist/src/serialization/beast2/v5/codec.js +67 -15
- package/dist/src/serialization/beast2/v5/codec.js.map +1 -1
- package/dist/src/serialization/beast2/v5/frames.d.ts +13 -8
- package/dist/src/serialization/beast2/v5/frames.d.ts.map +1 -1
- package/dist/src/serialization/beast2/v5/frames.js +16 -10
- package/dist/src/serialization/beast2/v5/frames.js.map +1 -1
- package/dist/src/serialization/beast2/v5/index.spec.js +300 -24
- package/dist/src/serialization/beast2/v5/index.spec.js.map +1 -1
- package/dist/src/serialization/beast2/v5/inflate.d.ts +19 -0
- package/dist/src/serialization/beast2/v5/inflate.d.ts.map +1 -0
- package/dist/src/serialization/beast2/v5/inflate.js +273 -0
- package/dist/src/serialization/beast2/v5/inflate.js.map +1 -0
- package/dist/src/serialization/beast2/v5/stream.d.ts +126 -5
- package/dist/src/serialization/beast2/v5/stream.d.ts.map +1 -1
- package/dist/src/serialization/beast2/v5/stream.js +405 -21
- package/dist/src/serialization/beast2/v5/stream.js.map +1 -1
- package/dist/src/serialization/index.d.ts +1 -1
- package/dist/src/serialization/index.d.ts.map +1 -1
- package/dist/src/serialization/index.js +1 -1
- package/dist/src/serialization/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
import {} from "../../../type_of_type.js";
|
|
23
23
|
import { BufferWriter, BufferReader } from "../../binary-utils.js";
|
|
24
24
|
import { SourceMap } from "../../../location.js";
|
|
25
|
+
import { compareFor } from "../../../comparison.js";
|
|
26
|
+
import { SortedSet } from "../../../containers/sortedset.js";
|
|
27
|
+
import { SortedMap } from "../../../containers/sortedmap.js";
|
|
25
28
|
import { buildPlatformContext } from "../shared.js";
|
|
26
29
|
import { writeTypeSection, readTypeSection, asTypeValue } from "./type-section.js";
|
|
27
30
|
import { FrameReader, writeFrame } from "./frames.js";
|
|
@@ -33,6 +36,13 @@ function checkSegmented(typeValue) {
|
|
|
33
36
|
}
|
|
34
37
|
return typeValue.type;
|
|
35
38
|
}
|
|
39
|
+
/** The East comparator over a stream's order key — Set elements or Dict
|
|
40
|
+
* keys; `null` for Array roots, which have no order contract. */
|
|
41
|
+
function orderCmpFor(typeValue, kind) {
|
|
42
|
+
if (kind === "Array")
|
|
43
|
+
return null;
|
|
44
|
+
return compareFor(kind === "Set" ? typeValue.value : typeValue.value.key);
|
|
45
|
+
}
|
|
36
46
|
// =============================================================================
|
|
37
47
|
// Streaming writer
|
|
38
48
|
// =============================================================================
|
|
@@ -68,6 +78,9 @@ export class Beast2Writer {
|
|
|
68
78
|
ctx;
|
|
69
79
|
encodeElems;
|
|
70
80
|
index = [];
|
|
81
|
+
orderCmp;
|
|
82
|
+
lastKey;
|
|
83
|
+
hasLast = false;
|
|
71
84
|
bytesWritten = 0;
|
|
72
85
|
finished = false;
|
|
73
86
|
/**
|
|
@@ -83,6 +96,7 @@ export class Beast2Writer {
|
|
|
83
96
|
this.codec = options?.codec ?? "deflate";
|
|
84
97
|
this.selfContained = options?.selfContained ?? true;
|
|
85
98
|
this.withIndex = options?.index ?? true;
|
|
99
|
+
this.orderCmp = orderCmpFor(typeValue, this.kind);
|
|
86
100
|
const sourceMap = options?.sourceMap ?? null;
|
|
87
101
|
this.ctx = createV5EncodeContext(sourceMap, this.selfContained);
|
|
88
102
|
const typeCtx = new Map();
|
|
@@ -123,8 +137,15 @@ export class Beast2Writer {
|
|
|
123
137
|
* Empty batches are skipped — a segment count is never zero, so the stream
|
|
124
138
|
* terminator stays unambiguous.
|
|
125
139
|
*
|
|
140
|
+
* Set/Dict batches must continue the stream's strict East (key) order:
|
|
141
|
+
* segment content is the canonical value split at segment boundaries, so
|
|
142
|
+
* each batch must be internally ascending and start above the previous
|
|
143
|
+
* batch's last key. Pre-sort into batches (a `SortedMap`/`SortedSet` slice,
|
|
144
|
+
* or an external sort), or encode arrival order as an Array of entries.
|
|
145
|
+
*
|
|
126
146
|
* @param batch - a value of the declared collection type
|
|
127
|
-
* @throws {Error} When called after {@link finish}
|
|
147
|
+
* @throws {Error} When called after {@link finish}, or when a Set/Dict
|
|
148
|
+
* batch violates the stream's strict ascending (key) order.
|
|
128
149
|
*/
|
|
129
150
|
write(batch) {
|
|
130
151
|
if (this.finished)
|
|
@@ -132,6 +153,8 @@ export class Beast2Writer {
|
|
|
132
153
|
const count = this.kind === "Array" ? batch.length : batch.size;
|
|
133
154
|
if (count === 0)
|
|
134
155
|
return;
|
|
156
|
+
if (this.orderCmp)
|
|
157
|
+
this.checkAscent(batch);
|
|
135
158
|
if (this.selfContained) {
|
|
136
159
|
this.ctx.containerIndex.clear();
|
|
137
160
|
this.ctx.segmentBaseDef = this.ctx.containerCount;
|
|
@@ -163,6 +186,20 @@ export class Beast2Writer {
|
|
|
163
186
|
}
|
|
164
187
|
this.emit(tail.toUint8Array());
|
|
165
188
|
}
|
|
189
|
+
/** Validates that a Set/Dict batch continues the stream's strict ascent
|
|
190
|
+
* in East (key) order — within the batch and against the previous batch. */
|
|
191
|
+
checkAscent(batch) {
|
|
192
|
+
const keys = this.kind === "Set" ? batch : batch.keys();
|
|
193
|
+
for (const k of keys) {
|
|
194
|
+
if (this.hasLast && this.orderCmp(this.lastKey, k) >= 0) {
|
|
195
|
+
throw new Error(`beast2 v5: ${this.kind} stream batches must be strictly ascending in East ` +
|
|
196
|
+
`${this.kind === "Dict" ? "key" : "element"} order — segment content is the canonical value; ` +
|
|
197
|
+
`pre-sort batches, or encode arrival order as an Array`);
|
|
198
|
+
}
|
|
199
|
+
this.lastKey = k;
|
|
200
|
+
this.hasLast = true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
166
203
|
emit(bytes) {
|
|
167
204
|
this.bytesWritten += bytes.length;
|
|
168
205
|
this.sink(bytes);
|
|
@@ -218,6 +255,129 @@ export function encodeBeast2SegmentsFor(type, options) {
|
|
|
218
255
|
};
|
|
219
256
|
}
|
|
220
257
|
// =============================================================================
|
|
258
|
+
// Paged whole-value encode
|
|
259
|
+
// =============================================================================
|
|
260
|
+
/** Default element cap per segment for {@link encodeBeast2PagedFor}. Small
|
|
261
|
+
* enough that one segment decodes cheaply, large enough that frames stay far
|
|
262
|
+
* above the compression threshold and per-segment overhead (frame header +
|
|
263
|
+
* index entry) is negligible. */
|
|
264
|
+
export const BEAST2_PAGED_BATCH_DEFAULT = 1_000;
|
|
265
|
+
/** Default wire-byte target per segment for {@link encodeBeast2PagedFor}.
|
|
266
|
+
* Wide rows would otherwise make element-capped segments arbitrarily large —
|
|
267
|
+
* and a paging reader decodes whole segments, so segment size IS the random-
|
|
268
|
+
* access cost. Batching adapts toward this target from measured output. */
|
|
269
|
+
export const BEAST2_PAGED_TARGET_BYTES_DEFAULT = 2 * 1024 * 1024;
|
|
270
|
+
/** The probe batch that seeds the byte-adaptive batching — small, so one
|
|
271
|
+
* pathologically wide first batch cannot blow past the target unmeasured. */
|
|
272
|
+
const PAGED_PROBE_BATCH = 16;
|
|
273
|
+
/**
|
|
274
|
+
* Builds a curried paged encoder: `encode(value)` writes one whole collection
|
|
275
|
+
* value as a segmented, self-contained, indexed v5 blob — `batchSize` elements
|
|
276
|
+
* per segment.
|
|
277
|
+
*
|
|
278
|
+
* The write-side sibling of {@link openBeast2PagesFor}: a blob written this
|
|
279
|
+
* way supports random access ({@link Beast2Pages.segment} /
|
|
280
|
+
* {@link Beast2Pages.element} / {@link Beast2Pages.slice}) without decoding
|
|
281
|
+
* the rest. Decoding the whole blob through the ordinary entry points yields
|
|
282
|
+
* exactly the input value. Note the bytes differ from the whole-value
|
|
283
|
+
* `encodeBeast2For` encode of the same value (segment framing is part of the
|
|
284
|
+
* bytes), so content-addressed stores hash the two forms differently.
|
|
285
|
+
*
|
|
286
|
+
* @param type - the collection type (Array/Set/Dict)
|
|
287
|
+
* @param options - batch size, codec, and source map options
|
|
288
|
+
* @returns a function encoding a collection value to an indexed v5 blob
|
|
289
|
+
* @throws {TypeError} When `type` is not an Array, Set or Dict type.
|
|
290
|
+
*/
|
|
291
|
+
export function encodeBeast2PagedFor(type, options) {
|
|
292
|
+
const typeValue = asTypeValue(type);
|
|
293
|
+
const kind = checkSegmented(typeValue);
|
|
294
|
+
const cmp = orderCmpFor(typeValue, kind);
|
|
295
|
+
const batchCap = Math.max(1, Math.floor(options?.batchSize ?? BEAST2_PAGED_BATCH_DEFAULT));
|
|
296
|
+
const targetBytes = Math.max(1, Math.floor(options?.targetSegmentBytes ?? BEAST2_PAGED_TARGET_BYTES_DEFAULT));
|
|
297
|
+
const writerOptions = {
|
|
298
|
+
...(options?.codec !== undefined && { codec: options.codec }),
|
|
299
|
+
...(options?.sourceMap !== undefined && { sourceMap: options.sourceMap }),
|
|
300
|
+
};
|
|
301
|
+
return (value) => {
|
|
302
|
+
const makeBatch = kind === "Array" ? (items) => items
|
|
303
|
+
: kind === "Set" ? (items) => new Set(items)
|
|
304
|
+
: (items) => new Map(items);
|
|
305
|
+
// Canonical source order: SortedSet/SortedMap iterate in East order
|
|
306
|
+
// already; a plain Set/Map (insertion order) is sorted first — segments
|
|
307
|
+
// must hold the canonical value, and the writer validates the ascent.
|
|
308
|
+
const iterable = kind === "Dict"
|
|
309
|
+
? (value instanceof SortedMap
|
|
310
|
+
? value.entries()
|
|
311
|
+
: [...value.entries()].sort((a, b) => cmp(a[0], b[0])))
|
|
312
|
+
: kind === "Set"
|
|
313
|
+
? (value instanceof SortedSet
|
|
314
|
+
? value
|
|
315
|
+
: [...value].sort(cmp))
|
|
316
|
+
: value;
|
|
317
|
+
// Byte-adaptive batching: a throwaway scratch encode of the first few
|
|
318
|
+
// elements measures the average wire size, and batches then target
|
|
319
|
+
// `targetSegmentBytes` (never above the element cap). Re-encoding the
|
|
320
|
+
// probe costs a handful of elements; the real stream starts with
|
|
321
|
+
// full-size, right-sized segments. Batching is a pure function of the
|
|
322
|
+
// value, so the bytes stay deterministic for content-addressing.
|
|
323
|
+
const items = iterable[Symbol.iterator]();
|
|
324
|
+
const probe = [];
|
|
325
|
+
while (probe.length < PAGED_PROBE_BATCH) {
|
|
326
|
+
const n = items.next();
|
|
327
|
+
if (n.done)
|
|
328
|
+
break;
|
|
329
|
+
probe.push(n.value);
|
|
330
|
+
}
|
|
331
|
+
let nextBatch = batchCap;
|
|
332
|
+
if (probe.length > 0) {
|
|
333
|
+
let scratchBytes = 0;
|
|
334
|
+
let scratchHeader = 0;
|
|
335
|
+
const scratch = new Beast2Writer(typeValue, (b) => { scratchBytes += b.length; }, writerOptions);
|
|
336
|
+
scratchHeader = scratchBytes;
|
|
337
|
+
scratch.write(makeBatch(probe));
|
|
338
|
+
const avg = Math.max(1, (scratchBytes - scratchHeader) / probe.length);
|
|
339
|
+
nextBatch = Math.max(1, Math.min(batchCap, Math.floor(targetBytes / avg)));
|
|
340
|
+
}
|
|
341
|
+
const chunks = [];
|
|
342
|
+
let bodyBytes = 0;
|
|
343
|
+
const writer = new Beast2Writer(typeValue, (b) => {
|
|
344
|
+
chunks.push(b);
|
|
345
|
+
bodyBytes += b.length;
|
|
346
|
+
}, writerOptions);
|
|
347
|
+
const headerBytes = bodyBytes;
|
|
348
|
+
let written = 0;
|
|
349
|
+
let batch = [];
|
|
350
|
+
const flush = () => {
|
|
351
|
+
if (batch.length === 0)
|
|
352
|
+
return;
|
|
353
|
+
writer.write(makeBatch(batch));
|
|
354
|
+
written += batch.length;
|
|
355
|
+
batch = [];
|
|
356
|
+
// Refine toward the target as real output accumulates (drifting data).
|
|
357
|
+
const avg = Math.max(1, (bodyBytes - headerBytes) / written);
|
|
358
|
+
nextBatch = Math.max(1, Math.min(batchCap, Math.floor(targetBytes / avg)));
|
|
359
|
+
};
|
|
360
|
+
const pump = (item) => {
|
|
361
|
+
batch.push(item);
|
|
362
|
+
if (batch.length >= nextBatch)
|
|
363
|
+
flush();
|
|
364
|
+
};
|
|
365
|
+
for (const p of probe)
|
|
366
|
+
pump(p);
|
|
367
|
+
for (let n = items.next(); !n.done; n = items.next())
|
|
368
|
+
pump(n.value);
|
|
369
|
+
flush();
|
|
370
|
+
writer.finish();
|
|
371
|
+
const out = new Uint8Array(bodyBytes);
|
|
372
|
+
let pos = 0;
|
|
373
|
+
for (const c of chunks) {
|
|
374
|
+
out.set(c, pos);
|
|
375
|
+
pos += c.length;
|
|
376
|
+
}
|
|
377
|
+
return out;
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
// =============================================================================
|
|
221
381
|
// Segment iterator
|
|
222
382
|
// =============================================================================
|
|
223
383
|
/** Verifies the v5 magic without dispatching (stream APIs are v5-only). */
|
|
@@ -243,16 +403,30 @@ function openSegmented(data, typeValue) {
|
|
|
243
403
|
const sourceMap = readSourceMapSectionV5(reader);
|
|
244
404
|
return { kind, sourceMap, frameOffset: reader.offset };
|
|
245
405
|
}
|
|
246
|
-
/** Builds the per-segment decode closure shared by the iterator and pages.
|
|
406
|
+
/** Builds the per-segment decode closure shared by the iterator and pages.
|
|
407
|
+
*
|
|
408
|
+
* For Set/Dict, an `order` state threads the strict-ascent validation: each
|
|
409
|
+
* decoded element/key must exceed `order.prev`. Passing one state across
|
|
410
|
+
* consecutive segments extends the check over the segment boundary; a fresh
|
|
411
|
+
* state validates a single segment in isolation. Violations are corruption
|
|
412
|
+
* (the wire must hold the canonical value), never data to repair. */
|
|
247
413
|
function buildSegmentDecoder(typeValue, kind) {
|
|
248
414
|
const typeCtx = new Map();
|
|
415
|
+
const cmp = orderCmpFor(typeValue, kind);
|
|
249
416
|
if (kind === "Dict") {
|
|
250
417
|
const key = buildV5Decoder(typeValue.value.key, typeCtx);
|
|
251
418
|
const val = buildV5Decoder(typeValue.value.value, typeCtx);
|
|
252
|
-
return (reader, ctx, n) => {
|
|
419
|
+
return (reader, ctx, n, order) => {
|
|
253
420
|
const map = new Map();
|
|
254
421
|
for (let i = 0; i < n; i++) {
|
|
255
422
|
const k = key(reader, ctx);
|
|
423
|
+
if (order) {
|
|
424
|
+
if (order.has && cmp(order.prev, k) >= 0) {
|
|
425
|
+
throw new Error(`beast2 v5: Dict keys are not strictly ascending in East order — the wire must hold the canonical value (corrupt or pre-contract blob)`);
|
|
426
|
+
}
|
|
427
|
+
order.prev = k;
|
|
428
|
+
order.has = true;
|
|
429
|
+
}
|
|
256
430
|
const v = val(reader, ctx);
|
|
257
431
|
map.set(k, v);
|
|
258
432
|
}
|
|
@@ -261,10 +435,19 @@ function buildSegmentDecoder(typeValue, kind) {
|
|
|
261
435
|
}
|
|
262
436
|
const elem = buildV5Decoder(typeValue.value, typeCtx);
|
|
263
437
|
if (kind === "Set") {
|
|
264
|
-
return (reader, ctx, n) => {
|
|
438
|
+
return (reader, ctx, n, order) => {
|
|
265
439
|
const set = new Set();
|
|
266
|
-
for (let i = 0; i < n; i++)
|
|
267
|
-
|
|
440
|
+
for (let i = 0; i < n; i++) {
|
|
441
|
+
const item = elem(reader, ctx);
|
|
442
|
+
if (order) {
|
|
443
|
+
if (order.has && cmp(order.prev, item) >= 0) {
|
|
444
|
+
throw new Error(`beast2 v5: Set elements are not strictly ascending in East order — the wire must hold the canonical value (corrupt or pre-contract blob)`);
|
|
445
|
+
}
|
|
446
|
+
order.prev = item;
|
|
447
|
+
order.has = true;
|
|
448
|
+
}
|
|
449
|
+
set.add(item);
|
|
450
|
+
}
|
|
268
451
|
return set;
|
|
269
452
|
};
|
|
270
453
|
}
|
|
@@ -301,13 +484,16 @@ export function iterBeast2SegmentsFor(type, options) {
|
|
|
301
484
|
// The root container is definition 0 — segments never alias it, but the
|
|
302
485
|
// definition numbering must match the writer's.
|
|
303
486
|
ctx.containers.push(kind === "Array" ? [] : kind === "Set" ? new Set() : new Map());
|
|
487
|
+
// One order state across all segments: Set/Dict streams must ascend
|
|
488
|
+
// strictly over the whole stream, including across segment boundaries.
|
|
489
|
+
const order = kind === "Array" ? undefined : { prev: undefined, has: false };
|
|
304
490
|
for (;;) {
|
|
305
491
|
if (reader.offset === reader.buffer.length)
|
|
306
492
|
reader = cursor.next();
|
|
307
493
|
const n = reader.readVarint();
|
|
308
494
|
if (n === 0)
|
|
309
495
|
break;
|
|
310
|
-
yield decodeSegment(reader, ctx, n);
|
|
496
|
+
yield decodeSegment(reader, ctx, n, order);
|
|
311
497
|
}
|
|
312
498
|
if (reader.offset !== reader.buffer.length) {
|
|
313
499
|
throw new Error(`beast2 v5: ${reader.buffer.length - reader.offset} logical bytes after the root terminator`);
|
|
@@ -328,23 +514,37 @@ export function iterBeast2SegmentsFor(type, options) {
|
|
|
328
514
|
* {@link segment} seeks to and decodes exactly one segment. Requires the blob
|
|
329
515
|
* to carry an index (written by default by {@link Beast2Writer}); random
|
|
330
516
|
* access additionally requires self-contained segments.
|
|
517
|
+
*
|
|
518
|
+
* Set/Dict blobs page like Arrays: the wire holds the canonical value split
|
|
519
|
+
* at segment boundaries (strictly ascending, disjoint segments), so row
|
|
520
|
+
* windows ({@link slice}) and key lookups ({@link get}) address the sorted
|
|
521
|
+
* order directly. The first Set/Dict access verifies the segment fences
|
|
522
|
+
* (each segment's first key, probed without decoding whole segments) ascend
|
|
523
|
+
* strictly, and every decoded segment is validated internally and against
|
|
524
|
+
* the next fence — a blob violating the canonical-order contract fails with
|
|
525
|
+
* a corruption error rather than mis-addressing rows.
|
|
331
526
|
*/
|
|
332
527
|
export class Beast2Pages {
|
|
333
528
|
/** Per-segment element counts from the index (pairs for Dict roots). */
|
|
334
529
|
counts;
|
|
335
|
-
/** Sum of all segment counts
|
|
336
|
-
*
|
|
337
|
-
*
|
|
530
|
+
/** Sum of all segment counts — the exact element (pair) count for every
|
|
531
|
+
* root kind: Set/Dict segments are disjoint ranges of the canonical
|
|
532
|
+
* value, so counts never overlap. */
|
|
338
533
|
elementCount;
|
|
339
534
|
/** Whether segments are independently decodable. */
|
|
340
535
|
selfContained;
|
|
341
536
|
data;
|
|
342
537
|
indexData;
|
|
343
538
|
kind;
|
|
539
|
+
typeValue;
|
|
344
540
|
sourceMap;
|
|
345
541
|
decodeSegment;
|
|
346
542
|
platform;
|
|
347
543
|
cumulative;
|
|
544
|
+
orderCmp;
|
|
545
|
+
/** First key/element of each segment, in segment order (Set/Dict only). */
|
|
546
|
+
fences = null;
|
|
547
|
+
fenceDec = null;
|
|
348
548
|
/** @internal Use {@link openBeast2PagesFor}. */
|
|
349
549
|
constructor(data, typeValue, options) {
|
|
350
550
|
const { kind, sourceMap, frameOffset } = openSegmented(data, typeValue);
|
|
@@ -356,12 +556,14 @@ export class Beast2Pages {
|
|
|
356
556
|
this.data = data;
|
|
357
557
|
this.indexData = index;
|
|
358
558
|
this.kind = kind;
|
|
559
|
+
this.typeValue = typeValue;
|
|
359
560
|
this.sourceMap = sourceMap;
|
|
360
561
|
this.decodeSegment = buildSegmentDecoder(typeValue, kind);
|
|
361
562
|
this.platform = options;
|
|
362
563
|
this.counts = index.counts;
|
|
363
564
|
this.elementCount = index.totalCount;
|
|
364
565
|
this.selfContained = index.selfContained;
|
|
566
|
+
this.orderCmp = orderCmpFor(typeValue, kind);
|
|
365
567
|
this.cumulative = new Array(index.counts.length);
|
|
366
568
|
let sum = 0;
|
|
367
569
|
for (let i = 0; i < index.counts.length; i++) {
|
|
@@ -376,12 +578,22 @@ export class Beast2Pages {
|
|
|
376
578
|
/**
|
|
377
579
|
* Decodes one segment by index.
|
|
378
580
|
*
|
|
581
|
+
* Set/Dict segments are validated for strict internal ascent as they
|
|
582
|
+
* decode (the canonical-order contract); a violation is a corruption
|
|
583
|
+
* error, not data.
|
|
584
|
+
*
|
|
379
585
|
* @param i - zero-based segment index
|
|
380
586
|
* @returns the segment's decoded collection
|
|
381
587
|
* @throws {Error} When the blob is not self-contained (segments cannot be
|
|
382
|
-
* decoded independently)
|
|
588
|
+
* decoded independently), `i` is out of range, or a Set/Dict segment
|
|
589
|
+
* violates strict ascending order.
|
|
383
590
|
*/
|
|
384
591
|
segment(i) {
|
|
592
|
+
const order = this.kind === "Array" ? undefined : { prev: undefined, has: false };
|
|
593
|
+
return this.decodeSegmentCore(i, order);
|
|
594
|
+
}
|
|
595
|
+
/** Seeks to and decodes segment `i`, threading the caller's order state. */
|
|
596
|
+
decodeSegmentCore(i, order) {
|
|
385
597
|
if (!this.selfContained) {
|
|
386
598
|
throw new Error(`beast2 v5: blob has cross-segment aliasing — random access needs self-contained segments`);
|
|
387
599
|
}
|
|
@@ -395,12 +607,67 @@ export class Beast2Pages {
|
|
|
395
607
|
throw new Error(`beast2 v5: segment ${i} declares ${n} elements, index says ${this.indexData.counts[i]}`);
|
|
396
608
|
}
|
|
397
609
|
const ctx = { containers: [], sourceMap: this.sourceMap, ...buildPlatformContext(this.platform) };
|
|
398
|
-
const value = this.decodeSegment(reader, ctx, n);
|
|
610
|
+
const value = this.decodeSegment(reader, ctx, n, order);
|
|
399
611
|
if (reader.offset !== reader.buffer.length) {
|
|
400
612
|
throw new Error(`beast2 v5: ${reader.buffer.length - reader.offset} logical bytes after segment ${i}`);
|
|
401
613
|
}
|
|
402
614
|
return value;
|
|
403
615
|
}
|
|
616
|
+
/** Decodes just the first key/element of segment `i` — a bounded probe
|
|
617
|
+
* (one frame inflate, one element decode), not a whole-segment decode. */
|
|
618
|
+
firstKey(i) {
|
|
619
|
+
if (!this.fenceDec) {
|
|
620
|
+
const keyType = this.kind === "Set" ? this.typeValue.value : this.typeValue.value.key;
|
|
621
|
+
this.fenceDec = buildV5Decoder(keyType);
|
|
622
|
+
}
|
|
623
|
+
const cursor = new FrameReader(this.data, this.indexData.offsets[i]);
|
|
624
|
+
const reader = cursor.next();
|
|
625
|
+
reader.readVarint(); // element count — segments are never empty
|
|
626
|
+
const ctx = { containers: [], sourceMap: this.sourceMap, ...buildPlatformContext(this.platform) };
|
|
627
|
+
return this.fenceDec(reader, ctx);
|
|
628
|
+
}
|
|
629
|
+
/** Probes and verifies the segment fences once: each segment's first
|
|
630
|
+
* key/element must ascend strictly across segments. */
|
|
631
|
+
verifyFences() {
|
|
632
|
+
if (this.fences)
|
|
633
|
+
return this.fences;
|
|
634
|
+
if (!this.selfContained) {
|
|
635
|
+
throw new Error(`beast2 v5: blob has cross-segment aliasing — random access needs self-contained segments`);
|
|
636
|
+
}
|
|
637
|
+
const n = this.indexData.offsets.length;
|
|
638
|
+
const fences = new Array(n);
|
|
639
|
+
for (let i = 0; i < n; i++)
|
|
640
|
+
fences[i] = this.firstKey(i);
|
|
641
|
+
for (let i = 1; i < n; i++) {
|
|
642
|
+
if (this.orderCmp(fences[i - 1], fences[i]) >= 0) {
|
|
643
|
+
throw new Error(`beast2 v5: segments ${i - 1} and ${i} are not disjoint ascending ${this.kind === "Dict" ? "key" : "element"} ranges — the wire must hold the canonical value (corrupt or pre-contract blob)`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
this.fences = fences;
|
|
647
|
+
return fences;
|
|
648
|
+
}
|
|
649
|
+
/** Decodes segment `i` with order threading, then checks its tail stays
|
|
650
|
+
* below the next segment's fence (segments must be disjoint ranges). */
|
|
651
|
+
decodeDisjoint(i, order, fences) {
|
|
652
|
+
const value = this.decodeSegmentCore(i, order);
|
|
653
|
+
if (i + 1 < fences.length && order.has && this.orderCmp(order.prev, fences[i + 1]) >= 0) {
|
|
654
|
+
throw new Error(`beast2 v5: segments ${i} and ${i + 1} are not disjoint ascending ${this.kind === "Dict" ? "key" : "element"} ranges — the wire must hold the canonical value (corrupt or pre-contract blob)`);
|
|
655
|
+
}
|
|
656
|
+
return value;
|
|
657
|
+
}
|
|
658
|
+
/** Binary-searches the cumulative counts for the segment owning `row`,
|
|
659
|
+
* returning its index and the global row of its first element. */
|
|
660
|
+
rowSegment(row) {
|
|
661
|
+
let lo = 0, hi = this.cumulative.length - 1;
|
|
662
|
+
while (lo < hi) {
|
|
663
|
+
const mid = (lo + hi) >> 1;
|
|
664
|
+
if (this.cumulative[mid] <= row)
|
|
665
|
+
lo = mid + 1;
|
|
666
|
+
else
|
|
667
|
+
hi = mid;
|
|
668
|
+
}
|
|
669
|
+
return { seg: lo, base: lo === 0 ? 0 : this.cumulative[lo - 1] };
|
|
670
|
+
}
|
|
404
671
|
/**
|
|
405
672
|
* Reads one element by row index (Array roots only): binary-searches the
|
|
406
673
|
* index, decodes that single segment, and returns the row.
|
|
@@ -416,18 +683,135 @@ export class Beast2Pages {
|
|
|
416
683
|
if (row < 0 || row >= this.elementCount) {
|
|
417
684
|
throw new Error(`beast2 v5: element ${row} out of range (${this.elementCount} elements)`);
|
|
418
685
|
}
|
|
419
|
-
|
|
420
|
-
|
|
686
|
+
const { seg, base } = this.rowSegment(row);
|
|
687
|
+
const segment = this.segment(seg);
|
|
688
|
+
return segment[row - base];
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Reads a window of the collection by row range, decoding only the
|
|
692
|
+
* segments the window touches.
|
|
693
|
+
*
|
|
694
|
+
* Rows address stream order — for Array roots the element order, for
|
|
695
|
+
* Set/Dict roots the canonical East (key) order, since segments are
|
|
696
|
+
* disjoint ascending ranges. Returns a collection value of the root kind
|
|
697
|
+
* holding the window (an array, `Set`, or `Map` in that order).
|
|
698
|
+
*
|
|
699
|
+
* Clamps like `Array.prototype.slice`: a window past the end returns the
|
|
700
|
+
* available tail (or an empty collection), never throws for being short.
|
|
701
|
+
*
|
|
702
|
+
* @param offset - zero-based row of the window's first element
|
|
703
|
+
* @param limit - maximum number of elements (pairs) to return
|
|
704
|
+
* @returns a collection of the root kind with the window's contents
|
|
705
|
+
* @throws {Error} When `offset`/`limit` are negative or fractional, the
|
|
706
|
+
* blob is not self-contained, or a Set/Dict blob violates the
|
|
707
|
+
* canonical-order contract (non-ascending or overlapping segments).
|
|
708
|
+
*/
|
|
709
|
+
slice(offset, limit) {
|
|
710
|
+
if (!Number.isInteger(offset) || offset < 0 || !Number.isInteger(limit) || limit < 0) {
|
|
711
|
+
throw new Error(`beast2 v5: slice(${offset}, ${limit}) — offset and limit must be non-negative integers`);
|
|
712
|
+
}
|
|
713
|
+
const empty = () => this.kind === "Array" ? [] : this.kind === "Set" ? new Set() : new Map();
|
|
714
|
+
if (limit === 0 || offset >= this.elementCount)
|
|
715
|
+
return empty();
|
|
716
|
+
if (this.kind === "Array") {
|
|
717
|
+
const out = [];
|
|
718
|
+
let { seg, base } = this.rowSegment(offset);
|
|
719
|
+
while (out.length < limit && seg < this.cumulative.length) {
|
|
720
|
+
const segment = this.segment(seg);
|
|
721
|
+
for (let i = Math.max(0, offset - base); i < segment.length && out.length < limit; i++) {
|
|
722
|
+
out.push(segment[i]);
|
|
723
|
+
}
|
|
724
|
+
base += segment.length;
|
|
725
|
+
seg++;
|
|
726
|
+
}
|
|
727
|
+
return out;
|
|
728
|
+
}
|
|
729
|
+
// Set/Dict: rows address the canonical sorted order. Verify the fence
|
|
730
|
+
// chain once, then decode the touched segments with one running order
|
|
731
|
+
// state (validating ascent inside and across them) and a tail check
|
|
732
|
+
// against the fence of the first untouched segment.
|
|
733
|
+
const fences = this.verifyFences();
|
|
734
|
+
const isSet = this.kind === "Set";
|
|
735
|
+
const out = (isSet ? new Set() : new Map());
|
|
736
|
+
let taken = 0;
|
|
737
|
+
let { seg, base } = this.rowSegment(offset);
|
|
738
|
+
const order = { prev: undefined, has: false };
|
|
739
|
+
while (taken < limit && seg < this.cumulative.length) {
|
|
740
|
+
const segment = this.decodeDisjoint(seg, order, fences);
|
|
741
|
+
let skip = Math.max(0, offset - base);
|
|
742
|
+
if (isSet) {
|
|
743
|
+
for (const item of segment) {
|
|
744
|
+
if (skip > 0) {
|
|
745
|
+
skip--;
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (taken >= limit)
|
|
749
|
+
break;
|
|
750
|
+
out.add(item);
|
|
751
|
+
taken++;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
for (const [k, v] of segment.entries()) {
|
|
756
|
+
if (skip > 0) {
|
|
757
|
+
skip--;
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (taken >= limit)
|
|
761
|
+
break;
|
|
762
|
+
out.set(k, v);
|
|
763
|
+
taken++;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
base += segment.size;
|
|
767
|
+
seg++;
|
|
768
|
+
}
|
|
769
|
+
return out;
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Looks up one Set element or Dict value by key (Set/Dict roots only):
|
|
773
|
+
* binary-searches the verified segment fences for the only segment whose
|
|
774
|
+
* range can hold the key, decodes it, and scans for an East-equal match.
|
|
775
|
+
*
|
|
776
|
+
* @param key - the Set element or Dict key to look up
|
|
777
|
+
* @returns the Dict value (or the stored Set element) for `key`, or
|
|
778
|
+
* `undefined` when the collection does not contain it
|
|
779
|
+
* @throws {Error} When the root is an Array, the blob is not
|
|
780
|
+
* self-contained, or the blob violates the canonical-order contract.
|
|
781
|
+
*/
|
|
782
|
+
get(key) {
|
|
783
|
+
if (this.kind === "Array") {
|
|
784
|
+
throw new Error(`beast2 v5: get() addresses Set and Dict roots; this blob holds Array — use element() or slice()`);
|
|
785
|
+
}
|
|
786
|
+
if (this.elementCount === 0)
|
|
787
|
+
return undefined;
|
|
788
|
+
const fences = this.verifyFences();
|
|
789
|
+
// Greatest segment whose fence is <= key; a key below every fence is
|
|
790
|
+
// below the collection's minimum.
|
|
791
|
+
let lo = 0, hi = fences.length - 1;
|
|
792
|
+
if (this.orderCmp(key, fences[0]) < 0)
|
|
793
|
+
return undefined;
|
|
421
794
|
while (lo < hi) {
|
|
422
|
-
const mid = (lo + hi) >> 1;
|
|
423
|
-
if (this.
|
|
424
|
-
lo = mid
|
|
795
|
+
const mid = (lo + hi + 1) >> 1;
|
|
796
|
+
if (this.orderCmp(fences[mid], key) <= 0)
|
|
797
|
+
lo = mid;
|
|
425
798
|
else
|
|
426
|
-
hi = mid;
|
|
799
|
+
hi = mid - 1;
|
|
800
|
+
}
|
|
801
|
+
const order = { prev: undefined, has: false };
|
|
802
|
+
const segment = this.decodeDisjoint(lo, order, fences);
|
|
803
|
+
if (this.kind === "Set") {
|
|
804
|
+
for (const item of segment) {
|
|
805
|
+
if (this.orderCmp(item, key) === 0)
|
|
806
|
+
return item;
|
|
807
|
+
}
|
|
808
|
+
return undefined;
|
|
809
|
+
}
|
|
810
|
+
for (const [k, v] of segment.entries()) {
|
|
811
|
+
if (this.orderCmp(k, key) === 0)
|
|
812
|
+
return v;
|
|
427
813
|
}
|
|
428
|
-
|
|
429
|
-
const seg = this.segment(lo);
|
|
430
|
-
return seg[row - base];
|
|
814
|
+
return undefined;
|
|
431
815
|
}
|
|
432
816
|
}
|
|
433
817
|
/**
|