@cloudbitmaps/core 0.15.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-B7IGS3W6.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;
@@ -494,6 +512,8 @@ var SegmentEngine = class {
494
512
  * they deliver, as a read whose entry an invalidation dropped does not (streams hold no `openReads` entry).
495
513
  */
496
514
  openStreams = /* @__PURE__ */ new Set();
515
+ /** How many times {@link invalidate} has been called: a read compares it with the count it began under. */
516
+ invalidations = 0;
497
517
  constructor(deps) {
498
518
  this.storage = deps.storage;
499
519
  this.cache = deps.cache;
@@ -578,8 +598,15 @@ var SegmentEngine = class {
578
598
  * coalesced ranges when the source has `getChunks`, opening 1, 2, 4 … ranges wide up to {@link DEFAULT_INTERSECT_CONCURRENCY},
579
599
  * else the chunk-by-chunk window of that many reads. `close` stops what is left of it.
580
600
  */
581
- chunkSequence(seg, chunkKeys, gen) {
582
- const streamed = this.openStreamed(seg, chunkKeys, gen, DEFAULT_INTERSECT_CONCURRENCY, 1);
601
+ chunkSequence(seg, chunkKeys, gen, epoch) {
602
+ const streamed = this.openStreamed(
603
+ seg,
604
+ chunkKeys,
605
+ gen,
606
+ epoch,
607
+ DEFAULT_INTERSECT_CONCURRENCY,
608
+ 1
609
+ );
583
610
  if (streamed !== void 0) {
584
611
  return {
585
612
  take: (chunkKey) => this.streamedChunk(streamed, chunkKey),
@@ -604,10 +631,11 @@ var SegmentEngine = class {
604
631
  return range === void 0 ? this.iterateAll(seg) : this.iterateRange(seg, range);
605
632
  }
606
633
  async *iterateAll(seg) {
634
+ const epoch = this.invalidations;
607
635
  const chunkKeys = await this.chunkKeys(seg);
608
636
  checkBudget(this.budget, chunkKeys.length, "iterate");
609
637
  const gen = await this.cacheVersion(seg);
610
- const chunks = this.chunkSequence(seg, chunkKeys, gen);
638
+ const chunks = this.chunkSequence(seg, chunkKeys, gen, epoch);
611
639
  try {
612
640
  for (const chunkKey of chunkKeys) {
613
641
  const chunk = await chunks.take(chunkKey);
@@ -619,12 +647,13 @@ var SegmentEngine = class {
619
647
  }
620
648
  }
621
649
  async *iterateRange(seg, range) {
650
+ const epoch = this.invalidations;
622
651
  const w = windowOf(range) ?? WHOLE_ID_SPACE;
623
652
  if (w === "empty") return;
624
653
  const chunkKeys = keysWithin(await this.chunkKeys(seg), w);
625
654
  checkBudget(this.budget, chunkKeys.length, "iterate");
626
655
  const gen = await this.cacheVersion(seg);
627
- const chunks = this.chunkSequence(seg, chunkKeys, gen);
656
+ const chunks = this.chunkSequence(seg, chunkKeys, gen, epoch);
628
657
  try {
629
658
  for (const chunkKey of chunkKeys) {
630
659
  const chunk = await chunks.take(chunkKey);
@@ -646,12 +675,13 @@ var SegmentEngine = class {
646
675
  * the caller's to keep.
647
676
  */
648
677
  async *iterateBatches(seg, range) {
678
+ const epoch = this.invalidations;
649
679
  const w = windowOf(range);
650
680
  if (w === "empty") return;
651
681
  const chunkKeys = w === null ? await this.chunkKeys(seg) : keysWithin(await this.chunkKeys(seg), w);
652
682
  checkBudget(this.budget, chunkKeys.length, "iterate");
653
683
  const gen = await this.cacheVersion(seg);
654
- const chunks = this.chunkSequence(seg, chunkKeys, gen);
684
+ const chunks = this.chunkSequence(seg, chunkKeys, gen, epoch);
655
685
  try {
656
686
  for (const chunkKey of chunkKeys) {
657
687
  const chunk = await chunks.take(chunkKey);
@@ -663,6 +693,104 @@ var SegmentEngine = class {
663
693
  chunks.close();
664
694
  }
665
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
+ }
666
794
  /**
667
795
  * `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
668
796
  *
@@ -731,6 +859,26 @@ var SegmentEngine = class {
731
859
  andNotBatches(seg, excludes, options) {
732
860
  return this.combineBatches([seg], excludes, "all", "andNot", options);
733
861
  }
862
+ /**
863
+ * {@link intersect}, as the chunks the result is made of: `{ chunkKey, bitmap }` in ascending key order, each a
864
+ * non-empty bitmap of the chunk's 16-bit remainders, cut to the read's range. What a materialisation writes
865
+ * straight into its new generation, with no id built on the way.
866
+ *
867
+ * **Each bitmap belongs to the caller, who may change it:** it is the combine's own accumulator or a bitmap built
868
+ * for the range's edge, never a chunk the cache holds or another operand's. The reads, the budget, the metric and the
869
+ * errors are the id reads' own.
870
+ */
871
+ intersectChunks(segs, options) {
872
+ return this.combineChunks(segs, options?.exclude ?? [], "all", "intersect", options);
873
+ }
874
+ /** {@link union}, as chunks: see {@link intersectChunks}. */
875
+ unionChunks(segs, options) {
876
+ return this.combineChunks(segs, options?.exclude ?? [], "any", "union", options);
877
+ }
878
+ /** {@link andNot}, as chunks: see {@link intersectChunks}. */
879
+ andNotChunks(seg, excludes, options) {
880
+ return this.combineChunks([seg], excludes, "all", "andNot", options);
881
+ }
734
882
  /**
735
883
  * Everything a combine decides before it reads a chunk: validation, each operand's keys and generation, the
736
884
  * absent-operand refusal, key alignment, the budget and the metric. `null` when the range is empty. The per-id
@@ -753,10 +901,11 @@ var SegmentEngine = class {
753
901
  const w = windowOf(options);
754
902
  if (w === "empty") return null;
755
903
  const extract = async (seg) => {
904
+ const epoch = this.invalidations;
756
905
  const all = await this.chunkKeys(seg);
757
906
  const gen = await this.cacheVersion(seg);
758
907
  const keys = w === null ? all : keysWithin(all, w);
759
- return { seg, keys: new Set(keys), chunkless: all.length === 0, gen };
908
+ return { seg, keys: new Set(keys), chunkless: all.length === 0, gen, epoch };
760
909
  };
761
910
  const [operands, excludes] = await Promise.all([
762
911
  Promise.all(segs.map(extract)),
@@ -801,14 +950,15 @@ var SegmentEngine = class {
801
950
  const open4 = (o, wanted) => {
802
951
  o.streamed = this.openStreamed(
803
952
  o.seg,
804
- common.filter(wanted),
953
+ wanted === void 0 ? common : common.filter(wanted),
805
954
  o.gen,
955
+ o.epoch,
806
956
  limit,
807
957
  COMBINE_RANGE_START
808
958
  );
809
- if (o.streamed?.stream) streams.push(o.streamed);
959
+ if (o.streamed) streams.push(o.streamed);
810
960
  };
811
- for (const o of operands) open4(o, mode === "all" ? () => true : (k) => o.keys.has(k));
961
+ for (const o of operands) open4(o, mode === "all" ? void 0 : (k) => o.keys.has(k));
812
962
  if (streamExcludes) for (const e of excludes) open4(e, (k) => e.keys.has(k));
813
963
  return {
814
964
  w,
@@ -853,6 +1003,28 @@ var SegmentEngine = class {
853
1003
  for (const stream of plan.streams) this.closeStreamed(stream);
854
1004
  }
855
1005
  }
1006
+ /** The chunk read: the plan every combine shares, then each key's result, cut to the range at its edges. */
1007
+ async *combineChunks(segs, excludeSegs, mode, op, options) {
1008
+ const plan = await this.combinePlan(segs, excludeSegs, mode, op, options);
1009
+ if (plan === null) return;
1010
+ const { w, window } = plan;
1011
+ try {
1012
+ for (let slot = await window.next(); slot !== void 0; slot = await window.next()) {
1013
+ if (!slot.result || slot.result.isEmpty) continue;
1014
+ const bitmap = w !== null && isEdge(slot.key, w) ? this.cutToWindow(slot.result, slot.key, w) : slot.result;
1015
+ if (!bitmap.isEmpty) yield { chunkKey: slot.key, bitmap };
1016
+ }
1017
+ } finally {
1018
+ for (const stream of plan.streams) this.closeStreamed(stream);
1019
+ }
1020
+ }
1021
+ /** An edge chunk's bitmap cut to the window, as a new bitmap: the remainders `edgeIds` would yield. */
1022
+ cutToWindow(chunk, chunkKey, w) {
1023
+ const from = chunkKey === w.loKey ? w.loRem : 0;
1024
+ const to = chunkKey === w.hiKey ? w.hiRem : MAX_REMAINDER;
1025
+ const rem = chunk.toUint32Array ? chunk.toUint32Array() : Uint32Array.from(chunk);
1026
+ return this.codec.fromValues(rem.subarray(firstAbove(rem, from - 1), firstAbove(rem, to)));
1027
+ }
856
1028
  /**
857
1029
  * The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
858
1030
  * Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
@@ -902,25 +1074,43 @@ var SegmentEngine = class {
902
1074
  return op.streamed === void 0 ? this.storageChunk({ ...op.seg, chunkKey }, op.gen) : this.streamedChunk(op.streamed, chunkKey);
903
1075
  }
904
1076
  /**
905
- * Open `keys` of `seg` as a stream of coalesced ranges, or `undefined` when the read is per key: a source with no
906
- * `getChunks`, a segment with no generation, or nothing to read. The chunks already decoded in the cache are taken out
907
- * of the key list first and served from there, so a repeat read asks the source for nothing it holds. Nothing is
908
- * requested until the first chunk is asked for.
1077
+ * `keys` of `seg` as a stream of coalesced ranges, or `undefined` when the read is per key: a source with no
1078
+ * `getChunks`, a segment with no generation, or nothing to read. Nothing is requested, and no cache lookup made, until
1079
+ * the first chunk is asked for (see {@link StreamedChunks}).
909
1080
  */
910
- openStreamed(seg, keys, gen, concurrency, rampStart) {
1081
+ openStreamed(seg, keys, gen, epoch, concurrency, rampStart) {
1082
+ if (this.storage.getChunks === void 0 || gen === null || keys.length === 0) return void 0;
1083
+ return {
1084
+ seg,
1085
+ gen,
1086
+ epoch,
1087
+ keys,
1088
+ concurrency,
1089
+ rampStart,
1090
+ opened: false,
1091
+ inStream: void 0,
1092
+ stream: void 0,
1093
+ invalidated: false
1094
+ };
1095
+ }
1096
+ /**
1097
+ * Open the stream of `streamed`, at its first uncached chunk `from`: over that chunk and the later keys the cache does
1098
+ * not hold. Each later key is counted as a cache hit or miss here, so a key is counted once whichever way it is served.
1099
+ */
1100
+ startStream(streamed, from) {
1101
+ const { seg, gen } = streamed;
911
1102
  const getChunks = this.storage.getChunks;
912
- if (getChunks === void 0 || gen === null || keys.length === 0) return void 0;
913
- let wanted = keys;
914
- let inStream;
1103
+ let wanted = streamed.keys.slice(streamed.keys.indexOf(from));
915
1104
  if (this.cache) {
916
- const misses = [];
917
- for (const chunkKey of keys) {
1105
+ const misses = [from];
1106
+ for (let i = 1; i < wanted.length; i++) {
1107
+ const chunkKey = wanted[i];
918
1108
  const hit = this.cache.get(this.chunkCacheKey({ ...seg, chunkKey }, gen));
919
1109
  if (!hit) misses.push(chunkKey);
920
1110
  if (this.metricsOn) this.metrics.onEvent({ kind: "cache", hit: hit !== void 0 });
921
1111
  }
1112
+ if (misses.length < wanted.length) streamed.inStream = new Set(misses);
922
1113
  wanted = misses;
923
- if (misses.length < keys.length) inStream = new Set(misses);
924
1114
  }
925
1115
  const onRequest = this.metricsOn ? (request) => {
926
1116
  this.metrics.onEvent({
@@ -931,27 +1121,36 @@ var SegmentEngine = class {
931
1121
  ms: request.ms
932
1122
  });
933
1123
  } : void 0;
934
- const stream = wanted.length === 0 ? void 0 : new ChunkStream(
1124
+ streamed.stream = new ChunkStream(
935
1125
  getChunks.call(this.storage, seg, wanted, {
936
- concurrency,
937
- ramp: rampStart,
1126
+ concurrency: streamed.concurrency,
1127
+ ramp: streamed.rampStart,
938
1128
  ...onRequest === void 0 ? {} : { onRequest }
939
1129
  })
940
1130
  );
941
- const streamed = { seg, gen, inStream, stream, invalidated: false };
942
- if (stream) this.openStreams.add(streamed);
943
- return streamed;
1131
+ streamed.opened = true;
1132
+ streamed.invalidated = this.storage.currentVersion === void 0 && this.invalidations !== streamed.epoch;
1133
+ this.openStreams.add(streamed);
944
1134
  }
945
1135
  closeStreamed(streamed) {
946
1136
  streamed.stream?.close();
947
1137
  this.openStreams.delete(streamed);
948
1138
  }
949
1139
  /**
950
- * The decoded chunk at `chunkKey` of a streamed read: the next chunk of its stream, or, for a chunk the cache held
951
- * when the read opened, the cached instance while it is still there, and a read of that chunk alone once it is not.
1140
+ * The decoded chunk at `chunkKey` of a streamed read: from the cache while the stream is unopened and the cache holds
1141
+ * it; else the next chunk of its stream, or, for a chunk the cache held when the stream opened, the cached instance
1142
+ * while it is still there, and a read of that chunk alone once it is not.
952
1143
  */
953
1144
  streamedChunk(streamed, chunkKey) {
954
- if (streamed.inStream !== void 0 && !streamed.inStream.has(chunkKey)) {
1145
+ if (!streamed.opened) {
1146
+ const cache = this.cache;
1147
+ if (cache) {
1148
+ const hit = cache.get(this.chunkCacheKey({ ...streamed.seg, chunkKey }, streamed.gen));
1149
+ if (this.metricsOn) this.metrics.onEvent({ kind: "cache", hit: hit !== void 0 });
1150
+ if (hit) return Promise.resolve(hit);
1151
+ }
1152
+ this.startStream(streamed, chunkKey);
1153
+ } else if (streamed.inStream !== void 0 && !streamed.inStream.has(chunkKey)) {
955
1154
  const ref = { ...streamed.seg, chunkKey };
956
1155
  const hit = this.cache?.get(this.chunkCacheKey(ref, streamed.gen));
957
1156
  return hit ? Promise.resolve(hit) : this.storageChunk(ref, streamed.gen);
@@ -1074,6 +1273,7 @@ var SegmentEngine = class {
1074
1273
  */
1075
1274
  invalidate(ref) {
1076
1275
  const prefix = segmentPrefix(ref);
1276
+ this.invalidations += 1;
1077
1277
  for (const streamed of this.openStreams) {
1078
1278
  if (segmentPrefix(streamed.seg) === prefix) streamed.invalidated = true;
1079
1279
  }
@@ -1360,63 +1560,121 @@ async function nextLoadGeneration(ref, deps, row) {
1360
1560
  }
1361
1561
  var LIST_COLLECTION_CADENCE = 16;
1362
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) {
1363
1566
  const keep = options.keep ?? 1;
1567
+ const quiet = options.quiet === true;
1364
1568
  if (!Number.isInteger(keep) || keep < 0) {
1365
1569
  throw new ValidationError(`keep must be a non-negative integer; got ${String(keep)}`);
1366
1570
  }
1367
1571
  const record = await deps.registry.get(ref);
1368
- if (record === null) return [];
1572
+ if (record === null) return { deleted: [], kept: [], complete: true };
1369
1573
  const current = record.currentGen;
1370
1574
  const seen = /* @__PURE__ */ new Set();
1371
1575
  for await (const key of deps.storage.list(ref)) seen.add(key.generation);
1372
1576
  const gens = [...seen];
1373
1577
  const after = await deps.registry.get(ref);
1374
- if (after === null)
1578
+ if (after === null) {
1579
+ if (quiet) return { deleted: [], kept: [], complete: false };
1375
1580
  throw new WriteConflictError(`registry row for segment ${ref.segment} was purged mid-pass`);
1581
+ }
1376
1582
  if (record.status === "destroyed" && after.token !== record.token) {
1583
+ if (quiet) return { deleted: [], kept: [], complete: false };
1377
1584
  throw new WriteConflictError(
1378
1585
  `segment ${ref.segment} changed incarnation while its generations were being listed`
1379
1586
  );
1380
1587
  }
1381
1588
  const cutoff = current === null || after.currentGen === null ? null : Math.min(current, after.currentGen);
1382
- 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) => {
1383
1592
  const still = await deps.registry.get(ref);
1384
1593
  if (still === null) {
1594
+ if (quiet) return "stop";
1385
1595
  throw new WriteConflictError(`registry row for segment ${ref.segment} was purged mid-pass`);
1386
1596
  }
1387
1597
  const ok = record.status === "destroyed" ? still.token === after.token : cutoff === null || still.currentGen !== null && still.currentGen >= cutoff;
1388
- 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";
1389
1602
  throw new WriteConflictError(
1390
1603
  `segment ${ref.segment} changed incarnation while its generations were being collected`
1391
1604
  );
1392
1605
  }
1606
+ if (guard !== void 0 && heldGenerations(still, guard.now()).has(generation)) return "skip";
1607
+ return "delete";
1393
1608
  };
1394
- 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 ? (
1395
1613
  // No Storage pointer yet, so "below current" selects nothing and there is nothing safe to infer: an object
1396
1614
  // here is either a load about to publish or an orphan we cannot tell apart from it. Deleting would race
1397
1615
  // that publish into a dangling pointer. It is collected once a pointer exists.
1398
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))
1399
1620
  ) : (
1400
1621
  // Delete generations below the cutoff, except the newest `keep` of them (the grace window).
1401
- gens.filter((g) => g < cutoff).sort((a, b) => b - a).slice(keep)
1622
+ below.slice(keep)
1402
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 = [];
1403
1627
  for (const generation of toDelete) {
1404
- 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
+ }
1405
1634
  await deps.storage.delete({ namespace: ref.namespace, segment: ref.segment, generation });
1635
+ done.push(generation);
1406
1636
  }
1407
- 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
+ };
1408
1643
  }
1409
1644
  async function collectAfterLoad(ref, deps, options) {
1410
- const { generation, keep, byName, proveCurrent } = options;
1645
+ const { generation, keep, byName, currentGone, kept, proveCurrent, leases } = options;
1411
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 });
1412
1650
  const periodic = generation % LIST_COLLECTION_CADENCE === 0;
1413
- if (byName && keep <= 1 && !periodic) {
1414
- if (keep === 1 && proveCurrent !== void 0 && !await objectIsThere(deps.storage, proveCurrent)) {
1415
- 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;
1416
1672
  }
1417
- return collectByName(ref, deps, { generation, keep });
1418
1673
  }
1419
- 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]);
1420
1678
  }
1421
1679
  async function objectIsThere(storage, key) {
1422
1680
  try {
@@ -1426,21 +1684,20 @@ async function objectIsThere(storage, key) {
1426
1684
  return false;
1427
1685
  }
1428
1686
  }
1429
- async function collectByName(ref, deps, options) {
1430
- const { generation, keep } = options;
1431
- if (!Number.isInteger(keep) || keep < 0 || keep > 1) {
1432
- 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);
1433
1699
  }
1434
- const evicted = generation - keep - 1;
1435
- if (evicted < 0) return [];
1436
- const still = await deps.registry.get(ref);
1437
- if (still === null || still.currentGen === null || still.currentGen < generation) return [];
1438
- await deps.storage.delete({
1439
- namespace: ref.namespace,
1440
- segment: ref.segment,
1441
- generation: evicted
1442
- });
1443
- return [evicted];
1700
+ return deleted;
1444
1701
  }
1445
1702
 
1446
1703
  // src/core/reader-defaults.ts
@@ -1491,7 +1748,7 @@ var HOURS_PER_MONTH = 730;
1491
1748
  var SECONDS_PER_MONTH = HOURS_PER_MONTH * 3600;
1492
1749
  var GIB = 1024 ** 3;
1493
1750
  var STORE_LOAD_PUT_CLASS = 1 + 1 / LIST_COLLECTION_CADENCE;
1494
- var STORE_LOAD_POINTER_READS = 3 + 2 / LIST_COLLECTION_CADENCE;
1751
+ var STORE_LOAD_POINTER_READS = 2 + 2 / LIST_COLLECTION_CADENCE;
1495
1752
  var STORE_LOAD_EXISTENCE_CHECKS = 2 - 1 / LIST_COLLECTION_CADENCE;
1496
1753
  var RETENTION_SWEEP_REQUESTS = deepFreeze({
1497
1754
  conditionalDelete: {
@@ -1946,13 +2203,30 @@ async function runExport(reader, registry, sink, options = {}) {
1946
2203
  const segments = [];
1947
2204
  const failed = [];
1948
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;
1949
2213
  const exportRef = async (ref) => {
1950
2214
  let writer;
1951
2215
  let count = 0;
1952
2216
  let bytes = 0;
1953
2217
  try {
1954
2218
  validateSegmentRef(ref);
1955
- 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();
1956
2230
  writer = await sink.open(ref, ext);
1957
2231
  if (format === "roaring") {
1958
2232
  const bm = requireCodec(options.codec, "runExport({ format: 'roaring' })").empty();
@@ -1983,6 +2257,7 @@ async function runExport(reader, registry, sink, options = {}) {
1983
2257
  }
1984
2258
  }
1985
2259
  } catch (err) {
2260
+ if (err === unusable) throw err;
1986
2261
  if (writer !== void 0) {
1987
2262
  try {
1988
2263
  await writer.abort?.();
@@ -2108,6 +2383,10 @@ var MemoryRegistryDriver = class {
2108
2383
  this.rows.set(key, structuredClone(recordFromNew(ref, checked, this.now(), token)));
2109
2384
  return { token };
2110
2385
  }
2386
+ /**
2387
+ * A caller's `held` row (see `RegistryWriteOptions`) has no read to spare here: the token is compared with the stored row in the same step
2388
+ * as the write, in memory, so a stale one fails exactly as a lost race does.
2389
+ */
2111
2390
  async compareAndSwap(ref, expected, patch) {
2112
2391
  validateSegmentRef(ref);
2113
2392
  const checked = validateRegistryPatch(patch);
@@ -2463,6 +2742,10 @@ var LocalFsRegistryDriver = class {
2463
2742
  return { token };
2464
2743
  });
2465
2744
  }
2745
+ /**
2746
+ * A caller's `held` row (see `RegistryWriteOptions`) has no read to spare here: the row's file is read under its lock and its token compared
2747
+ * there, a local read and no request, so a stale one fails exactly as a lost race does.
2748
+ */
2466
2749
  async compareAndSwap(ref, expected, patch) {
2467
2750
  const checked = validateRegistryPatch(patch);
2468
2751
  const path = registryRowPath(this.root, ref);
@@ -3717,6 +4000,12 @@ var DecodedLoadInput = class {
3717
4000
  }
3718
4001
  bitmap;
3719
4002
  };
4003
+ var ChunkLoadInput = class {
4004
+ constructor(chunks) {
4005
+ this.chunks = chunks;
4006
+ }
4007
+ chunks;
4008
+ };
3720
4009
  function tagOf(value) {
3721
4010
  return Object.prototype.toString.call(value).slice(8, -1);
3722
4011
  }
@@ -3750,7 +4039,7 @@ function isShared(buffer) {
3750
4039
  }
3751
4040
  function overCap(what, length) {
3752
4041
  return new ValidationError(
3753
- `${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.`
3754
4043
  );
3755
4044
  }
3756
4045
  function prepareLoadInput(input, codec) {
@@ -3768,7 +4057,9 @@ function prepareLoadInput(input, codec) {
3768
4057
  if (Symbol.iterator in input || Symbol.asyncIterator in input) return input;
3769
4058
  const keys = Object.keys(input);
3770
4059
  if (keys.length === 1 && keys[0] === "serialized") {
3771
- return decode(input.serialized, "{ serialized }", codec);
4060
+ return new DecodedLoadInput(
4061
+ decodeSerialized(input.serialized, codec, "{ serialized }")
4062
+ );
3772
4063
  }
3773
4064
  if (keys.length === 1 && keys[0] === "bitmap") {
3774
4065
  const bitmap = input.bitmap;
@@ -3780,13 +4071,13 @@ function prepareLoadInput(input, codec) {
3780
4071
  const size = bitmap.getSerializationSizeInBytes("portable");
3781
4072
  if (typeof size === "number" && size > MAX_SERIALIZED_LOAD_BYTES) throw overCap(what, size);
3782
4073
  }
3783
- return decode(bitmap.serialize("portable"), what, codec);
4074
+ return new DecodedLoadInput(decodeSerialized(bitmap.serialize("portable"), codec, what));
3784
4075
  }
3785
4076
  throw new ValidationError(
3786
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.`
3787
4078
  );
3788
4079
  }
3789
- function decode(bytes, what, codec) {
4080
+ function decodeSerialized(bytes, codec, what = "the buffer") {
3790
4081
  if (typeof bytes !== "object" || bytes === null || tagOf(bytes) !== "Uint8Array") {
3791
4082
  throw new ValidationError(`${what} must be a Uint8Array of portable Roaring bytes`);
3792
4083
  }
@@ -3794,9 +4085,7 @@ function decode(bytes, what, codec) {
3794
4085
  if (view.byteLength > MAX_SERIALIZED_LOAD_BYTES) throw overCap(what, view.byteLength);
3795
4086
  const own = isShared(view.buffer) ? new Uint8Array(view) : view;
3796
4087
  try {
3797
- return new DecodedLoadInput(
3798
- codec.safeDeserialize(own, MAX_SERIALIZED_LOAD_BYTES, { whole: true })
3799
- );
4088
+ return codec.safeDeserialize(own, MAX_SERIALIZED_LOAD_BYTES, { whole: true });
3800
4089
  } catch (err) {
3801
4090
  if (!isIntegrityError(err)) throw err;
3802
4091
  throw new ValidationError(`${what} is not a valid bitmap: ${err.message}`);
@@ -3939,6 +4228,8 @@ var notThePinned = (ref, generation) => new NotFoundError(
3939
4228
  function versionOf(generation, lineage) {
3940
4229
  return lineage === void 0 ? String(generation) : `${generation}:${String(lineage)}`;
3941
4230
  }
4231
+ var PinMoved = class extends Error {
4232
+ };
3942
4233
  function summaryOfGeneration(generation, description) {
3943
4234
  const { cardinality, metadata } = description;
3944
4235
  return metadata === void 0 || Object.keys(metadata).length === 0 ? { generation, cardinality } : { generation, cardinality, metadata: Object.freeze({ ...metadata }) };
@@ -4305,17 +4596,102 @@ var CrbmStorageChunkSource = class {
4305
4596
  * and a `keep: 0` sweep can land in that gap, and nothing has been handed out yet, so the generation current
4306
4597
  * once the swept one is gone is exactly what a pin taken now should hold. One retry, then propagate.
4307
4598
  */
4308
- 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
+ }
4309
4672
  try {
4310
- return await this.pinOnce(ref);
4673
+ await this.readerAt(ref, generation, version, fingerprint);
4311
4674
  } catch (err) {
4312
- if (!isNotFoundError(err)) throw err;
4313
- 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;
4314
4679
  }
4680
+ if (!memoised) {
4681
+ const held = this.heldKey(ref, version, fingerprint);
4682
+ this.replacedPins.delete(held);
4683
+ this.checking.delete(held);
4684
+ }
4685
+ return { generation, version, fingerprint };
4315
4686
  }
4316
- async pinOnce(ref) {
4317
- const live = await this.resolveLive(ref);
4687
+ async pinOnce(ref, lease) {
4688
+ let live = await this.resolveLive(ref);
4318
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
+ }
4319
4695
  const target = live.target;
4320
4696
  const version = versionOf(target.generation, target.lineage);
4321
4697
  let reader;
@@ -4611,6 +4987,7 @@ var CrbmStorageChunkSource = class {
4611
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));
4612
4988
  const crypto = once(() => this.cryptoForRead(ref, generation, keys, unwrap));
4613
4989
  return {
4990
+ row: record,
4614
4991
  target: { generation, lineage: record.token, wrappedDeks: record.wrappedDeks },
4615
4992
  crypto,
4616
4993
  unwrap,
@@ -5048,19 +5425,36 @@ function refuseCleartextOntoKey(key, record, cleartext) {
5048
5425
  }
5049
5426
  var UNANSWERED_RESENDS = 3;
5050
5427
  var UNANSWERED_RESEND_BASE_MS = 500;
5051
- 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 = {}) {
5052
5439
  let fresh;
5053
5440
  let failedOn;
5054
5441
  let unanswered;
5055
5442
  let resends = 0;
5443
+ const churn = leaseChurn({ clock: options.clock, rng: options.rng });
5444
+ let throughChurn = false;
5445
+ let waited;
5056
5446
  let sawUnanswered = false;
5057
5447
  let lastRecord;
5058
5448
  for (let attempt = 0; attempt < 5; attempt++) {
5059
5449
  const reused = attempt === 0 && options.row !== void 0;
5450
+ const holdsRow = fresh === void 0;
5060
5451
  const record = fresh !== void 0 ? fresh : reused ? options.row ?? null : await registry.get(key);
5452
+ const write = holdsRow ? { held: record } : void 0;
5061
5453
  fresh = void 0;
5062
5454
  unanswered = void 0;
5063
5455
  lastRecord = record;
5456
+ let next;
5457
+ let wrote;
5064
5458
  if (failedOn !== void 0) {
5065
5459
  let landed;
5066
5460
  try {
@@ -5068,48 +5462,78 @@ async function publishGeneration(registry, key, options = {}) {
5068
5462
  } catch (proofErr) {
5069
5463
  throw outcomeUnknown(key, proofErr);
5070
5464
  }
5071
- if (landed) return true;
5465
+ if (landed)
5466
+ return {
5467
+ published: true,
5468
+ kept: { ...nextKept(failedOn, key.generation, options.keep), token: void 0 }
5469
+ };
5072
5470
  if (record !== null && record.status === "active" && record.currentGen === key.generation) {
5073
- return false;
5471
+ return REFUSED;
5074
5472
  }
5075
5473
  failedOn = void 0;
5076
5474
  }
5077
5475
  try {
5078
5476
  if (options.expectFrom !== void 0 && record?.currentGen !== options.expectFrom) {
5079
- return false;
5477
+ return REFUSED;
5080
5478
  }
5081
5479
  if (options.expectAbsent === true && record !== null) {
5082
- return false;
5480
+ return REFUSED;
5083
5481
  }
5482
+ throughChurn = false;
5084
5483
  if (options.expectToken !== void 0 && record?.token !== options.expectToken) {
5085
- 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
+ }
5086
5497
  }
5087
5498
  if (record === null && options.summary !== void 0 && "sealed" in options.summary && options.wrappedDeks === void 0) {
5088
5499
  if (reused) continue;
5089
- return false;
5500
+ return REFUSED;
5090
5501
  }
5091
5502
  if (record === null) {
5092
- await registry.create(key, {
5093
- currentGen: key.generation,
5094
- wrappedDeks: options.wrappedDeks,
5095
- ...options.summary === void 0 ? {} : { summary: options.summary }
5096
- });
5503
+ next = nextKept(null, key.generation, options.keep);
5504
+ wrote = (await registry.create(
5505
+ key,
5506
+ {
5507
+ currentGen: key.generation,
5508
+ wrappedDeks: options.wrappedDeks,
5509
+ ...options.summary === void 0 ? {} : { summary: options.summary },
5510
+ ...next.list === void 0 ? {} : { keptGens: next.list }
5511
+ },
5512
+ write
5513
+ )).token;
5097
5514
  } else if (record.status === "destroyed") {
5098
5515
  throw new ValidationError(
5099
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.`
5100
5517
  );
5101
5518
  } else if (record.currentGen === null) {
5102
5519
  refuseCleartextOntoKey(key, record, options.cleartext);
5103
- await registry.compareAndSwap(key, record.token, {
5104
- currentGen: key.generation,
5105
- ...options.wrappedDeks === void 0 ? {} : { wrappedDeks: options.wrappedDeks },
5106
- ...options.summary === void 0 ? {} : { summary: options.summary }
5107
- });
5520
+ next = nextKept(record, key.generation, options.keep);
5521
+ wrote = (await registry.compareAndSwap(
5522
+ key,
5523
+ record.token,
5524
+ {
5525
+ currentGen: key.generation,
5526
+ ...options.wrappedDeks === void 0 ? {} : { wrappedDeks: options.wrappedDeks },
5527
+ ...options.summary === void 0 ? {} : { summary: options.summary },
5528
+ ...next.list === void 0 ? {} : { keptGens: next.list }
5529
+ },
5530
+ write
5531
+ )).token;
5108
5532
  } else if (record.currentGen > key.generation) {
5109
- return false;
5533
+ return REFUSED;
5110
5534
  } else if (record.currentGen === key.generation) {
5111
5535
  if (reused) continue;
5112
- return true;
5536
+ return { published: true };
5113
5537
  } else {
5114
5538
  if (options.wrappedDeks !== void 0 && options.wrappedDeks.length > 0) {
5115
5539
  throw new ValidationError(
@@ -5117,15 +5541,27 @@ async function publishGeneration(registry, key, options = {}) {
5117
5541
  );
5118
5542
  }
5119
5543
  refuseCleartextOntoKey(key, record, options.cleartext);
5120
- await registry.compareAndSwap(key, record.token, {
5121
- currentGen: key.generation,
5122
- ...options.summary === void 0 ? {} : { summary: options.summary }
5123
- });
5544
+ next = nextKept(record, key.generation, options.keep);
5545
+ wrote = (await registry.compareAndSwap(
5546
+ key,
5547
+ record.token,
5548
+ {
5549
+ currentGen: key.generation,
5550
+ ...options.summary === void 0 ? {} : { summary: options.summary },
5551
+ ...next.list === void 0 ? {} : { keptGens: next.list },
5552
+ ...leasesPatch(record, options)
5553
+ },
5554
+ write
5555
+ )).token;
5124
5556
  }
5125
- return true;
5557
+ return {
5558
+ published: true,
5559
+ kept: { ...next ?? { list: void 0, evict: [] }, token: wrote }
5560
+ };
5126
5561
  } catch (err) {
5127
5562
  if (isWriteConflictError(err)) {
5128
5563
  failedOn = record;
5564
+ if (throughChurn) attempt -= 1;
5129
5565
  continue;
5130
5566
  }
5131
5567
  if (!isTransientError(err)) throw err;
@@ -5154,10 +5590,12 @@ async function publishGeneration(registry, key, options = {}) {
5154
5590
  }
5155
5591
  const final = await registry.get(key);
5156
5592
  if (final !== null && final.currentGen !== null && final.currentGen >= key.generation) {
5157
- if (final.currentGen !== key.generation) return false;
5158
- if (!sawUnanswered) return true;
5593
+ if (final.currentGen !== key.generation) return REFUSED;
5594
+ if (!sawUnanswered) return { published: true };
5159
5595
  try {
5160
- return await landedHere(final, lastRecord ?? null, key, options.holdsOwnObject);
5596
+ return {
5597
+ published: await landedHere(final, lastRecord ?? null, key, options.holdsOwnObject)
5598
+ };
5161
5599
  } catch (proofErr) {
5162
5600
  throw outcomeUnknown(key, proofErr);
5163
5601
  }
@@ -5167,6 +5605,10 @@ async function publishGeneration(registry, key, options = {}) {
5167
5605
  `publishGeneration: contention setting currentGen for ${key.segment}`
5168
5606
  );
5169
5607
  }
5608
+ var REFUSED = { published: false };
5609
+ async function publishGeneration(registry, key, options = {}) {
5610
+ return (await publishGenerationKept(registry, key, options)).published;
5611
+ }
5170
5612
  async function landedHere(now, before, key, holdsOwnObject) {
5171
5613
  if (now === null || now.status === "destroyed" || now.currentGen !== key.generation) return false;
5172
5614
  if (before !== null && !sameIncarnation(before, now)) return false;
@@ -5206,6 +5648,8 @@ async function bulkLoadAhead(driver, ref, ahead, ids, options = {}) {
5206
5648
  await pause();
5207
5649
  chunks = ids.bitmap.encodeChunks();
5208
5650
  await pause();
5651
+ } else if (ids instanceof ChunkLoadInput) {
5652
+ chunks = encodeEach(rangeCheckedAtWrite(await collectChunks(ids, codec, options.clock)));
5209
5653
  } else {
5210
5654
  chunks = encodeEach(
5211
5655
  await bucketIds(ids instanceof DecodedLoadInput ? ids.bitmap : ids, codec, options.clock)
@@ -5298,6 +5742,52 @@ async function bulkLoadAhead(driver, ref, ahead, ids, options = {}) {
5298
5742
  fingerprint
5299
5743
  };
5300
5744
  }
5745
+ function* rangeCheckedAtWrite(chunks) {
5746
+ for (const chunk of chunks) {
5747
+ const max = chunk.bitmap.maximum?.();
5748
+ if (max !== void 0 && max > MAX_REMAINDER) {
5749
+ throw new ValidationError(
5750
+ `chunk ${chunk.chunkKey} holds ${max}, outside the 16-bit range [0, ${MAX_REMAINDER}]`
5751
+ );
5752
+ }
5753
+ yield chunk;
5754
+ }
5755
+ }
5756
+ async function collectChunks(input, codec, clock) {
5757
+ const chunks = [];
5758
+ const tick = yieldEvery(clock);
5759
+ let last = -1;
5760
+ for await (const chunk of input.chunks) {
5761
+ if (typeof chunk !== "object" || chunk === null) {
5762
+ throw new ValidationError(
5763
+ `a chunk must be an object with a chunkKey and a bitmap; got ${String(chunk)}`
5764
+ );
5765
+ }
5766
+ const { chunkKey, bitmap } = chunk;
5767
+ if (codec.owns?.(bitmap) !== true) {
5768
+ throw new ValidationError(
5769
+ `chunk ${String(chunkKey)}: its bitmap is not one this load's codec made`
5770
+ );
5771
+ }
5772
+ if (!Number.isInteger(chunkKey) || chunkKey < 0 || chunkKey > MAX_REMAINDER) {
5773
+ throw new ValidationError(`a chunk's key must be an integer in [0, 65535]; got ${chunkKey}`);
5774
+ }
5775
+ if (chunkKey <= last) {
5776
+ throw new ValidationError(`chunk ${chunkKey} does not ascend: the last was ${last}`);
5777
+ }
5778
+ last = chunkKey;
5779
+ const max = bitmap.maximum?.();
5780
+ if (max !== void 0 && max > MAX_REMAINDER) {
5781
+ throw new ValidationError(
5782
+ `chunk ${chunkKey} holds ${max}, outside the 16-bit range [0, ${MAX_REMAINDER}]`
5783
+ );
5784
+ }
5785
+ chunks.push({ chunkKey, bitmap });
5786
+ const pause = tick();
5787
+ if (pause !== null) await pause;
5788
+ }
5789
+ return chunks;
5790
+ }
5301
5791
  async function bucketIds(ids, codec, clock) {
5302
5792
  const byChunk = /* @__PURE__ */ new Map();
5303
5793
  const pendingByChunk = /* @__PURE__ */ new Map();
@@ -5575,7 +6065,25 @@ async function currentCardinality(ref, deps, record, unwrapped) {
5575
6065
  for (const n of reader.cardinalities().values()) total += n;
5576
6066
  return { cardinality: total, fromSummary: false };
5577
6067
  }
5578
- async function loadSegment(ref, input, deps, options = {}) {
6068
+ function loadSegment(ref, input, deps, options = {}) {
6069
+ return runLoad(ref, (codec) => prepareLoadInput(input, codec), deps, options);
6070
+ }
6071
+ function loadSegmentChunks(ref, chunks, deps, options = {}) {
6072
+ return runLoad(
6073
+ ref,
6074
+ () => {
6075
+ if (typeof chunks !== "object" || chunks === null || !(Symbol.asyncIterator in chunks)) {
6076
+ throw new ValidationError(
6077
+ "loadSegmentChunks takes an async iterable of { chunkKey, bitmap }"
6078
+ );
6079
+ }
6080
+ return new ChunkLoadInput(chunks);
6081
+ },
6082
+ deps,
6083
+ options
6084
+ );
6085
+ }
6086
+ async function runLoad(ref, prepare, deps, options) {
5579
6087
  validateUserRef(ref);
5580
6088
  const codec = requireCodec(deps.codec, "loadSegment");
5581
6089
  const keep = options.keep ?? 1;
@@ -5601,7 +6109,7 @@ async function loadSegment(ref, input, deps, options = {}) {
5601
6109
  throw new ValidationError(message);
5602
6110
  });
5603
6111
  const audit = safeAudit(options.audit ?? NOOP_AUDIT);
5604
- const ids = prepareLoadInput(input, codec);
6112
+ const ids = prepare(codec);
5605
6113
  assertRegistryCanWrite(deps.registry, "load");
5606
6114
  const row = await deps.registry.get(ref);
5607
6115
  const fromToken = row?.token;
@@ -5658,7 +6166,7 @@ async function loadSegment(ref, input, deps, options = {}) {
5658
6166
  const key = { namespace: ref.namespace, segment: ref.segment, generation };
5659
6167
  const reclaim = async () => {
5660
6168
  const now = await deps.registry.get(ref);
5661
- if (now !== null && (now.token === fromToken || now.status === "destroyed")) {
6169
+ if (now !== null && (now.token === fromToken || now.status === "destroyed" || row !== null && onlyLeasesDiffer(row, now))) {
5662
6170
  await deps.storage.delete(key);
5663
6171
  return;
5664
6172
  }
@@ -5700,14 +6208,17 @@ async function loadSegment(ref, input, deps, options = {}) {
5700
6208
  if (guard?.minRetained !== void 0 && before !== null) {
5701
6209
  if (written.cardinality < before * guard.minRetained) return refuse("min-retained");
5702
6210
  }
6211
+ const leasesNow = deps.clock === void 0 ? void 0 : () => deps.clock.now();
5703
6212
  let published;
5704
6213
  try {
5705
- published = await publishGeneration(deps.registry, key, {
6214
+ published = await publishGenerationKept(deps.registry, key, {
6215
+ keep,
6216
+ leasesNow,
5706
6217
  row,
5707
6218
  wrappedDeks: written.wrappedDeks,
5708
6219
  summary: written.summary,
5709
6220
  cleartext: !written.encrypted,
5710
- ...fromToken === void 0 ? {} : { expectToken: fromToken },
6221
+ ...fromToken === void 0 ? {} : { expectToken: fromToken, expectRow: row },
5711
6222
  ...needsBefore && fromGeneration !== void 0 ? { expectFrom: fromGeneration } : {},
5712
6223
  // The third case, and the one the two fences above structurally cannot cover: the guard judged a segment
5713
6224
  // that had NO ROW. Both `expectFrom` and `expectToken` compare against a value read from a row, so with no
@@ -5731,7 +6242,7 @@ async function loadSegment(ref, input, deps, options = {}) {
5731
6242
  if (isValidationError(err) || err instanceof KeyUnavailableError) await reclaim();
5732
6243
  throw err;
5733
6244
  }
5734
- if (!published) return refuse("superseded");
6245
+ if (!published.published) return refuse("superseded");
5735
6246
  audit.onEvent({
5736
6247
  kind: "segment.publish",
5737
6248
  segment: ref.segment,
@@ -5742,7 +6253,10 @@ async function loadSegment(ref, input, deps, options = {}) {
5742
6253
  const collected = await collectAfterLoad(ref, deps, {
5743
6254
  generation,
5744
6255
  keep,
5745
- 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,
5746
6260
  ...current.fromSummary && fromGeneration !== void 0 ? { proveCurrent: { ...ref, generation: fromGeneration } } : {}
5747
6261
  });
5748
6262
  return {
@@ -5841,10 +6355,25 @@ async function rollbackSegment(ref, toGeneration, deps, options = {}) {
5841
6355
  { cardinality: reader.count(), metadata: reader.metadata },
5842
6356
  aead
5843
6357
  );
5844
- const { token } = await deps.registry.compareAndSwap(ref, record.token, {
5845
- currentGen: toGeneration,
5846
- summary
5847
- });
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
+ }
5848
6377
  let stillThere = false;
5849
6378
  for await (const key of deps.storage.list(ref)) {
5850
6379
  if (key.generation === toGeneration) {
@@ -5867,13 +6396,31 @@ async function rollbackSegment(ref, toGeneration, deps, options = {}) {
5867
6396
  }
5868
6397
  if (!stillThere || replaced) {
5869
6398
  let undone = false;
5870
- try {
5871
- await deps.registry.compareAndSwap(ref, token, {
5872
- currentGen: record.currentGen,
5873
- summary: describingSummary(record)
5874
- });
5875
- undone = true;
5876
- } 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
+ }
5877
6424
  }
5878
6425
  throw new NotFoundError(
5879
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.`)
@@ -5937,7 +6484,7 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
5937
6484
  if (row === null) return "absent";
5938
6485
  if (row.status === "destroyed") return "destroyed";
5939
6486
  if (row.currentGen === null) return "no-generation";
5940
- return row.currentGen === from && row.token === fromToken ? null : "superseded";
6487
+ return row.currentGen === from && (row.token === fromToken || onlyLeasesDiffer(record, row)) ? null : "superseded";
5941
6488
  };
5942
6489
  const holds = async (generation2) => {
5943
6490
  const key2 = { ...base, generation: generation2 };
@@ -6108,7 +6655,13 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
6108
6655
  const published = await publishGeneration(deps.registry, key, {
6109
6656
  expectFrom: from,
6110
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,
6111
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,
6112
6665
  // A write that ends without an answer is settled by reading the row, and a pointer at this number is this
6113
6666
  // rewrite's only over the object it wrote: the footer proves it, so another incarnation's cannot pass for it.
6114
6667
  holdsOwnObject: () => provesOwnObject(deps.storage, key, fingerprint),
@@ -6441,8 +6994,17 @@ async function listGenerations2(storage, ref) {
6441
6994
  }
6442
6995
  async function shredSegment(ref, deps, allowCleartext, op = "destroySegment") {
6443
6996
  const base = { segment: ref.segment, namespace: ref.namespace };
6997
+ const churn = leaseChurn(deps);
6998
+ let lost;
6444
6999
  for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
6445
- 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
+ }
6446
7008
  if (record === null) {
6447
7009
  return { ...base, destroyed: false, cryptoShredded: false, reason: "absent" };
6448
7010
  }
@@ -6461,11 +7023,14 @@ async function shredSegment(ref, deps, allowCleartext, op = "destroySegment") {
6461
7023
  keyId: void 0,
6462
7024
  // The current generation's cached count and metadata go with it: sealed, they cannot be opened without
6463
7025
  // the wrappings; clear, they would outlive the segment on its tombstone.
6464
- 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
6465
7029
  });
6466
7030
  return { ...base, destroyed: true, cryptoShredded: encrypted };
6467
7031
  } catch (err) {
6468
7032
  if (!isWriteConflictError(err)) throw err;
7033
+ lost = record;
6469
7034
  }
6470
7035
  }
6471
7036
  throw new WriteConflictError(`${op}: contention shredding "${ref.segment}" \u2014 retry`);
@@ -6533,8 +7098,17 @@ async function setSegmentRetention(ref, deps, policy) {
6533
7098
  validateRetentionPolicy(policy);
6534
7099
  const base = { segment: ref.segment, namespace: ref.namespace };
6535
7100
  let previousExpiresAt;
7101
+ const churn = leaseChurn(deps);
7102
+ let lost;
6536
7103
  for (let attempt = 0; attempt < RETENTION_CAS_ATTEMPTS; attempt += 1) {
6537
- 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
+ }
6538
7112
  try {
6539
7113
  previousExpiresAt = readExpiresAt(record);
6540
7114
  if (record === null) {
@@ -6561,6 +7135,7 @@ async function setSegmentRetention(ref, deps, policy) {
6561
7135
  };
6562
7136
  } catch (err) {
6563
7137
  if (!isWriteConflictError(err)) throw err;
7138
+ lost = record ?? void 0;
6564
7139
  }
6565
7140
  }
6566
7141
  throw new WriteConflictError(
@@ -6569,8 +7144,17 @@ async function setSegmentRetention(ref, deps, policy) {
6569
7144
  }
6570
7145
  async function clearSegmentRetention(ref, deps) {
6571
7146
  validateUserRef(ref);
7147
+ const churn = leaseChurn(deps);
7148
+ let lost;
6572
7149
  for (let attempt = 0; attempt < RETENTION_CAS_ATTEMPTS; attempt += 1) {
6573
- 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
+ }
6574
7158
  if (record === null) return false;
6575
7159
  if (record.status === "destroyed") return false;
6576
7160
  if (record.retention === void 0 || !(EXPIRES_AT in record.retention)) return false;
@@ -6584,6 +7168,7 @@ async function clearSegmentRetention(ref, deps) {
6584
7168
  return true;
6585
7169
  } catch (err) {
6586
7170
  if (!isWriteConflictError(err)) throw err;
7171
+ lost = record;
6587
7172
  }
6588
7173
  }
6589
7174
  throw new WriteConflictError(
@@ -6955,6 +7540,31 @@ function failureReason(err) {
6955
7540
  return `failed: ${err instanceof Error ? err.message : String(err)}`;
6956
7541
  }
6957
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
+
6958
7568
  // src/core/consistency.ts
6959
7569
  var DEFAULT_CHECK_CONCURRENCY = 8;
6960
7570
  async function generationsPresent(storage, ref) {
@@ -7158,7 +7768,12 @@ export {
7158
7768
  InProcessKeystore,
7159
7769
  IntegrityError,
7160
7770
  KeyUnavailableError,
7771
+ LEASE_SKEW_MS,
7772
+ LeaseExpiredError,
7773
+ LeaseLimitError,
7161
7774
  LocalFsStorage,
7775
+ MAX_LEASES_PER_SEGMENT,
7776
+ MAX_LEASE_MS,
7162
7777
  MIN_EXPIRES_AT_MS,
7163
7778
  MemoryStorage,
7164
7779
  NOOP_METRICS,
@@ -7177,6 +7792,7 @@ export {
7177
7792
  checkBudget,
7178
7793
  clearSegmentRetention,
7179
7794
  collectWithinBudget,
7795
+ decodeSerialized,
7180
7796
  destroySegment,
7181
7797
  dropSegment,
7182
7798
  eraseIdFromSegment,
@@ -7187,6 +7803,8 @@ export {
7187
7803
  groundedReport,
7188
7804
  isCloudRoaringError,
7189
7805
  isIntegrityError,
7806
+ isLeaseExpiredError,
7807
+ isLeaseLimitError,
7190
7808
  isNotFoundError,
7191
7809
  isStorageBackend,
7192
7810
  isTransientError,
@@ -7195,8 +7813,11 @@ export {
7195
7813
  listGenerations,
7196
7814
  listSegments,
7197
7815
  loadSegment,
7816
+ loadSegmentChunks,
7198
7817
  mapWithConcurrency,
7199
7818
  readRetentionPolicy,
7819
+ reapRegistryTombstones,
7820
+ releaseLease,
7200
7821
  resolveBudget,
7201
7822
  resolvePerOpBudget,
7202
7823
  retireExpired,
@@ -7208,6 +7829,7 @@ export {
7208
7829
  segmentKey,
7209
7830
  setSegmentRetention,
7210
7831
  splitId,
7832
+ takeLease,
7211
7833
  withRetry
7212
7834
  };
7213
7835
  //# sourceMappingURL=index.js.map