@cloudbitmaps/roaring 0.11.2 → 0.12.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.
@@ -122,11 +122,47 @@ function overrun(bytes, offset, length, what) {
122
122
  );
123
123
  }
124
124
  function checkPortableLayout(bytes) {
125
- walk(bytes, null);
125
+ return walk(bytes, null);
126
126
  }
127
- function walk(bytes, out) {
127
+ var SINGLE_HEADER_BYTES = 16;
128
+ var SINGLE_RUN_HEADER_BYTES = 9;
129
+ function* containerPayloads(bytes) {
130
+ if (bytes.byteLength === 0) return;
131
+ const { view, count, runFlagsAt, descriptiveAt, bodiesAt } = header(bytes);
132
+ let pos = bodiesAt;
133
+ let previousKey = -1;
134
+ for (let i = 0; i < count; i++) {
135
+ const key = view.getUint16(descriptiveAt + i * 4, true);
136
+ if (key <= previousKey) {
137
+ throw new IntegrityError(
138
+ `portable roaring: container keys are not strictly ascending (${previousKey} then ${key} at index ${i})`
139
+ );
140
+ }
141
+ previousKey = key;
142
+ const cardinality = view.getUint16(descriptiveAt + i * 4 + 2, true) + 1;
143
+ const isRun = runFlagsAt >= 0 && (bytes[runFlagsAt + (i >>> 3)] & 1 << (i & 7)) !== 0;
144
+ const kind = isRun ? RUN : cardinality > ARRAY_MAX_CARDINALITY ? BITMAP : ARRAY;
145
+ const size = checkContainer(bytes, view, pos, kind, cardinality, i);
146
+ const headerBytes = isRun ? SINGLE_RUN_HEADER_BYTES : SINGLE_HEADER_BYTES;
147
+ const payload = new Uint8Array(headerBytes + size);
148
+ const out = new DataView(payload.buffer);
149
+ if (isRun) {
150
+ out.setUint32(0, SERIAL_COOKIE, true);
151
+ payload[4] = 1;
152
+ out.setUint16(7, cardinality - 1, true);
153
+ } else {
154
+ out.setUint32(0, SERIAL_COOKIE_NO_RUNCONTAINER, true);
155
+ out.setUint32(4, 1, true);
156
+ out.setUint16(10, cardinality - 1, true);
157
+ out.setUint32(12, SINGLE_HEADER_BYTES, true);
158
+ }
159
+ payload.set(bytes.subarray(pos, pos + size), headerBytes);
160
+ pos += size;
161
+ yield { chunkKey: key, payload, cardinality };
162
+ }
163
+ }
164
+ function header(bytes) {
128
165
  const length = bytes.byteLength;
129
- if (length === 0) return;
130
166
  const view = new DataView(bytes.buffer, bytes.byteOffset, length);
131
167
  if (length < 4) throw overrun(bytes, 0, 4, "the cookie");
132
168
  const cookie = view.getUint32(0, true);
@@ -159,6 +195,12 @@ function walk(bytes, out) {
159
195
  offsetsAt = pos;
160
196
  pos += count * 4;
161
197
  }
198
+ return { view, count, runFlagsAt, descriptiveAt, offsetsAt, bodiesAt: pos };
199
+ }
200
+ function walk(bytes, out) {
201
+ if (bytes.byteLength === 0) return 0;
202
+ const { view, count, runFlagsAt, descriptiveAt, offsetsAt, bodiesAt } = header(bytes);
203
+ let pos = bodiesAt;
162
204
  let previousKey = -1;
163
205
  for (let i = 0; i < count; i++) {
164
206
  const key = view.getUint16(descriptiveAt + i * 4, true);
@@ -179,35 +221,43 @@ function walk(bytes, out) {
179
221
  );
180
222
  }
181
223
  }
182
- let size;
183
- if (kind === ARRAY) {
184
- size = cardinality * 2;
185
- if (pos + size > length) throw overrun(bytes, pos, size, `container ${i} (array)`);
186
- checkArray(view, pos, cardinality, i);
187
- } else if (kind === BITMAP) {
188
- size = BITMAP_CONTAINER_BYTES;
189
- if (pos + size > length) throw overrun(bytes, pos, size, `container ${i} (bitmap)`);
190
- const bits = popcount(view, pos);
191
- if (bits !== cardinality) {
192
- throw new IntegrityError(
193
- `portable roaring: container ${i} (bitmap) holds ${bits} values, but its header says ${cardinality}`
194
- );
195
- }
196
- } else {
197
- if (pos + 2 > length) throw overrun(bytes, pos, 2, `container ${i} (run count)`);
198
- const runs = view.getUint16(pos, true);
199
- size = 2 + runs * 4;
200
- if (pos + size > length) throw overrun(bytes, pos, size, `container ${i} (run)`);
201
- const covered = checkRuns(view, pos + 2, runs, i);
202
- if (covered !== cardinality) {
203
- throw new IntegrityError(
204
- `portable roaring: container ${i} (run) covers ${covered} values, but its header says ${cardinality}`
205
- );
206
- }
207
- }
224
+ const size = checkContainer(bytes, view, pos, kind, cardinality, i);
208
225
  out?.push({ key, cardinality, kind, offset: pos });
209
226
  pos += size;
210
227
  }
