@cloudbitmaps/core 0.3.0 → 0.4.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/dist/index.cjs +244 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +168 -9
- package/dist/index.d.ts +168 -9
- package/dist/index.js +244 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -91,6 +91,14 @@ function applyRemove(delta, remainder) {
|
|
|
91
91
|
delta.removes.add(remainder);
|
|
92
92
|
delta.adds.remove(remainder);
|
|
93
93
|
}
|
|
94
|
+
function applyAddAll(delta, ids) {
|
|
95
|
+
delta.adds.orInPlace(ids);
|
|
96
|
+
delta.removes.andNotInPlace(ids);
|
|
97
|
+
}
|
|
98
|
+
function applyRemoveAll(delta, ids) {
|
|
99
|
+
delta.removes.orInPlace(ids);
|
|
100
|
+
delta.adds.andNotInPlace(ids);
|
|
101
|
+
}
|
|
94
102
|
function effective(cold, delta) {
|
|
95
103
|
const out = cold.clone();
|
|
96
104
|
out.orInPlace(delta.adds);
|
|
@@ -214,6 +222,19 @@ async function mapWithConcurrency(items, limit, fn) {
|
|
|
214
222
|
return results;
|
|
215
223
|
}
|
|
216
224
|
|
|
225
|
+
// src/core/cooperative.ts
|
|
226
|
+
var YIELD_EVERY = 1024;
|
|
227
|
+
function yieldEvery(clock, everyN = YIELD_EVERY) {
|
|
228
|
+
if (clock === void 0) return () => null;
|
|
229
|
+
const pause = clock.yieldNow !== void 0 ? () => clock.yieldNow() : () => clock.sleep(1);
|
|
230
|
+
let n = 0;
|
|
231
|
+
return () => {
|
|
232
|
+
if (++n < everyN) return null;
|
|
233
|
+
n = 0;
|
|
234
|
+
return pause();
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
217
238
|
// src/core/keys.ts
|
|
218
239
|
var FIELD = " ";
|
|
219
240
|
var NO_NAMESPACE = "/";
|
|
@@ -396,6 +417,8 @@ async function withRetry(op, policy, deps) {
|
|
|
396
417
|
var DEFAULT_MAX_BITMAP_BYTES = 1 << 20;
|
|
397
418
|
var DEFAULT_MAX_RETRIES = 16;
|
|
398
419
|
var DEFAULT_INTERSECT_CONCURRENCY = 8;
|
|
420
|
+
var MUTATE_STAGE_IDS = 1 << 20;
|
|
421
|
+
var YIELD_EVERY_IDS = 1 << 14;
|
|
399
422
|
var InstantClock = class {
|
|
400
423
|
now() {
|
|
401
424
|
return 0;
|
|
@@ -410,6 +433,7 @@ var ZeroRng = class {
|
|
|
410
433
|
}
|
|
411
434
|
};
|
|
412
435
|
var DEFAULT_MAX_WARM_SCAN_BYTES = 64 * 1024 * 1024;
|
|
436
|
+
var DEFAULT_WRITE_CONCURRENCY = 4;
|
|
413
437
|
var SegmentEngine = class {
|
|
414
438
|
warm;
|
|
415
439
|
cold;
|
|
@@ -439,7 +463,7 @@ var SegmentEngine = class {
|
|
|
439
463
|
this.rng = deps.rng ?? new ZeroRng();
|
|
440
464
|
this.occBackoff = deps.occBackoff ?? DEFAULT_OCC_BACKOFF;
|
|
441
465
|
this.readOpts = { consistent: (deps.warmReadConsistency ?? "strong") !== "eventual" };
|
|
442
|
-
this.writeConcurrency = deps.writeConcurrency ??
|
|
466
|
+
this.writeConcurrency = deps.writeConcurrency ?? DEFAULT_WRITE_CONCURRENCY;
|
|
443
467
|
this.maxWarmScanBytes = deps.maxWarmScanBytes ?? DEFAULT_MAX_WARM_SCAN_BYTES;
|
|
444
468
|
if (!Number.isFinite(this.maxWarmScanBytes) || this.maxWarmScanBytes < 1) {
|
|
445
469
|
throw new ValidationError(
|
|
@@ -459,10 +483,10 @@ var SegmentEngine = class {
|
|
|
459
483
|
return this.readModifyWrite(seg, chunkKey, (d) => applyRemove(d, remainder));
|
|
460
484
|
}
|
|
461
485
|
addMany(seg, ids) {
|
|
462
|
-
return this.mutateMany(seg, ids,
|
|
486
|
+
return this.mutateMany(seg, ids, applyAddAll);
|
|
463
487
|
}
|
|
464
488
|
removeMany(seg, ids) {
|
|
465
|
-
return this.mutateMany(seg, ids,
|
|
489
|
+
return this.mutateMany(seg, ids, applyRemoveAll);
|
|
466
490
|
}
|
|
467
491
|
async has(seg, id, opts) {
|
|
468
492
|
const { chunkKey, remainder } = splitId(id);
|
|
@@ -543,9 +567,44 @@ var SegmentEngine = class {
|
|
|
543
567
|
* re-resolves fresh (bypassing the TTL), which can hop generations even sub-TTL — still whole/immutable per
|
|
544
568
|
* read, never torn. Full intra-op snapshot isolation is deferred (gap #4).
|
|
545
569
|
*/
|
|
546
|
-
|
|
570
|
+
/**
|
|
571
|
+
* `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
|
|
572
|
+
*
|
|
573
|
+
* The `exclude` operands are what make suppression **compose without materialising**. Applying suppression
|
|
574
|
+
* afterwards (intersect into a temp segment, then remove) writes an intermediate segment that nobody wants;
|
|
575
|
+
* applying it here folds it into the same chunk-aligned pass. It is also cheaper than it looks: an exclude
|
|
576
|
+
* operand is read **only at the keys that survived the intersection**, so a 61,000-chunk global opt-out list
|
|
577
|
+
* intersected against a narrow audience costs a handful of reads, not 61,000.
|
|
578
|
+
*/
|
|
579
|
+
intersect(segs, options) {
|
|
580
|
+
return this.combine(segs, options?.exclude ?? [], "all", "intersect", options);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* `segs[0] ∪ segs[1] ∪ …`, minus every id in `options.exclude` — streamed ascending.
|
|
584
|
+
*
|
|
585
|
+
* **Union is the one composite read with no chunk-skipping**, and that is a property of the operation rather
|
|
586
|
+
* than of this implementation: an id in *any* operand belongs to the result, so every chunk of every operand
|
|
587
|
+
* has to be read. `intersect` can prune a key absent from any operand; union cannot prune anything. It is
|
|
588
|
+
* budgeted exactly like `intersect` so a wide union is refused rather than quietly billed.
|
|
589
|
+
*/
|
|
590
|
+
union(segs, options) {
|
|
591
|
+
return this.combine(segs, options?.exclude ?? [], "any", "union", options);
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* `seg \ (excludes[0] ∪ excludes[1] ∪ …)` — streamed ascending. Suppression on its own.
|
|
595
|
+
*
|
|
596
|
+
* Reads every chunk of `seg` (any of them may survive) but each exclude **only where it overlaps `seg`**, so
|
|
597
|
+
* the cost scales with the segment being filtered rather than with the size of the suppression list.
|
|
598
|
+
*/
|
|
599
|
+
andNot(seg, excludes, options) {
|
|
600
|
+
return this.combine([seg], excludes, "all", "andNot", options);
|
|
601
|
+
}
|
|
602
|
+
async *combine(segs, excludeSegs, mode, op, options) {
|
|
547
603
|
if (segs.length === 0) {
|
|
548
|
-
throw new ValidationError(
|
|
604
|
+
throw new ValidationError(`${op} requires at least one segment`);
|
|
605
|
+
}
|
|
606
|
+
if (op === "andNot" && excludeSegs.length === 0) {
|
|
607
|
+
throw new ValidationError("andNot requires at least one segment to subtract");
|
|
549
608
|
}
|
|
550
609
|
const limit = options?.concurrency ?? DEFAULT_INTERSECT_CONCURRENCY;
|
|
551
610
|
if (!Number.isInteger(limit) || limit < 1) {
|
|
@@ -554,34 +613,52 @@ var SegmentEngine = class {
|
|
|
554
613
|
);
|
|
555
614
|
}
|
|
556
615
|
const budget = resolvePerOpBudget(options?.budget, this.budget);
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
616
|
+
const extract = async (seg) => {
|
|
617
|
+
const warmRows = await this.collectWarm(seg, op, null);
|
|
618
|
+
const keys = await this.chunkKeys(seg, warmRows);
|
|
619
|
+
const gen = await this.currentGen(seg);
|
|
620
|
+
return { seg, warmRows, keys: new Set(keys), gen };
|
|
621
|
+
};
|
|
622
|
+
const [operands, excludes] = await Promise.all([
|
|
623
|
+
Promise.all(segs.map(extract)),
|
|
624
|
+
Promise.all(excludeSegs.map(extract))
|
|
625
|
+
]);
|
|
626
|
+
const candidates = [];
|
|
627
|
+
if (mode === "all") {
|
|
628
|
+
const pivot = operands.reduce((a, b) => a.keys.size <= b.keys.size ? a : b);
|
|
629
|
+
for (const k of pivot.keys) {
|
|
630
|
+
if (operands.every((o) => o.keys.has(k))) candidates.push(k);
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
const seen = /* @__PURE__ */ new Set();
|
|
634
|
+
for (const o of operands) for (const k of o.keys) seen.add(k);
|
|
635
|
+
for (const k of seen) candidates.push(k);
|
|
636
|
+
}
|
|
637
|
+
candidates.sort((a, b) => a - b);
|
|
638
|
+
const common = candidates;
|
|
639
|
+
let units = 0;
|
|
640
|
+
for (const key of common) {
|
|
641
|
+
for (const o of operands) if (mode === "all" || o.keys.has(key)) units += 1;
|
|
642
|
+
for (const e of excludes) if (e.keys.has(key)) units += 1;
|
|
643
|
+
}
|
|
644
|
+
checkBudget(budget, units, op);
|
|
572
645
|
if (this.metricsOn) {
|
|
573
646
|
const distinctKeys = /* @__PURE__ */ new Set();
|
|
574
|
-
for (const
|
|
575
|
-
for (const k of
|
|
647
|
+
for (const op2 of operands) {
|
|
648
|
+
for (const k of op2.keys) distinctKeys.add(k);
|
|
576
649
|
}
|
|
650
|
+
for (const e of excludes) for (const k of e.keys) distinctKeys.add(k);
|
|
577
651
|
this.metrics.onEvent({
|
|
578
652
|
kind: "intersect",
|
|
579
|
-
|
|
653
|
+
op,
|
|
654
|
+
operands: segs.length + excludes.length,
|
|
580
655
|
fetchedChunks: common.length,
|
|
656
|
+
// Never negative: under 'any' every candidate key came from some operand, so `distinctKeys` is a
|
|
657
|
+
// superset of `common`; excludes only ever add keys here.
|
|
581
658
|
skippedChunks: distinctKeys.size - common.length
|
|
582
659
|
});
|
|
583
660
|
}
|
|
584
|
-
const startAt = (key) => this.
|
|
661
|
+
const startAt = (key) => this.combineChunk(operands, excludes, mode, key).then(
|
|
585
662
|
(result) => ({ key, result }),
|
|
586
663
|
(error) => ({ key, result: null, error })
|
|
587
664
|
);
|
|
@@ -603,16 +680,33 @@ var SegmentEngine = class {
|
|
|
603
680
|
* first operand's effective chunk and mutates it in place — safe because `effectiveChunk` always returns
|
|
604
681
|
* a FRESH bitmap (never a cached/shared Cold instance), so this can't poison the HOT cache.
|
|
605
682
|
*/
|
|
606
|
-
async
|
|
683
|
+
async combineChunk(operands, excludes, mode, chunkKey) {
|
|
684
|
+
const present = mode === "all" ? operands : operands.filter((o) => o.keys.has(chunkKey));
|
|
685
|
+
if (present.length === 0) return null;
|
|
607
686
|
const chunks = await Promise.all(
|
|
608
|
-
|
|
687
|
+
present.map((op) => this.effectiveChunk(op.seg, chunkKey, op.warmRows, op.gen))
|
|
609
688
|
);
|
|
610
689
|
const acc = chunks[0];
|
|
611
690
|
for (let i = 1; i < chunks.length; i++) {
|
|
612
|
-
|
|
613
|
-
|
|
691
|
+
if (mode === "all") {
|
|
692
|
+
acc.andInPlace(chunks[i]);
|
|
693
|
+
if (acc.isEmpty) return null;
|
|
694
|
+
} else {
|
|
695
|
+
acc.orInPlace(chunks[i]);
|
|
696
|
+
}
|
|
614
697
|
}
|
|
615
|
-
|
|
698
|
+
if (acc.isEmpty) return null;
|
|
699
|
+
const relevant = excludes.filter((e) => e.keys.has(chunkKey));
|
|
700
|
+
if (relevant.length > 0) {
|
|
701
|
+
const cuts = await Promise.all(
|
|
702
|
+
relevant.map((e) => this.effectiveChunk(e.seg, chunkKey, e.warmRows, e.gen))
|
|
703
|
+
);
|
|
704
|
+
for (const cut of cuts) {
|
|
705
|
+
acc.andNotInPlace(cut);
|
|
706
|
+
if (acc.isEmpty) return null;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return acc;
|
|
616
710
|
}
|
|
617
711
|
/**
|
|
618
712
|
* Materialize `seg[0] ∩ seg[1] ∩ …` into `dest` via batched `addMany` (streaming, bounded memory).
|
|
@@ -620,9 +714,30 @@ var SegmentEngine = class {
|
|
|
620
714
|
* `dest` is added to, not replaced.
|
|
621
715
|
*/
|
|
622
716
|
async intersectInto(dest, segs, options) {
|
|
623
|
-
|
|
717
|
+
await this.drainInto(dest, this.intersect(segs, options), options?.batchSize);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Materialize `segs[0] ∪ segs[1] ∪ …` (minus `options.exclude`) into `dest`. **Not atomic**, like
|
|
721
|
+
* {@link intersectInto}. See {@link union} for why this one cannot skip any chunk.
|
|
722
|
+
*/
|
|
723
|
+
async unionInto(dest, segs, options) {
|
|
724
|
+
await this.drainInto(dest, this.union(segs, options), options?.batchSize);
|
|
725
|
+
}
|
|
726
|
+
/** Materialize `seg \ (excludes…)` into `dest`. **Not atomic**, like {@link intersectInto}. */
|
|
727
|
+
async andNotInto(dest, seg, excludes, options) {
|
|
728
|
+
await this.drainInto(dest, this.andNot(seg, excludes, options), options?.batchSize);
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Stream a combine's results into `dest` in bounded batches.
|
|
732
|
+
*
|
|
733
|
+
* `dest` is **added to, not replaced**, and this is **not atomic** — ids land chunk-by-chunk, so a failure
|
|
734
|
+
* mid-stream leaves a partial result, exactly as `addMany` does. Batching keeps memory bounded no matter how
|
|
735
|
+
* large the result is.
|
|
736
|
+
*/
|
|
737
|
+
async drainInto(dest, source, batchSizeOption) {
|
|
738
|
+
const batchSize = Math.max(1, batchSizeOption ?? 4096);
|
|
624
739
|
let batch = [];
|
|
625
|
-
for await (const id of
|
|
740
|
+
for await (const id of source) {
|
|
626
741
|
batch.push(id);
|
|
627
742
|
if (batch.length >= batchSize) {
|
|
628
743
|
await this.addMany(dest, batch);
|
|
@@ -631,19 +746,77 @@ var SegmentEngine = class {
|
|
|
631
746
|
}
|
|
632
747
|
if (batch.length > 0) await this.addMany(dest, batch);
|
|
633
748
|
}
|
|
749
|
+
/**
|
|
750
|
+
* Group ids by chunk, then apply each chunk's whole batch in a single OCC read-modify-write.
|
|
751
|
+
*
|
|
752
|
+
* Accepts a **sync or async** iterable, so an Athena/BigQuery cursor streams straight in without the caller
|
|
753
|
+
* hand-batching `page → addMany(page)`.
|
|
754
|
+
*
|
|
755
|
+
* **The pending ids are held as BITMAPS, not as JS numbers, and that is the load-bearing choice.** Streaming
|
|
756
|
+
* input is unbounded by definition, so something has to give. The obvious answer — buffer N ids, flush them to
|
|
757
|
+
* the backend, repeat — bounds memory and is quietly catastrophic on cost: with ids arriving in arbitrary
|
|
758
|
+
* order, every flush touches nearly every chunk again, so an 11M-id stream at a 1M-id buffer would issue
|
|
759
|
+
* **11x** the OCC round-trips of a single pass. In a library whose headline claim is honest cost, silently
|
|
760
|
+
* multiplying someone's write bill by the size of their input is not a bounded-memory strategy; it is a
|
|
761
|
+
* different bug.
|
|
762
|
+
*
|
|
763
|
+
* So the staging buffer is capped, but it folds into per-chunk **compressed bitmaps** rather than being
|
|
764
|
+
* written out. Peak memory is the roaring representation of the ids seen so far — which is what the segment
|
|
765
|
+
* itself costs, and the floor for any implementation that writes each chunk exactly once — while the backend
|
|
766
|
+
* still sees exactly one read-modify-write per distinct chunk regardless of stream length. This is strictly
|
|
767
|
+
* less memory than the previous sync-only version, which held every remainder as an uncompressed JS number.
|
|
768
|
+
*
|
|
769
|
+
* There is no way to do better without a sorted stream: a chunk can only be written once it is known that no
|
|
770
|
+
* further ids for it will arrive, and unsorted input never gives that guarantee. Callers streaming *very*
|
|
771
|
+
* large sets want `bulkLoadCrbmGeneration` instead — it writes one immutable Cold object rather than tens of
|
|
772
|
+
* thousands of warm rows. The crossover is documented in the guide.
|
|
773
|
+
*/
|
|
634
774
|
async mutateMany(seg, ids, apply) {
|
|
635
775
|
const byChunk = /* @__PURE__ */ new Map();
|
|
636
|
-
|
|
776
|
+
const staged = /* @__PURE__ */ new Map();
|
|
777
|
+
let pending = 0;
|
|
778
|
+
const tickId = yieldEvery(this.clock, YIELD_EVERY_IDS);
|
|
779
|
+
const compact = () => {
|
|
780
|
+
for (const [chunkKey, rems] of staged) {
|
|
781
|
+
if (rems.length === 0) continue;
|
|
782
|
+
const existing = byChunk.get(chunkKey);
|
|
783
|
+
if (existing === void 0) byChunk.set(chunkKey, this.codec.fromValues(rems));
|
|
784
|
+
else existing.addMany(rems);
|
|
785
|
+
rems.length = 0;
|
|
786
|
+
}
|
|
787
|
+
pending = 0;
|
|
788
|
+
};
|
|
789
|
+
const stage = (id) => {
|
|
637
790
|
const { chunkKey, remainder } = splitId(id);
|
|
638
|
-
|
|
791
|
+
let rems = staged.get(chunkKey);
|
|
792
|
+
if (rems === void 0) {
|
|
793
|
+
rems = [];
|
|
794
|
+
staged.set(chunkKey, rems);
|
|
795
|
+
}
|
|
639
796
|
rems.push(remainder);
|
|
640
|
-
|
|
797
|
+
pending += 1;
|
|
798
|
+
};
|
|
799
|
+
if (Symbol.asyncIterator in ids) {
|
|
800
|
+
for await (const id of ids) {
|
|
801
|
+
stage(id);
|
|
802
|
+
if (pending >= MUTATE_STAGE_IDS) compact();
|
|
803
|
+
const pause = tickId();
|
|
804
|
+
if (pause !== null) await pause;
|
|
805
|
+
}
|
|
806
|
+
} else {
|
|
807
|
+
for (const id of ids) {
|
|
808
|
+
stage(id);
|
|
809
|
+
if (pending >= MUTATE_STAGE_IDS) compact();
|
|
810
|
+
const pause = tickId();
|
|
811
|
+
if (pause !== null) await pause;
|
|
812
|
+
}
|
|
641
813
|
}
|
|
814
|
+
compact();
|
|
642
815
|
await mapWithConcurrency(
|
|
643
816
|
[...byChunk],
|
|
644
817
|
this.writeConcurrency,
|
|
645
|
-
([chunkKey,
|
|
646
|
-
|
|
818
|
+
([chunkKey, bitmap]) => this.readModifyWrite(seg, chunkKey, (d) => {
|
|
819
|
+
apply(d, bitmap);
|
|
647
820
|
})
|
|
648
821
|
);
|
|
649
822
|
}
|
|
@@ -1772,6 +1945,7 @@ function coldBlobReader(driver, key) {
|
|
|
1772
1945
|
}
|
|
1773
1946
|
var DEFAULT_CURRENT_GEN_TTL_MS = 2e3;
|
|
1774
1947
|
var BULK_FLUSH_IDS = 1 << 20;
|
|
1948
|
+
var YIELD_EVERY_IDS2 = 1 << 14;
|
|
1775
1949
|
var DEFAULT_MAX_OPEN_SEGMENTS = 1024;
|
|
1776
1950
|
var DEFAULT_MAX_OPEN_INDEX_BYTES = 64 * 1024 * 1024;
|
|
1777
1951
|
var CrbmColdChunkSource = class {
|
|
@@ -1990,11 +2164,14 @@ var CrbmColdChunkSource = class {
|
|
|
1990
2164
|
};
|
|
1991
2165
|
function writeCrbmGeneration(driver, key, chunks, options = {}) {
|
|
1992
2166
|
const sorted = [...chunks].sort((a, b) => a.chunkKey - b.chunkKey);
|
|
2167
|
+
const tick = yieldEvery(options.clock);
|
|
1993
2168
|
return driver.putImmutable(key, async (sink) => {
|
|
1994
2169
|
const writer = new CrbmWriter(sink, { generation: key.generation, crypto: options.crypto });
|
|
1995
2170
|
for (const { chunkKey, bitmap } of sorted) {
|
|
1996
2171
|
if (bitmap.isEmpty) continue;
|
|
1997
2172
|
await writer.addChunk(chunkKey, bitmap.serialize(), bitmap.size);
|
|
2173
|
+
const pause = tick();
|
|
2174
|
+
if (pause !== null) await pause;
|
|
1998
2175
|
}
|
|
1999
2176
|
await writer.finish();
|
|
2000
2177
|
});
|
|
@@ -2002,6 +2179,7 @@ function writeCrbmGeneration(driver, key, chunks, options = {}) {
|
|
|
2002
2179
|
async function writeCrbmGenerationStream(driver, key, chunks, options = {}) {
|
|
2003
2180
|
const chunkKeys = [];
|
|
2004
2181
|
let cardinality = 0;
|
|
2182
|
+
const tick = yieldEvery(options.clock);
|
|
2005
2183
|
const { size, sha256 } = await driver.putImmutable(key, async (sink) => {
|
|
2006
2184
|
const writer = new CrbmWriter(sink, { generation: key.generation, crypto: options.crypto });
|
|
2007
2185
|
for await (const { chunkKey, bitmap } of chunks) {
|
|
@@ -2009,6 +2187,8 @@ async function writeCrbmGenerationStream(driver, key, chunks, options = {}) {
|
|
|
2009
2187
|
await writer.addChunk(chunkKey, bitmap.serialize(), bitmap.size);
|
|
2010
2188
|
chunkKeys.push(chunkKey);
|
|
2011
2189
|
cardinality += bitmap.size;
|
|
2190
|
+
const pause = tick();
|
|
2191
|
+
if (pause !== null) await pause;
|
|
2012
2192
|
}
|
|
2013
2193
|
await writer.finish();
|
|
2014
2194
|
});
|
|
@@ -2051,17 +2231,21 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
|
|
|
2051
2231
|
const byChunk = /* @__PURE__ */ new Map();
|
|
2052
2232
|
const pendingByChunk = /* @__PURE__ */ new Map();
|
|
2053
2233
|
let pending = 0;
|
|
2054
|
-
const
|
|
2234
|
+
const tickChunk = yieldEvery(options.clock);
|
|
2235
|
+
const tickId = yieldEvery(options.clock, YIELD_EVERY_IDS2);
|
|
2236
|
+
const flushPending = async () => {
|
|
2055
2237
|
for (const [chunkKey, rems] of pendingByChunk) {
|
|
2056
2238
|
if (rems.length === 0) continue;
|
|
2057
2239
|
const existingBitmap = byChunk.get(chunkKey);
|
|
2058
2240
|
if (existingBitmap === void 0) byChunk.set(chunkKey, codec.fromValues(rems));
|
|
2059
2241
|
else existingBitmap.addMany(rems);
|
|
2060
2242
|
rems.length = 0;
|
|
2243
|
+
const pause = tickChunk();
|
|
2244
|
+
if (pause !== null) await pause;
|
|
2061
2245
|
}
|
|
2062
2246
|
pending = 0;
|
|
2063
2247
|
};
|
|
2064
|
-
|
|
2248
|
+
const ingest = (id) => {
|
|
2065
2249
|
const { chunkKey, remainder } = splitId(id);
|
|
2066
2250
|
let bucket = pendingByChunk.get(chunkKey);
|
|
2067
2251
|
if (bucket === void 0) {
|
|
@@ -2069,14 +2253,29 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
|
|
|
2069
2253
|
pendingByChunk.set(chunkKey, bucket);
|
|
2070
2254
|
}
|
|
2071
2255
|
bucket.push(remainder);
|
|
2072
|
-
|
|
2256
|
+
return ++pending >= BULK_FLUSH_IDS;
|
|
2257
|
+
};
|
|
2258
|
+
if (Symbol.asyncIterator in ids) {
|
|
2259
|
+
for await (const id of ids) {
|
|
2260
|
+
if (ingest(id)) await flushPending();
|
|
2261
|
+
const pause = tickId();
|
|
2262
|
+
if (pause !== null) await pause;
|
|
2263
|
+
}
|
|
2264
|
+
} else {
|
|
2265
|
+
for (const id of ids) {
|
|
2266
|
+
if (ingest(id)) await flushPending();
|
|
2267
|
+
const pause = tickId();
|
|
2268
|
+
if (pause !== null) await pause;
|
|
2269
|
+
}
|
|
2073
2270
|
}
|
|
2074
|
-
flushPending();
|
|
2271
|
+
await flushPending();
|
|
2075
2272
|
let cardinality = 0;
|
|
2076
2273
|
const chunks = [];
|
|
2077
2274
|
for (const [chunkKey, bitmap] of byChunk) {
|
|
2078
2275
|
cardinality += bitmap.size;
|
|
2079
2276
|
chunks.push({ chunkKey, bitmap });
|
|
2277
|
+
const pause = tickChunk();
|
|
2278
|
+
if (pause !== null) await pause;
|
|
2080
2279
|
}
|
|
2081
2280
|
if (options.keystore !== void 0 && options.registry === void 0) {
|
|
2082
2281
|
throw new ValidationError("bulk-load encryption requires a registry to store the wrapped DEK");
|
|
@@ -2099,7 +2298,10 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
|
|
|
2099
2298
|
crypto = { aead: minted.aead, aadFor: (scope) => aadFor(key, key.generation, scope) };
|
|
2100
2299
|
}
|
|
2101
2300
|
}
|
|
2102
|
-
const { size, sha256 } = await writeCrbmGeneration(driver, key, chunks, {
|
|
2301
|
+
const { size, sha256 } = await writeCrbmGeneration(driver, key, chunks, {
|
|
2302
|
+
crypto,
|
|
2303
|
+
clock: options.clock
|
|
2304
|
+
});
|
|
2103
2305
|
if (options.registry !== void 0) {
|
|
2104
2306
|
const becameCurrent = await publishGeneration(options.registry, key, {
|
|
2105
2307
|
wrappedDeks: newWrapped
|
|
@@ -2115,13 +2317,6 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
|
|
|
2115
2317
|
}
|
|
2116
2318
|
return { size, sha256, chunkCount: chunks.length, cardinality };
|
|
2117
2319
|
}
|
|
2118
|
-
async function* toAsyncIterable(src) {
|
|
2119
|
-
if (Symbol.asyncIterator in src) {
|
|
2120
|
-
yield* src;
|
|
2121
|
-
} else {
|
|
2122
|
-
yield* src;
|
|
2123
|
-
}
|
|
2124
|
-
}
|
|
2125
2320
|
|
|
2126
2321
|
// src/core/compaction.ts
|
|
2127
2322
|
var DEFAULT_MAX_BITMAP_BYTES2 = 1 << 20;
|
|
@@ -4090,6 +4285,7 @@ exports.DEFAULT_MAX_WARM_SCAN_BYTES = DEFAULT_MAX_WARM_SCAN_BYTES;
|
|
|
4090
4285
|
exports.DEFAULT_OCC_BACKOFF = DEFAULT_OCC_BACKOFF;
|
|
4091
4286
|
exports.DEFAULT_PRICING = DEFAULT_PRICING;
|
|
4092
4287
|
exports.DEFAULT_RETRY_POLICY = DEFAULT_RETRY_POLICY;
|
|
4288
|
+
exports.DEFAULT_WRITE_CONCURRENCY = DEFAULT_WRITE_CONCURRENCY;
|
|
4093
4289
|
exports.InProcessKeystore = InProcessKeystore;
|
|
4094
4290
|
exports.IntegrityError = IntegrityError;
|
|
4095
4291
|
exports.KeyUnavailableError = KeyUnavailableError;
|