@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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  boundedAll,
3
3
  mapWithConcurrency
4
- } from "./chunk-BUDAVGBI.js";
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-WPER7R2R.js";
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-2B2QF4FU.js";
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(`chunk key from a tier is out of range: ${k}`);
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(`chunk key from a tier is listed twice: ${keys[i]}`);
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, chunkKey) {
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 ${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`
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, chunkKey, maxBitmapBytes) {
222
+ function decodeChunkBytes(codec, bytes, maxBitmapBytes) {
223
223
  const bitmap = codec.safeDeserialize(bytes, maxBitmapBytes);
224
- assertChunkPayloadInRange(bitmap, chunkKey);
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(`chunk stream ended before chunk ${key}`);
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, chunkKey, this.maxBitmapBytes);
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, ref.chunkKey, this.maxBitmapBytes);
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(`chunk ${extent.key} is not inside the chunk region, in order`);
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(`varint value must be a u32; got ${value}`);
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 ${chunkKey} payload out of bounds`);
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 ${chunkKey} payload CRC mismatch`);
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 ${chunkKey} encrypted payload too short`);
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 has a duplicate chunkKey ${chunkKey}`);
3509
+ throw crbmIntegrity(`.crbm index entry ${i} repeats a chunk key`);
3297
3510
  }
3298
- if (chunkKey > 65535) throw crbmIntegrity(`.crbm chunkKey ${chunkKey} out of range`);
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 chunk ${chunkKey} length ${len} invalid (most is ${maxPayloadBytes})`
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 chunk ${chunkKey} cardinality ${card} invalid`);
3519
+ throw crbmIntegrity(`.crbm index entry ${i} has a cardinality ${card} invalid`);
3306
3520
  }
3307
3521
  if (offset + len > payloadEnd) {
3308
- throw crbmIntegrity(`.crbm chunk ${chunkKey} payload out of bounds`);
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 [key, pin] of pins) {
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 "${key}" names generation ${held.generation} without its version and object fingerprint`
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 sharedByteLength = typeof SharedArrayBuffer === "function" ? Object.getOwnPropertyDescriptor(SharedArrayBuffer.prototype, "byteLength")?.get : void 0;
5509
+ var plainByteLength = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, "byteLength")?.get;
5510
+ var hasSharedMemory = typeof SharedArrayBuffer === "function";
5279
5511
  function isShared(buffer) {
5280
- if (sharedByteLength === void 0) return false;
5512
+ if (!hasSharedMemory) return false;
5281
5513
  try {
5282
- sharedByteLength.call(buffer);
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(`chunkKey must be a u16; got ${chunkKey}`);
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(`chunk ${chunkKey} does not ascend: the last was ${last}`);
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 ${key} but its generation holds no bytes for it`
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 chunk ${key}`);
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.safeDeserialize(bytes, maxBytes).has(remainder);
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.safeDeserialize(bytes, maxBytes);
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(`the chunk stream ended before chunk ${k}`);
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.safeDeserialize(bytes, maxBytes);
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,