228
+ return pos;
229
+ }
230
+ function checkContainer(bytes, view, pos, kind, cardinality, i) {
231
+ const length = bytes.byteLength;
232
+ if (kind === ARRAY) {
233
+ const size2 = cardinality * 2;
234
+ if (pos + size2 > length) throw overrun(bytes, pos, size2, `container ${i} (array)`);
235
+ checkArray(view, pos, cardinality, i);
236
+ return size2;
237
+ }
238
+ if (kind === BITMAP) {
239
+ if (pos + BITMAP_CONTAINER_BYTES > length) {
240
+ throw overrun(bytes, pos, BITMAP_CONTAINER_BYTES, `container ${i} (bitmap)`);
241
+ }
242
+ const bits = popcount(view, pos);
243
+ if (bits !== cardinality) {
244
+ throw new IntegrityError(
245
+ `portable roaring: container ${i} (bitmap) holds ${bits} values, but its header says ${cardinality}`
246
+ );
247
+ }
248
+ return BITMAP_CONTAINER_BYTES;
249
+ }
250
+ if (pos + 2 > length) throw overrun(bytes, pos, 2, `container ${i} (run count)`);
251
+ const runs = view.getUint16(pos, true);
252
+ const size = 2 + runs * 4;
253
+ if (pos + size > length) throw overrun(bytes, pos, size, `container ${i} (run)`);
254
+ const covered = checkRuns(view, pos + 2, runs, i);
255
+ if (covered !== cardinality) {
256
+ throw new IntegrityError(
257
+ `portable roaring: container ${i} (run) covers ${covered} values, but its header says ${cardinality}`
258
+ );
259
+ }
260
+ return size;
211
261
  }
