@cloudbitmaps/core 0.13.0 → 0.14.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.
@@ -52,6 +52,17 @@ export interface CodecBitmap {
52
52
  /** Ascending iterator over the set values. */
53
53
  [Symbol.iterator](): IterableIterator<number>;
54
54
  toArray(): number[];
55
+ /**
56
+ * The set's values as one ascending `Uint32Array` the caller owns: what a batch read converts a chunk with, in one
57
+ * native step instead of one iterator step per value.
58
+ *
59
+ * **Optional.** A codec without it is read through its iterator, which is correct and slower.
60
+ *
61
+ * **It must return a NEW array on every call, which the caller owns:** the engine adds the chunk's base to each
62
+ * value in place and hands the array on, so an array that is shared, cached or a view of the bitmap's own storage
63
+ * would be corrupted, and would corrupt the bitmap. It holds exactly what the iterator yields, ascending.
64
+ */
65
+ toUint32Array?(): Uint32Array;
55
66
  /**
56
67
  * The largest value in the set, or `undefined` when empty.
57
68
  *
@@ -128,6 +128,13 @@ export declare class SegmentEngine {
128
128
  iterate(seg: SegmentRef, range?: IdRange): AsyncGenerator<number>;
129
129
  private iterateAll;
130
130
  private iterateRange;
131
+ /**
132
+ * {@link iterate}, one `Uint32Array` per chunk instead of one id at a time: the same chunks, in the same order, with
133
+ * the same ids (a range cut at its edges the same way), and no array for a chunk that holds none of them. The
134
+ * reads, the budget charge and the window are the per-id read's. Each array is at most 65,536 ids (256 KiB) and is
135
+ * the caller's to keep.
136
+ */
137
+ iterateBatches(seg: SegmentRef, range?: IdRange): AsyncGenerator<Uint32Array>;
131
138
  /**
132
139
  * `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
133
140
  *
@@ -176,7 +183,20 @@ export declare class SegmentEngine {
176
183
  * the cost scales with the segment being filtered rather than with the size of the suppression list.
177
184
  */
178
185
  andNot(seg: SegmentRef, excludes: readonly SegmentRef[], options?: Omit<CombineOptions, 'exclude'>): AsyncGenerator<number>;
186
+ /** {@link intersect}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
187
+ intersectBatches(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<Uint32Array>;
188
+ /** {@link union}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
189
+ unionBatches(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<Uint32Array>;
190
+ /** {@link andNot}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
191
+ andNotBatches(seg: SegmentRef, excludes: readonly SegmentRef[], options?: Omit<CombineOptions, 'exclude'>): AsyncGenerator<Uint32Array>;
192
+ /**
193
+ * Everything a combine decides before it reads a chunk: validation, each operand's keys and generation, the
194
+ * absent-operand refusal, key alignment, the budget and the metric. `null` when the range is empty. The per-id
195
+ * read and the batch read share it, so the two cannot disagree about what is read or charged.
196
+ */
197
+ private combinePlan;
179
198
  private combine;
199
+ private combineBatches;
180
200
  /**
181
201
  * The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
182
202
  * Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
package/dist/index.js CHANGED
@@ -372,6 +372,65 @@ function keysWithin(keys, w) {
372
372
  };
373
373
  return keys.slice(firstAtLeast(w.loKey), firstAtLeast(w.hiKey + 1));
374
374
  }
375
+ var CombineWindow = class {
376
+ constructor(keys, limit, read) {
377
+ this.keys = keys;
378
+ this.limit = limit;
379
+ this.read = read;
380
+ this.fill();
381
+ }
382
+ keys;
383
+ limit;
384
+ read;
385
+ inFlight = [];
386
+ next_ = 0;
387
+ taken = 0;
388
+ fill() {
389
+ const width = Math.min(
390
+ this.limit,
391
+ Math.max(COMBINE_WINDOW_START, 2 ** Math.min(this.taken, 30))
392
+ );
393
+ while (this.next_ < this.keys.length && this.inFlight.length < width) {
394
+ const key = this.keys[this.next_++];
395
+ this.inFlight.push(
396
+ this.read(key).then(
397
+ (result) => ({ key, result }),
398
+ (error) => ({ key, result: null, error })
399
+ )
400
+ );
401
+ }
402
+ }
403
+ /** The next key's chunk in ascending key order, `undefined` when the keys are used up; throws that key's error. */
404
+ async next() {
405
+ if (this.inFlight.length === 0) return void 0;
406
+ const slot = await this.inFlight.shift();
407
+ this.taken += 1;
408
+ this.fill();
409
+ if (slot.error !== void 0) throw slot.error;
410
+ return slot;
411
+ }
412
+ };
413
+ function chunkIds(chunk, chunkKey, w) {
414
+ let rem = chunk.toUint32Array ? chunk.toUint32Array() : Uint32Array.from(chunk);
415
+ if (w !== null && isEdge(chunkKey, w)) {
416
+ const from = chunkKey === w.loKey ? w.loRem : 0;
417
+ const to = chunkKey === w.hiKey ? w.hiRem : MAX_REMAINDER;
418
+ rem = rem.slice(firstAbove(rem, from - 1), firstAbove(rem, to));
419
+ }
420
+ const base = chunkKey * CHUNK_COUNT;
421
+ for (let i = 0; i < rem.length; i++) rem[i] = base + (rem[i] & MAX_REMAINDER);
422
+ return rem;
423
+ }
424
+ function firstAbove(a, value) {
425
+ let lo = 0;
426
+ let hi = a.length;
427
+ while (lo < hi) {
428
+ const mid = lo + hi >>> 1;
429
+ if (a[mid] <= value) lo = mid + 1;
430
+ else hi = mid;
431
+ }
432
+ return lo;
433
+ }
375
434
  var SegmentEngine = class {
376
435
  storage;
377
436
  cache;
@@ -507,6 +566,26 @@ var SegmentEngine = class {
507
566
  }
508
567
  }
