@cloudbitmaps/core 0.4.0 → 0.4.1
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 +66 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +110 -20
- package/dist/index.d.ts +110 -20
- package/dist/index.js +66 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -226,7 +226,9 @@ async function mapWithConcurrency(items, limit, fn) {
|
|
|
226
226
|
var YIELD_EVERY = 1024;
|
|
227
227
|
function yieldEvery(clock, everyN = YIELD_EVERY) {
|
|
228
228
|
if (clock === void 0) return () => null;
|
|
229
|
-
const
|
|
229
|
+
const sleep = clock.sleep;
|
|
230
|
+
const pause = clock.yieldNow !== void 0 ? () => clock.yieldNow() : sleep !== void 0 ? () => sleep.call(clock, 1) : void 0;
|
|
231
|
+
if (pause === void 0) return () => null;
|
|
230
232
|
let n = 0;
|
|
231
233
|
return () => {
|
|
232
234
|
if (++n < everyN) return null;
|
|
@@ -426,6 +428,17 @@ var InstantClock = class {
|
|
|
426
428
|
sleep() {
|
|
427
429
|
return Promise.resolve();
|
|
428
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* An explicit no-op, so the intent is stated rather than inferred.
|
|
433
|
+
*
|
|
434
|
+
* Without it, `yieldEvery` would fall back to `sleep(1)` — and this clock's `sleep` ignores its argument and
|
|
435
|
+
* resolves on a microtask, so the "yield" would silently do nothing while every doc comment claimed it did
|
|
436
|
+
* something. This wiring has no real event loop to be courteous to (it is the clock used when nobody injects
|
|
437
|
+
* one), so no-op is the right behaviour; it just has to be a decision rather than an accident.
|
|
438
|
+
*/
|
|
439
|
+
yieldNow() {
|
|
440
|
+
return Promise.resolve();
|
|
441
|
+
}
|
|
429
442
|
};
|
|
430
443
|
var ZeroRng = class {
|
|
431
444
|
next() {
|
|
@@ -543,6 +556,17 @@ var SegmentEngine = class {
|
|
|
543
556
|
}
|
|
544
557
|
}
|
|
545
558
|
/**
|
|
559
|
+
* `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
|
|
560
|
+
*
|
|
561
|
+
* The `exclude` operands are what make suppression **compose without materialising**. Applying suppression
|
|
562
|
+
* afterwards (intersect into a temp segment, then remove) writes an intermediate segment that nobody wants;
|
|
563
|
+
* applying it here folds it into the same chunk-aligned pass. It is also cheaper than it looks: an exclude
|
|
564
|
+
* operand is read **only at the keys that survived the intersection** — the cost is bounded by the
|
|
565
|
+
* audience's surviving key count and **never scales with the size of the suppression list**. Subtracting a
|
|
566
|
+
* 61,000-chunk global opt-out list costs at most one read per surviving key, not 61,000.
|
|
567
|
+
*
|
|
568
|
+
* ---
|
|
569
|
+
*
|
|
546
570
|
* Chunk-skipping intersection. Computes
|
|
547
571
|
* `seg[0] ∩ seg[1] ∩ …` and streams the result ids ascending. It aligns each segment's *effective*
|
|
548
572
|
* chunk-key set (Warm ∪ Cold), keeps only keys present in **all** operands (a key missing from any
|
|
@@ -567,15 +591,6 @@ var SegmentEngine = class {
|
|
|
567
591
|
* re-resolves fresh (bypassing the TTL), which can hop generations even sub-TTL — still whole/immutable per
|
|
568
592
|
* read, never torn. Full intra-op snapshot isolation is deferred (gap #4).
|
|
569
593
|
*/
|
|
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
594
|
intersect(segs, options) {
|
|
580
595
|
return this.combine(segs, options?.exclude ?? [], "all", "intersect", options);
|
|
581
596
|
}
|
|
@@ -763,8 +778,10 @@ var SegmentEngine = class {
|
|
|
763
778
|
* So the staging buffer is capped, but it folds into per-chunk **compressed bitmaps** rather than being
|
|
764
779
|
* written out. Peak memory is the roaring representation of the ids seen so far — which is what the segment
|
|
765
780
|
* 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.
|
|
767
|
-
* less
|
|
781
|
+
* still sees exactly one read-modify-write per distinct chunk regardless of stream length. Above the staging cap
|
|
782
|
+
* this holds less than the previous sync-only version, which kept every remainder as an uncompressed JS
|
|
783
|
+
* number; below it — the common `addMany` — nothing is compacted until the end and the two are comparable.
|
|
784
|
+
* The guarantee is that a stream of *any* length stays bounded, not that every call got smaller.
|
|
768
785
|
*
|
|
769
786
|
* There is no way to do better without a sorted stream: a chunk can only be written once it is known that no
|
|
770
787
|
* further ids for it will arrive, and unsorted input never gives that guarantee. Callers streaming *very*
|
|
@@ -807,8 +824,6 @@ var SegmentEngine = class {
|
|
|
807
824
|
for (const id of ids) {
|
|
808
825
|
stage(id);
|
|
809
826
|
if (pending >= MUTATE_STAGE_IDS) compact();
|
|
810
|
-
const pause = tickId();
|
|
811
|
-
if (pause !== null) await pause;
|
|
812
827
|
}
|
|
813
828
|
}
|
|
814
829
|
compact();
|
|
@@ -2422,6 +2437,8 @@ async function compactSegmentInner(ref, deps, options) {
|
|
|
2422
2437
|
let tally;
|
|
2423
2438
|
try {
|
|
2424
2439
|
tally = await writeCrbmGenerationStream(deps.cold, gen0, stream, {
|
|
2440
|
+
clock: deps.clock,
|
|
2441
|
+
// cooperative if the caller's clock can yield; a no-op if it only tells the time
|
|
2425
2442
|
crypto: cryptoAt(aead, 0)
|
|
2426
2443
|
});
|
|
2427
2444
|
} catch (err) {
|
|
@@ -2549,7 +2566,8 @@ async function compactSegmentInner(ref, deps, options) {
|
|
|
2549
2566
|
deps.cold,
|
|
2550
2567
|
{ namespace: ref.namespace, segment: ref.segment, generation: newGen },
|
|
2551
2568
|
stream,
|
|
2552
|
-
cryptoAt(aead, newGen)
|
|
2569
|
+
cryptoAt(aead, newGen),
|
|
2570
|
+
deps.clock
|
|
2553
2571
|
);
|
|
2554
2572
|
try {
|
|
2555
2573
|
await deps.registry.compareAndSwap(ref, leaseToken, {
|
|
@@ -2646,8 +2664,8 @@ async function* mergeChunksStream(ref, g, pinned, deps, maxBytes, readCrypto) {
|
|
|
2646
2664
|
if (!merged.isEmpty) yield { chunkKey, bitmap: merged };
|
|
2647
2665
|
}
|
|
2648
2666
|
}
|
|
2649
|
-
async function stageGeneration(cold, key, chunks, crypto) {
|
|
2650
|
-
const tally = await writeCrbmGenerationStream(cold, key, chunks, { crypto });
|
|
2667
|
+
async function stageGeneration(cold, key, chunks, crypto, clock) {
|
|
2668
|
+
const tally = await writeCrbmGenerationStream(cold, key, chunks, { crypto, clock });
|
|
2651
2669
|
await verifyGeneration(cold, key, tally, crypto);
|
|
2652
2670
|
}
|
|
2653
2671
|
async function verifyGeneration(cold, key, expected, crypto) {
|
|
@@ -4145,17 +4163,43 @@ var RetryingWarmDriver = class {
|
|
|
4145
4163
|
deleteConditional(ref, expected) {
|
|
4146
4164
|
return withRetry(() => this.inner.deleteConditional(ref, expected), this.policy, this.deps);
|
|
4147
4165
|
}
|
|
4166
|
+
/**
|
|
4167
|
+
* Stream the inner driver's rows, retrying only until the first one arrives.
|
|
4168
|
+
*
|
|
4169
|
+
* **This used to buffer the whole scan** — `for await (…) out.push(row)` inside `withRetry`, then
|
|
4170
|
+
* `yield* out` — which made the retry trivially safe and quietly defeated every bound above it. The engine's
|
|
4171
|
+
* `collectWarm` refuses *during* enumeration precisely so resident memory is `O(ceiling)` rather than
|
|
4172
|
+
* `O(segment)`; with the buffering wrapper in place (and it is wired by default) it saw its first row only
|
|
4173
|
+
* after the entire segment was already in memory. Measured on a 500-row segment under
|
|
4174
|
+
* `budget: { maxRequests: 3 }`: **500 rows materialised with the default wiring, 4 with `retry: false`**.
|
|
4175
|
+
* Both paths threw the same `BudgetExceededError`, which is why no test caught it — the error was never the
|
|
4176
|
+
* distinguishing observable, the row count was.
|
|
4177
|
+
*
|
|
4178
|
+
* **The trade this makes.** A mid-stream fault now propagates instead of being retried. It cannot be retried
|
|
4179
|
+
* honestly: rows have already been handed to the consumer, so restarting the iterator would re-yield them,
|
|
4180
|
+
* and the driver interface exposes no resumption token to continue from. Retry is kept where it pays —
|
|
4181
|
+
* establishing the scan, which is where transient connect/throttle failures actually land — and bounded
|
|
4182
|
+
* memory wins the conflict, because invariant 6 ("bounded memory & cost, always") is a hard invariant and
|
|
4183
|
+
* mid-scan retry is not. A caller who needs whole-scan retry can wrap the operation at their own level,
|
|
4184
|
+
* where re-reading from the start is safe.
|
|
4185
|
+
*/
|
|
4148
4186
|
async *listChunks(ref, opts) {
|
|
4149
|
-
const
|
|
4187
|
+
const started = await withRetry(
|
|
4150
4188
|
async () => {
|
|
4151
|
-
const
|
|
4152
|
-
|
|
4153
|
-
return
|
|
4189
|
+
const iterator = this.inner.listChunks(ref, opts)[Symbol.asyncIterator]();
|
|
4190
|
+
const first = await iterator.next();
|
|
4191
|
+
return { iterator, first };
|
|
4154
4192
|
},
|
|
4155
4193
|
this.policy,
|
|
4156
4194
|
this.deps
|
|
4157
4195
|
);
|
|
4158
|
-
|
|
4196
|
+
if (started.first.done === true) return;
|
|
4197
|
+
yield started.first.value;
|
|
4198
|
+
for (; ; ) {
|
|
4199
|
+
const next = await started.iterator.next();
|
|
4200
|
+
if (next.done === true) return;
|
|
4201
|
+
yield next.value;
|
|
4202
|
+
}
|
|
4159
4203
|
}
|
|
4160
4204
|
};
|
|
4161
4205
|
var RetryingColdChunkSource = class {
|