@cloudbitmaps/core 0.2.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 +342 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +229 -9
- package/dist/index.d.ts +229 -9
- package/dist/index.js +339 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -37,6 +37,14 @@ function applyRemove(delta, remainder) {
|
|
|
37
37
|
delta.removes.add(remainder);
|
|
38
38
|
delta.adds.remove(remainder);
|
|
39
39
|
}
|
|
40
|
+
function applyAddAll(delta, ids) {
|
|
41
|
+
delta.adds.orInPlace(ids);
|
|
42
|
+
delta.removes.andNotInPlace(ids);
|
|
43
|
+
}
|
|
44
|
+
function applyRemoveAll(delta, ids) {
|
|
45
|
+
delta.removes.orInPlace(ids);
|
|
46
|
+
delta.adds.andNotInPlace(ids);
|
|
47
|
+
}
|
|
40
48
|
function effective(cold, delta) {
|
|
41
49
|
const out = cold.clone();
|
|
42
50
|
out.orInPlace(delta.adds);
|
|
@@ -117,6 +125,31 @@ function checkBudget(budget, projectedRequests, op) {
|
|
|
117
125
|
);
|
|
118
126
|
}
|
|
119
127
|
}
|
|
128
|
+
async function collectWithinBudget(source, budget, op) {
|
|
129
|
+
const out = [];
|
|
130
|
+
for await (const item of source) {
|
|
131
|
+
out.push(item);
|
|
132
|
+
if (budget !== null && out.length > budget.maxRequests) {
|
|
133
|
+
throw new BudgetExceededError(
|
|
134
|
+
`${op} would fan out to more than ${budget.maxRequests} units, over the per-op budget \u2014 the scan was abandoned at that point rather than completed, so no exact total is available. Raise \`budget.maxRequests\`, override it per-op, or set \`budget: false\``
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/core/cooperative.ts
|
|
142
|
+
var YIELD_EVERY = 1024;
|
|
143
|
+
function yieldEvery(clock, everyN = YIELD_EVERY) {
|
|
144
|
+
if (clock === void 0) return () => null;
|
|
145
|
+
const pause = clock.yieldNow !== void 0 ? () => clock.yieldNow() : () => clock.sleep(1);
|
|
146
|
+
let n = 0;
|
|
147
|
+
return () => {
|
|
148
|
+
if (++n < everyN) return null;
|
|
149
|
+
n = 0;
|
|
150
|
+
return pause();
|
|
151
|
+
};
|
|
152
|
+
}
|
|
120
153
|
|
|
121
154
|
// src/core/keys.ts
|
|
122
155
|
var FIELD = " ";
|
|
@@ -297,6 +330,8 @@ async function withRetry(op, policy, deps) {
|
|
|
297
330
|
var DEFAULT_MAX_BITMAP_BYTES = 1 << 20;
|
|
298
331
|
var DEFAULT_MAX_RETRIES = 16;
|
|
299
332
|
var DEFAULT_INTERSECT_CONCURRENCY = 8;
|
|
333
|
+
var MUTATE_STAGE_IDS = 1 << 20;
|
|
334
|
+
var YIELD_EVERY_IDS = 1 << 14;
|
|
300
335
|
var InstantClock = class {
|
|
301
336
|
now() {
|
|
302
337
|
return 0;
|
|
@@ -310,6 +345,8 @@ var ZeroRng = class {
|
|
|
310
345
|
return 0;
|
|
311
346
|
}
|
|
312
347
|
};
|
|
348
|
+
var DEFAULT_MAX_WARM_SCAN_BYTES = 64 * 1024 * 1024;
|
|
349
|
+
var DEFAULT_WRITE_CONCURRENCY = 4;
|
|
313
350
|
var SegmentEngine = class {
|
|
314
351
|
warm;
|
|
315
352
|
cold;
|
|
@@ -325,6 +362,7 @@ var SegmentEngine = class {
|
|
|
325
362
|
/** Consistency for READ-path Warm fetches (precomputed; OCC RMW passes strong explicitly, see `warmGet`). */
|
|
326
363
|
readOpts;
|
|
327
364
|
writeConcurrency;
|
|
365
|
+
maxWarmScanBytes;
|
|
328
366
|
/** Resolved per-op budget (null = disabled); undefined deps ⇒ the generous default. See {@link checkBudget}. */
|
|
329
367
|
budget;
|
|
330
368
|
constructor(deps) {
|
|
@@ -338,7 +376,13 @@ var SegmentEngine = class {
|
|
|
338
376
|
this.rng = deps.rng ?? new ZeroRng();
|
|
339
377
|
this.occBackoff = deps.occBackoff ?? DEFAULT_OCC_BACKOFF;
|
|
340
378
|
this.readOpts = { consistent: (deps.warmReadConsistency ?? "strong") !== "eventual" };
|
|
341
|
-
this.writeConcurrency = deps.writeConcurrency ??
|
|
379
|
+
this.writeConcurrency = deps.writeConcurrency ?? DEFAULT_WRITE_CONCURRENCY;
|
|
380
|
+
this.maxWarmScanBytes = deps.maxWarmScanBytes ?? DEFAULT_MAX_WARM_SCAN_BYTES;
|
|
381
|
+
if (!Number.isFinite(this.maxWarmScanBytes) || this.maxWarmScanBytes < 1) {
|
|
382
|
+
throw new ValidationError(
|
|
383
|
+
`maxWarmScanBytes must be a finite number >= 1 (got ${String(deps.maxWarmScanBytes)})`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
342
386
|
this.budget = deps.budget === void 0 ? DEFAULT_BUDGET : deps.budget;
|
|
343
387
|
this.metrics = safeMetrics(deps.metrics ?? NOOP_METRICS);
|
|
344
388
|
this.metricsOn = this.metrics !== NOOP_METRICS;
|
|
@@ -352,10 +396,10 @@ var SegmentEngine = class {
|
|
|
352
396
|
return this.readModifyWrite(seg, chunkKey, (d) => applyRemove(d, remainder));
|
|
353
397
|
}
|
|
354
398
|
addMany(seg, ids) {
|
|
355
|
-
return this.mutateMany(seg, ids,
|
|
399
|
+
return this.mutateMany(seg, ids, applyAddAll);
|
|
356
400
|
}
|
|
357
401
|
removeMany(seg, ids) {
|
|
358
|
-
return this.mutateMany(seg, ids,
|
|
402
|
+
return this.mutateMany(seg, ids, applyRemoveAll);
|
|
359
403
|
}
|
|
360
404
|
async has(seg, id, opts) {
|
|
361
405
|
const { chunkKey, remainder } = splitId(id);
|
|
@@ -370,7 +414,7 @@ var SegmentEngine = class {
|
|
|
370
414
|
return cold ? cold.has(remainder) : false;
|
|
371
415
|
}
|
|
372
416
|
async count(seg) {
|
|
373
|
-
const warmRows = await this.collectWarm(seg);
|
|
417
|
+
const warmRows = await this.collectWarm(seg, "count", this.budget?.maxRequests ?? null);
|
|
374
418
|
const cardinalities = this.cold.cardinalities ? await this.cold.cardinalities(seg) : null;
|
|
375
419
|
if (!cardinalities) {
|
|
376
420
|
const chunkKeys = await this.chunkKeys(seg, warmRows);
|
|
@@ -402,7 +446,7 @@ var SegmentEngine = class {
|
|
|
402
446
|
return this.cold.sizeOf ? this.cold.sizeOf(seg) : Promise.resolve(null);
|
|
403
447
|
}
|
|
404
448
|
async *iterate(seg) {
|
|
405
|
-
const warmRows = await this.collectWarm(seg);
|
|
449
|
+
const warmRows = await this.collectWarm(seg, "iterate", this.budget?.maxRequests ?? null);
|
|
406
450
|
const chunkKeys = await this.chunkKeys(seg, warmRows);
|
|
407
451
|
checkBudget(this.budget, chunkKeys.length, "iterate");
|
|
408
452
|
const gen = await this.currentGen(seg);
|
|
@@ -436,9 +480,44 @@ var SegmentEngine = class {
|
|
|
436
480
|
* re-resolves fresh (bypassing the TTL), which can hop generations even sub-TTL — still whole/immutable per
|
|
437
481
|
* read, never torn. Full intra-op snapshot isolation is deferred (gap #4).
|
|
438
482
|
*/
|
|
439
|
-
|
|
483
|
+
/**
|
|
484
|
+
* `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
|
|
485
|
+
*
|
|
486
|
+
* The `exclude` operands are what make suppression **compose without materialising**. Applying suppression
|
|
487
|
+
* afterwards (intersect into a temp segment, then remove) writes an intermediate segment that nobody wants;
|
|
488
|
+
* applying it here folds it into the same chunk-aligned pass. It is also cheaper than it looks: an exclude
|
|
489
|
+
* operand is read **only at the keys that survived the intersection**, so a 61,000-chunk global opt-out list
|
|
490
|
+
* intersected against a narrow audience costs a handful of reads, not 61,000.
|
|
491
|
+
*/
|
|
492
|
+
intersect(segs, options) {
|
|
493
|
+
return this.combine(segs, options?.exclude ?? [], "all", "intersect", options);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* `segs[0] ∪ segs[1] ∪ …`, minus every id in `options.exclude` — streamed ascending.
|
|
497
|
+
*
|
|
498
|
+
* **Union is the one composite read with no chunk-skipping**, and that is a property of the operation rather
|
|
499
|
+
* than of this implementation: an id in *any* operand belongs to the result, so every chunk of every operand
|
|
500
|
+
* has to be read. `intersect` can prune a key absent from any operand; union cannot prune anything. It is
|
|
501
|
+
* budgeted exactly like `intersect` so a wide union is refused rather than quietly billed.
|
|
502
|
+
*/
|
|
503
|
+
union(segs, options) {
|
|
504
|
+
return this.combine(segs, options?.exclude ?? [], "any", "union", options);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* `seg \ (excludes[0] ∪ excludes[1] ∪ …)` — streamed ascending. Suppression on its own.
|
|
508
|
+
*
|
|
509
|
+
* Reads every chunk of `seg` (any of them may survive) but each exclude **only where it overlaps `seg`**, so
|
|
510
|
+
* the cost scales with the segment being filtered rather than with the size of the suppression list.
|
|
511
|
+
*/
|
|
512
|
+
andNot(seg, excludes, options) {
|
|
513
|
+
return this.combine([seg], excludes, "all", "andNot", options);
|
|
514
|
+
}
|
|
515
|
+
async *combine(segs, excludeSegs, mode, op, options) {
|
|
440
516
|
if (segs.length === 0) {
|
|
441
|
-
throw new ValidationError(
|
|
517
|
+
throw new ValidationError(`${op} requires at least one segment`);
|
|
518
|
+
}
|
|
519
|
+
if (op === "andNot" && excludeSegs.length === 0) {
|
|
520
|
+
throw new ValidationError("andNot requires at least one segment to subtract");
|
|
442
521
|
}
|
|
443
522
|
const limit = options?.concurrency ?? DEFAULT_INTERSECT_CONCURRENCY;
|
|
444
523
|
if (!Number.isInteger(limit) || limit < 1) {
|
|
@@ -447,34 +526,52 @@ var SegmentEngine = class {
|
|
|
447
526
|
);
|
|
448
527
|
}
|
|
449
528
|
const budget = resolvePerOpBudget(options?.budget, this.budget);
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
529
|
+
const extract = async (seg) => {
|
|
530
|
+
const warmRows = await this.collectWarm(seg, op, null);
|
|
531
|
+
const keys = await this.chunkKeys(seg, warmRows);
|
|
532
|
+
const gen = await this.currentGen(seg);
|
|
533
|
+
return { seg, warmRows, keys: new Set(keys), gen };
|
|
534
|
+
};
|
|
535
|
+
const [operands, excludes] = await Promise.all([
|
|
536
|
+
Promise.all(segs.map(extract)),
|
|
537
|
+
Promise.all(excludeSegs.map(extract))
|
|
538
|
+
]);
|
|
539
|
+
const candidates = [];
|
|
540
|
+
if (mode === "all") {
|
|
541
|
+
const pivot = operands.reduce((a, b) => a.keys.size <= b.keys.size ? a : b);
|
|
542
|
+
for (const k of pivot.keys) {
|
|
543
|
+
if (operands.every((o) => o.keys.has(k))) candidates.push(k);
|
|
544
|
+
}
|
|
545
|
+
} else {
|
|
546
|
+
const seen = /* @__PURE__ */ new Set();
|
|
547
|
+
for (const o of operands) for (const k of o.keys) seen.add(k);
|
|
548
|
+
for (const k of seen) candidates.push(k);
|
|
549
|
+
}
|
|
550
|
+
candidates.sort((a, b) => a - b);
|
|
551
|
+
const common = candidates;
|
|
552
|
+
let units = 0;
|
|
553
|
+
for (const key of common) {
|
|
554
|
+
for (const o of operands) if (mode === "all" || o.keys.has(key)) units += 1;
|
|
555
|
+
for (const e of excludes) if (e.keys.has(key)) units += 1;
|
|
556
|
+
}
|
|
557
|
+
checkBudget(budget, units, op);
|
|
465
558
|
if (this.metricsOn) {
|
|
466
559
|
const distinctKeys = /* @__PURE__ */ new Set();
|
|
467
|
-
for (const
|
|
468
|
-
for (const k of
|
|
560
|
+
for (const op2 of operands) {
|
|
561
|
+
for (const k of op2.keys) distinctKeys.add(k);
|
|
469
562
|
}
|
|
563
|
+
for (const e of excludes) for (const k of e.keys) distinctKeys.add(k);
|
|
470
564
|
this.metrics.onEvent({
|
|
471
565
|
kind: "intersect",
|
|
472
|
-
|
|
566
|
+
op,
|
|
567
|
+
operands: segs.length + excludes.length,
|
|
473
568
|
fetchedChunks: common.length,
|
|
569
|
+
// Never negative: under 'any' every candidate key came from some operand, so `distinctKeys` is a
|
|
570
|
+
// superset of `common`; excludes only ever add keys here.
|
|
474
571
|
skippedChunks: distinctKeys.size - common.length
|
|
475
572
|
});
|
|
476
573
|
}
|
|
477
|
-
const startAt = (key) => this.
|
|
574
|
+
const startAt = (key) => this.combineChunk(operands, excludes, mode, key).then(
|
|
478
575
|
(result) => ({ key, result }),
|
|
479
576
|
(error) => ({ key, result: null, error })
|
|
480
577
|
);
|
|
@@ -496,16 +593,33 @@ var SegmentEngine = class {
|
|
|
496
593
|
* first operand's effective chunk and mutates it in place — safe because `effectiveChunk` always returns
|
|
497
594
|
* a FRESH bitmap (never a cached/shared Cold instance), so this can't poison the HOT cache.
|
|
498
595
|
*/
|
|
499
|
-
async
|
|
596
|
+
async combineChunk(operands, excludes, mode, chunkKey) {
|
|
597
|
+
const present = mode === "all" ? operands : operands.filter((o) => o.keys.has(chunkKey));
|
|
598
|
+
if (present.length === 0) return null;
|
|
500
599
|
const chunks = await Promise.all(
|
|
501
|
-
|
|
600
|
+
present.map((op) => this.effectiveChunk(op.seg, chunkKey, op.warmRows, op.gen))
|
|
502
601
|
);
|
|
503
602
|
const acc = chunks[0];
|
|
504
603
|
for (let i = 1; i < chunks.length; i++) {
|
|
505
|
-
|
|
506
|
-
|
|
604
|
+
if (mode === "all") {
|
|
605
|
+
acc.andInPlace(chunks[i]);
|
|
606
|
+
if (acc.isEmpty) return null;
|
|
607
|
+
} else {
|
|
608
|
+
acc.orInPlace(chunks[i]);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (acc.isEmpty) return null;
|
|
612
|
+
const relevant = excludes.filter((e) => e.keys.has(chunkKey));
|
|
613
|
+
if (relevant.length > 0) {
|
|
614
|
+
const cuts = await Promise.all(
|
|
615
|
+
relevant.map((e) => this.effectiveChunk(e.seg, chunkKey, e.warmRows, e.gen))
|
|
616
|
+
);
|
|
617
|
+
for (const cut of cuts) {
|
|
618
|
+
acc.andNotInPlace(cut);
|
|
619
|
+
if (acc.isEmpty) return null;
|
|
620
|
+
}
|
|
507
621
|
}
|
|
508
|
-
return acc
|
|
622
|
+
return acc;
|
|
509
623
|
}
|
|
510
624
|
/**
|
|
511
625
|
* Materialize `seg[0] ∩ seg[1] ∩ …` into `dest` via batched `addMany` (streaming, bounded memory).
|
|
@@ -513,9 +627,30 @@ var SegmentEngine = class {
|
|
|
513
627
|
* `dest` is added to, not replaced.
|
|
514
628
|
*/
|
|
515
629
|
async intersectInto(dest, segs, options) {
|
|
516
|
-
|
|
630
|
+
await this.drainInto(dest, this.intersect(segs, options), options?.batchSize);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Materialize `segs[0] ∪ segs[1] ∪ …` (minus `options.exclude`) into `dest`. **Not atomic**, like
|
|
634
|
+
* {@link intersectInto}. See {@link union} for why this one cannot skip any chunk.
|
|
635
|
+
*/
|
|
636
|
+
async unionInto(dest, segs, options) {
|
|
637
|
+
await this.drainInto(dest, this.union(segs, options), options?.batchSize);
|
|
638
|
+
}
|
|
639
|
+
/** Materialize `seg \ (excludes…)` into `dest`. **Not atomic**, like {@link intersectInto}. */
|
|
640
|
+
async andNotInto(dest, seg, excludes, options) {
|
|
641
|
+
await this.drainInto(dest, this.andNot(seg, excludes, options), options?.batchSize);
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Stream a combine's results into `dest` in bounded batches.
|
|
645
|
+
*
|
|
646
|
+
* `dest` is **added to, not replaced**, and this is **not atomic** — ids land chunk-by-chunk, so a failure
|
|
647
|
+
* mid-stream leaves a partial result, exactly as `addMany` does. Batching keeps memory bounded no matter how
|
|
648
|
+
* large the result is.
|
|
649
|
+
*/
|
|
650
|
+
async drainInto(dest, source, batchSizeOption) {
|
|
651
|
+
const batchSize = Math.max(1, batchSizeOption ?? 4096);
|
|
517
652
|
let batch = [];
|
|
518
|
-
for await (const id of
|
|
653
|
+
for await (const id of source) {
|
|
519
654
|
batch.push(id);
|
|
520
655
|
if (batch.length >= batchSize) {
|
|
521
656
|
await this.addMany(dest, batch);
|
|
@@ -524,19 +659,77 @@ var SegmentEngine = class {
|
|
|
524
659
|
}
|
|
525
660
|
if (batch.length > 0) await this.addMany(dest, batch);
|
|
526
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* Group ids by chunk, then apply each chunk's whole batch in a single OCC read-modify-write.
|
|
664
|
+
*
|
|
665
|
+
* Accepts a **sync or async** iterable, so an Athena/BigQuery cursor streams straight in without the caller
|
|
666
|
+
* hand-batching `page → addMany(page)`.
|
|
667
|
+
*
|
|
668
|
+
* **The pending ids are held as BITMAPS, not as JS numbers, and that is the load-bearing choice.** Streaming
|
|
669
|
+
* input is unbounded by definition, so something has to give. The obvious answer — buffer N ids, flush them to
|
|
670
|
+
* the backend, repeat — bounds memory and is quietly catastrophic on cost: with ids arriving in arbitrary
|
|
671
|
+
* order, every flush touches nearly every chunk again, so an 11M-id stream at a 1M-id buffer would issue
|
|
672
|
+
* **11x** the OCC round-trips of a single pass. In a library whose headline claim is honest cost, silently
|
|
673
|
+
* multiplying someone's write bill by the size of their input is not a bounded-memory strategy; it is a
|
|
674
|
+
* different bug.
|
|
675
|
+
*
|
|
676
|
+
* So the staging buffer is capped, but it folds into per-chunk **compressed bitmaps** rather than being
|
|
677
|
+
* written out. Peak memory is the roaring representation of the ids seen so far — which is what the segment
|
|
678
|
+
* itself costs, and the floor for any implementation that writes each chunk exactly once — while the backend
|
|
679
|
+
* still sees exactly one read-modify-write per distinct chunk regardless of stream length. This is strictly
|
|
680
|
+
* less memory than the previous sync-only version, which held every remainder as an uncompressed JS number.
|
|
681
|
+
*
|
|
682
|
+
* There is no way to do better without a sorted stream: a chunk can only be written once it is known that no
|
|
683
|
+
* further ids for it will arrive, and unsorted input never gives that guarantee. Callers streaming *very*
|
|
684
|
+
* large sets want `bulkLoadCrbmGeneration` instead — it writes one immutable Cold object rather than tens of
|
|
685
|
+
* thousands of warm rows. The crossover is documented in the guide.
|
|
686
|
+
*/
|
|
527
687
|
async mutateMany(seg, ids, apply) {
|
|
528
688
|
const byChunk = /* @__PURE__ */ new Map();
|
|
529
|
-
|
|
689
|
+
const staged = /* @__PURE__ */ new Map();
|
|
690
|
+
let pending = 0;
|
|
691
|
+
const tickId = yieldEvery(this.clock, YIELD_EVERY_IDS);
|
|
692
|
+
const compact = () => {
|
|
693
|
+
for (const [chunkKey, rems] of staged) {
|
|
694
|
+
if (rems.length === 0) continue;
|
|
695
|
+
const existing = byChunk.get(chunkKey);
|
|
696
|
+
if (existing === void 0) byChunk.set(chunkKey, this.codec.fromValues(rems));
|
|
697
|
+
else existing.addMany(rems);
|
|
698
|
+
rems.length = 0;
|
|
699
|
+
}
|
|
700
|
+
pending = 0;
|
|
701
|
+
};
|
|
702
|
+
const stage = (id) => {
|
|
530
703
|
const { chunkKey, remainder } = splitId(id);
|
|
531
|
-
|
|
704
|
+
let rems = staged.get(chunkKey);
|
|
705
|
+
if (rems === void 0) {
|
|
706
|
+
rems = [];
|
|
707
|
+
staged.set(chunkKey, rems);
|
|
708
|
+
}
|
|
532
709
|
rems.push(remainder);
|
|
533
|
-
|
|
710
|
+
pending += 1;
|
|
711
|
+
};
|
|
712
|
+
if (Symbol.asyncIterator in ids) {
|
|
713
|
+
for await (const id of ids) {
|
|
714
|
+
stage(id);
|
|
715
|
+
if (pending >= MUTATE_STAGE_IDS) compact();
|
|
716
|
+
const pause = tickId();
|
|
717
|
+
if (pause !== null) await pause;
|
|
718
|
+
}
|
|
719
|
+
} else {
|
|
720
|
+
for (const id of ids) {
|
|
721
|
+
stage(id);
|
|
722
|
+
if (pending >= MUTATE_STAGE_IDS) compact();
|
|
723
|
+
const pause = tickId();
|
|
724
|
+
if (pause !== null) await pause;
|
|
725
|
+
}
|
|
534
726
|
}
|
|
727
|
+
compact();
|
|
535
728
|
await mapWithConcurrency(
|
|
536
729
|
[...byChunk],
|
|
537
730
|
this.writeConcurrency,
|
|
538
|
-
([chunkKey,
|
|
539
|
-
|
|
731
|
+
([chunkKey, bitmap]) => this.readModifyWrite(seg, chunkKey, (d) => {
|
|
732
|
+
apply(d, bitmap);
|
|
540
733
|
})
|
|
541
734
|
);
|
|
542
735
|
}
|
|
@@ -590,12 +783,22 @@ var SegmentEngine = class {
|
|
|
590
783
|
}
|
|
591
784
|
return row;
|
|
592
785
|
}
|
|
593
|
-
async collectWarm(seg) {
|
|
786
|
+
async collectWarm(seg, op, rowCeiling) {
|
|
594
787
|
const rows = /* @__PURE__ */ new Map();
|
|
595
788
|
let bytes = 0;
|
|
596
789
|
for await (const row of this.warm.listChunks(seg, this.readOpts)) {
|
|
597
790
|
rows.set(row.chunkKey, row.bytes);
|
|
598
791
|
bytes += row.bytes.length;
|
|
792
|
+
if (bytes > this.maxWarmScanBytes) {
|
|
793
|
+
throw new BudgetExceededError(
|
|
794
|
+
`${op} would hold more than ${this.maxWarmScanBytes} bytes of warm deltas resident for one segment \u2014 the scan was abandoned there rather than completed. Raise \`maxWarmScanBytes\`, narrow the segment, or compact it so fewer chunks carry warm deltas. (This ceiling is independent of \`budget\` and stays in force when \`budget: false\`.)`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
if (rowCeiling !== null && rows.size > rowCeiling) {
|
|
798
|
+
throw new BudgetExceededError(
|
|
799
|
+
`${op} would read more than ${rowCeiling} warm chunk rows, over the per-op budget \u2014 the scan was abandoned at that point rather than completed, so no exact total is available. Raise \`budget.maxRequests\`, override it per-op, or set \`budget: false\``
|
|
800
|
+
);
|
|
801
|
+
}
|
|
599
802
|
}
|
|
600
803
|
if (this.metricsOn) {
|
|
601
804
|
this.metrics.onEvent({
|
|
@@ -1083,22 +1286,44 @@ function aadFor(ref, generation, scope) {
|
|
|
1083
1286
|
}
|
|
1084
1287
|
|
|
1085
1288
|
// src/core/crbm/crc32c.ts
|
|
1086
|
-
var
|
|
1289
|
+
var TABLES = (() => {
|
|
1087
1290
|
const POLY = 2197175160;
|
|
1088
|
-
const
|
|
1291
|
+
const t0 = new Uint32Array(256);
|
|
1089
1292
|
for (let n = 0; n < 256; n++) {
|
|
1090
1293
|
let c = n;
|
|
1091
|
-
for (let k = 0; k < 8; k++)
|
|
1092
|
-
|
|
1294
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? POLY ^ c >>> 1 : c >>> 1;
|
|
1295
|
+
t0[n] = c >>> 0;
|
|
1296
|
+
}
|
|
1297
|
+
const tables = [t0];
|
|
1298
|
+
for (let k = 1; k < 8; k++) {
|
|
1299
|
+
const prev = tables[k - 1];
|
|
1300
|
+
const next = new Uint32Array(256);
|
|
1301
|
+
for (let n = 0; n < 256; n++) {
|
|
1302
|
+
const v = prev[n];
|
|
1303
|
+
next[n] = (v >>> 8 ^ t0[v & 255]) >>> 0;
|
|
1093
1304
|
}
|
|
1094
|
-
|
|
1305
|
+
tables.push(next);
|
|
1095
1306
|
}
|
|
1096
|
-
return
|
|
1307
|
+
return tables;
|
|
1097
1308
|
})();
|
|
1309
|
+
var T0 = TABLES[0];
|
|
1310
|
+
var T1 = TABLES[1];
|
|
1311
|
+
var T2 = TABLES[2];
|
|
1312
|
+
var T3 = TABLES[3];
|
|
1313
|
+
var T4 = TABLES[4];
|
|
1314
|
+
var T5 = TABLES[5];
|
|
1315
|
+
var T6 = TABLES[6];
|
|
1316
|
+
var T7 = TABLES[7];
|
|
1098
1317
|
function crc32c(bytes, seed = 0) {
|
|
1099
1318
|
let crc = (seed ^ 4294967295) >>> 0;
|
|
1100
|
-
|
|
1101
|
-
|
|
1319
|
+
let i = 0;
|
|
1320
|
+
const n8 = bytes.length - bytes.length % 8;
|
|
1321
|
+
for (; i < n8; i += 8) {
|
|
1322
|
+
crc ^= bytes[i] | bytes[i + 1] << 8 | bytes[i + 2] << 16 | bytes[i + 3] << 24;
|
|
1323
|
+
crc = (T7[crc & 255] ^ T6[crc >>> 8 & 255] ^ T5[crc >>> 16 & 255] ^ T4[crc >>> 24 & 255] ^ T3[bytes[i + 4]] ^ T2[bytes[i + 5]] ^ T1[bytes[i + 6]] ^ T0[bytes[i + 7]]) >>> 0;
|
|
1324
|
+
}
|
|
1325
|
+
for (; i < bytes.length; i++) {
|
|
1326
|
+
crc = (T0[(crc ^ bytes[i]) & 255] ^ crc >>> 8) >>> 0;
|
|
1102
1327
|
}
|
|
1103
1328
|
return (crc ^ 4294967295) >>> 0;
|
|
1104
1329
|
}
|
|
@@ -1609,6 +1834,8 @@ function coldBlobReader(driver, key) {
|
|
|
1609
1834
|
};
|
|
1610
1835
|
}
|
|
1611
1836
|
var DEFAULT_CURRENT_GEN_TTL_MS = 2e3;
|
|
1837
|
+
var BULK_FLUSH_IDS = 1 << 20;
|
|
1838
|
+
var YIELD_EVERY_IDS2 = 1 << 14;
|
|
1612
1839
|
var DEFAULT_MAX_OPEN_SEGMENTS = 1024;
|
|
1613
1840
|
var DEFAULT_MAX_OPEN_INDEX_BYTES = 64 * 1024 * 1024;
|
|
1614
1841
|
var CrbmColdChunkSource = class {
|
|
@@ -1827,11 +2054,14 @@ var CrbmColdChunkSource = class {
|
|
|
1827
2054
|
};
|
|
1828
2055
|
function writeCrbmGeneration(driver, key, chunks, options = {}) {
|
|
1829
2056
|
const sorted = [...chunks].sort((a, b) => a.chunkKey - b.chunkKey);
|
|
2057
|
+
const tick = yieldEvery(options.clock);
|
|
1830
2058
|
return driver.putImmutable(key, async (sink) => {
|
|
1831
2059
|
const writer = new CrbmWriter(sink, { generation: key.generation, crypto: options.crypto });
|
|
1832
2060
|
for (const { chunkKey, bitmap } of sorted) {
|
|
1833
2061
|
if (bitmap.isEmpty) continue;
|
|
1834
2062
|
await writer.addChunk(chunkKey, bitmap.serialize(), bitmap.size);
|
|
2063
|
+
const pause = tick();
|
|
2064
|
+
if (pause !== null) await pause;
|
|
1835
2065
|
}
|
|
1836
2066
|
await writer.finish();
|
|
1837
2067
|
});
|
|
@@ -1839,6 +2069,7 @@ function writeCrbmGeneration(driver, key, chunks, options = {}) {
|
|
|
1839
2069
|
async function writeCrbmGenerationStream(driver, key, chunks, options = {}) {
|
|
1840
2070
|
const chunkKeys = [];
|
|
1841
2071
|
let cardinality = 0;
|
|
2072
|
+
const tick = yieldEvery(options.clock);
|
|
1842
2073
|
const { size, sha256 } = await driver.putImmutable(key, async (sink) => {
|
|
1843
2074
|
const writer = new CrbmWriter(sink, { generation: key.generation, crypto: options.crypto });
|
|
1844
2075
|
for await (const { chunkKey, bitmap } of chunks) {
|
|
@@ -1846,6 +2077,8 @@ async function writeCrbmGenerationStream(driver, key, chunks, options = {}) {
|
|
|
1846
2077
|
await writer.addChunk(chunkKey, bitmap.serialize(), bitmap.size);
|
|
1847
2078
|
chunkKeys.push(chunkKey);
|
|
1848
2079
|
cardinality += bitmap.size;
|
|
2080
|
+
const pause = tick();
|
|
2081
|
+
if (pause !== null) await pause;
|
|
1849
2082
|
}
|
|
1850
2083
|
await writer.finish();
|
|
1851
2084
|
});
|
|
@@ -1886,20 +2119,53 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
|
|
|
1886
2119
|
}
|
|
1887
2120
|
const codec = requireCodec(options.codec, "bulkLoadCrbmGeneration");
|
|
1888
2121
|
const byChunk = /* @__PURE__ */ new Map();
|
|
1889
|
-
|
|
2122
|
+
const pendingByChunk = /* @__PURE__ */ new Map();
|
|
2123
|
+
let pending = 0;
|
|
2124
|
+
const tickChunk = yieldEvery(options.clock);
|
|
2125
|
+
const tickId = yieldEvery(options.clock, YIELD_EVERY_IDS2);
|
|
2126
|
+
const flushPending = async () => {
|
|
2127
|
+
for (const [chunkKey, rems] of pendingByChunk) {
|
|
2128
|
+
if (rems.length === 0) continue;
|
|
2129
|
+
const existingBitmap = byChunk.get(chunkKey);
|
|
2130
|
+
if (existingBitmap === void 0) byChunk.set(chunkKey, codec.fromValues(rems));
|
|
2131
|
+
else existingBitmap.addMany(rems);
|
|
2132
|
+
rems.length = 0;
|
|
2133
|
+
const pause = tickChunk();
|
|
2134
|
+
if (pause !== null) await pause;
|
|
2135
|
+
}
|
|
2136
|
+
pending = 0;
|
|
2137
|
+
};
|
|
2138
|
+
const ingest = (id) => {
|
|
1890
2139
|
const { chunkKey, remainder } = splitId(id);
|
|
1891
|
-
let
|
|
1892
|
-
if (
|
|
1893
|
-
|
|
1894
|
-
|
|
2140
|
+
let bucket = pendingByChunk.get(chunkKey);
|
|
2141
|
+
if (bucket === void 0) {
|
|
2142
|
+
bucket = [];
|
|
2143
|
+
pendingByChunk.set(chunkKey, bucket);
|
|
2144
|
+
}
|
|
2145
|
+
bucket.push(remainder);
|
|
2146
|
+
return ++pending >= BULK_FLUSH_IDS;
|
|
2147
|
+
};
|
|
2148
|
+
if (Symbol.asyncIterator in ids) {
|
|
2149
|
+
for await (const id of ids) {
|
|
2150
|
+
if (ingest(id)) await flushPending();
|
|
2151
|
+
const pause = tickId();
|
|
2152
|
+
if (pause !== null) await pause;
|
|
2153
|
+
}
|
|
2154
|
+
} else {
|
|
2155
|
+
for (const id of ids) {
|
|
2156
|
+
if (ingest(id)) await flushPending();
|
|
2157
|
+
const pause = tickId();
|
|
2158
|
+
if (pause !== null) await pause;
|
|
1895
2159
|
}
|
|
1896
|
-
bitmap.add(remainder);
|
|
1897
2160
|
}
|
|
2161
|
+
await flushPending();
|
|
1898
2162
|
let cardinality = 0;
|
|
1899
2163
|
const chunks = [];
|
|
1900
2164
|
for (const [chunkKey, bitmap] of byChunk) {
|
|
1901
2165
|
cardinality += bitmap.size;
|
|
1902
2166
|
chunks.push({ chunkKey, bitmap });
|
|
2167
|
+
const pause = tickChunk();
|
|
2168
|
+
if (pause !== null) await pause;
|
|
1903
2169
|
}
|
|
1904
2170
|
if (options.keystore !== void 0 && options.registry === void 0) {
|
|
1905
2171
|
throw new ValidationError("bulk-load encryption requires a registry to store the wrapped DEK");
|
|
@@ -1922,7 +2188,10 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
|
|
|
1922
2188
|
crypto = { aead: minted.aead, aadFor: (scope) => aadFor(key, key.generation, scope) };
|
|
1923
2189
|
}
|
|
1924
2190
|
}
|
|
1925
|
-
const { size, sha256 } = await writeCrbmGeneration(driver, key, chunks, {
|
|
2191
|
+
const { size, sha256 } = await writeCrbmGeneration(driver, key, chunks, {
|
|
2192
|
+
crypto,
|
|
2193
|
+
clock: options.clock
|
|
2194
|
+
});
|
|
1926
2195
|
if (options.registry !== void 0) {
|
|
1927
2196
|
const becameCurrent = await publishGeneration(options.registry, key, {
|
|
1928
2197
|
wrappedDeks: newWrapped
|
|
@@ -1938,13 +2207,6 @@ async function bulkLoadCrbmGeneration(driver, key, ids, options = {}) {
|
|
|
1938
2207
|
}
|
|
1939
2208
|
return { size, sha256, chunkCount: chunks.length, cardinality };
|
|
1940
2209
|
}
|
|
1941
|
-
async function* toAsyncIterable(src) {
|
|
1942
|
-
if (Symbol.asyncIterator in src) {
|
|
1943
|
-
yield* src;
|
|
1944
|
-
} else {
|
|
1945
|
-
yield* src;
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
2210
|
|
|
1949
2211
|
// src/core/compaction.ts
|
|
1950
2212
|
var DEFAULT_MAX_BITMAP_BYTES2 = 1 << 20;
|
|
@@ -3472,6 +3734,7 @@ async function eraseWarm(warm, ref) {
|
|
|
3472
3734
|
}
|
|
3473
3735
|
|
|
3474
3736
|
// src/core/consistency.ts
|
|
3737
|
+
var DEFAULT_MAX_SCAN_SEGMENTS = 25e4;
|
|
3475
3738
|
var DEFAULT_CHECK_CONCURRENCY = 8;
|
|
3476
3739
|
async function listGenerations(cold, ref) {
|
|
3477
3740
|
const present = /* @__PURE__ */ new Set();
|
|
@@ -3483,8 +3746,21 @@ async function runConsistencyCheck(deps, options = {}) {
|
|
|
3483
3746
|
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
3484
3747
|
throw new ValidationError(`concurrency must be a positive integer; got ${concurrency}`);
|
|
3485
3748
|
}
|
|
3749
|
+
const maxScanSegments = options.maxScanSegments ?? DEFAULT_MAX_SCAN_SEGMENTS;
|
|
3750
|
+
if (!Number.isFinite(maxScanSegments) || maxScanSegments < 1) {
|
|
3751
|
+
throw new ValidationError(
|
|
3752
|
+
`maxScanSegments must be a finite number >= 1; got ${String(options.maxScanSegments)}`
|
|
3753
|
+
);
|
|
3754
|
+
}
|
|
3486
3755
|
const recs = [];
|
|
3487
|
-
for await (const rec of deps.registry.list(options.namespace))
|
|
3756
|
+
for await (const rec of deps.registry.list(options.namespace)) {
|
|
3757
|
+
recs.push(rec);
|
|
3758
|
+
if (recs.length > maxScanSegments) {
|
|
3759
|
+
throw new BudgetExceededError(
|
|
3760
|
+
`checkConsistency would enumerate more than ${maxScanSegments} segments \u2014 the scan was abandoned there rather than completed. Narrow it with \`namespace\`, or raise \`maxScanSegments\` if the fleet really is that large and the memory is available.`
|
|
3761
|
+
);
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3488
3764
|
const results = await mapWithConcurrency(recs, concurrency, async (rec) => {
|
|
3489
3765
|
if (rec.status === "destroyed") return { kind: "ok" };
|
|
3490
3766
|
const ref = { segment: rec.segment, namespace: rec.namespace };
|
|
@@ -3676,6 +3952,6 @@ var RetryingRegistryDriver = class {
|
|
|
3676
3952
|
}
|
|
3677
3953
|
};
|
|
3678
3954
|
|
|
3679
|
-
export { AWS_US_EAST_1_ONDEMAND, BoundedLru, BufferReader, BufferSink, CountingMetricsSink, CrbmColdChunkSource, CrbmReader, CrbmWriter, DEFAULT_BUDGET, DEFAULT_OCC_BACKOFF, DEFAULT_PRICING, DEFAULT_RETRY_POLICY, InProcessKeystore, LocalFsColdDriver, LocalFsRegistryDriver, LocalFsWarmDriver, MemoryColdChunkSource, MemoryColdDriver, MemoryRegistryDriver, MemoryWarmDriver, NOOP_AUDIT, NOOP_METRICS, NodeAead, RecordingAuditSink, RetryingColdChunkSource, RetryingColdDriver, RetryingRegistryDriver, RetryingWarmDriver, SegmentEngine, aadFor, bulkLoadCrbmGeneration, checkBudget, chunkRefKey, compactSegment, destroySegment, eraseNamespace, estimateCost, findCompactable, gcOrphanGenerations, groundedReport, isTransient, joinId, publishGeneration, resolveBudget, resolvePerOpBudget, runCompactionCycle, runConsistencyCheck, runExport, safeMetrics, segmentKey, splitId, validateCompactionOptions, withRetry, writeCrbmGeneration };
|
|
3955
|
+
export { AWS_US_EAST_1_ONDEMAND, BoundedLru, BufferReader, BufferSink, CountingMetricsSink, CrbmColdChunkSource, CrbmReader, CrbmWriter, DEFAULT_BUDGET, DEFAULT_MAX_SCAN_SEGMENTS, DEFAULT_MAX_WARM_SCAN_BYTES, DEFAULT_OCC_BACKOFF, DEFAULT_PRICING, DEFAULT_RETRY_POLICY, DEFAULT_WRITE_CONCURRENCY, InProcessKeystore, LocalFsColdDriver, LocalFsRegistryDriver, LocalFsWarmDriver, MemoryColdChunkSource, MemoryColdDriver, MemoryRegistryDriver, MemoryWarmDriver, NOOP_AUDIT, NOOP_METRICS, NodeAead, RecordingAuditSink, RetryingColdChunkSource, RetryingColdDriver, RetryingRegistryDriver, RetryingWarmDriver, SegmentEngine, aadFor, bulkLoadCrbmGeneration, checkBudget, chunkRefKey, collectWithinBudget, compactSegment, destroySegment, eraseNamespace, estimateCost, findCompactable, gcOrphanGenerations, groundedReport, isTransient, joinId, publishGeneration, resolveBudget, resolvePerOpBudget, runCompactionCycle, runConsistencyCheck, runExport, safeMetrics, segmentKey, splitId, validateCompactionOptions, withRetry, writeCrbmGeneration };
|
|
3680
3956
|
//# sourceMappingURL=index.js.map
|
|
3681
3957
|
//# sourceMappingURL=index.js.map
|