@cloudbitmaps/core 0.4.0 → 0.5.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 +81 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +129 -27
- package/dist/index.d.ts +129 -27
- package/dist/index.js +81 -23
- package/dist/index.js.map +1 -1
- package/dist/mysql/index.cjs.map +1 -1
- package/dist/mysql/index.js.map +1 -1
- package/dist/postgres/index.cjs.map +1 -1
- package/dist/postgres/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();
|
|
@@ -2203,6 +2218,10 @@ async function publishGeneration(registry, key, options = {}) {
|
|
|
2203
2218
|
currentGen: key.generation,
|
|
2204
2219
|
wrappedDeks: options.wrappedDeks
|
|
2205
2220
|
});
|
|
2221
|
+
} else if (record.status === "destroyed") {
|
|
2222
|
+
throw new ValidationError(
|
|
2223
|
+
`segment "${key.segment}" was destroyed (crypto-shredded) while generation ${key.generation} was being written \u2014 refusing to publish it; the written object is unreadable. Use a new segment.`
|
|
2224
|
+
);
|
|
2206
2225
|
} else if (record.currentGen > key.generation) {
|
|
2207
2226
|
return false;
|
|
2208
2227
|
} else if (record.currentGen === key.generation) {
|
|
@@ -2422,6 +2441,8 @@ async function compactSegmentInner(ref, deps, options) {
|
|
|
2422
2441
|
let tally;
|
|
2423
2442
|
try {
|
|
2424
2443
|
tally = await writeCrbmGenerationStream(deps.cold, gen0, stream, {
|
|
2444
|
+
clock: deps.clock,
|
|
2445
|
+
// cooperative if the caller's clock can yield; a no-op if it only tells the time
|
|
2425
2446
|
crypto: cryptoAt(aead, 0)
|
|
2426
2447
|
});
|
|
2427
2448
|
} catch (err) {
|
|
@@ -2549,7 +2570,8 @@ async function compactSegmentInner(ref, deps, options) {
|
|
|
2549
2570
|
deps.cold,
|
|
2550
2571
|
{ namespace: ref.namespace, segment: ref.segment, generation: newGen },
|
|
2551
2572
|
stream,
|
|
2552
|
-
cryptoAt(aead, newGen)
|
|
2573
|
+
cryptoAt(aead, newGen),
|
|
2574
|
+
deps.clock
|
|
2553
2575
|
);
|
|
2554
2576
|
try {
|
|
2555
2577
|
await deps.registry.compareAndSwap(ref, leaseToken, {
|
|
@@ -2646,8 +2668,8 @@ async function* mergeChunksStream(ref, g, pinned, deps, maxBytes, readCrypto) {
|
|
|
2646
2668
|
if (!merged.isEmpty) yield { chunkKey, bitmap: merged };
|
|
2647
2669
|
}
|
|
2648
2670
|
}
|
|
2649
|
-
async function stageGeneration(cold, key, chunks, crypto) {
|
|
2650
|
-
const tally = await writeCrbmGenerationStream(cold, key, chunks, { crypto });
|
|
2671
|
+
async function stageGeneration(cold, key, chunks, crypto, clock) {
|
|
2672
|
+
const tally = await writeCrbmGenerationStream(cold, key, chunks, { crypto, clock });
|
|
2651
2673
|
await verifyGeneration(cold, key, tally, crypto);
|
|
2652
2674
|
}
|
|
2653
2675
|
async function verifyGeneration(cold, key, expected, crypto) {
|
|
@@ -4028,12 +4050,13 @@ async function shredSegment(ref, deps, allowCleartext) {
|
|
|
4028
4050
|
}
|
|
4029
4051
|
async function eraseWarm(warm, ref) {
|
|
4030
4052
|
let deleted = 0;
|
|
4053
|
+
let conflicts = 0;
|
|
4031
4054
|
for (let pass = 0; pass < MAX_WARM_PASSES; pass++) {
|
|
4055
|
+
conflicts = 0;
|
|
4032
4056
|
const rows = [];
|
|
4033
4057
|
for await (const row of warm.listChunks(ref))
|
|
4034
4058
|
rows.push({ chunkKey: row.chunkKey, token: row.token });
|
|
4035
4059
|
if (rows.length === 0) break;
|
|
4036
|
-
let conflicts = 0;
|
|
4037
4060
|
for (const { chunkKey, token } of rows) {
|
|
4038
4061
|
const chunkRef = { namespace: ref.namespace, segment: ref.segment, chunkKey };
|
|
4039
4062
|
try {
|
|
@@ -4046,6 +4069,11 @@ async function eraseWarm(warm, ref) {
|
|
|
4046
4069
|
}
|
|
4047
4070
|
if (conflicts === 0) break;
|
|
4048
4071
|
}
|
|
4072
|
+
if (conflicts > 0) {
|
|
4073
|
+
throw new WriteConflictError(
|
|
4074
|
+
`destroySegment: ${conflicts} warm row(s) of "${ref.segment}" were rewritten during every one of ${MAX_WARM_PASSES} erase passes \u2014 the segment was NOT destroyed and its cleartext warm deltas are still readable. Stop writes to this segment and retry.`
|
|
4075
|
+
);
|
|
4076
|
+
}
|
|
4049
4077
|
return deleted;
|
|
4050
4078
|
}
|
|
4051
4079
|
|
|
@@ -4145,17 +4173,47 @@ var RetryingWarmDriver = class {
|
|
|
4145
4173
|
deleteConditional(ref, expected) {
|
|
4146
4174
|
return withRetry(() => this.inner.deleteConditional(ref, expected), this.policy, this.deps);
|
|
4147
4175
|
}
|
|
4176
|
+
/**
|
|
4177
|
+
* Stream the inner driver's rows, retrying only until the first one arrives.
|
|
4178
|
+
*
|
|
4179
|
+
* **This used to buffer the whole scan** — `for await (…) out.push(row)` inside `withRetry`, then
|
|
4180
|
+
* `yield* out` — which made the retry trivially safe and quietly defeated every bound above it. The engine's
|
|
4181
|
+
* `collectWarm` refuses *during* enumeration precisely so resident memory is `O(ceiling)` rather than
|
|
4182
|
+
* `O(segment)`; with the buffering wrapper in place (and it is wired by default) it saw its first row only
|
|
4183
|
+
* after the entire segment was already in memory. Measured on a 500-row segment under
|
|
4184
|
+
* `budget: { maxRequests: 3 }`: **500 rows materialised with the default wiring, 4 with `retry: false`**.
|
|
4185
|
+
* Both paths threw the same `BudgetExceededError`, which is why no test caught it — the error was never the
|
|
4186
|
+
* distinguishing observable, the row count was.
|
|
4187
|
+
*
|
|
4188
|
+
* **The trade this makes.** A mid-stream fault now propagates instead of being retried. It cannot be retried
|
|
4189
|
+
* honestly: rows have already been handed to the consumer, so restarting the iterator would re-yield them,
|
|
4190
|
+
* and the driver interface exposes no resumption token to continue from. Retry is kept where it pays —
|
|
4191
|
+
* establishing the scan, which is where transient connect/throttle failures actually land — and bounded
|
|
4192
|
+
* memory wins the conflict, because invariant 6 ("bounded memory & cost, always") is a hard invariant and
|
|
4193
|
+
* mid-scan retry is not. A caller who needs whole-scan retry can wrap the operation at their own level,
|
|
4194
|
+
* where re-reading from the start is safe.
|
|
4195
|
+
*/
|
|
4148
4196
|
async *listChunks(ref, opts) {
|
|
4149
|
-
const
|
|
4197
|
+
const started = await withRetry(
|
|
4150
4198
|
async () => {
|
|
4151
|
-
const
|
|
4152
|
-
|
|
4153
|
-
return
|
|
4199
|
+
const iterator = this.inner.listChunks(ref, opts)[Symbol.asyncIterator]();
|
|
4200
|
+
const first = await iterator.next();
|
|
4201
|
+
return { iterator, first };
|
|
4154
4202
|
},
|
|
4155
4203
|
this.policy,
|
|
4156
4204
|
this.deps
|
|
4157
4205
|
);
|
|
4158
|
-
|
|
4206
|
+
if (started.first.done === true) return;
|
|
4207
|
+
try {
|
|
4208
|
+
yield started.first.value;
|
|
4209
|
+
for (; ; ) {
|
|
4210
|
+
const next = await started.iterator.next();
|
|
4211
|
+
if (next.done === true) return;
|
|
4212
|
+
yield next.value;
|
|
4213
|
+
}
|
|
4214
|
+
} finally {
|
|
4215
|
+
await started.iterator.return?.();
|
|
4216
|
+
}
|
|
4159
4217
|
}
|
|
4160
4218
|
};
|
|
4161
4219
|
var RetryingColdChunkSource = class {
|