@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.
Files changed (27) hide show
  1. package/dist/src/serialization/beast2/index.d.ts +5 -3
  2. package/dist/src/serialization/beast2/index.d.ts.map +1 -1
  3. package/dist/src/serialization/beast2/index.js +1 -1
  4. package/dist/src/serialization/beast2/index.js.map +1 -1
  5. package/dist/src/serialization/beast2/v5/codec.d.ts +8 -1
  6. package/dist/src/serialization/beast2/v5/codec.d.ts.map +1 -1
  7. package/dist/src/serialization/beast2/v5/codec.js +67 -15
  8. package/dist/src/serialization/beast2/v5/codec.js.map +1 -1
  9. package/dist/src/serialization/beast2/v5/frames.d.ts +13 -8
  10. package/dist/src/serialization/beast2/v5/frames.d.ts.map +1 -1
  11. package/dist/src/serialization/beast2/v5/frames.js +16 -10
  12. package/dist/src/serialization/beast2/v5/frames.js.map +1 -1
  13. package/dist/src/serialization/beast2/v5/index.spec.js +300 -24
  14. package/dist/src/serialization/beast2/v5/index.spec.js.map +1 -1
  15. package/dist/src/serialization/beast2/v5/inflate.d.ts +19 -0
  16. package/dist/src/serialization/beast2/v5/inflate.d.ts.map +1 -0
  17. package/dist/src/serialization/beast2/v5/inflate.js +273 -0
  18. package/dist/src/serialization/beast2/v5/inflate.js.map +1 -0
  19. package/dist/src/serialization/beast2/v5/stream.d.ts +126 -5
  20. package/dist/src/serialization/beast2/v5/stream.d.ts.map +1 -1
  21. package/dist/src/serialization/beast2/v5/stream.js +405 -21
  22. package/dist/src/serialization/beast2/v5/stream.js.map +1 -1
  23. package/dist/src/serialization/index.d.ts +1 -1
  24. package/dist/src/serialization/index.d.ts.map +1 -1
  25. package/dist/src/serialization/index.js +1 -1
  26. package/dist/src/serialization/index.js.map +1 -1
  27. 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
- set.add(elem(reader, ctx));
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. For Array roots this is the exact element
336
- * count; for Set/Dict roots it is an upper bound (cross-segment duplicate
337
- * keys collapse on merge). */
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) or `i` is out of range.
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
- // Binary search the cumulative counts for the owning segment.
420
- let lo = 0, hi = this.cumulative.length - 1;
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.cumulative[mid] <= row)
424
- lo = mid + 1;
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
- const base = lo === 0 ? 0 : this.cumulative[lo - 1];
429
- const seg = this.segment(lo);
430
- return seg[row - base];
814
+ return undefined;
431
815
  }
432
816
  }
433
817
  /**