212
262
  function checkArray(view, at, cardinality, index) {
213
263
  let previous = -1;
@@ -276,18 +326,25 @@ var SafeBitmap = class _SafeBitmap {
276
326
  /**
277
327
  * Cap the size, check the structure, then deserialize with the **portable** format.
278
328
  * Throws `IntegrityError` if the input exceeds `maxBytes`, is not a well-formed portable bitmap, or fails to
279
- * decode — the native addon is never handed unbounded, malformed or unsafe-format input.
329
+ * decode — the native addon is never handed unbounded, malformed or unsafe-format input. With `whole`, bytes
330
+ * after the bitmap's last container are refused too; without it they are ignored, as the native decoder ignores
331
+ * them.
280
332
  *
281
333
  * The structural check is what makes the decoded bitmap's answers mean anything. The native deserializer
282
334
  * bounds its reads and checks nothing else, so bytes of the right length and the wrong shape (containers or
283
335
  * values out of order, overlapping runs, a cardinality that disagrees with the bits) decode into a bitmap
284
336
  * whose `maximum()`, `has()` and `size` are wrong, and some of those shapes crash the process when used.
285
337
  */
286
- static safeDeserialize(bytes, maxBytes) {
338
+ static safeDeserialize(bytes, maxBytes, options = {}) {
287
339
  if (bytes.length > maxBytes) {
288
340
  throw new IntegrityError2(`serialized bitmap is ${bytes.length}B, exceeds cap ${maxBytes}B`);
289
341
  }
290
- checkPortableLayout(bytes);
342
+ const end = checkPortableLayout(bytes);
343
+ if (options.whole === true && end !== bytes.byteLength) {
344
+ throw new IntegrityError2(
345
+ `portable roaring: ${bytes.byteLength - end} bytes follow the bitmap's last container, which ends at byte ${end}. One buffer holds one bitmap: to load several as one segment, combine them first (RoaringBitmap32.orMany) and serialize the result.`
346
+ );
347
+ }
291
348
  try {
292
349
  return new _SafeBitmap(RoaringBitmap32.deserialize(bytes, DeserializationFormat.portable));
293
350
  } catch (err) {
@@ -314,10 +371,29 @@ var SafeBitmap = class _SafeBitmap {
314
371
  *
315
372
  * Called only when writing an immutable objects generation — see {@link CodecBitmap.optimize} for why the read
316
373
  * path deliberately does not.
374
+ *
375
+ * CANONICAL, which `runOptimize()` alone is not. Where a container's run and array encodings are the same size
376
+ * (cardinality = 2 × runs + 1: three values in one run, five in two), CRoaring keeps whichever kind the
377
+ * container already has. A chunk built from ids starts as an array and stays one; the same set reached through
378
+ * `addRange`, an earlier `runOptimize()` or an operation over run containers is a run, and serializes to other
379
+ * bytes. Undoing run compression first starts every container where one built from ids starts, so the bytes
380
+ * depend on membership alone. On a bitmap built from ids there is nothing to undo.
317
381
  */
318
382
  optimize() {
383
+ this.bitmap.removeRunCompression();
319
384
  this.bitmap.runOptimize();
320
385
  }
386
+ /**
387
+ * This set as storage chunks, cut from its own containers: {@link optimize} and one `serialize` when called, then
388
+ * a lazy header walk that copies one container out per step (`containerPayloads`). Per container and per byte,
389
+ * never per id, and the payloads are the ones {@link SafeBitmap.fromValues} of each chunk's low 16 bits would
390
+ * serialize to. The walk trusts the bytes it cuts because the native serializer wrote them, from a bitmap that
391
+ * passed the structural check when it was decoded.
392
+ */
393
+ encodeChunks() {
394
+ this.optimize();
395
+ return containerPayloads(this.serialize());
396
+ }
321
397
  /**
322
398
  * Largest value, or `undefined` when empty. O(1) — roaring reads it off the last container, so the engine's
323
399
  * per-chunk range assertion costs one call per chunk rather than a walk per id. The last container holds the
@@ -374,10 +450,13 @@ var SafeBitmap = class _SafeBitmap {
374
450
  return this.bitmap.toArray();
375
451
  }
376
452
  };
453
+ function bitmapAsLoadInput(input) {
454
+ return input instanceof RoaringBitmap32 ? { bitmap: input } : input;
455
+ }
377
456
  var roaringCodec = {
378
457
  empty: () => SafeBitmap.empty(),
379
458
  fromValues: (values) => SafeBitmap.fromValues(values),
380
- safeDeserialize: (bytes, maxBytes) => SafeBitmap.safeDeserialize(bytes, maxBytes)
459
+ safeDeserialize: (bytes, maxBytes, options) => SafeBitmap.safeDeserialize(bytes, maxBytes, options)
381
460
  };
382
461
 
383
462
  // src/system-clock.ts
@@ -591,6 +670,7 @@ var CloudRoaring = class _CloudRoaring {
591
670
  retryOptions;
592
671
  crbmSource;
593
672
  clock;
673
+ rng;
594
674
  metrics;
595
675
  // The store's own drivers, kept so the lifecycle helpers and the `*Into` verbs reuse them instead of making
596
676
  // you re-pass deps. `storageDriver` is set only when `storage` was a raw IStorageDriver (a pre-built StorageChunkSource has
@@ -696,6 +776,7 @@ var CloudRoaring = class _CloudRoaring {
696
776
  this.cache = cache;
697
777
  this.crbmSource = resolved.source instanceof CrbmStorageChunkSource ? resolved.source : void 0;
698
778
  this.clock = clock;
779
+ this.rng = rng;
699
780
  this.metrics = metrics;
700
781
  this.storageDriver = resolved.driver;
701
782
  this.registry = resolved.registry;
@@ -723,10 +804,12 @@ var CloudRoaring = class _CloudRoaring {
723
804
  storage: this.storageDriver,
724
805
  registry: this.registry,
725
806
  clock: this.clock,
807
+ rng: this.rng,
726
808
  codec: roaringCodec,
727
809
  // facade injects the flagship codec
728
810
  keystore: this.keystore,
729
- requireEncryption: this.requireEncryption
811
+ requireEncryption: this.requireEncryption,
812
+ readRetry: this.retryOptions
730
813
  };
731
814
  }
732
815
  /** Get a handle to a segment. Validates the name (non-empty, well-formed, within the encoded-length cap). */
@@ -756,7 +839,7 @@ var CloudRoaring = class _CloudRoaring {
756
839
  /**
757
840
  * Write `ids` as a **new generation of `dest`** and publish it — the shared body of the `*Into` verbs. An `*Into`
758
841
  * materialisation is a load, and publishes the same way (hard invariant 1): every load that finds a row fences its
759
- * publish on the row's token; a guarded load (the default, since the empty refusal reads the current generation)
842
+ * publish on the row's token; a guarded load (the default, since the empty refusal needs the size of the current generation)
760
843
  * also fences on the pointer it judged (`expectFrom`), and one that found no row fences on that absence instead.
761
844
  * Only an unguarded load (`allowEmpty: true` and no `guard.minRetained`) onto a segment with no row publishes bare
762
845
  * forward-only. The destination's previous generation stays readable until the publish lands (readers re-resolve
@@ -794,12 +877,14 @@ var CloudRoaring = class _CloudRoaring {
794
877
  * **And by default it collects nothing**, unlike `load()`. See the `keep` default below.
795
878
  */
796
879
  async materialize(dest, ids, op, options) {
797
- const deps = this.lifecycleDeps(op);
880
+ const deps = { ...this.lifecycleDeps(op), collectByListing: true };
881
+ const metadata = options?.metadata;
798
882
  let result;
799
883
  try {
800
884
  result = await loadSegment(dest, ids, deps, {
801
885
  ...options?.allowEmpty === void 0 ? {} : { allowEmpty: options.allowEmpty },
802
886
  ...options?.guard === void 0 ? {} : { guard: options.guard },
887
+ ...metadata === void 0 ? {} : { metadata },
803
888
  // COLLECT NOTHING by default, which `loadSegment` does not — it keeps a grace window of 1 and deletes
804
889
  // the rest. By default a materialisation collects nothing: the guide states "**It deletes nothing**,
805
890
  // unlike `load()`. The destination's previous generations stay in the bucket until you collect them", and
@@ -815,9 +900,9 @@ var CloudRoaring = class _CloudRoaring {
815
900
  }
816
901
  const reason = result.reason;
817
902
  if (reason === "superseded") {
818
- const wrote = result.size > 0 ? `generation ${result.generation} was written and did not become current` : `nothing was written \u2014 another writer took generation ${result.generation} first`;
903
+ const wrote = result.size > 0 ? `generation ${result.generation} was written and is not current` : `nothing was written \u2014 another writer took generation ${result.generation} first`;
819
904
  throw new WriteConflictError(
820
- `${op}: the destination "${dest.segment}" changed while this materialisation was in flight, so it never became current: ${wrote}. The pointer may have moved, the row may have been rewritten (a retention policy does this) or purged. Re-read the destination and re-run.`
905
+ `${op}: the destination "${dest.segment}" changed while this materialisation was in flight, so its result is not the destination's current generation: ${wrote}. The pointer may have moved, the row may have been rewritten (a retention policy does this), dropped or purged. Re-read the destination and re-run.`
821
906
  );
822
907
  }
823
908
  return {
@@ -1023,7 +1108,8 @@ var CloudRoaring = class _CloudRoaring {
1023
1108
  * for a `destroyed` tombstone, because a read answers empty in both cases.
1024
1109
  *
1025
1110
  * Two states answer `true` where a read still gives you nothing: a torn restore (a live pointer whose object
1026
- * was deleted) makes reads *throw* rather than answer empty — `checkConsistency` is the call for that — and
1111
+ * was deleted) makes reads of the object *throw* rather than answer empty, while a cold `count()` answers the number
1112
+ * its row records — `checkConsistency` is the call for that — and
1027
1113
  * a handle carrying an expired `expiresAt` reads empty by a rule that lives on the handle, not the row.
1028
1114
  *
1029
1115
  * Not a lock: the answer can change the moment it returns. If it has to hold, use the fence built for that —
@@ -1085,13 +1171,20 @@ var CloudRoaring = class _CloudRoaring {
1085
1171
  * one cannot.
1086
1172
  *
1087
1173
  * It refuses rather than guesses: a generation not in the bucket (collected, or never written) throws
1088
- * `NotFoundError` naming what *is* available, and a crypto-shredded segment throws
1089
- * {@link ValidationError} because every generation of it is unreadable. Rolling to the generation already
1090
- * current is a no-op that reports itself.
1174
+ * `NotFoundError` naming what *is* available, a crypto-shredded segment throws
1175
+ * {@link ValidationError} because every generation of it is unreadable, and a target that is cleartext under an
1176
+ * encrypted segment, or encrypted under a cleartext one, throws {@link IntegrityError} from one tail read of it,
1177
+ * because every read would refuse it. Rolling to the generation already current is a no-op that reports itself.
1178
+ *
1179
+ * The same tail read (and a range read when the target's index is longer than it) gives the target's id count and
1180
+ * metadata, which the rollback writes into the row with the pointer, so a reader that sees the target as current sees
1181
+ * what describes it. The store's keystore opens an encrypted target for this; without it, or when it cannot open the
1182
+ * key for any reason, the segment still rolls back and the row carries no summary of the target.
1091
1183
  *
1092
1184
  * It deletes nothing. The generations above the new pointer stay put — which is what makes this reversible —
1093
- * and are then *above* `currentGen`, where collection never looks. They remain until a load numbers above them
1094
- * (collection then keeps the newest `keep` of what is below its pointer), {@link CloudRoaring.dropSegment}
1185
+ * and are then *above* `currentGen`, where collection never looks. They remain until loads pass them (the first
1186
+ * load whose number one of them holds numbers above them all, and collection then keeps the newest `keep` of what is
1187
+ * below its pointer), {@link CloudRoaring.dropSegment}
1095
1188
  * deletes them, or {@link CloudRoaring.eraseSubject} does: all of those present when it rewrites, and only those holding
1096
1189
  * the id when the current generation does not. An operator who has just undone a bad load should not have the
1097
1190
  * evidence collected out from under them, while a rollback target that still holds erased data would make the
@@ -1113,7 +1206,14 @@ var CloudRoaring = class _CloudRoaring {
1113
1206
  }
1114
1207
  }
1115
1208
  /**
1116
- * **Replace this segment's contents** with `ids`, as one new immutable generation, and make it current.
1209
+ * **Replace this segment's contents** with `input`, as one new immutable generation, and make it current.
1210
+ *
1211
+ * `input` is ids (any sync or async iterable of integers in `[0, 2^32)`), or a whole bitmap: `{ bitmap }`, anything
1212
+ * with `serialize('portable')` such as `roaring`'s `RoaringBitmap32`, or `{ serialized }`, portable Roaring bytes.
1213
+ * A bitmap is checked (size cap, structure, safe deserializer) before the first request, written from its own
1214
+ * containers with no per-id work, and gives the generation byte for byte the one its ids would. A bare
1215
+ * `RoaringBitmap32` passed as ids loads as `{ bitmap }`. A `Uint8Array` or `Buffer` passed as ids is refused with
1216
+ * `ValidationError`, since each byte would load as an id: pass bytes as `{ serialized }`.
1117
1217
  *
1118
1218
  * The whole write path in one call: take the next generation number, write the object, check the result is
1119
1219
  * plausible, move the pointer, collect what the move superseded. Composed by hand those are four functions and
@@ -1127,6 +1227,14 @@ var CloudRoaring = class _CloudRoaring {
1127
1227
  * if (!r.published) console.warn(`load refused: ${r.reason}`);
1128
1228
  * ```
1129
1229
  *
1230
+ * `metadata` attaches a small record of your own to the generation: a flat object of string keys and string or finite
1231
+ * number values, at most 1,024 bytes as canonical JSON and no key over 128 bytes. A record that breaks a rule throws
1232
+ * `ValidationError` before the load makes a request. It is copied when you call, stored in the generation's object,
1233
+ * and written to the segment's row with the generation's id count by the write that moves the pointer, so a reader
1234
+ * that sees the generation as current sees its metadata. It never changes after that, a rollback puts the target's own
1235
+ * back, and an erasure carries it over without scanning it, so keep a subject's id out of it. It is sealed under the
1236
+ * segment's key on an encrypted segment.
1237
+ *
1130
1238
  * **A load REPLACES.** Whatever the stream contains is what the segment contains afterwards, so an upstream
1131
1239
  * query that returns fewer rows than usual is a shrink nobody asked for and an empty one is a wipe — both
1132
1240
  * reported as a successful write, because at the storage layer they are one. That is what `guard` and the
@@ -1150,20 +1258,39 @@ var CloudRoaring = class _CloudRoaring {
1150
1258
  * so it leaves the orphan rather than risk deleting live data. The orphan is an ordinary generation once a later
1151
1259
  * one is current above it, and collection counts it within `keep`.
1152
1260
  *
1261
+ * **Collection is by name for the default `keep`.** With `keep` of 0 or 1, a load that found nothing above the
1262
+ * pointer deletes the one generation its publish pushed out of the window and lists nothing; it lists the segment's
1263
+ * objects on every sixteenth generation, and on any load that met an object above the pointer or whose check found
1264
+ * the current generation's object gone, to take what the name-only passes leave, such as the generations an
1265
+ * earlier, wider `keep` held. `keep` of 2 or more lists on every load. {@link LoadResult.collected} then names what
1266
+ * the pass deleted by name, and that generation may have been gone already.
1267
+ *
1153
1268
  * What it **throws** for is a fault rather than an outcome: invalid options or ids, and a crypto-shredded
1154
1269
  * segment (`ValidationError`); a key the keystore cannot provide (`KeyUnavailableError`); a current generation
1155
- * that will not open when a guard reads its size (`IntegrityError`); a driver failure; and a collection pass
1156
- * that could not prove the segment was still the same one (`WriteConflictError`). The last can be raised
1157
- * **after** the publish already landed, so a throw does not by itself mean the load did not take effect —
1158
- * re-read the pointer rather than assuming.
1270
+ * that will not open when a guard has to read its size, which it does only when the row has no usable summary of it
1271
+ * (`IntegrityError`); a driver failure; and a collection pass
1272
+ * by listing that could not prove the segment was still the same one (`WriteConflictError`). That one, and a
1273
+ * failure in the collection's own reads or deletes, can be raised **after** the publish already landed, so a throw
1274
+ * does not by itself mean the load did not take effect — re-read the pointer rather than assuming. A collection by
1275
+ * name that finds the segment changed returns an empty `collected` instead.
1276
+ *
1277
+ * **A `TransientError` from the registry write can leave the publish unsettled, and deletes nothing.** The
1278
+ * generation's object is sent again after a throttle where the backend allows it (a write id tells a first send
1279
+ * that landed from another writer's object), and a driver sends each registry write once. When that write ends
1280
+ * without an answer, the load reads the row: its own landed write is `published: true`, a row that has moved on is
1281
+ * `superseded`, and a row still as the write found it gets a fresh compare-and-swap from the version just read, at
1282
+ * most three times, after a wait on the store's clock. Still unanswered, the load throws the registry's `TransientError` and keeps its object, which
1283
+ * a write may still point the row at. Re-run the load: it numbers past that object, and collection removes it once a
1284
+ * generation above it is current. A `'superseded'` refusal that follows an unanswered write sets `unanswered: true`
1285
+ * on its audit event: that write may have landed, and the generation been current for a while, first.
1159
1286
  *
1160
1287
  * Needs a backend (throws {@link UnsupportedError} otherwise).
1161
1288
  */
1162
- async load(ref, ids, options = {}) {
1289
+ async load(ref, input, options = {}) {
1163
1290
  validateSegmentRef(ref);
1164
1291
  const deps = this.lifecycleDeps("load");
1165
1292
  try {
1166
- return await loadSegment(ref, ids, deps, options);
1293
+ return await loadSegment(ref, bitmapAsLoadInput(input), deps, options);
1167
1294
  } finally {
1168
1295
  this.engine.invalidate(ref);
1169
1296
  }
@@ -1193,9 +1320,12 @@ var CloudRoaring = class _CloudRoaring {
1193
1320
  * forever and quietly. Branch on `dropped`, and treat `reason: 'absent'` as the alert.
1194
1321
  *
1195
1322
  * **Inspect `generationsRemaining`.** Empty is the normal outcome; non-empty means the storage was NOT fully
1196
- * reclaimed and the drop should be re-run. A load that was already writing when the tombstone landed still
1197
- * finishes its object, so a single sweep can miss it — this call re-sweeps and then reports whatever it still
1198
- * could not remove rather than returning a result that looks like a clean drop.
1323
+ * reclaimed and the drop should be re-run. A load that had read the segment before the tombstone landed, whether it
1324
+ * was writing or still consuming its ids, can still write its object, so a single sweep can miss it — this call
1325
+ * re-sweeps and then reports whatever it still could not remove rather than returning a result that looks like a
1326
+ * clean drop. A load that writes after the last sweep deletes its own object once its publish is refused; only one
1327
+ * whose process stops in between, or whose publish fails without a definite answer (a lost response, a timeout),
1328
+ * leaves it, for a re-run of the drop.
1199
1329
  *
1200
1330
  * Reads become empty within `cache.genTtlMs` (default 2 s), not instantly: a store that had already read this
1201
1331
  * segment may answer from its cached generation + cached chunks until that window lapses, or, while the registry
@@ -1303,6 +1433,12 @@ var CloudRoaring = class _CloudRoaring {
1303
1433
  * wall-clock knob too, and `retired` counts deletions only — a dry run reports `wouldRetire` instead, so a
1304
1434
  * dashboard summing `retired` can never show a phantom deletion.
1305
1435
  *
1436
+ * **Check `purgeFaults`.** A delete the registry refuses for a reason other than a lost race (a policy that denies
1437
+ * delete, an Azure blob with a snapshot, a raw provider error), whether of a tombstone or of a due-index pointer, is
1438
+ * counted there, with the first one's reason in `firstPurgeFault`. A refused purge is `skipped` in the ledger, is not
1439
+ * charged to `limit`, so it never holds the retirements behind it, and purging stops for the rest of the call after
1440
+ * three refused purges in a row (a purge that succeeds starts the count again).
1441
+ *
1306
1442
  * It also **deletes the tombstone rows its own past retirements left**, after `tombstoneGraceMs` (default 24 h)
1307
1443
  * and only once that segment's Storage generations are provably gone — collecting a straggler generation itself
1308
1444
  * first, since nothing else ever would for a tombstoned segment. Attribution is a **positive marker the sweep
@@ -1310,7 +1446,12 @@ var CloudRoaring = class _CloudRoaring {
1310
1446
  * `retention` untouched, so setting a policy and then honouring a right-to-erasure request mid-window produces
1311
1447
  * exactly that row, and deleting it would destroy the Art. 17 attestation and un-fence the name. By default a
1312
1448
  * segment that held nothing has its row deleted in the pass that retires it, since that row would only fence the name.
1313
- * Pass `purgeTombstones: false` to keep every tombstone, that row included.
1449
+ * Pass `purgeTombstones: false` to keep every tombstone, that row included. On a backend whose registry reports
1450
+ * `conditionalDelete` (AWS S3, Azure Blob, the local filesystem and memory, by default; GCS when you set `conditionalDelete: true`)
1451
+ * the purge removes the row from the bucket for good, by a delete the store applies only to the version it judged,
1452
+ * so a full sweep reads what is live or inside its grace rather than every name a namespace ever held; elsewhere,
1453
+ * and for a row a release before 0.12 wrote, it leaves a tombstone. Each retirement files a pointer in the due index
1454
+ * under the day its tombstone's grace ends, so `scan: 'index'` purges as well as retires.
1314
1455
  *
1315
1456
  * Needs the store built with a **backend** (throws {@link UnsupportedError} otherwise),
1316
1457
  * because retiring a segment deletes its storage objects. `now` defaults to the store's clock.
@@ -1481,6 +1622,13 @@ var CloudRoaring = class _CloudRoaring {
1481
1622
  * (throws {@link UnsupportedError} otherwise). `destroyed` (crypto-shredded) segments are skipped. Pair it with
1482
1623
  * the DR runbook (docs/guide/disaster-recovery.md).
1483
1624
  *
1625
+ * A segment whose pointer names a missing object is the torn restore this reports; a cold `count()` of it still
1626
+ * answers the number its row records, while a read of the object throws, so the count alone never shows it. With
1627
+ * `summaries: true` it also opens each segment's current object (one tail read each) and reports a segment whose
1628
+ * row's summary says another id count or metadata than the object holds as `summary-mismatch`, which is what a
1629
+ * restore of the registry from another point than the bucket leaves a count to answer. A sealed summary is held
1630
+ * against its object when this store has the key, and counted in `summariesUnchecked` when it does not.
1631
+ *
1484
1632
  * It holds the registry rows it enumerates resident, at most 250,000 of them, and throws
1485
1633
  * `BudgetExceededError` past that rather than report a partial scan as a whole one. This method takes no
1486
1634
  * ceiling of its own: narrow the scan with `namespace`, or call `runConsistencyCheck` over the backend's
@@ -1488,7 +1636,10 @@ var CloudRoaring = class _CloudRoaring {
1488
1636
  */
1489
1637
  async checkConsistency(options = {}) {
1490
1638
  const deps = this.lifecycleDeps("checkConsistency");
1491
- return runConsistencyCheck({ storage: deps.storage, registry: deps.registry }, options);
1639
+ return runConsistencyCheck(
1640
+ { storage: deps.storage, registry: deps.registry, keystore: deps.keystore },
1641
+ options
1642
+ );
1492
1643
  }
1493
1644
  /**
1494
1645
  * **Export ("eject") every registered segment's current generation** through the injected `sink`, using only
@@ -1735,10 +1886,18 @@ var Segment = class _Segment {
1735
1886
  return this.timed("has", () => this.engine.has(this.ref, id));
1736
1887
  }
1737
1888
  /**
1738
- * Cardinality — summed from the `.crbm` index with **zero payload reads** on a loaded segment.
1739
- *
1740
- * What this trusts: the answer is the sum of the per-chunk cardinalities the index records, and no payload is
1741
- * decoded to confirm it. The index is checked for internal consistency when the object is opened (each key in
1889
+ * Cardinality of the generation this handle reads: **one registry read when cold, none when warm, and no read of the
1890
+ * object**, with **zero payload reads**. The registry row records the id count of the generation it names, written
1891
+ * by the write that made it current, and a count answers from that. A row with no summary it can use (one written
1892
+ * before rows carried it, one that names another generation, a sealed one that does not open) sends the count to the
1893
+ * `.crbm` index, summed with a tail read of the object. A segment whose pointer names a missing object (a torn
1894
+ * restore) still counts the row's number, while a read of the object throws; `checkConsistency` is what finds it.
1895
+ *
1896
+ * What this trusts: the row's summary, or the index's sum, and no payload is decoded to confirm it. The summary is
1897
+ * used only for the generation it names, on an active row, and a sealed one only if it opens under the segment's
1898
+ * key; it is held against the object whenever the object is opened anyway (a `has`, an `iterate`, a combine, a
1899
+ * `pin()`), at no extra request, and a disagreement stops this store using it, and fails nothing. It is not
1900
+ * confirmed on the cold path, so a party who can write the registry row can make a count wrong. The index is checked for internal consistency when the object is opened (each key in
1742
1901
  * range and ascending, each cardinality in `1..65536`, each payload inside the payload region, and, on an
1743
1902
  * unencrypted object, the footer's chunk count and total agreeing with the index), and a corrupt index that is
1744
1903
  * still internally consistent yields a wrong count. `iterate()` and the combines decode the payloads, whose
@@ -1748,6 +1907,24 @@ var Segment = class _Segment {
1748
1907
  if (this.expired()) return Promise.resolve(0);
1749
1908
  return this.timed("count", () => this.engine.count(this.ref));
1750
1909
  }
1910
+ /**
1911
+ * What the generation this handle reads is, from one resolution: its number, its id count and the metadata it
1912
+ * was loaded with (absent when it has none). It is what answers {@link Segment.count}, so the three describe one
1913
+ * generation and cannot straddle a publish. One registry read when cold, none while warm, and none on a pinned
1914
+ * handle, which answers for the generation it pinned. A segment with no generation, and an expired handle,
1915
+ * answer `{ generation: null, cardinality: 0 }`.
1916
+ *
1917
+ * Trust is as for `count()`: the registry row's word, not confirmed against the object until the object is
1918
+ * opened, when a disagreement makes this process stop using that row's summary.
1919
+ *
1920
+ * ```ts
1921
+ * const { generation, cardinality, metadata } = await store.segment('active-30d').stat();
1922
+ * ```
1923
+ */
1924
+ async stat() {
1925
+ if (this.expired()) return { generation: null, cardinality: 0 };
1926
+ return this.engine.stat(this.ref);
1927
+ }
1751
1928
  /**
1752
1929
  * Every id, ascending, streamed one chunk at a time. Pass a range to read part of the segment: the ids in
1753
1930
  * `(after, through]`, fetching only the chunks the range overlaps. The per-op budget is charged for every chunk in