@cloudbitmaps/core 0.16.0 → 0.17.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
@@ -5,6 +5,12 @@ import {
5
5
  DEFAULT_NAMESPACE,
6
6
  IntegrityError,
7
7
  KeyUnavailableError,
8
+ LEASE_SKEW_MS,
9
+ LeaseExpiredError,
10
+ LeaseLimitError,
11
+ MAX_KEPT_GENERATIONS,
12
+ MAX_LEASES_PER_SEGMENT,
13
+ MAX_LEASE_MS,
8
14
  MAX_METADATA_BYTES,
9
15
  MAX_NAME_LENGTH,
10
16
  NotFoundError,
@@ -25,25 +31,35 @@ import {
25
31
  encodeNameForPath,
26
32
  encodedNameLength,
27
33
  entropyIsAvailable,
34
+ heldGenerations,
28
35
  incarnationOf,
29
36
  isCloudRoaringError,
30
37
  isIntegrityError,
38
+ isLeaseExpiredError,
39
+ isLeaseLimitError,
40
+ isLive,
31
41
  isNotFoundError,
32
42
  isReservedNamespace,
33
43
  isStorageBackend,
34
44
  isTransientError,
35
45
  isValidationError,
36
46
  isWriteConflictError,
47
+ leaseChurn,
37
48
  mapWithConcurrency,
38
49
  metadataBytes,
39
50
  metadataFromBytes,
40
51
  namespacePathPart,
41
52
  newIncarnationToken,
53
+ nextKept,
42
54
  nextRegistryToken,
55
+ onlyLeasesDiffer,
43
56
  parseRegistryEnvelope,
44
57
  recordFromNew,
58
+ releaseLease,
45
59
  sameIncarnation,
46
60
  serializeRegistryEnvelope,
61
+ takeLease,
62
+ usableKeptGens,
47
63
  validateChunkKeyOrder,
48
64
  validateChunkRef,
49
65
  validateNewRegistryRecord,
@@ -52,7 +68,7 @@ import {
52
68
  validateUserNamespace,
53
69
  validateUserRef,
54
70
  webCryptoEntropy
55
- } from "./chunk-LFUNPMXZ.js";
71
+ } from "./chunk-CV7GJO3B.js";
56
72
 
57
73
  // src/core/bit-route.ts
58
74
  var U32_MAX = 4294967295;
@@ -332,6 +348,8 @@ var CountingMetricsSink = class _CountingMetricsSink {
332
348
  op.totalMs += event.ms;
333
349
  break;
334
350
  }
351
+ case "advisory":
352
+ break;
335
353
  default: {
336
354
  const exhaustive = event;
337
355
  return exhaustive;
@@ -675,6 +693,104 @@ var SegmentEngine = class {
675
693
  chunks.close();
676
694
  }
677
695
  }
696
+ /**
697
+ * The ids at 1-based ranks `n`, `2n`, `3n` … counted over the ids in `range`, ascending. The rank of each boundary
698
+ * is placed from the index's per-chunk counts, so only the chunks that hold a boundary are read, each once however
699
+ * many boundaries it holds, through the same coalesced stream, window and budget unit as {@link iterate}. A range with
700
+ * `after` inside a chunk reads that chunk too, to count the ids at or below `after`. A last partial window yields
701
+ * nothing.
702
+ *
703
+ * The counts place ids, and are trusted as `count()` trusts them: a chunk that is read must decode to the size its
704
+ * index says, or the read throws {@link IntegrityError} (invariant 5), but a chunk that is not read is not checked, so
705
+ * an index that lies consistently about one (on an unencrypted object, the sum still matches the footer's total,
706
+ * which opening the object checks before any boundary is placed) shifts the ranks after it. An encrypted object's
707
+ * index is authenticated and bound to the object, so a wrong count there needs the key. The index and the chunks must come from one
708
+ * generation (invariant 3): the caller reads through a source that holds one, a pinned one, and a source with no
709
+ * per-chunk counts is refused.
710
+ *
711
+ * Nothing is fetched when the chunks in range hold fewer than `n` ids. The budget is charged before the first fetch
712
+ * with an upper bound, the cut chunk plus one chunk per boundary the counts allow (capped by the chunks in range),
713
+ * which can exceed the chunks actually read. A range that cuts its first chunk reads it even when the ids above
714
+ * `after` turn out to be fewer than `n`, and one that cuts its last chunk may read it and yield nothing.
715
+ */
716
+ async *everyNth(seg, n, range) {
717
+ if (!Number.isSafeInteger(n) || n < 1) {
718
+ throw new ValidationError(`n must be a positive integer; got ${String(n)}`);
719
+ }
720
+ const epoch = this.invalidations;
721
+ const w = windowOf(range);
722
+ if (w === "empty") return;
723
+ const { loKey, loRem, hiKey, hiRem } = w ?? WHOLE_ID_SPACE;
724
+ if (this.storage.cardinalities === void 0) {
725
+ throw new UnsupportedError("everyNth needs a storage source that reports per-chunk counts");
726
+ }
727
+ const counts = await this.storage.cardinalities(seg);
728
+ if (counts === null) return;
729
+ const keys = keysWithin(
730
+ [...counts.keys()].sort((a, b) => a - b),
731
+ { loKey, loRem, hiKey, hiRem }
732
+ );
733
+ for (const k of keys) {
734
+ this.assertChunkKeyInRange(k);
735
+ const c = counts.get(k);
736
+ if (!Number.isInteger(c) || c < 1 || c > CHUNK_COUNT) {
737
+ throw new IntegrityError(`chunk cardinality from a tier is out of range: ${c}`);
738
+ }
739
+ }
740
+ const gen = await this.cacheVersion(seg);
741
+ const cutsFirst = loRem > 0 && keys[0] === loKey;
742
+ let upper = 0;
743
+ for (const k of keys) upper += counts.get(k);
744
+ if (upper < n) return;
745
+ const cut = cutsFirst ? 1 : 0;
746
+ checkBudget(this.budget, cut + Math.min(keys.length - cut, Math.floor(upper / n)), "everyNth");
747
+ let first;
748
+ let below = 0;
749
+ if (cutsFirst) {
750
+ first = this.chunkRemainders(
751
+ await this.storageChunk({ ...seg, chunkKey: loKey }, gen),
752
+ loKey,
753
+ counts
754
+ );
755
+ below = firstAbove(first, loRem - 1);
756
+ }
757
+ const hits = [];
758
+ let before = 0;
759
+ for (const key of keys) {
760
+ const skipped = key === loKey && cutsFirst ? below : 0;
761
+ const size = counts.get(key) - skipped;
762
+ const next = (Math.floor(before / n) + 1) * n;
763
+ if (next <= before + size) hits.push({ key, index: skipped + next - before - 1 });
764
+ before += size;
765
+ }
766
+ if (hits.length === 0) return;
767
+ const streamed = hits.filter((h) => !(cutsFirst && h.key === loKey)).map((h) => h.key);
768
+ const chunks = streamed.length > 0 ? this.chunkSequence(seg, streamed, gen, epoch) : void 0;
769
+ try {
770
+ for (const { key, index } of hits) {
771
+ const rem = cutsFirst && key === loKey && first !== void 0 ? first : this.chunkRemainders(await chunks.take(key), key, counts);
772
+ const base = key * CHUNK_COUNT;
773
+ for (let i = index; i < rem.length; i += n) {
774
+ const remainder = rem[i] & MAX_REMAINDER;
775
+ if (key === hiKey && remainder > hiRem) return;
776
+ yield base + remainder;
777
+ }
778
+ }
779
+ } finally {
780
+ chunks?.close();
781
+ }
782
+ }
783
+ /** A chunk's remainders ascending, once its size is confirmed against the index's count for it (invariant 5). */
784
+ chunkRemainders(chunk, chunkKey, counts) {
785
+ const expected = counts.get(chunkKey);
786
+ if (chunk === null || chunk.size !== expected) {
787
+ const got = chunk === null ? "no bytes" : `${chunk.size} ids`;
788
+ throw new IntegrityError(
789
+ `chunk ${chunkKey} holds ${got}, but the object's index says ${expected}`
790
+ );
791
+ }
792
+ return chunk.toUint32Array ? chunk.toUint32Array() : Uint32Array.from(chunk);
793
+ }
678
794
  /**
679
795
  * `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
680
796
  *
@@ -1444,63 +1560,121 @@ async function nextLoadGeneration(ref, deps, row) {
1444
1560
  }
1445
1561
  var LIST_COLLECTION_CADENCE = 16;
1446
1562
  async function gcOrphanGenerations(ref, deps, options = {}) {
1563
+ return (await listingPass(ref, deps, { keep: options.keep })).deleted;
1564
+ }
1565
+ async function listingPass(ref, deps, options) {
1447
1566
  const keep = options.keep ?? 1;
1567
+ const quiet = options.quiet === true;
1448
1568
  if (!Number.isInteger(keep) || keep < 0) {
1449
1569
  throw new ValidationError(`keep must be a non-negative integer; got ${String(keep)}`);
1450
1570
  }
1451
1571
  const record = await deps.registry.get(ref);
1452
- if (record === null) return [];
1572
+ if (record === null) return { deleted: [], kept: [], complete: true };
1453
1573
  const current = record.currentGen;
1454
1574
  const seen = /* @__PURE__ */ new Set();
1455
1575
  for await (const key of deps.storage.list(ref)) seen.add(key.generation);
1456
1576
  const gens = [...seen];
1457
1577
  const after = await deps.registry.get(ref);
1458
- if (after === null)
1578
+ if (after === null) {
1579
+ if (quiet) return { deleted: [], kept: [], complete: false };
1459
1580
  throw new WriteConflictError(`registry row for segment ${ref.segment} was purged mid-pass`);
1581
+ }
1460
1582
  if (record.status === "destroyed" && after.token !== record.token) {
1583
+ if (quiet) return { deleted: [], kept: [], complete: false };
1461
1584
  throw new WriteConflictError(
1462
1585
  `segment ${ref.segment} changed incarnation while its generations were being listed`
1463
1586
  );
1464
1587
  }
1465
1588
  const cutoff = current === null || after.currentGen === null ? null : Math.min(current, after.currentGen);
1466
- const stillCollectable = async () => {
1589
+ const named = record.status === "destroyed" ? void 0 : usableKeptGens(after);
1590
+ const protect = options.protect === void 0 || named === void 0 ? void 0 : /* @__PURE__ */ new Set([...options.protect, ...named]);
1591
+ const stillCollectable = async (generation) => {
1467
1592
  const still = await deps.registry.get(ref);
1468
1593
  if (still === null) {
1594
+ if (quiet) return "stop";
1469
1595
  throw new WriteConflictError(`registry row for segment ${ref.segment} was purged mid-pass`);
1470
1596
  }
1471
1597
  const ok = record.status === "destroyed" ? still.token === after.token : cutoff === null || still.currentGen !== null && still.currentGen >= cutoff;
1472
- if (!ok) {
1598
+ const now = protect === void 0 ? void 0 : usableKeptGens(still);
1599
+ const unnamed = protect === void 0 || record.status === "destroyed" || now !== void 0 && !now.includes(generation);
1600
+ if (!ok || !unnamed) {
1601
+ if (quiet) return "stop";
1473
1602
  throw new WriteConflictError(
1474
1603
  `segment ${ref.segment} changed incarnation while its generations were being collected`
1475
1604
  );
1476
1605
  }
1606
+ if (guard !== void 0 && heldGenerations(still, guard.now()).has(generation)) return "skip";
1607
+ return "delete";
1477
1608
  };
1478
- const toDelete = record.status === "destroyed" ? gens.sort((a, b) => a - b) : cutoff === null ? (
1609
+ const guard = record.status === "destroyed" ? void 0 : options.leases;
1610
+ const held = guard === void 0 ? void 0 : heldGenerations(after, guard.now());
1611
+ const below = record.status === "destroyed" || cutoff === null ? [] : gens.filter((g) => g < cutoff).sort((a, b) => b - a);
1612
+ const candidates = record.status === "destroyed" ? gens.sort((a, b) => a - b) : cutoff === null ? (
1479
1613
  // No Storage pointer yet, so "below current" selects nothing and there is nothing safe to infer: an object
1480
1614
  // here is either a load about to publish or an orphan we cannot tell apart from it. Deleting would race
1481
1615
  // that publish into a dangling pointer. It is collected once a pointer exists.
1482
1616
  []
1617
+ ) : protect !== void 0 ? (
1618
+ // The row's list is the window: everything below the pointer it does not name is garbage.
1619
+ below.filter((g) => !protect.has(g))
1483
1620
  ) : (
1484
1621
  // Delete generations below the cutoff, except the newest `keep` of them (the grace window).
1485
- gens.filter((g) => g < cutoff).sort((a, b) => b - a).slice(keep)
1622
+ below.slice(keep)
1486
1623
  );
1624
+ const leasedSpared = new Set(candidates.filter((g) => held?.has(g) === true));
1625
+ const toDelete = candidates.filter((g) => !leasedSpared.has(g));
1626
+ const done = [];
1487
1627
  for (const generation of toDelete) {
1488
- await stillCollectable();
1628
+ const verdict = await stillCollectable(generation);
1629
+ if (verdict === "stop") return { deleted: done, kept: [], complete: false };
1630
+ if (verdict === "skip") {
1631
+ leasedSpared.add(generation);
1632
+ continue;
1633
+ }
1489
1634
  await deps.storage.delete({ namespace: ref.namespace, segment: ref.segment, generation });
1635
+ done.push(generation);
1490
1636
  }
1491
- return toDelete;
1637
+ const deleted = new Set(done);
1638
+ return {
1639
+ deleted: done,
1640
+ kept: below.filter((g) => !deleted.has(g) && !leasedSpared.has(g)).reverse(),
1641
+ complete: true
1642
+ };
1492
1643
  }
1493
1644
  async function collectAfterLoad(ref, deps, options) {
1494
- const { generation, keep, byName, proveCurrent } = options;
1645
+ const { generation, keep, byName, currentGone, kept, proveCurrent, leases } = options;
1495
1646
  if (keep >= generation) return [];
1647
+ const list = kept?.list;
1648
+ if (list === void 0 || currentGone === true)
1649
+ return reconcile(ref, deps, { keep, kept, leases });
1496
1650
  const periodic = generation % LIST_COLLECTION_CADENCE === 0;
1497
- if (byName && keep <= 1 && !periodic) {
1498
- if (keep === 1 && proveCurrent !== void 0 && !await objectIsThere(deps.storage, proveCurrent)) {
1499
- return gcOrphanGenerations(ref, deps, { keep });
1651
+ if (byName && !periodic) {
1652
+ if (keep >= 1 && proveCurrent !== void 0 && !await objectIsThere(deps.storage, proveCurrent)) {
1653
+ return reconcile(ref, deps, { keep, kept, leases });
1654
+ }
1655
+ return deleteEvicted(ref, deps, { generation, evict: kept.evict, leases });
1656
+ }
1657
+ return (await listingPass(ref, deps, { keep, protect: list, quiet: true, leases })).deleted;
1658
+ }
1659
+ async function reconcile(ref, deps, options) {
1660
+ const { keep, kept, leases } = options;
1661
+ const {
1662
+ deleted,
1663
+ kept: present,
1664
+ complete
1665
+ } = await listingPass(ref, deps, { keep, quiet: true, leases });
1666
+ const token = kept?.token;
1667
+ if (complete && token !== void 0 && keep <= MAX_KEPT_GENERATIONS && !sameList(kept?.list, present)) {
1668
+ try {
1669
+ await deps.registry.compareAndSwap(ref, token, { keptGens: present });
1670
+ } catch (err) {
1671
+ if (!(err instanceof WriteConflictError)) throw err;
1500
1672
  }
1501
- return collectByName(ref, deps, { generation, keep });
1502
1673
  }
1503
- return gcOrphanGenerations(ref, deps, { keep });
1674
+ return deleted;
1675
+ }
1676
+ function sameList(a, b) {
1677
+ return a !== void 0 && a.length === b.length && a.every((g, i) => g === b[i]);
1504
1678
  }
1505
1679
  async function objectIsThere(storage, key) {
1506
1680
  try {
@@ -1510,21 +1684,20 @@ async function objectIsThere(storage, key) {
1510
1684
  return false;
1511
1685
  }
1512
1686
  }
1513
- async function collectByName(ref, deps, options) {
1514
- const { generation, keep } = options;
1515
- if (!Number.isInteger(keep) || keep < 0 || keep > 1) {
1516
- throw new ValidationError(`collecting by name needs a keep of 0 or 1; got ${String(keep)}`);
1687
+ async function deleteEvicted(ref, deps, options) {
1688
+ const deleted = [];
1689
+ for (const generation of options.evict) {
1690
+ const still = await deps.registry.get(ref);
1691
+ if (still === null || still.currentGen === null || still.currentGen < options.generation) break;
1692
+ const named = usableKeptGens(still);
1693
+ if (named === void 0 || generation >= still.currentGen || named.includes(generation)) break;
1694
+ if (options.leases !== void 0 && heldGenerations(still, options.leases.now()).has(generation)) {
1695
+ continue;
1696
+ }
1697
+ await deps.storage.delete({ namespace: ref.namespace, segment: ref.segment, generation });
1698
+ deleted.push(generation);
1517
1699
  }
1518
- const evicted = generation - keep - 1;
1519
- if (evicted < 0) return [];
1520
- const still = await deps.registry.get(ref);
1521
- if (still === null || still.currentGen === null || still.currentGen < generation) return [];
1522
- await deps.storage.delete({
1523
- namespace: ref.namespace,
1524
- segment: ref.segment,
1525
- generation: evicted
1526
- });
1527
- return [evicted];
1700
+ return deleted;
1528
1701
  }
1529
1702
 
1530
1703
  // src/core/reader-defaults.ts
@@ -2030,13 +2203,30 @@ async function runExport(reader, registry, sink, options = {}) {
2030
2203
  const segments = [];
2031
2204
  const failed = [];
2032
2205
  let totalIds = 0;
2206
+ if (typeof reader?.segment !== "function") {
2207
+ throw new ValidationError(
2208
+ "runExport: the reader must have a segment(name, { namespace? }) method returning a handle with pin(), as a store's does"
2209
+ );
2210
+ }
2211
+ let probed = false;
2212
+ let unusable;
2033
2213
  const exportRef = async (ref) => {
2034
2214
  let writer;
2035
2215
  let count = 0;
2036
2216
  let bytes = 0;
2037
2217
  try {
2038
2218
  validateSegmentRef(ref);
2039
- const ids = reader.segment(ref.segment, { namespace: ref.namespace }).iterate();
2219
+ const handle = reader.segment(ref.segment, { namespace: ref.namespace });
2220
+ if (!probed) {
2221
+ probed = true;
2222
+ if (typeof handle?.pin !== "function") {
2223
+ unusable = new ValidationError(
2224
+ "runExport: the reader's segment(name, { namespace? }) must return a handle with a pin() method; a segment is exported at one pinned generation"
2225
+ );
2226
+ throw unusable;
2227
+ }
2228
+ }
2229
+ const ids = (await handle.pin()).iterate();
2040
2230
  writer = await sink.open(ref, ext);
2041
2231
  if (format === "roaring") {
2042
2232
  const bm = requireCodec(options.codec, "runExport({ format: 'roaring' })").empty();
@@ -2067,6 +2257,7 @@ async function runExport(reader, registry, sink, options = {}) {
2067
2257
  }
2068
2258
  }
2069
2259
  } catch (err) {
2260
+ if (err === unusable) throw err;
2070
2261
  if (writer !== void 0) {
2071
2262
  try {
2072
2263
  await writer.abort?.();
@@ -3848,7 +4039,7 @@ function isShared(buffer) {
3848
4039
  }
3849
4040
  function overCap(what, length) {
3850
4041
  return new ValidationError(
3851
- `${what} is ${length} bytes, more than any canonical 32-bit bitmap serializes to (${MAX_SERIALIZED_LOAD_BYTES}). Call runOptimize() on the bitmap before serializing it.`
4042
+ `${what} is ${length} bytes, more than any canonical 32-bit bitmap serializes to (${MAX_SERIALIZED_LOAD_BYTES}). If it was serialized from a bitmap, call runOptimize() on the bitmap first.`
3852
4043
  );
3853
4044
  }
3854
4045
  function prepareLoadInput(input, codec) {
@@ -3866,7 +4057,9 @@ function prepareLoadInput(input, codec) {
3866
4057
  if (Symbol.iterator in input || Symbol.asyncIterator in input) return input;
3867
4058
  const keys = Object.keys(input);
3868
4059
  if (keys.length === 1 && keys[0] === "serialized") {
3869
- return decode(input.serialized, "{ serialized }", codec);
4060
+ return new DecodedLoadInput(
4061
+ decodeSerialized(input.serialized, codec, "{ serialized }")
4062
+ );
3870
4063
  }
3871
4064
  if (keys.length === 1 && keys[0] === "bitmap") {
3872
4065
  const bitmap = input.bitmap;
@@ -3878,13 +4071,13 @@ function prepareLoadInput(input, codec) {
3878
4071
  const size = bitmap.getSerializationSizeInBytes("portable");
3879
4072
  if (typeof size === "number" && size > MAX_SERIALIZED_LOAD_BYTES) throw overCap(what, size);
3880
4073
  }
3881
- return decode(bitmap.serialize("portable"), what, codec);
4074
+ return new DecodedLoadInput(decodeSerialized(bitmap.serialize("portable"), codec, what));
3882
4075
  }
3883
4076
  throw new ValidationError(
3884
4077
  `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.`
3885
4078
  );
3886
4079
  }
3887
- function decode(bytes, what, codec) {
4080
+ function decodeSerialized(bytes, codec, what = "the buffer") {
3888
4081
  if (typeof bytes !== "object" || bytes === null || tagOf(bytes) !== "Uint8Array") {
3889
4082
  throw new ValidationError(`${what} must be a Uint8Array of portable Roaring bytes`);
3890
4083
  }
@@ -3892,9 +4085,7 @@ function decode(bytes, what, codec) {
3892
4085
  if (view.byteLength > MAX_SERIALIZED_LOAD_BYTES) throw overCap(what, view.byteLength);
3893
4086
  const own = isShared(view.buffer) ? new Uint8Array(view) : view;
3894
4087
  try {
3895
- return new DecodedLoadInput(
3896
- codec.safeDeserialize(own, MAX_SERIALIZED_LOAD_BYTES, { whole: true })
3897
- );
4088
+ return codec.safeDeserialize(own, MAX_SERIALIZED_LOAD_BYTES, { whole: true });
3898
4089
  } catch (err) {
3899
4090
  if (!isIntegrityError(err)) throw err;
3900
4091
  throw new ValidationError(`${what} is not a valid bitmap: ${err.message}`);
@@ -4037,6 +4228,8 @@ var notThePinned = (ref, generation) => new NotFoundError(
4037
4228
  function versionOf(generation, lineage) {
4038
4229
  return lineage === void 0 ? String(generation) : `${generation}:${String(lineage)}`;
4039
4230
  }
4231
+ var PinMoved = class extends Error {
4232
+ };
4040
4233
  function summaryOfGeneration(generation, description) {
4041
4234
  const { cardinality, metadata } = description;
4042
4235
  return metadata === void 0 || Object.keys(metadata).length === 0 ? { generation, cardinality } : { generation, cardinality, metadata: Object.freeze({ ...metadata }) };
@@ -4403,17 +4596,102 @@ var CrbmStorageChunkSource = class {
4403
4596
  * and a `keep: 0` sweep can land in that gap, and nothing has been handed out yet, so the generation current
4404
4597
  * once the swept one is gone is exactly what a pin taken now should hold. One retry, then propagate.
4405
4598
  */
4406
- async pinGeneration(ref) {
4599
+ async pinGeneration(ref, lease) {
4600
+ for (let moved = 0; ; moved++) {
4601
+ try {
4602
+ try {
4603
+ return await this.pinOnce(ref, lease);
4604
+ } catch (err) {
4605
+ if (!isNotFoundError(err)) throw err;
4606
+ return await this.pinOnce(ref, lease);
4607
+ }
4608
+ } catch (err) {
4609
+ if (!(err instanceof PinMoved)) throw err;
4610
+ if (moved >= 2) {
4611
+ throw new WriteConflictError(
4612
+ `pin: the pointer of "${ref.segment}" moved on every attempt to lease its current generation`
4613
+ );
4614
+ }
4615
+ }
4616
+ }
4617
+ }
4618
+ /**
4619
+ * Open one named generation and verify it is the object `at.fingerprint` names, for a pin the caller reopens. One
4620
+ * row read and one tail read with a registry (none of the row for a store without one): the open is the one a
4621
+ * pinned read makes ({@link readerAt}), memoised under the version the pin will read by, so the pin's first read
4622
+ * finds it open. A generation that is gone, above the row's pointer, on a destroyed or absent row, or another
4623
+ * object than the fingerprint names, throws `NotFoundError`; nothing here reads empty.
4624
+ *
4625
+ * A generation counts as published while the pointer is at or above it. A rollback deletes nothing, so after one
4626
+ * and a later load, a generation it rolled back from can be reopened while its object is still stored and the
4627
+ * fingerprint matches. Like `pinGeneration`'s pinned reads, this opens the object without the live open's check
4628
+ * of the row's summary against it, since that check is made where the row's own generation is read.
4629
+ */
4630
+ async pinGenerationAt(ref, at, lease) {
4631
+ const { generation, fingerprint } = at;
4632
+ const gone = (why) => new NotFoundError(
4633
+ `segment "${ref.segment}" generation ${generation} cannot be pinned: ${why}`
4634
+ );
4635
+ let version = versionOf(generation, void 0);
4636
+ let installed;
4637
+ let pinKey;
4638
+ let memoised = false;
4639
+ if (this.registry !== void 0) {
4640
+ const record = await this.registry.get(ref);
4641
+ if (record === null || record.status === "destroyed")
4642
+ throw gone("the segment has no readable row");
4643
+ if (record.currentGen === null || generation > record.currentGen) {
4644
+ throw gone("it is not a published generation of this segment");
4645
+ }
4646
+ let lineage = record.token;
4647
+ if (lease !== void 0) {
4648
+ const taken = await lease.take(record);
4649
+ if (taken === "moved") throw gone("the segment moved while its lease was taken");
4650
+ lineage = taken.token;
4651
+ }
4652
+ version = versionOf(generation, lineage);
4653
+ const key = this.pinnedKey(ref, version);
4654
+ memoised = this.snapshots.peek(key) !== void 0;
4655
+ if (!memoised) {
4656
+ installed = this.install(
4657
+ key,
4658
+ Snapshot.eager(
4659
+ this.openForTarget(ref, {
4660
+ generation,
4661
+ lineage,
4662
+ wrappedDeks: record.wrappedDeks
4663
+ })
4664
+ )
4665
+ );
4666
+ }
4667
+ pinKey = key;
4668
+ }
4669
+ if (this.registry === void 0) {
4670
+ memoised = this.snapshots.peek(this.pinnedKey(ref, version, fingerprint)) !== void 0;
4671
+ }
4407
4672
  try {
4408
- return await this.pinOnce(ref);
4673
+ await this.readerAt(ref, generation, version, fingerprint);
4409
4674
  } catch (err) {
4410
- if (!isNotFoundError(err)) throw err;
4411
- return this.pinOnce(ref);
4675
+ if (installed !== void 0 && pinKey !== void 0 && this.snapshots.peek(pinKey) === installed) {
4676
+ this.snapshots.delete(pinKey);
4677
+ }
4678
+ throw err;
4679
+ }
4680
+ if (!memoised) {
4681
+ const held = this.heldKey(ref, version, fingerprint);
4682
+ this.replacedPins.delete(held);
4683
+ this.checking.delete(held);
4412
4684
  }
4685
+ return { generation, version, fingerprint };
4413
4686
  }
4414
- async pinOnce(ref) {
4415
- const live = await this.resolveLive(ref);
4687
+ async pinOnce(ref, lease) {
4688
+ let live = await this.resolveLive(ref);
4416
4689
  if (live === null) return null;
4690
+ if (lease !== void 0 && live.row !== void 0) {
4691
+ const taken = await lease.take(live.row);
4692
+ if (taken === "moved") throw new PinMoved();
4693
+ live = { ...live, target: { ...live.target, lineage: taken.token } };
4694
+ }
4417
4695
  const target = live.target;
4418
4696
  const version = versionOf(target.generation, target.lineage);
4419
4697
  let reader;
@@ -4709,6 +4987,7 @@ var CrbmStorageChunkSource = class {
4709
4987
  const unwrap = keys === void 0 || keys.length === 0 || this.keystore === void 0 ? void 0 : prior?.unwrap !== void 0 && sameKeys(prior.target.wrappedDeks, keys) ? prior.unwrap : once(() => this.keystore.openDek(keys));
4710
4988
  const crypto = once(() => this.cryptoForRead(ref, generation, keys, unwrap));
4711
4989
  return {
4990
+ row: record,
4712
4991
  target: { generation, lineage: record.token, wrappedDeks: record.wrappedDeks },
4713
4992
  crypto,
4714
4993
  unwrap,
@@ -5146,11 +5425,24 @@ function refuseCleartextOntoKey(key, record, cleartext) {
5146
5425
  }
5147
5426
  var UNANSWERED_RESENDS = 3;
5148
5427
  var UNANSWERED_RESEND_BASE_MS = 500;
5149
- async function publishGeneration(registry, key, options = {}) {
5428
+ function leasesPatch(record, options) {
5429
+ const held = record.leases;
5430
+ if (held === void 0) return {};
5431
+ if (options.clearLeases === true) return { leases: void 0 };
5432
+ if (options.leasesNow === void 0) return {};
5433
+ const now = options.leasesNow();
5434
+ const live = held.filter((e) => isLive(e, now));
5435
+ if (live.length === held.length) return {};
5436
+ return { leases: live.length === 0 ? void 0 : live };
5437
+ }
5438
+ async function publishGenerationKept(registry, key, options = {}) {
5150
5439
  let fresh;
5151
5440
  let failedOn;
5152
5441
  let unanswered;
5153
5442
  let resends = 0;
5443
+ const churn = leaseChurn({ clock: options.clock, rng: options.rng });
5444
+ let throughChurn = false;
5445
+ let waited;
5154
5446
  let sawUnanswered = false;
5155
5447
  let lastRecord;
5156
5448
  for (let attempt = 0; attempt < 5; attempt++) {
@@ -5161,6 +5453,8 @@ async function publishGeneration(registry, key, options = {}) {
5161
5453
  fresh = void 0;
5162
5454
  unanswered = void 0;
5163
5455
  lastRecord = record;
5456
+ let next;
5457
+ let wrote;
5164
5458
  if (failedOn !== void 0) {
5165
5459
  let landed;
5166
5460
  try {
@@ -5168,57 +5462,78 @@ async function publishGeneration(registry, key, options = {}) {
5168
5462
  } catch (proofErr) {
5169
5463
  throw outcomeUnknown(key, proofErr);
5170
5464
  }
5171
- if (landed) return true;
5465
+ if (landed)
5466
+ return {
5467
+ published: true,
5468
+ kept: { ...nextKept(failedOn, key.generation, options.keep), token: void 0 }
5469
+ };
5172
5470
  if (record !== null && record.status === "active" && record.currentGen === key.generation) {
5173
- return false;
5471
+ return REFUSED;
5174
5472
  }
5175
5473
  failedOn = void 0;
5176
5474
  }
5177
5475
  try {
5178
5476
  if (options.expectFrom !== void 0 && record?.currentGen !== options.expectFrom) {
5179
- return false;
5477
+ return REFUSED;
5180
5478
  }
5181
5479
  if (options.expectAbsent === true && record !== null) {
5182
- return false;
5480
+ return REFUSED;
5183
5481
  }
5482
+ throughChurn = false;
5184
5483
  if (options.expectToken !== void 0 && record?.token !== options.expectToken) {
5185
- return false;
5484
+ const held = options.expectRow;
5485
+ if (record !== null && held != null && record === waited && onlyLeasesDiffer(held, record)) {
5486
+ throughChurn = true;
5487
+ } else {
5488
+ const settled2 = record === waited ? void 0 : await churn.settle(held, record, () => registry.get(key));
5489
+ if (settled2 !== void 0) {
5490
+ waited = settled2.row;
5491
+ fresh = settled2.row;
5492
+ attempt -= 1;
5493
+ continue;
5494
+ }
5495
+ return REFUSED;
5496
+ }
5186
5497
  }
5187
5498
  if (record === null && options.summary !== void 0 && "sealed" in options.summary && options.wrappedDeks === void 0) {
5188
5499
  if (reused) continue;
5189
- return false;
5500
+ return REFUSED;
5190
5501
  }
5191
5502
  if (record === null) {
5192
- await registry.create(
5503
+ next = nextKept(null, key.generation, options.keep);
5504
+ wrote = (await registry.create(
5193
5505
  key,
5194
5506
  {
5195
5507
  currentGen: key.generation,
5196
5508
  wrappedDeks: options.wrappedDeks,
5197
- ...options.summary === void 0 ? {} : { summary: options.summary }
5509
+ ...options.summary === void 0 ? {} : { summary: options.summary },
5510
+ ...next.list === void 0 ? {} : { keptGens: next.list }
5198
5511
  },
5199
5512
  write
5200
- );
5513
+ )).token;
5201
5514
  } else if (record.status === "destroyed") {
5202
5515
  throw new ValidationError(
5203
5516
  `segment "${key.segment}" was destroyed (crypto-shredded) while generation ${key.generation} was being written \u2014 refusing to publish it; the written object is unreadable. Use a new segment.`
5204
5517
  );
5205
5518
  } else if (record.currentGen === null) {
5206
5519
  refuseCleartextOntoKey(key, record, options.cleartext);
5207
- await registry.compareAndSwap(
5520
+ next = nextKept(record, key.generation, options.keep);
5521
+ wrote = (await registry.compareAndSwap(
5208
5522
  key,
5209
5523
  record.token,
5210
5524
  {
5211
5525
  currentGen: key.generation,
5212
5526
  ...options.wrappedDeks === void 0 ? {} : { wrappedDeks: options.wrappedDeks },
5213
- ...options.summary === void 0 ? {} : { summary: options.summary }
5527
+ ...options.summary === void 0 ? {} : { summary: options.summary },
5528
+ ...next.list === void 0 ? {} : { keptGens: next.list }
5214
5529
  },
5215
5530
  write
5216
- );
5531
+ )).token;
5217
5532
  } else if (record.currentGen > key.generation) {
5218
- return false;
5533
+ return REFUSED;
5219
5534
  } else if (record.currentGen === key.generation) {
5220
5535
  if (reused) continue;
5221
- return true;
5536
+ return { published: true };
5222
5537
  } else {
5223
5538
  if (options.wrappedDeks !== void 0 && options.wrappedDeks.length > 0) {
5224
5539
  throw new ValidationError(
@@ -5226,20 +5541,27 @@ async function publishGeneration(registry, key, options = {}) {
5226
5541
  );
5227
5542
  }
5228
5543
  refuseCleartextOntoKey(key, record, options.cleartext);
5229
- await registry.compareAndSwap(
5544
+ next = nextKept(record, key.generation, options.keep);
5545
+ wrote = (await registry.compareAndSwap(
5230
5546
  key,
5231
5547
  record.token,
5232
5548
  {
5233
5549
  currentGen: key.generation,
5234
- ...options.summary === void 0 ? {} : { summary: options.summary }
5550
+ ...options.summary === void 0 ? {} : { summary: options.summary },
5551
+ ...next.list === void 0 ? {} : { keptGens: next.list },
5552
+ ...leasesPatch(record, options)
5235
5553
  },
5236
5554
  write
5237
- );
5555
+ )).token;
5238
5556
  }
5239
- return true;
5557
+ return {
5558
+ published: true,
5559
+ kept: { ...next ?? { list: void 0, evict: [] }, token: wrote }
5560
+ };
5240
5561
  } catch (err) {
5241
5562
  if (isWriteConflictError(err)) {
5242
5563
  failedOn = record;
5564
+ if (throughChurn) attempt -= 1;
5243
5565
  continue;
5244
5566
  }
5245
5567
  if (!isTransientError(err)) throw err;
@@ -5268,10 +5590,12 @@ async function publishGeneration(registry, key, options = {}) {
5268
5590
  }
5269
5591
  const final = await registry.get(key);
5270
5592
  if (final !== null && final.currentGen !== null && final.currentGen >= key.generation) {
5271
- if (final.currentGen !== key.generation) return false;
5272
- if (!sawUnanswered) return true;
5593
+ if (final.currentGen !== key.generation) return REFUSED;
5594
+ if (!sawUnanswered) return { published: true };
5273
5595
  try {
5274
- return await landedHere(final, lastRecord ?? null, key, options.holdsOwnObject);
5596
+ return {
5597
+ published: await landedHere(final, lastRecord ?? null, key, options.holdsOwnObject)
5598
+ };
5275
5599
  } catch (proofErr) {
5276
5600
  throw outcomeUnknown(key, proofErr);
5277
5601
  }
@@ -5281,6 +5605,10 @@ async function publishGeneration(registry, key, options = {}) {
5281
5605
  `publishGeneration: contention setting currentGen for ${key.segment}`
5282
5606
  );
5283
5607
  }
5608
+ var REFUSED = { published: false };
5609
+ async function publishGeneration(registry, key, options = {}) {
5610
+ return (await publishGenerationKept(registry, key, options)).published;
5611
+ }
5284
5612
  async function landedHere(now, before, key, holdsOwnObject) {
5285
5613
  if (now === null || now.status === "destroyed" || now.currentGen !== key.generation) return false;
5286
5614
  if (before !== null && !sameIncarnation(before, now)) return false;
@@ -5838,7 +6166,7 @@ async function runLoad(ref, prepare, deps, options) {
5838
6166
  const key = { namespace: ref.namespace, segment: ref.segment, generation };
5839
6167
  const reclaim = async () => {
5840
6168
  const now = await deps.registry.get(ref);
5841
- if (now !== null && (now.token === fromToken || now.status === "destroyed")) {
6169
+ if (now !== null && (now.token === fromToken || now.status === "destroyed" || row !== null && onlyLeasesDiffer(row, now))) {
5842
6170
  await deps.storage.delete(key);
5843
6171
  return;
5844
6172
  }
@@ -5880,14 +6208,17 @@ async function runLoad(ref, prepare, deps, options) {
5880
6208
  if (guard?.minRetained !== void 0 && before !== null) {
5881
6209
  if (written.cardinality < before * guard.minRetained) return refuse("min-retained");
5882
6210
  }
6211
+ const leasesNow = deps.clock === void 0 ? void 0 : () => deps.clock.now();
5883
6212
  let published;
5884
6213
  try {
5885
- published = await publishGeneration(deps.registry, key, {
6214
+ published = await publishGenerationKept(deps.registry, key, {
6215
+ keep,
6216
+ leasesNow,
5886
6217
  row,
5887
6218
  wrappedDeks: written.wrappedDeks,
5888
6219
  summary: written.summary,
5889
6220
  cleartext: !written.encrypted,
5890
- ...fromToken === void 0 ? {} : { expectToken: fromToken },
6221
+ ...fromToken === void 0 ? {} : { expectToken: fromToken, expectRow: row },
5891
6222
  ...needsBefore && fromGeneration !== void 0 ? { expectFrom: fromGeneration } : {},
5892
6223
  // The third case, and the one the two fences above structurally cannot cover: the guard judged a segment
5893
6224
  // that had NO ROW. Both `expectFrom` and `expectToken` compare against a value read from a row, so with no
@@ -5911,7 +6242,7 @@ async function runLoad(ref, prepare, deps, options) {
5911
6242
  if (isValidationError(err) || err instanceof KeyUnavailableError) await reclaim();
5912
6243
  throw err;
5913
6244
  }
5914
- if (!published) return refuse("superseded");
6245
+ if (!published.published) return refuse("superseded");
5915
6246
  audit.onEvent({
5916
6247
  kind: "segment.publish",
5917
6248
  segment: ref.segment,
@@ -5922,7 +6253,10 @@ async function runLoad(ref, prepare, deps, options) {
5922
6253
  const collected = await collectAfterLoad(ref, deps, {
5923
6254
  generation,
5924
6255
  keep,
5925
- byName: checked && !currentObjectGone && deps.collectByListing !== true,
6256
+ byName: checked && deps.collectByListing !== true,
6257
+ currentGone: currentObjectGone,
6258
+ ...leasesNow === void 0 ? {} : { leases: { now: leasesNow } },
6259
+ kept: published.kept,
5926
6260
  ...current.fromSummary && fromGeneration !== void 0 ? { proveCurrent: { ...ref, generation: fromGeneration } } : {}
5927
6261
  });
5928
6262
  return {
@@ -6021,10 +6355,25 @@ async function rollbackSegment(ref, toGeneration, deps, options = {}) {
6021
6355
  { cardinality: reader.count(), metadata: reader.metadata },
6022
6356
  aead
6023
6357
  );
6024
- const { token } = await deps.registry.compareAndSwap(ref, record.token, {
6025
- currentGen: toGeneration,
6026
- summary
6027
- });
6358
+ const churn = leaseChurn(deps);
6359
+ let against = record.token;
6360
+ let token;
6361
+ for (; ; ) {
6362
+ try {
6363
+ ({ token } = await deps.registry.compareAndSwap(ref, against, {
6364
+ currentGen: toGeneration,
6365
+ summary
6366
+ }));
6367
+ break;
6368
+ } catch (err) {
6369
+ if (!isWriteConflictError(err)) throw err;
6370
+ const seen = await deps.registry.get(ref);
6371
+ const settled2 = await churn.settle(record, seen, () => deps.registry.get(ref));
6372
+ const now = settled2?.row ?? null;
6373
+ if (settled2 === void 0 || now === null || !onlyLeasesDiffer(record, now)) throw err;
6374
+ against = now.token;
6375
+ }
6376
+ }
6028
6377
  let stillThere = false;
6029
6378
  for await (const key of deps.storage.list(ref)) {
6030
6379
  if (key.generation === toGeneration) {
@@ -6047,13 +6396,31 @@ async function rollbackSegment(ref, toGeneration, deps, options = {}) {
6047
6396
  }
6048
6397
  if (!stillThere || replaced) {
6049
6398
  let undone = false;
6050
- try {
6051
- await deps.registry.compareAndSwap(ref, token, {
6052
- currentGen: record.currentGen,
6053
- summary: describingSummary(record)
6054
- });
6055
- undone = true;
6056
- } catch {
6399
+ let undoAgainst = token;
6400
+ for (; ; ) {
6401
+ try {
6402
+ await deps.registry.compareAndSwap(ref, undoAgainst, {
6403
+ currentGen: record.currentGen,
6404
+ summary: describingSummary(record)
6405
+ });
6406
+ undone = true;
6407
+ break;
6408
+ } catch (err) {
6409
+ if (!isWriteConflictError(err)) break;
6410
+ const moved = ["currentGen", "summary", "keptGens"];
6411
+ let now;
6412
+ try {
6413
+ const seen = await deps.registry.get(ref);
6414
+ if (seen === null || seen.currentGen !== toGeneration) break;
6415
+ const settled2 = await churn.settle(record, seen, () => deps.registry.get(ref), moved);
6416
+ now = settled2?.row ?? null;
6417
+ } catch {
6418
+ break;
6419
+ }
6420
+ if (now === null || now.currentGen !== toGeneration || !onlyLeasesDiffer(record, now, moved))
6421
+ break;
6422
+ undoAgainst = now.token;
6423
+ }
6057
6424
  }
6058
6425
  throw new NotFoundError(
6059
6426
  `rollback: generation ${toGeneration} of "${ref.segment}" was ${replaced ? "replaced" : "collected"} while the pointer was moving` + (undone ? " \u2014 the pointer was put back" : `, and the move back failed, so the pointer may still name ${toGeneration} (a write that landed and lost its response reads as a failure here). Check which generation is current, then re-run a rollback to a generation that exists, or restore the object.`)
@@ -6117,7 +6484,7 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
6117
6484
  if (row === null) return "absent";
6118
6485
  if (row.status === "destroyed") return "destroyed";
6119
6486
  if (row.currentGen === null) return "no-generation";
6120
- return row.currentGen === from && row.token === fromToken ? null : "superseded";
6487
+ return row.currentGen === from && (row.token === fromToken || onlyLeasesDiffer(record, row)) ? null : "superseded";
6121
6488
  };
6122
6489
  const holds = async (generation2) => {
6123
6490
  const key2 = { ...base, generation: generation2 };
@@ -6288,7 +6655,13 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
6288
6655
  const published = await publishGeneration(deps.registry, key, {
6289
6656
  expectFrom: from,
6290
6657
  expectToken: fromToken,
6658
+ // A lease written while the rewrite streams moves the token and nothing it derived from: it does not refuse it.
6659
+ expectRow: record,
6291
6660
  summary,
6661
+ // The row then records that no generation below the new pointer is kept, as the collection below leaves it.
6662
+ keep: 0,
6663
+ // Every generation below the new pointer is deleted whatever a lease says, so the row drops its leases.
6664
+ clearLeases: true,
6292
6665
  // A write that ends without an answer is settled by reading the row, and a pointer at this number is this
6293
6666
  // rewrite's only over the object it wrote: the footer proves it, so another incarnation's cannot pass for it.
6294
6667
  holdsOwnObject: () => provesOwnObject(deps.storage, key, fingerprint),
@@ -6621,8 +6994,17 @@ async function listGenerations2(storage, ref) {
6621
6994
  }
6622
6995
  async function shredSegment(ref, deps, allowCleartext, op = "destroySegment") {
6623
6996
  const base = { segment: ref.segment, namespace: ref.namespace };
6997
+ const churn = leaseChurn(deps);
6998
+ let lost;
6624
6999
  for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
6625
- const record = await deps.registry.get(ref);
7000
+ let record = await deps.registry.get(ref);
7001
+ if (lost !== void 0) {
7002
+ const settled2 = await churn.settle(lost, record, () => deps.registry.get(ref));
7003
+ if (settled2 !== void 0) {
7004
+ record = settled2.row;
7005
+ attempt -= 1;
7006
+ }
7007
+ }
6626
7008
  if (record === null) {
6627
7009
  return { ...base, destroyed: false, cryptoShredded: false, reason: "absent" };
6628
7010
  }
@@ -6641,11 +7023,14 @@ async function shredSegment(ref, deps, allowCleartext, op = "destroySegment") {
6641
7023
  keyId: void 0,
6642
7024
  // The current generation's cached count and metadata go with it: sealed, they cannot be opened without
6643
7025
  // the wrappings; clear, they would outlive the segment on its tombstone.
6644
- summary: void 0
7026
+ summary: void 0,
7027
+ // A tombstone holds nothing: a lease names a generation of a segment that now resolves none.
7028
+ leases: void 0
6645
7029
  });
6646
7030
  return { ...base, destroyed: true, cryptoShredded: encrypted };
6647
7031
  } catch (err) {
6648
7032
  if (!isWriteConflictError(err)) throw err;
7033
+ lost = record;
6649
7034
  }
6650
7035
  }
6651
7036
  throw new WriteConflictError(`${op}: contention shredding "${ref.segment}" \u2014 retry`);
@@ -6713,8 +7098,17 @@ async function setSegmentRetention(ref, deps, policy) {
6713
7098
  validateRetentionPolicy(policy);
6714
7099
  const base = { segment: ref.segment, namespace: ref.namespace };
6715
7100
  let previousExpiresAt;
7101
+ const churn = leaseChurn(deps);
7102
+ let lost;
6716
7103
  for (let attempt = 0; attempt < RETENTION_CAS_ATTEMPTS; attempt += 1) {
6717
- const record = await deps.registry.get(ref);
7104
+ let record = await deps.registry.get(ref);
7105
+ if (lost !== void 0) {
7106
+ const settled2 = await churn.settle(lost, record, () => deps.registry.get(ref));
7107
+ if (settled2 !== void 0) {
7108
+ record = settled2.row;
7109
+ attempt -= 1;
7110
+ }
7111
+ }
6718
7112
  try {
6719
7113
  previousExpiresAt = readExpiresAt(record);
6720
7114
  if (record === null) {
@@ -6741,6 +7135,7 @@ async function setSegmentRetention(ref, deps, policy) {
6741
7135
  };
6742
7136
  } catch (err) {
6743
7137
  if (!isWriteConflictError(err)) throw err;
7138
+ lost = record ?? void 0;
6744
7139
  }
6745
7140
  }
6746
7141
  throw new WriteConflictError(
@@ -6749,8 +7144,17 @@ async function setSegmentRetention(ref, deps, policy) {
6749
7144
  }
6750
7145
  async function clearSegmentRetention(ref, deps) {
6751
7146
  validateUserRef(ref);
7147
+ const churn = leaseChurn(deps);
7148
+ let lost;
6752
7149
  for (let attempt = 0; attempt < RETENTION_CAS_ATTEMPTS; attempt += 1) {
6753
- const record = await deps.registry.get(ref);
7150
+ let record = await deps.registry.get(ref);
7151
+ if (lost !== void 0) {
7152
+ const settled2 = await churn.settle(lost, record, () => deps.registry.get(ref));
7153
+ if (settled2 !== void 0) {
7154
+ record = settled2.row;
7155
+ attempt -= 1;
7156
+ }
7157
+ }
6754
7158
  if (record === null) return false;
6755
7159
  if (record.status === "destroyed") return false;
6756
7160
  if (record.retention === void 0 || !(EXPIRES_AT in record.retention)) return false;
@@ -6764,6 +7168,7 @@ async function clearSegmentRetention(ref, deps) {
6764
7168
  return true;
6765
7169
  } catch (err) {
6766
7170
  if (!isWriteConflictError(err)) throw err;
7171
+ lost = record;
6767
7172
  }
6768
7173
  }
6769
7174
  throw new WriteConflictError(
@@ -7135,6 +7540,31 @@ function failureReason(err) {
7135
7540
  return `failed: ${err instanceof Error ? err.message : String(err)}`;
7136
7541
  }
7137
7542
 
7543
+ // src/core/registry-reap.ts
7544
+ var DEFAULT_REAP_LIMIT = 1e3;
7545
+ async function reapRegistryTombstones(registry, options = {}) {
7546
+ const dryRun = options.dryRun === true;
7547
+ if (!dryRun && options.confirmNoLegacyWriters !== true) {
7548
+ throw new ValidationError(
7549
+ "reapRegistryTombstones: a real run needs `confirmNoLegacyWriters: true`, your statement that no process on a release before 0.12 writes this registry (one that did would issue a removed row\u2019s tokens again). Pass `dryRun: true` to see what would be removed."
7550
+ );
7551
+ }
7552
+ const limit = options.limit ?? DEFAULT_REAP_LIMIT;
7553
+ if (!Number.isInteger(limit) || limit < 1) {
7554
+ throw new ValidationError(
7555
+ `reapRegistryTombstones: \`limit\` must be a positive integer; got ${String(limit)}`
7556
+ );
7557
+ }
7558
+ if (options.namespace !== void 0) validateUserNamespace(options.namespace);
7559
+ if (registry.reapLegacyTombstones === void 0) {
7560
+ throw new UnsupportedError(
7561
+ "reapRegistryTombstones: this registry keeps no deleted rows of a release before 0.12 to remove (only the object-store registries, S3, GCS and Azure Blob, do)"
7562
+ );
7563
+ }
7564
+ const request = options.namespace === void 0 ? { dryRun, limit } : { namespace: options.namespace, dryRun, limit };
7565
+ return registry.reapLegacyTombstones(request);
7566
+ }
7567
+
7138
7568
  // src/core/consistency.ts
7139
7569
  var DEFAULT_CHECK_CONCURRENCY = 8;
7140
7570
  async function generationsPresent(storage, ref) {
@@ -7338,7 +7768,12 @@ export {
7338
7768
  InProcessKeystore,
7339
7769
  IntegrityError,
7340
7770
  KeyUnavailableError,
7771
+ LEASE_SKEW_MS,
7772
+ LeaseExpiredError,
7773
+ LeaseLimitError,
7341
7774
  LocalFsStorage,
7775
+ MAX_LEASES_PER_SEGMENT,
7776
+ MAX_LEASE_MS,
7342
7777
  MIN_EXPIRES_AT_MS,
7343
7778
  MemoryStorage,
7344
7779
  NOOP_METRICS,
@@ -7357,6 +7792,7 @@ export {
7357
7792
  checkBudget,
7358
7793
  clearSegmentRetention,
7359
7794
  collectWithinBudget,
7795
+ decodeSerialized,
7360
7796
  destroySegment,
7361
7797
  dropSegment,
7362
7798
  eraseIdFromSegment,
@@ -7367,6 +7803,8 @@ export {
7367
7803
  groundedReport,
7368
7804
  isCloudRoaringError,
7369
7805
  isIntegrityError,
7806
+ isLeaseExpiredError,
7807
+ isLeaseLimitError,
7370
7808
  isNotFoundError,
7371
7809
  isStorageBackend,
7372
7810
  isTransientError,
@@ -7378,6 +7816,8 @@ export {
7378
7816
  loadSegmentChunks,
7379
7817
  mapWithConcurrency,
7380
7818
  readRetentionPolicy,
7819
+ reapRegistryTombstones,
7820
+ releaseLease,
7381
7821
  resolveBudget,
7382
7822
  resolvePerOpBudget,
7383
7823
  retireExpired,
@@ -7389,6 +7829,7 @@ export {
7389
7829
  segmentKey,
7390
7830
  setSegmentRetention,
7391
7831
  splitId,
7832
+ takeLease,
7392
7833
  withRetry
7393
7834
  };
7394
7835
  //# sourceMappingURL=index.js.map