@cloudbitmaps/core 0.25.0 → 0.27.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-45ML6YU4.js → chunk-A2UDUKNL.js} +2 -2
- package/dist/{chunk-WPER7R2R.js → chunk-KO3Z2RBA.js} +2 -2
- package/dist/{chunk-2B2QF4FU.js → chunk-UUNBQVKR.js} +203 -203
- package/dist/chunk-UUNBQVKR.js.map +7 -0
- package/dist/{chunk-BUDAVGBI.js → chunk-YNNGMHX2.js} +2 -2
- package/dist/core/codec.d.ts +2 -2
- package/dist/core/crbm-storage-source.d.ts +48 -2
- package/dist/core/engine.d.ts +53 -0
- package/dist/core/erase-id.d.ts +2 -1
- package/dist/core/erasure.d.ts +2 -1
- package/dist/core/load.d.ts +11 -0
- package/dist/core/pinned-storage-source.d.ts +11 -1
- package/dist/core/ports.d.ts +16 -1
- package/dist/core/rollback.d.ts +2 -1
- package/dist/driver-kit.js +16 -4
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/retry/retrying-chunk-source.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +513 -46
- package/dist/index.js.map +4 -4
- package/dist/node.js +9 -6
- package/dist/node.js.map +2 -2
- package/package.json +1 -1
- package/dist/chunk-2B2QF4FU.js.map +0 -7
- /package/dist/{chunk-45ML6YU4.js.map → chunk-A2UDUKNL.js.map} +0 -0
- /package/dist/{chunk-WPER7R2R.js.map → chunk-KO3Z2RBA.js.map} +0 -0
- /package/dist/{chunk-BUDAVGBI.js.map → chunk-YNNGMHX2.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-YNNGMHX2.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-KO3Z2RBA.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-UUNBQVKR.js";
|
|
120
120
|
|
|
121
121
|
// src/core/budget.ts
|
|
122
122
|
var DEFAULT_BUDGET = { maxRequests: 1e6 };
|
|
@@ -220,7 +220,7 @@ function assertChunkPayloadInRange(bitmap) {
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
function decodeChunkBytes(codec, bytes, maxBitmapBytes) {
|
|
223
|
-
const bitmap = codec.safeDeserialize(bytes, maxBitmapBytes);
|
|
223
|
+
const bitmap = codec.safeDeserialize(bytes, maxBitmapBytes, { whole: true });
|
|
224
224
|
assertChunkPayloadInRange(bitmap);
|
|
225
225
|
return bitmap;
|
|
226
226
|
}
|
|
@@ -674,6 +674,7 @@ var COMBINE_WINDOW_START = 8;
|
|
|
674
674
|
var COMBINE_RANGE_START = 4;
|
|
675
675
|
var OPERANDS_AT_ONCE = 16;
|
|
676
676
|
var BATCH_QUIET_HOPS = 16;
|
|
677
|
+
var COUNT_RELIST_ATTEMPTS = 4;
|
|
677
678
|
var ZERO_CLOCK = { now: () => 0 };
|
|
678
679
|
var CombineWindow = class {
|
|
679
680
|
constructor(keys, limit, read) {
|
|
@@ -797,6 +798,19 @@ function combineReads(keys, operands, excludes, mode, through) {
|
|
|
797
798
|
}
|
|
798
799
|
return units;
|
|
799
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
|
+
}
|
|
800
814
|
var SegmentEngine = class _SegmentEngine {
|
|
801
815
|
storage;
|
|
802
816
|
cache;
|
|
@@ -1514,6 +1528,209 @@ var SegmentEngine = class _SegmentEngine {
|
|
|
1514
1528
|
this.closeCombine(run.plan);
|
|
1515
1529
|
}
|
|
1516
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
|
+
}
|
|
1517
1734
|
/**
|
|
1518
1735
|
* The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
|
|
1519
1736
|
* Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
|
|
@@ -2635,8 +2852,39 @@ function checkedFooter(tail, size) {
|
|
|
2635
2852
|
if (crc32c(footer.subarray(0, FOOTER_CRC_COVERAGE)) !== storedFooterCrc) {
|
|
2636
2853
|
throw crbmIntegrity(".crbm footer CRC mismatch");
|
|
2637
2854
|
}
|
|
2855
|
+
if (footer[FOOTER.versionMajor] === VERSION_MAJOR) checkFooterFields(footer, fview, size);
|
|
2638
2856
|
return { footer, fview, storedFooterCrc };
|
|
2639
2857
|
}
|
|
2858
|
+
function checkPreamble(tail, versionMajor, versionMinor) {
|
|
2859
|
+
if (!magicMatches(tail, 0) || tail[4] !== versionMajor || tail[5] !== versionMinor) {
|
|
2860
|
+
throw crbmIntegrity(".crbm preamble magic/version mismatch with footer");
|
|
2861
|
+
}
|
|
2862
|
+
if (tail[6] !== 0 || tail[7] !== 0) {
|
|
2863
|
+
throw crbmIntegrity(".crbm preamble reserved bytes are not zero");
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
function allZero(bytes, from, to) {
|
|
2867
|
+
for (let i = from; i < to; i++) if (bytes[i] !== 0) return false;
|
|
2868
|
+
return true;
|
|
2869
|
+
}
|
|
2870
|
+
function checkFooterFields(footer, fview, size) {
|
|
2871
|
+
if (fview.getUint16(FOOTER.reserved2, true) !== 0) {
|
|
2872
|
+
throw crbmIntegrity(".crbm footer reserved2 is not zero");
|
|
2873
|
+
}
|
|
2874
|
+
if (!allZero(footer, FOOTER.keyId, FOOTER.chunkCount)) {
|
|
2875
|
+
throw crbmIntegrity(".crbm footer key_id is not zero");
|
|
2876
|
+
}
|
|
2877
|
+
if ((fview.getUint32(FOOTER.flags, true) & FLAG_ENCRYPTED) === 0 && !allZero(footer, FOOTER.indexTag, FOOTER.indexTag + AEAD_TAG_BYTES)) {
|
|
2878
|
+
throw crbmIntegrity(".crbm cleartext footer index_tag is not zero");
|
|
2879
|
+
}
|
|
2880
|
+
const indexOffset = readU64(fview, FOOTER.indexOffset, "index_offset");
|
|
2881
|
+
const indexLength = readU64(fview, FOOTER.indexLength, "index_length");
|
|
2882
|
+
if (indexOffset + indexLength !== size - FOOTER_BYTES) {
|
|
2883
|
+
throw crbmIntegrity(
|
|
2884
|
+
`.crbm index region [${indexOffset}, +${indexLength}) does not end where the footer begins (${size - FOOTER_BYTES})`
|
|
2885
|
+
);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2640
2888
|
async function readFooter(blob) {
|
|
2641
2889
|
const { bytes: tail, size } = await blob.getTail(FOOTER_BYTES);
|
|
2642
2890
|
const { fview, storedFooterCrc } = checkedFooter(tail, size);
|
|
@@ -2806,7 +3054,7 @@ var CrbmReader = class _CrbmReader {
|
|
|
2806
3054
|
}
|
|
2807
3055
|
const versionMinor = footer[FOOTER.versionMinor];
|
|
2808
3056
|
const hasExtension = (flags & FLAG_EXTENSION) !== 0;
|
|
2809
|
-
if (indexOffset < PAYLOAD_START
|
|
3057
|
+
if (indexOffset < PAYLOAD_START) {
|
|
2810
3058
|
throw crbmIntegrity(`.crbm index region [${indexOffset}, +${indexLength}) out of bounds`);
|
|
2811
3059
|
}
|
|
2812
3060
|
if (indexLength > maxIndexBytes) {
|
|
@@ -2814,9 +3062,7 @@ var CrbmReader = class _CrbmReader {
|
|
|
2814
3062
|
}
|
|
2815
3063
|
const tailCoversFrom = size - tail.length;
|
|
2816
3064
|
if (tailCoversFrom === 0) {
|
|
2817
|
-
|
|
2818
|
-
throw crbmIntegrity(".crbm preamble magic/version mismatch with footer");
|
|
2819
|
-
}
|
|
3065
|
+
checkPreamble(tail, versionMajor, versionMinor);
|
|
2820
3066
|
}
|
|
2821
3067
|
const lowest = hasExtension ? Math.max(PAYLOAD_START, indexOffset - EXT_TRAILER_BYTES - MAX_EXT_BYTES) : indexOffset;
|
|
2822
3068
|
let region;
|
|
@@ -3301,6 +3547,8 @@ function parseIndex(indexBytes, payloadEnd, maxPayloadBytes, minPayloadBytes = 1
|
|
|
3301
3547
|
if (card < 1 || card > MAX_CHUNK_CARDINALITY) {
|
|
3302
3548
|
throw crbmIntegrity(`.crbm index entry ${i} has a cardinality ${card} invalid`);
|
|
3303
3549
|
}
|
|
3550
|
+
if (offDelta !== 0)
|
|
3551
|
+
throw crbmIntegrity(`.crbm index entry ${i} leaves a gap before its payload`);
|
|
3304
3552
|
if (offset + len > payloadEnd) {
|
|
3305
3553
|
throw crbmIntegrity(`.crbm index entry ${i} has a payload out of bounds`);
|
|
3306
3554
|
}
|
|
@@ -3314,6 +3562,11 @@ function parseIndex(indexBytes, payloadEnd, maxPayloadBytes, minPayloadBytes = 1
|
|
|
3314
3562
|
prevKey = chunkKey;
|
|
3315
3563
|
prevEnd = offset + len;
|
|
3316
3564
|
}
|
|
3565
|
+
if (prevEnd !== payloadEnd) {
|
|
3566
|
+
throw crbmIntegrity(
|
|
3567
|
+
`.crbm payloads end at ${prevEnd}, not at the end of the payload region ${payloadEnd}`
|
|
3568
|
+
);
|
|
3569
|
+
}
|
|
3317
3570
|
if (count === capacity) {
|
|
3318
3571
|
return { keys, cardinalityMinusOne, lengths, crcs, offsets, cardinalitySum };
|
|
3319
3572
|
}
|
|
@@ -3601,6 +3854,9 @@ var notThePinned = (ref, generation) => new NotFoundError(
|
|
|
3601
3854
|
var goneUnder = (ref, generation) => new NotFoundError(
|
|
3602
3855
|
`segment "${ref.segment}" generation ${generation} can no longer be read: its registry row is gone or destroyed`
|
|
3603
3856
|
);
|
|
3857
|
+
var pinGone = (ref, generation) => new NotFoundError(
|
|
3858
|
+
`segment "${ref.segment}" generation ${generation}, which this handle pinned, can no longer be read: its registry row is gone or destroyed`
|
|
3859
|
+
);
|
|
3604
3860
|
function genKeyOf(ref, target) {
|
|
3605
3861
|
return { namespace: ref.namespace, segment: ref.segment, generation: target.generation };
|
|
3606
3862
|
}
|
|
@@ -3816,6 +4072,10 @@ var CrbmStorageChunkSource = class {
|
|
|
3816
4072
|
maxBytes: (maxOpenIndexBytes ?? DEFAULT_MAX_OPEN_INDEX_BYTES) / RESOLUTION_BYTES_DIVISOR,
|
|
3817
4073
|
clock: { now: () => 0 }
|
|
3818
4074
|
}) : void 0;
|
|
4075
|
+
this.pinRows = this.timedRefresh ? new BoundedLru({
|
|
4076
|
+
maxEntries: maxOpenSegments ?? DEFAULT_MAX_OPEN_SEGMENTS,
|
|
4077
|
+
clock: { now: () => 0 }
|
|
4078
|
+
}) : void 0;
|
|
3819
4079
|
this.distrusted = new BoundedLru({
|
|
3820
4080
|
maxEntries: maxOpenSegments ?? DEFAULT_MAX_OPEN_SEGMENTS,
|
|
3821
4081
|
clock: { now: () => 0 }
|
|
@@ -3870,6 +4130,8 @@ var CrbmStorageChunkSource = class {
|
|
|
3870
4130
|
* read. One forgotten before it ends, by {@link invalidate} or a pin, keeps nothing it finds.
|
|
3871
4131
|
*/
|
|
3872
4132
|
checking = /* @__PURE__ */ new Map();
|
|
4133
|
+
/** When each footer read in {@link checking} was sent. */
|
|
4134
|
+
footerSentAt = /* @__PURE__ */ new WeakMap();
|
|
3873
4135
|
/**
|
|
3874
4136
|
* How many times {@link invalidate} has run, so a read that waited can tell whether the store was told something
|
|
3875
4137
|
* meanwhile. An eviction from the reader cache is not an invalidation, and what a read found stands through one. It
|
|
@@ -3912,6 +4174,13 @@ var CrbmStorageChunkSource = class {
|
|
|
3912
4174
|
* lapse, so it is decided once.
|
|
3913
4175
|
*/
|
|
3914
4176
|
timedRefresh;
|
|
4177
|
+
/**
|
|
4178
|
+
* The row reads a pinned segment's rechecks share, by segment: one `registry.get` per `cache.genTtlMs` serves every pin,
|
|
4179
|
+
* handle and combine of the segment in this store. Only a source with a timed refresh keeps any.
|
|
4180
|
+
*/
|
|
4181
|
+
pinRows;
|
|
4182
|
+
/** Each pin's last check, by the record the handle holds, so a handle that is let go takes its record with it. */
|
|
4183
|
+
pinChecks = /* @__PURE__ */ new WeakMap();
|
|
3915
4184
|
/**
|
|
3916
4185
|
* The keys of the segments this source reads, kept rather than encoded again: a read asks for its segment's snapshot
|
|
3917
4186
|
* once per operand, and a stream before every cached chunk it serves, and encoding the names cost about a
|
|
@@ -4125,6 +4394,7 @@ var CrbmStorageChunkSource = class {
|
|
|
4125
4394
|
this.invalidated.invalidate(key);
|
|
4126
4395
|
this.snapshots.delete(key);
|
|
4127
4396
|
this.resolutions?.delete(key);
|
|
4397
|
+
this.pinRows?.delete(key);
|
|
4128
4398
|
const pinnedPrefix = `${key}@`;
|
|
4129
4399
|
this.distrusted.deleteWhere((k) => k.startsWith(pinnedPrefix));
|
|
4130
4400
|
this.snapshots.deleteWhere((k) => k.startsWith(pinnedPrefix));
|
|
@@ -4235,6 +4505,7 @@ var CrbmStorageChunkSource = class {
|
|
|
4235
4505
|
let installed;
|
|
4236
4506
|
let pinKey;
|
|
4237
4507
|
let memoised = false;
|
|
4508
|
+
const checkedAtMs = this.now();
|
|
4238
4509
|
if (this.registry !== void 0) {
|
|
4239
4510
|
const record = await this.registry.get(ref);
|
|
4240
4511
|
if (record === null || record.status === "destroyed")
|
|
@@ -4280,9 +4551,10 @@ var CrbmStorageChunkSource = class {
|
|
|
4280
4551
|
this.replacedPins.delete(held);
|
|
4281
4552
|
this.checking.delete(held);
|
|
4282
4553
|
}
|
|
4283
|
-
return { generation, version, fingerprint };
|
|
4554
|
+
return { generation, version, fingerprint, checkedAtMs };
|
|
4284
4555
|
}
|
|
4285
4556
|
async pinOnce(ref, lease) {
|
|
4557
|
+
const checkedAtMs = this.now();
|
|
4286
4558
|
const live = await this.resolveLive(ref);
|
|
4287
4559
|
if (live === null) return null;
|
|
4288
4560
|
if (lease !== void 0 && live.row !== void 0) {
|
|
@@ -4339,7 +4611,7 @@ var CrbmStorageChunkSource = class {
|
|
|
4339
4611
|
this.replacedPins.delete(held);
|
|
4340
4612
|
this.checking.delete(held);
|
|
4341
4613
|
}
|
|
4342
|
-
return { generation: target.generation, version, fingerprint: reader.fingerprint };
|
|
4614
|
+
return { generation: target.generation, version, fingerprint: reader.fingerprint, checkedAtMs };
|
|
4343
4615
|
}
|
|
4344
4616
|
/**
|
|
4345
4617
|
* Where a pinned reader of one object is memoised. With a registry that is its version, which names the object:
|
|
@@ -4375,7 +4647,7 @@ var CrbmStorageChunkSource = class {
|
|
|
4375
4647
|
* crypto-shred must not keep unwrapping a DEK whose wrapping the shred removed; a destroyed segment resolves no
|
|
4376
4648
|
* generation
|
|
4377
4649
|
* for anyone, and a pin's read of one fails. That holds from the pin's next open: a reader already open keeps
|
|
4378
|
-
* the key it unwrapped until it is evicted or
|
|
4650
|
+
* the key it unwrapped until it is evicted, invalidated, or its next check ({@link recheckPin}) finds the pin over.
|
|
4379
4651
|
*
|
|
4380
4652
|
* **And the object is checked**, when the caller says which one it pinned. A generation number is not an
|
|
4381
4653
|
* identity (invariant 1): once a name is purged and loaded again, its new segment starts again at generation 0,
|
|
@@ -4413,9 +4685,7 @@ var CrbmStorageChunkSource = class {
|
|
|
4413
4685
|
}
|
|
4414
4686
|
if (reader === null) {
|
|
4415
4687
|
if (version === void 0) return null;
|
|
4416
|
-
throw
|
|
4417
|
-
`segment "${ref.segment}" generation ${generation}, which this handle pinned, can no longer be read: its registry row is gone or destroyed`
|
|
4418
|
-
);
|
|
4688
|
+
throw pinGone(ref, generation);
|
|
4419
4689
|
}
|
|
4420
4690
|
if (fingerprint !== void 0 && reader.fingerprint !== fingerprint) {
|
|
4421
4691
|
if (this.invalidations === epoch)
|
|
@@ -4454,6 +4724,113 @@ var CrbmStorageChunkSource = class {
|
|
|
4454
4724
|
throw err;
|
|
4455
4725
|
}
|
|
4456
4726
|
}
|
|
4727
|
+
/**
|
|
4728
|
+
* Whether a pin's segment is due a look at its row: the pin was not checked within `cache.genTtlMs` (counted by
|
|
4729
|
+
* `Clock.now()` on access, no timer), the segment has been invalidated since, a check is under way, or the pin has
|
|
4730
|
+
* ended. A pin counts as checked from the instant its own row read was sent ({@link startPinChecks}), so one left
|
|
4731
|
+
* idle past `cache.genTtlMs` is checked at its first read; one with no record is due. Always `false` without a timed
|
|
4732
|
+
* refresh (no registry, no clock, or `cache.genTtlMs: 0`), where nothing bounds a pin. A compare and no await
|
|
4733
|
+
* otherwise, which is all a pinned read pays between checks.
|
|
4734
|
+
*/
|
|
4735
|
+
pinDue(ref, pin) {
|
|
4736
|
+
if (!this.timedRefresh) return false;
|
|
4737
|
+
const check = this.pinChecks.get(pin);
|
|
4738
|
+
return check === void 0 || check.check !== void 0 || check.ended !== void 0 || this.now() - check.at >= this.currentGenTtlMs || this.invalidated.invalidatedSince(this.keyOf(ref), check.since);
|
|
4739
|
+
}
|
|
4740
|
+
/** Start a new pin's checks from `checkedAtMs`, when the row read that verified it was sent. */
|
|
4741
|
+
startPinChecks(pin, checkedAtMs) {
|
|
4742
|
+
if (this.timedRefresh)
|
|
4743
|
+
this.pinChecks.set(pin, { at: checkedAtMs, since: this.invalidated.epoch });
|
|
4744
|
+
}
|
|
4745
|
+
/**
|
|
4746
|
+
* Settle a pin whose check is due: throw its `NotFoundError` when its segment has been destroyed or is gone, is
|
|
4747
|
+
* another incarnation, or holds another object under the pin's generation; return when it is still the pin's. One row
|
|
4748
|
+
* read per interval serves every pin of the segment ({@link pinRows}), and it decides alone for a row that is gone or
|
|
4749
|
+
* destroyed, a new incarnation, and a ledger that names the generation (its checksum is the pin's or not). A
|
|
4750
|
+
* generation the ledger does not name, or a row with no usable ledger, is not a verdict: the object's footer is read
|
|
4751
|
+
* once ({@link throwIfReplaced}, strict: a footer it cannot read fails the read). The pin counts as checked from
|
|
4752
|
+
* when the row read its verdict came from was sent, never from when the check began, so a row read shared from
|
|
4753
|
+
* earlier does not make a pin look fresher than it is. Every invalidation of the segment in this store makes the
|
|
4754
|
+
* next check read the row afresh, beside the one per interval. A transient fault keeps serving and asks again after {@link REFRESH_RETRY_MS}; any
|
|
4755
|
+
* other fault fails this read and is not remembered. A pin that has ended drops its reader, and so the key it
|
|
4756
|
+
* unwrapped, and fails every later read at once until the segment is invalidated.
|
|
4757
|
+
*/
|
|
4758
|
+
recheckPin(ref, generation, pin) {
|
|
4759
|
+
const prior = this.pinChecks.get(pin);
|
|
4760
|
+
if (prior?.check !== void 0) return prior.check;
|
|
4761
|
+
const segment = this.keyOf(ref);
|
|
4762
|
+
if (prior?.ended !== void 0 && !this.invalidated.invalidatedSince(segment, prior.since))
|
|
4763
|
+
return Promise.reject(
|
|
4764
|
+
prior.ended === "gone" ? pinGone(ref, generation) : notThePinned(ref, generation)
|
|
4765
|
+
);
|
|
4766
|
+
const next = { at: this.now(), since: this.invalidated.epoch };
|
|
4767
|
+
this.pinChecks.set(pin, next);
|
|
4768
|
+
next.check = this.settlePin(ref, generation, pin, next, segment).finally(() => {
|
|
4769
|
+
next.check = void 0;
|
|
4770
|
+
});
|
|
4771
|
+
return next.check;
|
|
4772
|
+
}
|
|
4773
|
+
async settlePin(ref, generation, pin, next, segment) {
|
|
4774
|
+
let ended;
|
|
4775
|
+
try {
|
|
4776
|
+
const read = this.pinRow(ref, segment);
|
|
4777
|
+
const facts = await read.facts;
|
|
4778
|
+
next.at = read.sentAtMs;
|
|
4779
|
+
const incarnation = incarnationOf(pin.version.slice(pin.version.indexOf(":") + 1));
|
|
4780
|
+
const named = facts.ledger?.find((e) => e.generation === generation);
|
|
4781
|
+
if (facts.ended || pin.version.includes(":") && incarnation !== void 0 && facts.incarnation !== void 0 && incarnation !== facts.incarnation) {
|
|
4782
|
+
ended = "gone";
|
|
4783
|
+
} else if (named === void 0) {
|
|
4784
|
+
try {
|
|
4785
|
+
const footerAt = await this.throwIfReplaced(
|
|
4786
|
+
ref,
|
|
4787
|
+
generation,
|
|
4788
|
+
pin.version,
|
|
4789
|
+
pin.fingerprint,
|
|
4790
|
+
true
|
|
4791
|
+
);
|
|
4792
|
+
next.at = Math.min(next.at, footerAt);
|
|
4793
|
+
} catch (err) {
|
|
4794
|
+
if (!isNotFoundError(err)) throw err;
|
|
4795
|
+
ended = "replaced";
|
|
4796
|
+
}
|
|
4797
|
+
} else if (named.footerCrc !== fingerprintParts(pin.fingerprint)?.footerCrc) {
|
|
4798
|
+
ended = "replaced";
|
|
4799
|
+
}
|
|
4800
|
+
} catch (err) {
|
|
4801
|
+
if (isTransientError(err)) {
|
|
4802
|
+
next.at = this.now() - (this.currentGenTtlMs - Math.min(this.currentGenTtlMs, REFRESH_RETRY_MS));
|
|
4803
|
+
return;
|
|
4804
|
+
}
|
|
4805
|
+
next.at = Number.NEGATIVE_INFINITY;
|
|
4806
|
+
throw err;
|
|
4807
|
+
}
|
|
4808
|
+
if (ended === void 0) return;
|
|
4809
|
+
if (!this.invalidated.invalidatedSince(segment, next.since)) next.ended = ended;
|
|
4810
|
+
if (ended === "replaced")
|
|
4811
|
+
this.replacedPins.set(this.heldKey(ref, pin.version, pin.fingerprint), true);
|
|
4812
|
+
this.snapshots.delete(this.pinnedKey(ref, pin.version, pin.fingerprint));
|
|
4813
|
+
throw ended === "gone" ? pinGone(ref, generation) : notThePinned(ref, generation);
|
|
4814
|
+
}
|
|
4815
|
+
/** The segment's row read of the last `cache.genTtlMs`, shared; sent now when there is none. */
|
|
4816
|
+
pinRow(ref, segment) {
|
|
4817
|
+
const now = this.now();
|
|
4818
|
+
const held = this.pinRows.peek(segment);
|
|
4819
|
+
if (held !== void 0 && now - held.sentAtMs < this.currentGenTtlMs) return held;
|
|
4820
|
+
const sent = {
|
|
4821
|
+
sentAtMs: now,
|
|
4822
|
+
facts: this.registry.get(ref).then((record) => ({
|
|
4823
|
+
ended: record === null || record.status === "destroyed",
|
|
4824
|
+
incarnation: record === null ? void 0 : incarnationOf(record.token),
|
|
4825
|
+
ledger: usableKnownGens(record)
|
|
4826
|
+
}))
|
|
4827
|
+
};
|
|
4828
|
+
this.pinRows.set(segment, sent);
|
|
4829
|
+
sent.facts.catch(() => {
|
|
4830
|
+
if (this.pinRows.peek(segment) === sent) this.pinRows.delete(segment);
|
|
4831
|
+
});
|
|
4832
|
+
return sent;
|
|
4833
|
+
}
|
|
4457
4834
|
/**
|
|
4458
4835
|
* Throws the pin's NotFoundError if the object under `generation`'s key is no longer the one the pin holds. Its
|
|
4459
4836
|
* footer says, read with no key and no row ({@link CrbmReader.sameObject}), so an object written under a key this
|
|
@@ -4461,9 +4838,10 @@ var CrbmStorageChunkSource = class {
|
|
|
4461
4838
|
* ({@link replacedPins}), and a check under way is shared by the reads that ask at once; neither "gone" nor "the
|
|
4462
4839
|
* same" is kept, since either can change. A transient fault is rethrown, for the store's retries to see for what it
|
|
4463
4840
|
* is. A footer that cannot be read otherwise says nothing about which object is there, so this returns, and the
|
|
4464
|
-
* caller's own error stands.
|
|
4841
|
+
* caller's own error stands; a caller with no error of its own passes `strict`, and that fault is raised. Returns
|
|
4842
|
+
* when the footer read that answered was sent (`Infinity` when none did).
|
|
4465
4843
|
*/
|
|
4466
|
-
async throwIfReplaced(ref, generation, version, fingerprint) {
|
|
4844
|
+
async throwIfReplaced(ref, generation, version, fingerprint, strict = false) {
|
|
4467
4845
|
const held = this.heldKey(ref, version, fingerprint);
|
|
4468
4846
|
if (this.replacedPins.get(held) !== void 0) throw notThePinned(ref, generation);
|
|
4469
4847
|
let check = this.checking.get(held);
|
|
@@ -4485,16 +4863,27 @@ var CrbmStorageChunkSource = class {
|
|
|
4485
4863
|
}
|
|
4486
4864
|
);
|
|
4487
4865
|
this.checking.set(held, started);
|
|
4866
|
+
this.footerSentAt.set(started, this.now());
|
|
4488
4867
|
check = started;
|
|
4489
4868
|
}
|
|
4490
4869
|
let replaced;
|
|
4491
4870
|
try {
|
|
4492
4871
|
replaced = await check;
|
|
4493
4872
|
} catch (err) {
|
|
4494
|
-
if (isTransientError(err)) throw err;
|
|
4495
|
-
return;
|
|
4873
|
+
if (strict || isTransientError(err)) throw err;
|
|
4874
|
+
return Number.POSITIVE_INFINITY;
|
|
4496
4875
|
}
|
|
4497
4876
|
if (replaced) throw notThePinned(ref, generation);
|
|
4877
|
+
return this.footerSentAt.get(check) ?? Number.NEGATIVE_INFINITY;
|
|
4878
|
+
}
|
|
4879
|
+
/**
|
|
4880
|
+
* Throws the pin's `NotFoundError` when the object under `generation`'s key is no longer the one `held` names: one
|
|
4881
|
+
* footer read, shared with a check already under way for the same pin. It fails closed: a footer that cannot be read,
|
|
4882
|
+
* or an object that is not a generation object of this store, raises its own fault rather than passing.
|
|
4883
|
+
*/
|
|
4884
|
+
async verifyHeld(ref, generation, held) {
|
|
4885
|
+
validateUserRef(ref, "read", PORT_KEYS);
|
|
4886
|
+
await this.throwIfReplaced(ref, generation, held.version, held.fingerprint, true);
|
|
4498
4887
|
}
|
|
4499
4888
|
/** Chunk keys of a specific generation — the pinned shape read. `held` as for {@link getChunkAt}. */
|
|
4500
4889
|
async listChunkKeysAt(ref, generation, held) {
|
|
@@ -4848,7 +5237,9 @@ var CrbmStorageChunkSource = class {
|
|
|
4848
5237
|
} catch (err) {
|
|
4849
5238
|
if (held === void 0) throw err;
|
|
4850
5239
|
if (!(isIntegrityError(err) || isValidationError(err))) throw err;
|
|
4851
|
-
const check = () =>
|
|
5240
|
+
const check = async () => {
|
|
5241
|
+
await this.throwIfReplaced(ref, generation, held.version, held.fingerprint);
|
|
5242
|
+
};
|
|
4852
5243
|
await viaRetry(retry, check);
|
|
4853
5244
|
throw err;
|
|
4854
5245
|
}
|
|
@@ -5087,10 +5478,21 @@ var PinnedStorageChunkSource = class {
|
|
|
5087
5478
|
pinFor(ref) {
|
|
5088
5479
|
return this.pins.get(segmentKey(ref));
|
|
5089
5480
|
}
|
|
5481
|
+
/**
|
|
5482
|
+
* The pin's recheck when it is due, else `undefined`: a compare and no await between checks. A pinned read waits for
|
|
5483
|
+
* it before it touches the reader or the chunk cache, and fails with its `NotFoundError` once the segment is
|
|
5484
|
+
* destroyed or the pin's generation erased ({@link CrbmStorageChunkSource.recheckPin}).
|
|
5485
|
+
*/
|
|
5486
|
+
due(ref, generation, pin) {
|
|
5487
|
+
return this.inner.pinDue(ref, pin) ? this.inner.recheckPin(ref, generation, pin) : void 0;
|
|
5488
|
+
}
|
|
5090
5489
|
getChunk(ref) {
|
|
5091
5490
|
const pin = this.pinFor(ref);
|
|
5092
5491
|
if (pin === void 0) return this.inner.getChunk(ref);
|
|
5093
|
-
|
|
5492
|
+
if (pin.generation === null) return Promise.resolve(null);
|
|
5493
|
+
const generation = pin.generation;
|
|
5494
|
+
const due = this.due(ref, generation, pin);
|
|
5495
|
+
return due === void 0 ? this.inner.getChunkAt(ref, generation, heldBy(pin)) : due.then(() => this.inner.getChunkAt(ref, generation, heldBy(pin)));
|
|
5094
5496
|
}
|
|
5095
5497
|
/**
|
|
5096
5498
|
* A pinned segment's chunks come from the pinned generation alone, under the version a pinned read keys by
|
|
@@ -5111,7 +5513,10 @@ var PinnedStorageChunkSource = class {
|
|
|
5111
5513
|
this.inner.getChunksAt(ref, pin.generation, keys, heldBy(pin), options)
|
|
5112
5514
|
);
|
|
5113
5515
|
try {
|
|
5114
|
-
while (await pull.advance())
|
|
5516
|
+
while (await pull.advance()) {
|
|
5517
|
+
if (this.inner.pinDue(ref, pin)) await this.inner.recheckPin(ref, pin.generation, pin);
|
|
5518
|
+
yield pull.take((chunk) => ({ ...chunk, version }));
|
|
5519
|
+
}
|
|
5115
5520
|
} finally {
|
|
5116
5521
|
await pull.close();
|
|
5117
5522
|
}
|
|
@@ -5119,27 +5524,42 @@ var PinnedStorageChunkSource = class {
|
|
|
5119
5524
|
listChunkKeys(ref) {
|
|
5120
5525
|
const pin = this.pinFor(ref);
|
|
5121
5526
|
if (pin === void 0) return this.inner.listChunkKeys(ref);
|
|
5122
|
-
|
|
5527
|
+
if (pin.generation === null) return Promise.resolve([]);
|
|
5528
|
+
const generation = pin.generation;
|
|
5529
|
+
const due = this.due(ref, generation, pin);
|
|
5530
|
+
return due === void 0 ? this.inner.listChunkKeysAt(ref, generation, heldBy(pin)) : due.then(() => this.inner.listChunkKeysAt(ref, generation, heldBy(pin)));
|
|
5123
5531
|
}
|
|
5124
5532
|
sizeOf(ref) {
|
|
5125
5533
|
const pin = this.pinFor(ref);
|
|
5126
5534
|
if (pin === void 0) return this.inner.sizeOf(ref);
|
|
5127
|
-
|
|
5535
|
+
if (pin.generation === null) return Promise.resolve(null);
|
|
5536
|
+
const generation = pin.generation;
|
|
5537
|
+
const due = this.due(ref, generation, pin);
|
|
5538
|
+
return due === void 0 ? this.inner.sizeOfAt(ref, generation, heldBy(pin)) : due.then(() => this.inner.sizeOfAt(ref, generation, heldBy(pin)));
|
|
5128
5539
|
}
|
|
5129
5540
|
cardinalities(ref) {
|
|
5130
5541
|
const pin = this.pinFor(ref);
|
|
5131
5542
|
if (pin === void 0) return this.inner.cardinalities(ref);
|
|
5132
|
-
|
|
5543
|
+
if (pin.generation === null) return Promise.resolve(null);
|
|
5544
|
+
const generation = pin.generation;
|
|
5545
|
+
const due = this.due(ref, generation, pin);
|
|
5546
|
+
return due === void 0 ? this.inner.cardinalitiesAt(ref, generation, heldBy(pin)) : due.then(() => this.inner.cardinalitiesAt(ref, generation, heldBy(pin)));
|
|
5133
5547
|
}
|
|
5134
5548
|
summary(ref) {
|
|
5135
5549
|
const pin = this.pinFor(ref);
|
|
5136
5550
|
if (pin === void 0) return this.inner.summary(ref);
|
|
5137
|
-
|
|
5551
|
+
if (pin.generation === null) return Promise.resolve(null);
|
|
5552
|
+
const generation = pin.generation;
|
|
5553
|
+
const due = this.due(ref, generation, pin);
|
|
5554
|
+
return due === void 0 ? this.inner.summaryAt(ref, generation, heldBy(pin)) : due.then(() => this.inner.summaryAt(ref, generation, heldBy(pin)));
|
|
5138
5555
|
}
|
|
5139
5556
|
stat(ref) {
|
|
5140
5557
|
const pin = this.pinFor(ref);
|
|
5141
5558
|
if (pin === void 0) return this.inner.stat(ref);
|
|
5142
|
-
|
|
5559
|
+
if (pin.generation === null) return Promise.resolve(null);
|
|
5560
|
+
const generation = pin.generation;
|
|
5561
|
+
const due = this.due(ref, generation, pin);
|
|
5562
|
+
return due === void 0 ? this.inner.statAt(ref, generation, heldBy(pin)) : due.then(() => this.inner.statAt(ref, generation, heldBy(pin)));
|
|
5143
5563
|
}
|
|
5144
5564
|
currentGeneration(ref) {
|
|
5145
5565
|
const pin = this.pinFor(ref);
|
|
@@ -5166,11 +5586,21 @@ var PinnedStorageChunkSource = class {
|
|
|
5166
5586
|
const pin = this.pinFor(ref);
|
|
5167
5587
|
if (pin === void 0) return this.inner.currentVersion(ref);
|
|
5168
5588
|
if (pin.version === null) return Promise.resolve(null);
|
|
5169
|
-
|
|
5589
|
+
const version = `pin ${pin.version}#${pin.fingerprint}`;
|
|
5590
|
+
const due = this.due(ref, pin.generation, pin);
|
|
5591
|
+
return due === void 0 ? Promise.resolve(version) : due.then(() => version);
|
|
5170
5592
|
}
|
|
5171
5593
|
exists(ref) {
|
|
5172
5594
|
return this.inner.exists(ref);
|
|
5173
5595
|
}
|
|
5596
|
+
/** Whether the object this segment's pin holds is still there: see {@link HeldObjectLookup.verifyPinned}. */
|
|
5597
|
+
verifyPinned(ref) {
|
|
5598
|
+
const pin = this.pinFor(ref);
|
|
5599
|
+
const held = pin === void 0 ? void 0 : heldBy(pin);
|
|
5600
|
+
if (pin === void 0 || held === void 0 || pin.generation === null)
|
|
5601
|
+
return Promise.resolve();
|
|
5602
|
+
return this.inner.verifyHeld(ref, pin.generation, held);
|
|
5603
|
+
}
|
|
5174
5604
|
invalidate(ref) {
|
|
5175
5605
|
this.inner.invalidate(ref);
|
|
5176
5606
|
}
|
|
@@ -6423,6 +6853,25 @@ async function rowDeleteSettled(registry, ref, expected, waits = {}) {
|
|
|
6423
6853
|
}
|
|
6424
6854
|
}
|
|
6425
6855
|
|
|
6856
|
+
// src/core/generation-floor.ts
|
|
6857
|
+
var STRAY_MARGIN = 1024;
|
|
6858
|
+
var MAX_REPORTED_STRAYS = 64;
|
|
6859
|
+
var NO_STRAYS = Object.freeze({
|
|
6860
|
+
count: 0,
|
|
6861
|
+
generations: Object.freeze([])
|
|
6862
|
+
});
|
|
6863
|
+
function floorOf(row) {
|
|
6864
|
+
let floor = row?.currentGen ?? -1;
|
|
6865
|
+
for (const e of row?.knownGens ?? []) if (e.generation > floor) floor = e.generation;
|
|
6866
|
+
for (const e of row?.leases ?? []) if (e.generation > floor) floor = e.generation;
|
|
6867
|
+
return floor;
|
|
6868
|
+
}
|
|
6869
|
+
function reportStrays(strays) {
|
|
6870
|
+
if (strays.length === 0) return NO_STRAYS;
|
|
6871
|
+
const generations = [...strays].sort((a, b) => a - b).slice(0, MAX_REPORTED_STRAYS);
|
|
6872
|
+
return { count: strays.length, generations };
|
|
6873
|
+
}
|
|
6874
|
+
|
|
6426
6875
|
// src/core/generation-gc.ts
|
|
6427
6876
|
function deleteGeneration(deps, ref, generation) {
|
|
6428
6877
|
const key = { namespace: ref.namespace, segment: ref.segment, generation };
|
|
@@ -6481,15 +6930,23 @@ function tookEffectOverMissing(ref, published, missing) {
|
|
|
6481
6930
|
return pointerOnDeletedError(ref, { kind: "load", published }, missing);
|
|
6482
6931
|
}
|
|
6483
6932
|
async function nextGeneration(ref, deps) {
|
|
6484
|
-
return aboveEverything(ref, deps, await deps.registry.get(ref));
|
|
6933
|
+
return (await aboveEverything(ref, deps, await deps.registry.get(ref))).generation;
|
|
6485
6934
|
}
|
|
6486
|
-
async function aboveEverything(ref, deps, record
|
|
6935
|
+
async function aboveEverything(ref, deps, record) {
|
|
6936
|
+
const all = [];
|
|
6937
|
+
for await (const key of deps.storage.list(ref)) all.push(key.generation);
|
|
6938
|
+
const limit = floorOf(record) + STRAY_MARGIN;
|
|
6939
|
+
const listed2 = all.filter((g) => g <= limit);
|
|
6487
6940
|
let highest = record?.currentGen ?? -1;
|
|
6488
|
-
for
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
return
|
|
6941
|
+
for (const g of listed2) if (g > highest) highest = g;
|
|
6942
|
+
const held = new Set(all);
|
|
6943
|
+
let generation = numberAbove(highest);
|
|
6944
|
+
while (held.has(generation)) generation = numberAbove(generation);
|
|
6945
|
+
return {
|
|
6946
|
+
generation,
|
|
6947
|
+
listed: listed2,
|
|
6948
|
+
strays: reportStrays(all.filter((g) => g > limit && g > generation))
|
|
6949
|
+
};
|
|
6493
6950
|
}
|
|
6494
6951
|
function numberAbove(highest) {
|
|
6495
6952
|
if (highest >= Number.MAX_SAFE_INTEGER) {
|
|
@@ -6502,10 +6959,9 @@ async function nextLoadGeneration(ref, deps, row) {
|
|
|
6502
6959
|
try {
|
|
6503
6960
|
await deps.storage.getTail({ namespace: ref.namespace, segment: ref.segment, generation }, 0);
|
|
6504
6961
|
} catch (err) {
|
|
6505
|
-
if (isNotFoundError(err)) return { generation, checked: true };
|
|
6962
|
+
if (isNotFoundError(err)) return { generation, checked: true, strays: NO_STRAYS };
|
|
6506
6963
|
}
|
|
6507
|
-
|
|
6508
|
-
return { generation: await aboveEverything(ref, deps, row, listed2), checked: false, listed: listed2 };
|
|
6964
|
+
return { ...await aboveEverything(ref, deps, row), checked: false };
|
|
6509
6965
|
}
|
|
6510
6966
|
var LIST_COLLECTION_CADENCE = 16;
|
|
6511
6967
|
async function gcOrphanGenerations(ref, deps, options = {}) {
|
|
@@ -6812,6 +7268,13 @@ async function judgeLoad(ref, cardinality, deps, options = {}) {
|
|
|
6812
7268
|
function sizeNeeded(guard, allowEmpty) {
|
|
6813
7269
|
return guard?.minRetained !== void 0 || guard?.maxGrowth !== void 0 || allowEmpty !== true;
|
|
6814
7270
|
}
|
|
7271
|
+
var WROTE_NOTHING = Object.freeze({
|
|
7272
|
+
size: 0,
|
|
7273
|
+
sha256: "",
|
|
7274
|
+
chunkCount: 0,
|
|
7275
|
+
cardinality: 0,
|
|
7276
|
+
collected: Object.freeze([])
|
|
7277
|
+
});
|
|
6815
7278
|
function lostNumberRace(err) {
|
|
6816
7279
|
return isWriteConflictError(err) && !isSourceError(err);
|
|
6817
7280
|
}
|
|
@@ -6926,7 +7389,7 @@ async function runLoad(ref, prepare, deps, options) {
|
|
|
6926
7389
|
);
|
|
6927
7390
|
} catch (err) {
|
|
6928
7391
|
if (!lostNumberRace(err)) throw err;
|
|
6929
|
-
const { generation: generation2 } = await numbering;
|
|
7392
|
+
const { generation: generation2, strays: strays2 } = await numbering;
|
|
6930
7393
|
audit.onEvent({
|
|
6931
7394
|
kind: "segment.load-refused",
|
|
6932
7395
|
segment: ref.segment,
|
|
@@ -6940,15 +7403,12 @@ async function runLoad(ref, prepare, deps, options) {
|
|
|
6940
7403
|
generation: generation2,
|
|
6941
7404
|
published: false,
|
|
6942
7405
|
reason: "superseded",
|
|
6943
|
-
|
|
6944
|
-
sha256: "",
|
|
6945
|
-
chunkCount: 0,
|
|
6946
|
-
cardinality: 0,
|
|
7406
|
+
...WROTE_NOTHING,
|
|
6947
7407
|
cardinalityBefore: before,
|
|
6948
|
-
|
|
7408
|
+
strays: strays2
|
|
6949
7409
|
};
|
|
6950
7410
|
}
|
|
6951
|
-
const { generation, checked, listed: listed2 } = await numbering;
|
|
7411
|
+
const { generation, checked, listed: listed2, strays } = await numbering;
|
|
6952
7412
|
const key = { namespace: ref.namespace, segment: ref.segment, generation };
|
|
6953
7413
|
const reclaim = async () => {
|
|
6954
7414
|
const watch = watchDeletes(deps, ref);
|
|
@@ -6991,7 +7451,8 @@ async function runLoad(ref, prepare, deps, options) {
|
|
|
6991
7451
|
chunkCount: written.chunkCount,
|
|
6992
7452
|
cardinality: written.cardinality,
|
|
6993
7453
|
cardinalityBefore: before,
|
|
6994
|
-
collected: []
|
|
7454
|
+
collected: [],
|
|
7455
|
+
strays
|
|
6995
7456
|
};
|
|
6996
7457
|
};
|
|
6997
7458
|
const refusal = guardRefusal(written.cardinality, before, guard, options.allowEmpty);
|
|
@@ -7087,7 +7548,8 @@ async function runLoad(ref, prepare, deps, options) {
|
|
|
7087
7548
|
chunkCount: written.chunkCount,
|
|
7088
7549
|
cardinality: written.cardinality,
|
|
7089
7550
|
cardinalityBefore: before,
|
|
7090
|
-
collected
|
|
7551
|
+
collected,
|
|
7552
|
+
strays
|
|
7091
7553
|
};
|
|
7092
7554
|
}
|
|
7093
7555
|
|
|
@@ -12023,6 +12485,7 @@ var RetryingStorageChunkSource = class {
|
|
|
12023
12485
|
invalidate;
|
|
12024
12486
|
exists;
|
|
12025
12487
|
currentVersion;
|
|
12488
|
+
verifyPinned;
|
|
12026
12489
|
constructor(inner, opts) {
|
|
12027
12490
|
this.inner = inner;
|
|
12028
12491
|
const r = toRetry(opts);
|
|
@@ -12068,6 +12531,10 @@ var RetryingStorageChunkSource = class {
|
|
|
12068
12531
|
if (innerExists) {
|
|
12069
12532
|
this.exists = (ref) => withRetry(() => innerExists.call(inner, ref), this.policy, this.deps);
|
|
12070
12533
|
}
|
|
12534
|
+
const innerVerify = inner.verifyPinned;
|
|
12535
|
+
if (innerVerify) {
|
|
12536
|
+
this.verifyPinned = (ref) => withRetry(() => innerVerify.call(inner, ref), this.policy, this.deps);
|
|
12537
|
+
}
|
|
12071
12538
|
const innerCurrentVersion = inner.currentVersion;
|
|
12072
12539
|
if (innerCurrentVersion) {
|
|
12073
12540
|
this.currentVersion = (ref) => withRetry(() => innerCurrentVersion.call(inner, ref), this.policy, this.deps);
|