@cloudbitmaps/core 0.24.0 → 0.26.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/{chunk-BUDAVGBI.js → chunk-7VNEBKC4.js} +2 -2
- package/dist/{chunk-45ML6YU4.js → chunk-LJLZRVYN.js} +2 -2
- package/dist/{chunk-WPER7R2R.js → chunk-MNXTZJZ6.js} +2 -2
- package/dist/{chunk-2B2QF4FU.js → chunk-UFOMYONZ.js} +1 -1
- package/dist/chunk-UFOMYONZ.js.map +7 -0
- package/dist/core/codec.d.ts +3 -3
- package/dist/core/cooperative.d.ts +1 -1
- package/dist/core/crbm-storage-source.d.ts +7 -1
- package/dist/core/engine.d.ts +53 -0
- package/dist/core/load-input.d.ts +9 -3
- package/dist/core/pinned-storage-source.d.ts +2 -0
- package/dist/core/ports.d.ts +16 -1
- package/dist/driver-kit.js +3 -3
- package/dist/drivers/retry/retrying-chunk-source.d.ts +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +299 -64
- package/dist/index.js.map +2 -2
- package/dist/node.js +3 -3
- package/dist/node.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-2B2QF4FU.js.map +0 -7
- /package/dist/{chunk-BUDAVGBI.js.map → chunk-7VNEBKC4.js.map} +0 -0
- /package/dist/{chunk-45ML6YU4.js.map → chunk-LJLZRVYN.js.map} +0 -0
- /package/dist/{chunk-WPER7R2R.js.map → chunk-MNXTZJZ6.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
boundedAll,
|
|
3
3
|
mapWithConcurrency
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-7VNEBKC4.js";
|
|
5
5
|
import {
|
|
6
6
|
BufferReader,
|
|
7
7
|
KeptSegmentKeys,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
segmentKey,
|
|
10
10
|
segmentPrefix,
|
|
11
11
|
shardOf
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-MNXTZJZ6.js";
|
|
13
13
|
import {
|
|
14
14
|
BudgetExceededError,
|
|
15
15
|
CHUNK_COUNT,
|
|
@@ -116,7 +116,7 @@ import {
|
|
|
116
116
|
validateUserRef,
|
|
117
117
|
windowOf,
|
|
118
118
|
withRetry
|
|
119
|
-
} from "./chunk-
|
|
119
|
+
} from "./chunk-UFOMYONZ.js";
|
|
120
120
|
|
|
121
121
|
// src/core/budget.ts
|
|
122
122
|
var DEFAULT_BUDGET = { maxRequests: 1e6 };
|
|
@@ -183,7 +183,7 @@ async function refuseAbsent(op, candidates, limit, exists, nameOf) {
|
|
|
183
183
|
// src/core/chunk-checks.ts
|
|
184
184
|
function assertChunkKeyInRange(k) {
|
|
185
185
|
if (!Number.isInteger(k) || k < 0 || k >= CHUNK_COUNT) {
|
|
186
|
-
throw new IntegrityError(
|
|
186
|
+
throw new IntegrityError("a chunk key from a tier is out of range");
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
function assertChunkCardinalityInRange(c) {
|
|
@@ -206,22 +206,22 @@ function checkedChunkKeys(listed2) {
|
|
|
206
206
|
keys.sort((a, b) => a - b);
|
|
207
207
|
for (let i = 1; i < keys.length; i++) {
|
|
208
208
|
if (keys[i] === keys[i - 1]) {
|
|
209
|
-
throw new IntegrityError(
|
|
209
|
+
throw new IntegrityError("a chunk key from a tier is listed twice");
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
return keys;
|
|
213
213
|
}
|
|
214
|
-
function assertChunkPayloadInRange(bitmap
|
|
214
|
+
function assertChunkPayloadInRange(bitmap) {
|
|
215
215
|
const max = bitmap.maximum?.();
|
|
216
216
|
if (max !== void 0 && max > MAX_REMAINDER) {
|
|
217
217
|
throw new IntegrityError(
|
|
218
|
-
`chunk
|
|
218
|
+
`a chunk payload holds a value outside the 16-bit remainder range [0, ${MAX_REMAINDER}] (a container key other than 0) \u2014 the stored object is corrupt or was not written by this codec`
|
|
219
219
|
);
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
|
-
function decodeChunkBytes(codec, bytes,
|
|
222
|
+
function decodeChunkBytes(codec, bytes, maxBitmapBytes) {
|
|
223
223
|
const bitmap = codec.safeDeserialize(bytes, maxBitmapBytes);
|
|
224
|
-
assertChunkPayloadInRange(bitmap
|
|
224
|
+
assertChunkPayloadInRange(bitmap);
|
|
225
225
|
return bitmap;
|
|
226
226
|
}
|
|
227
227
|
|
|
@@ -238,12 +238,10 @@ var ChunkStream = class {
|
|
|
238
238
|
return this.iterator.next().then(
|
|
239
239
|
(step) => {
|
|
240
240
|
if (step.done === true) {
|
|
241
|
-
throw this.failure?.error ?? new IntegrityError(
|
|
241
|
+
throw this.failure?.error ?? new IntegrityError("chunk stream ended before a chunk it was opened over");
|
|
242
242
|
}
|
|
243
243
|
if (step.value.key !== key) {
|
|
244
|
-
throw new IntegrityError(
|
|
245
|
-
`chunk stream answered chunk ${step.value.key} where ${key} was due`
|
|
246
|
-
);
|
|
244
|
+
throw new IntegrityError("chunk stream answered a chunk other than the one due");
|
|
247
245
|
}
|
|
248
246
|
return step.value;
|
|
249
247
|
},
|
|
@@ -676,6 +674,7 @@ var COMBINE_WINDOW_START = 8;
|
|
|
676
674
|
var COMBINE_RANGE_START = 4;
|
|
677
675
|
var OPERANDS_AT_ONCE = 16;
|
|
678
676
|
var BATCH_QUIET_HOPS = 16;
|
|
677
|
+
var COUNT_RELIST_ATTEMPTS = 4;
|
|
679
678
|
var ZERO_CLOCK = { now: () => 0 };
|
|
680
679
|
var CombineWindow = class {
|
|
681
680
|
constructor(keys, limit, read) {
|
|
@@ -799,6 +798,19 @@ function combineReads(keys, operands, excludes, mode, through) {
|
|
|
799
798
|
}
|
|
800
799
|
return units;
|
|
801
800
|
}
|
|
801
|
+
function countSteps(operands, excludes, mode, w) {
|
|
802
|
+
const steps = [];
|
|
803
|
+
const decode = [];
|
|
804
|
+
for (const key of alignKeys(operands, mode)) {
|
|
805
|
+
const holders = mode === "all" ? operands : operands.filter((o) => o.keys.has(key));
|
|
806
|
+
const only = holders.length === 1 ? holders[0] : void 0;
|
|
807
|
+
const n = only?.cards?.get(key);
|
|
808
|
+
const viaIndex = n !== void 0 && !excludes.some((e) => e.keys.has(key)) && !(w !== null && isEdge(key, w));
|
|
809
|
+
steps.push({ key, n: viaIndex ? n : -1 });
|
|
810
|
+
if (!viaIndex) decode.push(key);
|
|
811
|
+
}
|
|
812
|
+
return { steps, decode };
|
|
813
|
+
}
|
|
802
814
|
var SegmentEngine = class _SegmentEngine {
|
|
803
815
|
storage;
|
|
804
816
|
cache;
|
|
@@ -1201,9 +1213,7 @@ var SegmentEngine = class _SegmentEngine {
|
|
|
1201
1213
|
const expected = counts.get(chunkKey);
|
|
1202
1214
|
if (chunk === null || chunk.size !== expected) {
|
|
1203
1215
|
const got = chunk === null ? "no bytes" : `${chunk.size} ids`;
|
|
1204
|
-
throw new IntegrityError(
|
|
1205
|
-
`chunk ${chunkKey} holds ${got}, but the object's index says ${expected}`
|
|
1206
|
-
);
|
|
1216
|
+
throw new IntegrityError(`a chunk holds ${got}, but the object's index says ${expected}`);
|
|
1207
1217
|
}
|
|
1208
1218
|
return chunk.toUint32Array ? chunk.toUint32Array() : Uint32Array.from(chunk);
|
|
1209
1219
|
}
|
|
@@ -1518,6 +1528,209 @@ var SegmentEngine = class _SegmentEngine {
|
|
|
1518
1528
|
this.closeCombine(run.plan);
|
|
1519
1529
|
}
|
|
1520
1530
|
}
|
|
1531
|
+
/**
|
|
1532
|
+
* How many ids `segs[0] ∩ …` (`mode` 'all') or `segs[0] ∪ …` ('any') holds, minus `excludeSegs`, without streaming
|
|
1533
|
+
* them: the size of what {@link combine} would stream. Each chunk key is taken one of two ways. A key exactly one
|
|
1534
|
+
* include holds, that no exclude holds and the range does not cut, is answered from the index's count for it, with
|
|
1535
|
+
* no chunk read. Every other key is decoded and counted as the combine reads it, with the same plan, the same ranges
|
|
1536
|
+
* and the same window, over those keys alone. So an intersection reads what its stream reads, and a union or a
|
|
1537
|
+
* difference reads no more than its stream.
|
|
1538
|
+
*
|
|
1539
|
+
* The keys answered from the index are trusted as {@link count} trusts the index, and checked no further than the
|
|
1540
|
+
* reader checks it when it opens the object; a decoded key is exact. A source that reports no per-chunk counts has
|
|
1541
|
+
* every key decoded. A union or a difference of one operand with no exclude and no range is {@link count}.
|
|
1542
|
+
*
|
|
1543
|
+
* A running union or difference, or an intersection with an `exclude`, looks at its operands' versions every
|
|
1544
|
+
* `concurrency` keys, a key answered from the index being one taken, and any count looks before the first key after
|
|
1545
|
+
* an invalidation, and runs `options.guard` at each look. On a move it plans again above the last key taken, as a
|
|
1546
|
+
* combine does, so the bound on what it serves of an earlier generation is the combine's. A pinned operand's object
|
|
1547
|
+
* is looked up before the first key and at each look there is (one footer read each, a fault other than a transient
|
|
1548
|
+
* one raised as it is), so an erasure of what it pinned stops the count there with the pin's `NotFoundError`. An
|
|
1549
|
+
* intersection of two or more operands with no exclude decodes every key and so shows a move without a version
|
|
1550
|
+
* look; its guard still runs every `concurrency` keys.
|
|
1551
|
+
*/
|
|
1552
|
+
async combineCount(segs, excludeSegs, mode, op, options) {
|
|
1553
|
+
const label = `${op}Count`;
|
|
1554
|
+
if (segs.length === 0) throw new ValidationError(`${label} requires at least one segment`);
|
|
1555
|
+
const limit = options?.concurrency === void 0 ? DEFAULT_INTERSECT_CONCURRENCY : options.concurrency;
|
|
1556
|
+
requirePositiveInteger(limit, `${label}: concurrency`);
|
|
1557
|
+
const budget = resolvePerOpBudget(options?.budget, this.budget);
|
|
1558
|
+
const w = windowOf2(options, label);
|
|
1559
|
+
options?.guard?.();
|
|
1560
|
+
if (w === "empty") return 0;
|
|
1561
|
+
if (op !== "intersect" && segs.length === 1 && excludeSegs.length === 0 && w === null) {
|
|
1562
|
+
const n = await this.count(segs[0]);
|
|
1563
|
+
if (n === 0) {
|
|
1564
|
+
const none = {
|
|
1565
|
+
seg: segs[0],
|
|
1566
|
+
keys: /* @__PURE__ */ new Set(),
|
|
1567
|
+
chunkless: true,
|
|
1568
|
+
gen: void 0,
|
|
1569
|
+
epoch: 0
|
|
1570
|
+
};
|
|
1571
|
+
await this.refuseAbsentOperands([none], options?.allowAbsentOperands, label);
|
|
1572
|
+
}
|
|
1573
|
+
return n;
|
|
1574
|
+
}
|
|
1575
|
+
const epoch = this.scope.epoch;
|
|
1576
|
+
const [operands, excludes] = await this.countOperands(segs, excludeSegs, mode, w, -1);
|
|
1577
|
+
await this.refuseAbsentOperands(
|
|
1578
|
+
[...operands, ...excludes],
|
|
1579
|
+
options?.allowAbsentOperands,
|
|
1580
|
+
label
|
|
1581
|
+
);
|
|
1582
|
+
await this.verifyPinned([...operands, ...excludes]);
|
|
1583
|
+
const { steps, decode } = countSteps(operands, excludes, mode, w);
|
|
1584
|
+
checkBudget(budget, combineReads(decode, operands, excludes, mode, Infinity), label);
|
|
1585
|
+
if (this.metricsOn) {
|
|
1586
|
+
const distinct = /* @__PURE__ */ new Set();
|
|
1587
|
+
for (const o of [...operands, ...excludes]) for (const k of o.keys) distinct.add(k);
|
|
1588
|
+
this.metrics.onEvent({
|
|
1589
|
+
kind: "intersect",
|
|
1590
|
+
op,
|
|
1591
|
+
operands: segs.length + excludes.length,
|
|
1592
|
+
fetchedChunks: decode.length,
|
|
1593
|
+
skippedChunks: distinct.size - decode.length
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
const everyKey = mode === "all" && segs.length > 1 && excludeSegs.length === 0;
|
|
1597
|
+
const run = {
|
|
1598
|
+
segs,
|
|
1599
|
+
excludeSegs,
|
|
1600
|
+
mode,
|
|
1601
|
+
op,
|
|
1602
|
+
limit,
|
|
1603
|
+
budget,
|
|
1604
|
+
w,
|
|
1605
|
+
plan: this.openCombine(operands, excludes, mode, decode, limit),
|
|
1606
|
+
looks: this.looks(everyKey ? Infinity : limit, epoch),
|
|
1607
|
+
spent: 0,
|
|
1608
|
+
steps,
|
|
1609
|
+
at: 0,
|
|
1610
|
+
guard: options?.guard,
|
|
1611
|
+
...everyKey && options?.guard !== void 0 ? { guardEvery: { since: 0 } } : {}
|
|
1612
|
+
};
|
|
1613
|
+
try {
|
|
1614
|
+
return await this.runCount(run);
|
|
1615
|
+
} finally {
|
|
1616
|
+
this.closeCombine(run.plan);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
/** Take every step of `run`: the index's figure for one, the size of the next decoded chunk for another. */
|
|
1620
|
+
async runCount(run) {
|
|
1621
|
+
let total = 0;
|
|
1622
|
+
let last = -1;
|
|
1623
|
+
while (run.at < run.steps.length) {
|
|
1624
|
+
run.looks.left -= 1;
|
|
1625
|
+
if (run.guardEvery !== void 0) this.guardAtCadence(run);
|
|
1626
|
+
if (run.plan.moves.moved || this.lookDue(run.looks)) {
|
|
1627
|
+
await this.countLook(run, last);
|
|
1628
|
+
if (run.at >= run.steps.length) break;
|
|
1629
|
+
}
|
|
1630
|
+
const step = run.steps[run.at++];
|
|
1631
|
+
last = step.key;
|
|
1632
|
+
if (step.n >= 0) {
|
|
1633
|
+
total += step.n;
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
const slot = await run.plan.window.next();
|
|
1637
|
+
const chunk = slot?.result;
|
|
1638
|
+
if (chunk === null || chunk === void 0 || chunk.isEmpty) continue;
|
|
1639
|
+
total += run.w !== null && isEdge(step.key, run.w) ? cutToWindow(this.codec, chunk, step.key, run.w).size : chunk.size;
|
|
1640
|
+
}
|
|
1641
|
+
return total;
|
|
1642
|
+
}
|
|
1643
|
+
/** Run the guard every `limit` keys when the version looks are off (an AND of two or more includes). */
|
|
1644
|
+
guardAtCadence(run) {
|
|
1645
|
+
const cadence = run.guardEvery;
|
|
1646
|
+
if (cadence.since >= run.limit) {
|
|
1647
|
+
cadence.since = 0;
|
|
1648
|
+
run.guard();
|
|
1649
|
+
}
|
|
1650
|
+
cadence.since += 1;
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Look up each pinned operand's generation object, as a pinned read finds it: one footer read per pinned operand, and
|
|
1654
|
+
* the pin's `NotFoundError` when the object is gone or is another. An operand that is not pinned costs nothing.
|
|
1655
|
+
*/
|
|
1656
|
+
async verifyPinned(operands) {
|
|
1657
|
+
const verify = this.storage.verifyPinned;
|
|
1658
|
+
if (verify === void 0) return;
|
|
1659
|
+
await boundedAll(operands, OPERANDS_AT_ONCE, (o) => verify.call(this.storage, o.seg));
|
|
1660
|
+
}
|
|
1661
|
+
/** Look at every operand's version, as {@link nextSlotAfterLook} does, and plan the keys above `last` again on a move. */
|
|
1662
|
+
async countLook(run, last) {
|
|
1663
|
+
const plan = run.plan;
|
|
1664
|
+
plan.moves.moved = false;
|
|
1665
|
+
this.looked(run.looks);
|
|
1666
|
+
run.guard?.();
|
|
1667
|
+
const all = [...plan.operands, ...plan.excludes];
|
|
1668
|
+
await this.verifyPinned(all);
|
|
1669
|
+
const now = await boundedAll(all, OPERANDS_AT_ONCE, (o) => this.cacheVersion(o.seg));
|
|
1670
|
+
if (now.every((version, i) => version === all[i].gen)) return;
|
|
1671
|
+
this.closeCombine(plan);
|
|
1672
|
+
const spent = run.spent + combineReads(plan.keys, plan.operands, plan.excludes, run.mode, last);
|
|
1673
|
+
const [operands, excludes] = await this.countOperands(
|
|
1674
|
+
run.segs,
|
|
1675
|
+
run.excludeSegs,
|
|
1676
|
+
run.mode,
|
|
1677
|
+
run.w,
|
|
1678
|
+
last
|
|
1679
|
+
);
|
|
1680
|
+
const { steps, decode } = countSteps(operands, excludes, run.mode, run.w);
|
|
1681
|
+
checkBudget(
|
|
1682
|
+
run.budget,
|
|
1683
|
+
spent + combineReads(decode, operands, excludes, run.mode, Infinity),
|
|
1684
|
+
`${run.op}Count`
|
|
1685
|
+
);
|
|
1686
|
+
run.plan = this.openCombine(operands, excludes, run.mode, decode, run.limit);
|
|
1687
|
+
run.spent = spent;
|
|
1688
|
+
run.steps = steps;
|
|
1689
|
+
run.at = 0;
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* {@link combineOperands} for a count: each include also carries the per-chunk counts of the generation its keys were
|
|
1693
|
+
* listed from. The counts are asked for after the keys, and the two must name the same keys, or the operand is listed
|
|
1694
|
+
* again (a move between the two reads), and refused if a source keeps disagreeing with itself. A source with no
|
|
1695
|
+
* per-chunk counts gives none, and every key of that operand is decoded.
|
|
1696
|
+
*/
|
|
1697
|
+
async countOperands(segs, excludeSegs, mode, w, after) {
|
|
1698
|
+
const wantCounts = !(mode === "all" && segs.length > 1);
|
|
1699
|
+
const extract = async (seg, withCounts) => {
|
|
1700
|
+
for (let attempt = 0; attempt < COUNT_RELIST_ATTEMPTS; attempt++) {
|
|
1701
|
+
const { listed: listed2, gen, epoch } = await this.listKeys(seg);
|
|
1702
|
+
const cards = withCounts ? await this.chunkCounts(seg, listed2) : void 0;
|
|
1703
|
+
if (cards === "moved") continue;
|
|
1704
|
+
const keys = after < 0 && w === null ? listed2 : keysAbove(listed2, w, after);
|
|
1705
|
+
return { seg, keys: new Set(keys), chunkless: listed2.length === 0, gen, epoch, cards };
|
|
1706
|
+
}
|
|
1707
|
+
throw new IntegrityError("a segment's index and its chunk keys name different chunks");
|
|
1708
|
+
};
|
|
1709
|
+
const all = await boundedAll(
|
|
1710
|
+
[
|
|
1711
|
+
...segs.map((s) => [s, wantCounts]),
|
|
1712
|
+
...excludeSegs.map((s) => [s, false])
|
|
1713
|
+
],
|
|
1714
|
+
OPERANDS_AT_ONCE,
|
|
1715
|
+
([seg, withCounts]) => extract(seg, withCounts)
|
|
1716
|
+
);
|
|
1717
|
+
return [all.slice(0, segs.length), all.slice(segs.length)];
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* The per-chunk counts of `seg`, checked as untrusted tier data; `undefined` when the source reports none, and `'moved'`
|
|
1721
|
+
* when they name other keys than `listed`, which the segment's generation changing between the two reads explains.
|
|
1722
|
+
*/
|
|
1723
|
+
async chunkCounts(seg, listed2) {
|
|
1724
|
+
const counts = this.storage.cardinalities ? await this.storage.cardinalities(seg) : null;
|
|
1725
|
+
if (counts === null) return void 0;
|
|
1726
|
+
if (counts.size !== listed2.length) return "moved";
|
|
1727
|
+
for (const key of listed2) {
|
|
1728
|
+
const n = counts.get(key);
|
|
1729
|
+
if (n === void 0) return "moved";
|
|
1730
|
+
if (n !== 0) assertChunkCardinalityInRange(n);
|
|
1731
|
+
}
|
|
1732
|
+
return counts;
|
|
1733
|
+
}
|
|
1521
1734
|
/**
|
|
1522
1735
|
* The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
|
|
1523
1736
|
* Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
|
|
@@ -1871,7 +2084,7 @@ var SegmentEngine = class _SegmentEngine {
|
|
|
1871
2084
|
streamed.moves.moved = true;
|
|
1872
2085
|
}
|
|
1873
2086
|
if (read.bytes === null) return null;
|
|
1874
|
-
const bitmap = decodeChunkBytes(this.codec, read.bytes,
|
|
2087
|
+
const bitmap = decodeChunkBytes(this.codec, read.bytes, this.maxBitmapBytes);
|
|
1875
2088
|
if (this.cache && !streamed.invalidated && !this.scope.invalidatedSince(streamed.prefix, streamed.openedAt)) {
|
|
1876
2089
|
const version = this.streamedVersion(streamed.gen, read.version);
|
|
1877
2090
|
if (version !== null) {
|
|
@@ -2084,7 +2297,7 @@ var SegmentEngine = class _SegmentEngine {
|
|
|
2084
2297
|
}
|
|
2085
2298
|
}
|
|
2086
2299
|
if (!bytes) return null;
|
|
2087
|
-
const bitmap = decodeChunkBytes(this.codec, bytes,
|
|
2300
|
+
const bitmap = decodeChunkBytes(this.codec, bytes, this.maxBitmapBytes);
|
|
2088
2301
|
if (key !== "" && this.scope.isOpen(cacheKey, token)) this.cache?.set(key, bitmap);
|
|
2089
2302
|
return bitmap;
|
|
2090
2303
|
}
|
|
@@ -2545,7 +2758,7 @@ function* planChunkReads(extents, region) {
|
|
|
2545
2758
|
for (const extent of extents) {
|
|
2546
2759
|
const extentEnd = extent.offset + extent.length;
|
|
2547
2760
|
if (!(Number.isSafeInteger(extent.offset) && Number.isSafeInteger(extent.length) && extent.length >= 1 && extent.offset >= previousEnd && extentEnd <= region.end)) {
|
|
2548
|
-
throw crbmIntegrity(
|
|
2761
|
+
throw crbmIntegrity("a chunk is not inside the chunk region, in order");
|
|
2549
2762
|
}
|
|
2550
2763
|
previousEnd = extentEnd;
|
|
2551
2764
|
if (chunks.length > 0 && extent.offset - end <= MAX_COALESCE_GAP_BYTES && extentEnd - start <= MAX_COALESCED_READ_BYTES) {
|
|
@@ -2569,7 +2782,7 @@ function* planChunkReads(extents, region) {
|
|
|
2569
2782
|
var U32_CEIL = U32_MAX + 1;
|
|
2570
2783
|
function varintLength(value) {
|
|
2571
2784
|
if (!Number.isInteger(value) || value < 0 || value >= U32_CEIL) {
|
|
2572
|
-
throw new RangeError(
|
|
2785
|
+
throw new RangeError("varint value must be a u32");
|
|
2573
2786
|
}
|
|
2574
2787
|
if (value < 128) return 1;
|
|
2575
2788
|
if (value < 16384) return 2;
|
|
@@ -3016,7 +3229,7 @@ var CrbmReader = class _CrbmReader {
|
|
|
3016
3229
|
const offset = this.index.offsets[slot];
|
|
3017
3230
|
const length = this.index.lengths[slot];
|
|
3018
3231
|
if (offset < PAYLOAD_START || offset + length > this.objectSize - FOOTER_BYTES) {
|
|
3019
|
-
throw crbmIntegrity(`chunk ${
|
|
3232
|
+
throw crbmIntegrity(`chunk at position ${slot} payload out of bounds`);
|
|
3020
3233
|
}
|
|
3021
3234
|
const held = this.keptRange(offset, length);
|
|
3022
3235
|
return this.openChunk(slot, chunkKey, held ?? await this.blob.getRange(offset, length));
|
|
@@ -3043,11 +3256,11 @@ var CrbmReader = class _CrbmReader {
|
|
|
3043
3256
|
*/
|
|
3044
3257
|
openChunk(slot, chunkKey, bytes) {
|
|
3045
3258
|
if (crc32c(bytes) !== this.index.crcs[slot]) {
|
|
3046
|
-
throw crbmIntegrity(`chunk ${
|
|
3259
|
+
throw crbmIntegrity(`chunk at position ${slot} payload CRC mismatch`);
|
|
3047
3260
|
}
|
|
3048
3261
|
if (this.crypto === void 0) return bytes;
|
|
3049
3262
|
if (bytes.length < AEAD_NONCE_BYTES + AEAD_TAG_BYTES) {
|
|
3050
|
-
throw crbmIntegrity(`chunk ${
|
|
3263
|
+
throw crbmIntegrity(`chunk at position ${slot} encrypted payload too short`);
|
|
3051
3264
|
}
|
|
3052
3265
|
return this.crypto.aead.open(
|
|
3053
3266
|
{
|
|
@@ -3293,19 +3506,20 @@ function parseIndex(indexBytes, payloadEnd, maxPayloadBytes, minPayloadBytes = 1
|
|
|
3293
3506
|
const chunkKey = prevKey + keyDelta;
|
|
3294
3507
|
const offset = prevEnd + offDelta;
|
|
3295
3508
|
if (i > 0 && keyDelta === 0) {
|
|
3296
|
-
throw crbmIntegrity(`.crbm index
|
|
3509
|
+
throw crbmIntegrity(`.crbm index entry ${i} repeats a chunk key`);
|
|
3297
3510
|
}
|
|
3298
|
-
if (chunkKey > 65535)
|
|
3511
|
+
if (chunkKey > 65535)
|
|
3512
|
+
throw crbmIntegrity(`.crbm index entry ${i} has a chunk key out of range`);
|
|
3299
3513
|
if (len < minPayloadBytes || len > maxPayloadBytes) {
|
|
3300
3514
|
throw crbmIntegrity(
|
|
3301
|
-
`.crbm
|
|
3515
|
+
`.crbm index entry ${i} has a payload length ${len} invalid (most is ${maxPayloadBytes})`
|
|
3302
3516
|
);
|
|
3303
3517
|
}
|
|
3304
3518
|
if (card < 1 || card > MAX_CHUNK_CARDINALITY) {
|
|
3305
|
-
throw crbmIntegrity(`.crbm
|
|
3519
|
+
throw crbmIntegrity(`.crbm index entry ${i} has a cardinality ${card} invalid`);
|
|
3306
3520
|
}
|
|
3307
3521
|
if (offset + len > payloadEnd) {
|
|
3308
|
-
throw crbmIntegrity(`.crbm
|
|
3522
|
+
throw crbmIntegrity(`.crbm index entry ${i} has a payload out of bounds`);
|
|
3309
3523
|
}
|
|
3310
3524
|
keys[count] = chunkKey;
|
|
3311
3525
|
cardinalityMinusOne[count] = card - 1;
|
|
@@ -4464,9 +4678,9 @@ var CrbmStorageChunkSource = class {
|
|
|
4464
4678
|
* ({@link replacedPins}), and a check under way is shared by the reads that ask at once; neither "gone" nor "the
|
|
4465
4679
|
* same" is kept, since either can change. A transient fault is rethrown, for the store's retries to see for what it
|
|
4466
4680
|
* is. A footer that cannot be read otherwise says nothing about which object is there, so this returns, and the
|
|
4467
|
-
* caller's own error stands.
|
|
4681
|
+
* caller's own error stands; a caller with no error of its own passes `strict`, and that fault is raised.
|
|
4468
4682
|
*/
|
|
4469
|
-
async throwIfReplaced(ref, generation, version, fingerprint) {
|
|
4683
|
+
async throwIfReplaced(ref, generation, version, fingerprint, strict = false) {
|
|
4470
4684
|
const held = this.heldKey(ref, version, fingerprint);
|
|
4471
4685
|
if (this.replacedPins.get(held) !== void 0) throw notThePinned(ref, generation);
|
|
4472
4686
|
let check = this.checking.get(held);
|
|
@@ -4494,11 +4708,20 @@ var CrbmStorageChunkSource = class {
|
|
|
4494
4708
|
try {
|
|
4495
4709
|
replaced = await check;
|
|
4496
4710
|
} catch (err) {
|
|
4497
|
-
if (isTransientError(err)) throw err;
|
|
4711
|
+
if (strict || isTransientError(err)) throw err;
|
|
4498
4712
|
return;
|
|
4499
4713
|
}
|
|
4500
4714
|
if (replaced) throw notThePinned(ref, generation);
|
|
4501
4715
|
}
|
|
4716
|
+
/**
|
|
4717
|
+
* Throws the pin's `NotFoundError` when the object under `generation`'s key is no longer the one `held` names: one
|
|
4718
|
+
* footer read, shared with a check already under way for the same pin. It fails closed: a footer that cannot be read,
|
|
4719
|
+
* or an object that is not a generation object of this store, raises its own fault rather than passing.
|
|
4720
|
+
*/
|
|
4721
|
+
async verifyHeld(ref, generation, held) {
|
|
4722
|
+
validateUserRef(ref, "read", PORT_KEYS);
|
|
4723
|
+
await this.throwIfReplaced(ref, generation, held.version, held.fingerprint, true);
|
|
4724
|
+
}
|
|
4502
4725
|
/** Chunk keys of a specific generation — the pinned shape read. `held` as for {@link getChunkAt}. */
|
|
4503
4726
|
async listChunkKeysAt(ref, generation, held) {
|
|
4504
4727
|
validateUserRef(ref, "read", PORT_KEYS);
|
|
@@ -5070,11 +5293,11 @@ var PinnedStorageChunkSource = class {
|
|
|
5070
5293
|
constructor(inner, pins) {
|
|
5071
5294
|
this.inner = inner;
|
|
5072
5295
|
this.pins = pins;
|
|
5073
|
-
for (const [
|
|
5296
|
+
for (const [pinKey, pin] of pins) {
|
|
5074
5297
|
const held = pin;
|
|
5075
5298
|
if (held.generation !== null && (typeof held.version !== "string" || typeof held.fingerprint !== "string"))
|
|
5076
5299
|
throw new ValidationError(
|
|
5077
|
-
`pin for "${
|
|
5300
|
+
`pin for "${pinKey}" names generation ${held.generation} without its version and object fingerprint`
|
|
5078
5301
|
);
|
|
5079
5302
|
}
|
|
5080
5303
|
}
|
|
@@ -5174,6 +5397,14 @@ var PinnedStorageChunkSource = class {
|
|
|
5174
5397
|
exists(ref) {
|
|
5175
5398
|
return this.inner.exists(ref);
|
|
5176
5399
|
}
|
|
5400
|
+
/** Whether the object this segment's pin holds is still there: see {@link HeldObjectLookup.verifyPinned}. */
|
|
5401
|
+
verifyPinned(ref) {
|
|
5402
|
+
const pin = this.pinFor(ref);
|
|
5403
|
+
const held = pin === void 0 ? void 0 : heldBy(pin);
|
|
5404
|
+
if (pin === void 0 || held === void 0 || pin.generation === null)
|
|
5405
|
+
return Promise.resolve();
|
|
5406
|
+
return this.inner.verifyHeld(ref, pin.generation, held);
|
|
5407
|
+
}
|
|
5177
5408
|
invalidate(ref) {
|
|
5178
5409
|
this.inner.invalidate(ref);
|
|
5179
5410
|
}
|
|
@@ -5275,14 +5506,15 @@ function plainView(bytes, what) {
|
|
|
5275
5506
|
}
|
|
5276
5507
|
return length === 0 ? new Uint8Array(0) : new Uint8Array(buffer, offset, length);
|
|
5277
5508
|
}
|
|
5278
|
-
var
|
|
5509
|
+
var plainByteLength = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, "byteLength")?.get;
|
|
5510
|
+
var hasSharedMemory = typeof SharedArrayBuffer === "function";
|
|
5279
5511
|
function isShared(buffer) {
|
|
5280
|
-
if (
|
|
5512
|
+
if (!hasSharedMemory) return false;
|
|
5281
5513
|
try {
|
|
5282
|
-
|
|
5283
|
-
return true;
|
|
5284
|
-
} catch {
|
|
5514
|
+
plainByteLength.call(buffer);
|
|
5285
5515
|
return false;
|
|
5516
|
+
} catch {
|
|
5517
|
+
return true;
|
|
5286
5518
|
}
|
|
5287
5519
|
}
|
|
5288
5520
|
function overCap(what, length) {
|
|
@@ -5323,11 +5555,22 @@ function prepareLoadInput(input, codec) {
|
|
|
5323
5555
|
`a load takes ids (an iterable of integers), { serialized } or { bitmap }, as the only key; got an object with keys [${keys.join(", ")}]. Load options go in the next argument.`
|
|
5324
5556
|
);
|
|
5325
5557
|
}
|
|
5326
|
-
function decodeSerialized(bytes, codec, what = "the buffer") {
|
|
5558
|
+
function decodeSerialized(bytes, codec, what = "the buffer", options = {}) {
|
|
5327
5559
|
if (typeof bytes !== "object" || bytes === null || tagOf(bytes) !== "Uint8Array") {
|
|
5328
5560
|
throw new ValidationError(`${what} must be a Uint8Array of portable Roaring bytes`);
|
|
5329
5561
|
}
|
|
5330
5562
|
const view = plainView(bytes, what);
|
|
5563
|
+
if (view.byteLength === 0 && options.refuseEmpty === true) {
|
|
5564
|
+
throw new ValidationError(
|
|
5565
|
+
`${what} is empty or detached: an empty buffer is not a bitmap, and an empty bitmap is built without bytes`
|
|
5566
|
+
);
|
|
5567
|
+
}
|
|
5568
|
+
const { maxBytes } = options;
|
|
5569
|
+
if (maxBytes !== void 0 && maxBytes < MAX_SERIALIZED_LOAD_BYTES && view.byteLength > maxBytes) {
|
|
5570
|
+
throw new ValidationError(
|
|
5571
|
+
`${what} is ${view.byteLength} bytes, more than the ${maxBytes} this call allows (maxBytes)`
|
|
5572
|
+
);
|
|
5573
|
+
}
|
|
5331
5574
|
if (view.byteLength > MAX_SERIALIZED_LOAD_BYTES) throw overCap(what, view.byteLength);
|
|
5332
5575
|
const own = isShared(view.buffer) ? new Uint8Array(view) : view;
|
|
5333
5576
|
try {
|
|
@@ -5465,12 +5708,10 @@ var CrbmWriter = class {
|
|
|
5465
5708
|
async addChunk(chunkKey, payload, cardinality) {
|
|
5466
5709
|
if (this.finished) throw new ValidationError("CrbmWriter already finished");
|
|
5467
5710
|
if (!Number.isInteger(chunkKey) || chunkKey < 0 || chunkKey > 65535) {
|
|
5468
|
-
throw new ValidationError(
|
|
5711
|
+
throw new ValidationError("chunkKey must be a u16");
|
|
5469
5712
|
}
|
|
5470
5713
|
if (chunkKey <= this.lastKey) {
|
|
5471
|
-
throw new ValidationError(
|
|
5472
|
-
`chunkKey ${chunkKey} not strictly ascending (last ${this.lastKey})`
|
|
5473
|
-
);
|
|
5714
|
+
throw new ValidationError("chunkKey is not strictly ascending");
|
|
5474
5715
|
}
|
|
5475
5716
|
if (!Number.isInteger(cardinality) || cardinality < 1 || cardinality > MAX_CHUNK_CARDINALITY) {
|
|
5476
5717
|
throw new ValidationError(
|
|
@@ -6280,7 +6521,7 @@ async function collectChunks(input, codec, clock) {
|
|
|
6280
6521
|
throw new ValidationError("a chunk's key must be an integer in [0, 65535]");
|
|
6281
6522
|
}
|
|
6282
6523
|
if (chunkKey <= last) {
|
|
6283
|
-
throw new ValidationError(
|
|
6524
|
+
throw new ValidationError("a chunk's key does not ascend past the previous chunk's");
|
|
6284
6525
|
}
|
|
6285
6526
|
last = chunkKey;
|
|
6286
6527
|
const max = bitmap.maximum?.();
|
|
@@ -8671,7 +8912,7 @@ var Run = class {
|
|
|
8671
8912
|
failOperand(
|
|
8672
8913
|
c,
|
|
8673
8914
|
new IntegrityError(
|
|
8674
|
-
`operand "${c.op.spec.name}" lists chunk
|
|
8915
|
+
`operand "${c.op.spec.name}" lists a chunk its generation holds no bytes for`
|
|
8675
8916
|
),
|
|
8676
8917
|
victims ?? wanting(c)
|
|
8677
8918
|
);
|
|
@@ -8680,13 +8921,10 @@ var Run = class {
|
|
|
8680
8921
|
try {
|
|
8681
8922
|
const resident = residentBytes(bytes.length);
|
|
8682
8923
|
if (!this.ledger.reserve(resident, makeRoom)) {
|
|
8683
|
-
throw this.refuse(resident, `operand "${c.op.spec.name}" at
|
|
8924
|
+
throw this.refuse(resident, `operand "${c.op.spec.name}" at one of its chunks`);
|
|
8684
8925
|
}
|
|
8685
8926
|
held += resident;
|
|
8686
|
-
decoded.set(
|
|
8687
|
-
c.op.index,
|
|
8688
|
-
decodeChunkBytes(this.codec, bytes, key, this.maxBitmapBytes)
|
|
8689
|
-
);
|
|
8927
|
+
decoded.set(c.op.index, decodeChunkBytes(this.codec, bytes, this.maxBitmapBytes));
|
|
8690
8928
|
} catch (error) {
|
|
8691
8929
|
failOperand(c, error, victims ?? wanting(c));
|
|
8692
8930
|
}
|
|
@@ -9593,7 +9831,7 @@ async function holds(c, generation) {
|
|
|
9593
9831
|
}
|
|
9594
9832
|
throw err;
|
|
9595
9833
|
}
|
|
9596
|
-
return bytes !== null && codec
|
|
9834
|
+
return bytes !== null && decodeChunkBytes(codec, bytes, maxBytes).has(remainder);
|
|
9597
9835
|
} catch (err) {
|
|
9598
9836
|
if (isNotFoundError(err)) return null;
|
|
9599
9837
|
throw err;
|
|
@@ -10004,8 +10242,7 @@ async function stage(c) {
|
|
|
10004
10242
|
c.fromFingerprint = reader.fingerprint;
|
|
10005
10243
|
const bytes = await read(() => reader.getChunk(chunkKey));
|
|
10006
10244
|
if (bytes === null) return await notInCurrent(c);
|
|
10007
|
-
const target = codec
|
|
10008
|
-
assertRemaindersInRange(target, chunkKey);
|
|
10245
|
+
const target = decodeChunkBytes(codec, bytes, maxBytes);
|
|
10009
10246
|
if (!target.has(remainder)) return await notInCurrent(c);
|
|
10010
10247
|
target.remove(remainder);
|
|
10011
10248
|
assertRegistryCanWrite(deps.registry, "eraseIdFromSegment");
|
|
@@ -10291,26 +10528,17 @@ async function* rewrite(reader, chunkKey, replacement, codec, maxBytes, read) {
|
|
|
10291
10528
|
}
|
|
10292
10529
|
const item = await stream.next();
|
|
10293
10530
|
if (item.done === true || item.value.key !== k) {
|
|
10294
|
-
throw new IntegrityError(
|
|
10531
|
+
throw new IntegrityError("the chunk stream ended before a chunk the rewrite needs");
|
|
10295
10532
|
}
|
|
10296
10533
|
const bytes = item.value.bytes;
|
|
10297
10534
|
if (bytes === null) continue;
|
|
10298
|
-
const bitmap = codec
|
|
10299
|
-
assertRemaindersInRange(bitmap, k);
|
|
10535
|
+
const bitmap = decodeChunkBytes(codec, bytes, maxBytes);
|
|
10300
10536
|
yield { chunkKey: k, bitmap };
|
|
10301
10537
|
}
|
|
10302
10538
|
} finally {
|
|
10303
10539
|
await stream.return(void 0);
|
|
10304
10540
|
}
|
|
10305
10541
|
}
|
|
10306
|
-
function assertRemaindersInRange(bitmap, chunkKey) {
|
|
10307
|
-
const max = bitmap.maximum?.();
|
|
10308
|
-
if (max !== void 0 && max > MAX_REMAINDER) {
|
|
10309
|
-
throw new IntegrityError(
|
|
10310
|
-
`chunk ${chunkKey} payload holds value ${max}, outside the 16-bit remainder range [0, ${MAX_REMAINDER}] \u2014 the stored object is corrupt or was not written by this codec; refusing to carry it into a new generation`
|
|
10311
|
-
);
|
|
10312
|
-
}
|
|
10313
|
-
}
|
|
10314
10542
|
|
|
10315
10543
|
// src/core/cas-retry.ts
|
|
10316
10544
|
async function casRetry(deps, ref, plan, step) {
|
|
@@ -12029,6 +12257,7 @@ var RetryingStorageChunkSource = class {
|
|
|
12029
12257
|
invalidate;
|
|
12030
12258
|
exists;
|
|
12031
12259
|
currentVersion;
|
|
12260
|
+
verifyPinned;
|
|
12032
12261
|
constructor(inner, opts) {
|
|
12033
12262
|
this.inner = inner;
|
|
12034
12263
|
const r = toRetry(opts);
|
|
@@ -12074,6 +12303,10 @@ var RetryingStorageChunkSource = class {
|
|
|
12074
12303
|
if (innerExists) {
|
|
12075
12304
|
this.exists = (ref) => withRetry(() => innerExists.call(inner, ref), this.policy, this.deps);
|
|
12076
12305
|
}
|
|
12306
|
+
const innerVerify = inner.verifyPinned;
|
|
12307
|
+
if (innerVerify) {
|
|
12308
|
+
this.verifyPinned = (ref) => withRetry(() => innerVerify.call(inner, ref), this.policy, this.deps);
|
|
12309
|
+
}
|
|
12077
12310
|
const innerCurrentVersion = inner.currentVersion;
|
|
12078
12311
|
if (innerCurrentVersion) {
|
|
12079
12312
|
this.currentVersion = (ref) => withRetry(() => innerCurrentVersion.call(inner, ref), this.policy, this.deps);
|
|
@@ -12104,6 +12337,7 @@ export {
|
|
|
12104
12337
|
LeaseLimitError,
|
|
12105
12338
|
MAX_LEASES_PER_SEGMENT,
|
|
12106
12339
|
MAX_LEASE_MS,
|
|
12340
|
+
MAX_SERIALIZED_LOAD_BYTES,
|
|
12107
12341
|
MIN_EXPIRES_AT_MS,
|
|
12108
12342
|
NOOP_METRICS,
|
|
12109
12343
|
NotFoundError,
|
|
@@ -12139,6 +12373,7 @@ export {
|
|
|
12139
12373
|
mapWithConcurrency,
|
|
12140
12374
|
readRetentionPolicy,
|
|
12141
12375
|
rebindCombineMany,
|
|
12376
|
+
refusalOfId,
|
|
12142
12377
|
refuseUnknownRefKeys,
|
|
12143
12378
|
releaseLease,
|
|
12144
12379
|
resolveBudget,
|