509
568
  }
569
+ /**
570
+ * {@link iterate}, one `Uint32Array` per chunk instead of one id at a time: the same chunks, in the same order, with
571
+ * the same ids (a range cut at its edges the same way), and no array for a chunk that holds none of them. The
572
+ * reads, the budget charge and the window are the per-id read's. Each array is at most 65,536 ids (256 KiB) and is
573
+ * the caller's to keep.
574
+ */
575
+ async *iterateBatches(seg, range) {
576
+ const w = windowOf(range);
577
+ if (w === "empty") return;
578
+ const chunkKeys = w === null ? await this.chunkKeys(seg) : keysWithin(await this.chunkKeys(seg), w);
579
+ checkBudget(this.budget, chunkKeys.length, "iterate");
580
+ const gen = await this.cacheVersion(seg);
581
+ const window = this.chunkWindow(seg, chunkKeys, gen, true);
582
+ for (const chunkKey of chunkKeys) {
583
+ const chunk = await window.take();
584
+ if (chunk === null) continue;
585
+ const ids = chunkIds(chunk, chunkKey, w);
586
+ if (ids.length > 0) yield ids;
587
+ }
588
+ }
510
589
  /**
511
590
  * `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
512
591
  *
@@ -561,7 +640,24 @@ var SegmentEngine = class {
561
640
  andNot(seg, excludes, options) {
562
641
  return this.combine([seg], excludes, "all", "andNot", options);
563
642
  }
564
- async *combine(segs, excludeSegs, mode, op, options) {
643
+ /** {@link intersect}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
644
+ intersectBatches(segs, options) {
645
+ return this.combineBatches(segs, options?.exclude ?? [], "all", "intersect", options);
646
+ }
647
+ /** {@link union}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
648
+ unionBatches(segs, options) {
649
+ return this.combineBatches(segs, options?.exclude ?? [], "any", "union", options);
650
+ }
651
+ /** {@link andNot}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
652
+ andNotBatches(seg, excludes, options) {
653
+ return this.combineBatches([seg], excludes, "all", "andNot", options);
654
+ }
655
+ /**
656
+ * Everything a combine decides before it reads a chunk: validation, each operand's keys and generation, the
657
+ * absent-operand refusal, key alignment, the budget and the metric. `null` when the range is empty. The per-id
658
+ * read and the batch read share it, so the two cannot disagree about what is read or charged.
659
+ */
660
+ async combinePlan(segs, excludeSegs, mode, op, options) {
565
661
  if (segs.length === 0) {
566
662
  throw new ValidationError(`${op} requires at least one segment`);
567
663
  }
@@ -576,7 +672,7 @@ var SegmentEngine = class {
576
672
  }
577
673
  const budget = resolvePerOpBudget(options?.budget, this.budget);
578
674
  const w = windowOf(options);
579
- if (w === "empty") return;
675
+ if (w === "empty") return null;
580
676
  const extract = async (seg) => {
581
677
  const all = await this.chunkKeys(seg);
582
678
  const gen = await this.cacheVersion(seg);
@@ -621,24 +717,20 @@ var SegmentEngine = class {
621
717
  skippedChunks: distinctKeys.size - common.length
622
718
  });
623
719
  }
624
- const startAt = (key) => this.combineChunk(operands, excludes, mode, key).then(
625
- (result) => ({ key, result }),
626
- (error) => ({ key, result: null, error })
627
- );
628
- const inFlight = [];
629
- let next = 0;
630
- let taken = 0;
631
- const fill = () => {
632
- const width = Math.min(limit, Math.max(COMBINE_WINDOW_START, 2 ** Math.min(taken, 30)));
633
- while (next < common.length && inFlight.length < width)
634
- inFlight.push(startAt(common[next++]));
720
+ return {
721
+ w,
722
+ window: new CombineWindow(
723
+ common,
724
+ limit,
725
+ (key) => this.combineChunk(operands, excludes, mode, key)
726
+ )
635
727
  };
636
- fill();
637
- while (inFlight.length > 0) {
638
- const slot = await inFlight.shift();
639
- taken += 1;
640
- fill();
641
- if (slot.error !== void 0) throw slot.error;
728
+ }
729
+ async *combine(segs, excludeSegs, mode, op, options) {
730
+ const plan = await this.combinePlan(segs, excludeSegs, mode, op, options);
731
+ if (plan === null) return;
732
+ const { w, window } = plan;
733
+ for (let slot = await window.next(); slot !== void 0; slot = await window.next()) {
642
734
  if (slot.result && !slot.result.isEmpty) {
643
735
  if (w !== null && isEdge(slot.key, w)) {
644
736
  for (const id of edgeIds(slot.result, slot.key, w)) yield id;
@@ -648,6 +740,17 @@ var SegmentEngine = class {
648
740
  }
649
741
  }
650
742
  }
743
+ async *combineBatches(segs, excludeSegs, mode, op, options) {
744
+ const plan = await this.combinePlan(segs, excludeSegs, mode, op, options);
745
+ if (plan === null) return;
746
+ const { w, window } = plan;
747
+ for (let slot = await window.next(); slot !== void 0; slot = await window.next()) {
748
+ if (slot.result) {
749
+ const ids = chunkIds(slot.result, slot.key, w);
750
+ if (ids.length > 0) yield ids;
751
+ }
752
+ }
753
+ }
651
754
  /**
652
755
  * The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
653
756
  * Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).