@cloudbitmaps/roaring 0.16.0 → 0.18.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.d.ts CHANGED
@@ -23,8 +23,10 @@
23
23
  * `checkConsistency`, `exportSegments` — reuse the store's own drivers, so you never re-pass them (they need the
24
24
  * store built with a backend). See the README and the getting-started guide.
25
25
  */
26
- import type { BudgetOption, GenerationEntry, GenerationMetadata, LoadGuard, LoadInput, LoadOptions, LoadRefusal, LoadResult, RollbackResult, Clock, StorageChunkSource, ConsistencyReport, CostReport, DropResult, EstimateInput, ExportManifest, ExportOptions, ExportSink, IAuditSink, IStorageDriver, IKeystore, IMetricsSink, StorageBackend, PricingProfile, RetentionPolicy, RetireExpiredOptions, RetireExpiredResult, RetryPolicy, Rng, SetRetentionResult, PinnedAt, IdRange, SegmentRef, Workload } from '@cloudbitmaps/core';
26
+ import type { BudgetOption, GenerationEntry, GenerationMetadata, LoadGuard, LoadInput, LoadOptions, LoadRefusal, LoadResult, ReapRegistryTombstonesOptions, ReapRegistryTombstonesResult, RollbackResult, Clock, StorageChunkSource, ConsistencyReport, CostReport, DropResult, EstimateInput, ExportManifest, ExportOptions, ExportSink, IAuditSink, IStorageDriver, IKeystore, IMetricsSink, StorageBackend, PricingProfile, RetentionPolicy, RetireExpiredOptions, RetireExpiredResult, RetryPolicy, Rng, SetRetentionResult, PinnedAt, IdRange, SegmentRef, Workload } from '@cloudbitmaps/core';
27
27
  import type { SegmentInfo } from '@cloudbitmaps/core';
28
+ import type { CombineExpr, CombineManyFeedRecord, CombineManyOperandStats, CombineManyOutputStats, CombineManyStats } from '@cloudbitmaps/core';
29
+ import { deserializePortable } from './roaring-codec.js';
28
30
  /**
29
31
  * Wiring for a {@link CloudRoaring} store. **Only `storage` is required** — the minimal call is
30
32
  * `new CloudRoaring({ storage: backend })`. A backend resolves generations with one strong read, reads
@@ -297,6 +299,142 @@ export interface EraseSubjectResult {
297
299
  * in prose means the compiler enforces it, and a caller's exhaustive `switch` has no dead branch.
298
300
  */
299
301
  export type MaterializeRefusal = Exclude<LoadRefusal, 'superseded'>;
302
+ /**
303
+ * An expression over the operands of a {@link CloudRoaring.materializeMany} call: a string names an operand; a node holds
304
+ * exactly one of `and`, `or` or `andNot` over a non-empty list of expressions. `andNot` subtracts every entry after the
305
+ * first from the first, so it takes at least two. Plain JSON, so it can be logged and sent between tasks.
306
+ */
307
+ export type Expr = CombineExpr;
308
+ /** What the call did to one operand: see {@link MaterializeManyStats.operands}. */
309
+ export type MaterializeManyOperandStats = CombineManyOperandStats;
310
+ /** What the call did to one output: see {@link MaterializeManyStats.outputs}. */
311
+ export type MaterializeManyOutputStats = CombineManyOutputStats;
312
+ /**
313
+ * What a {@link CloudRoaring.materializeMany} call did: the groups it ran, its requests by class, the chunk-read budget it
314
+ * used, the most it held resident, and, for each operand, the generation it was pinned at, began at and ended at.
315
+ */
316
+ export type MaterializeManyStats = CombineManyStats;
317
+ /** One output of a {@link CloudRoaring.materializeMany} call. */
318
+ export interface MaterializeManyOutput {
319
+ /** The segment this output becomes a new generation of. Not an operand of the call, and named by no other output. */
320
+ readonly dest: Segment;
321
+ /** What to compute. */
322
+ readonly expr: Expr;
323
+ /** Subtracted from the result, as the entries of an `andNot` after the first are. Pinned operands it names are re-checked before the publish. */
324
+ readonly exclude?: Expr[];
325
+ /** As on {@link MaterializeOptions}. */
326
+ readonly guard?: LoadGuard;
327
+ /** As on {@link MaterializeOptions}. */
328
+ readonly allowEmpty?: boolean;
329
+ /** As on {@link MaterializeOptions}. */
330
+ readonly metadata?: GenerationMetadata;
331
+ /** As on {@link MaterializeOptions}. */
332
+ readonly audit?: IAuditSink;
333
+ /** Generations to keep below the new one, as on {@link MaterializeOptions}; overrides the call's `keep`. */
334
+ readonly keep?: number;
335
+ }
336
+ /**
337
+ * One record of the feed of {@link CloudRoaring.materializeMany}: the ids some of the fed operands hold at one chunk key.
338
+ * `key` is `id >>> 16`, an integer from 0 to 65,535, never below the previous record's; a key may arrive as several
339
+ * consecutive records, each carrying some of the operands, and one record per key is the usual shape. `operands` maps a
340
+ * declared fed name to its ids at the key: a real `Uint32Array` (not an `Int32Array`, `Buffer`, array or look-alike),
341
+ * strictly ascending, every id inside the key. A name is given at most once per key, and an empty array is the same as
342
+ * leaving the name out. The record is copied as it arrives, so the caller may drop or reuse its arrays once it has yielded it.
343
+ */
344
+ export type MaterializeManyFeedRecord = CombineManyFeedRecord;
345
+ /**
346
+ * Operands that arrive as records in chunk-key order instead of being stored, for {@link MaterializeManyOptions.feed}.
347
+ * Outputs name a fed operand as they name a stored one.
348
+ */
349
+ export interface MaterializeManyFeed {
350
+ /** The fed operand names, declared up front. A name is either stored or fed, never both. */
351
+ readonly names: string[];
352
+ /** The records, read once, in key order. */
353
+ readonly records: AsyncIterable<MaterializeManyFeedRecord>;
354
+ /**
355
+ * How many ids each fed operand holds over the whole feed, **required**: an object keyed by exactly `names`, or a
356
+ * function returning one (or a promise of one), called once after the last record and before any fed output is
357
+ * published. A feed whose count of ids for any operand differs (one that ended early, or skipped a key) refuses every
358
+ * fed output; so does a count that is missing, extra, thrown or rejected.
359
+ */
360
+ readonly counts: (() => Record<string, number> | Promise<Record<string, number>>) | Record<string, number>;
361
+ }
362
+ /** The options of {@link CloudRoaring.materializeMany}. */
363
+ export interface MaterializeManyOptions extends IdRange {
364
+ /**
365
+ * The operands the expressions name: stored segments, and operands held in memory from {@link CloudRoaring.memory}. A held
366
+ * operand is accepted here and nowhere else.
367
+ */
368
+ readonly operands: Record<string, Segment | MemoryOperand>;
369
+ /** The outputs, in the order their results come back. */
370
+ readonly outputs: ReadonlyArray<MaterializeManyOutput>;
371
+ /**
372
+ * Generations to keep below each new one: a non-negative integer, **required**, since a materialisation otherwise keeps
373
+ * every generation and a call of a thousand outputs would leave a thousand uncollected destinations on every refresh.
374
+ * An output's own `keep` overrides it.
375
+ */
376
+ readonly keep: number;
377
+ /**
378
+ * Read every stored operand at the generation it has when the call starts, for the whole call (default `true`): the call
379
+ * pins each one that is not already pinned. It is one generation per operand, **not one instant across operands**.
380
+ * `false` reads each live, as a combine does, which can describe two generations of one operand and break its pruning, and runs
381
+ * **no re-check** of an operand an output subtracts: an opt-out list can move during the call and the outputs that subtract it are
382
+ * published (`stats.operands` still reports, from one read at the end, that it moved).
383
+ * An operand passed already pinned is used as it is whatever this says.
384
+ */
385
+ readonly pin?: boolean;
386
+ /**
387
+ * The most resident bytes the call counts at once, default 256 MiB: the buffered outputs, the operand streams, the
388
+ * chunks being evaluated and the plan, counted as resident memory. Past it, outputs run in groups and the operands are read
389
+ * again for each group. Process memory is more than the count (measured 1.6 to 5 times; see the guide).
390
+ */
391
+ readonly maxBufferedBytes?: number;
392
+ /**
393
+ * The most chunk reads the call may make, counted over all its groups. Default: the store's own `budget` when it was built
394
+ * with one (a ceiling below the plan throws `BudgetExceededError` before any chunk is read), else what the plan needs,
395
+ * with headroom. `false` lifts it. The budget used is in the result's stats.
396
+ */
397
+ readonly budget?: BudgetOption;
398
+ /** Outputs published at once, default 8. */
399
+ readonly publishConcurrency?: number;
400
+ /** Range requests held ahead per operand, default 1. */
401
+ readonly concurrency?: number;
402
+ /** As on every combine. */
403
+ readonly allowAbsentOperands?: boolean;
404
+ /**
405
+ * Operands fed as records in chunk-key order, beside the stored ones; an output names a fed operand as it names a stored
406
+ * one. **A feed is read once, so a call with one runs its outputs as one group, and `maxBufferedBytes` is then required**:
407
+ * the feed's records and the fed outputs are counted against it as they arrive, and a fed call fails as soon as the count
408
+ * passes it. A fed output is published only after the whole feed was read and every end-of-feed check passed.
409
+ */
410
+ readonly feed?: MaterializeManyFeed;
411
+ /**
412
+ * Fed and held operand names allowed to hold no id. A declared fed name that appears in no record is refused at the end
413
+ * of the feed, and a held operand that holds no id is refused before any request, because an operand that is empty is
414
+ * usually an upstream query that failed quietly. Naming something that is neither a fed nor a held operand is a
415
+ * {@link ValidationError}.
416
+ */
417
+ readonly mayBeEmpty?: readonly string[];
418
+ }
419
+ /**
420
+ * What one output of {@link CloudRoaring.materializeMany} came to: exactly what its `*Into` would have returned (a
421
+ * {@link MaterializeResult}, published or refused with its `reason`), or `{ published: false, error }` for what it would
422
+ * have thrown, or for what stopped it: a lost race ({@link WriteConflictError}), a publish whose outcome is unknown
423
+ * ({@link TransientError}), a damaged operand ({@link IntegrityError}), a lapsed lease ({@link LeaseExpiredError}), a
424
+ * pinned exclude that moved ({@link StaleOperandError}), memory ({@link BudgetExceededError}), or, for an output that names a
425
+ * fed operand, a bad feed ({@link ValidationError}) or an erasure in this store while the call ran ({@link StaleOperandError}
426
+ * with `reason: 'erased'`).
427
+ */
428
+ export type MaterializeManyOutcome = MaterializeResult | {
429
+ readonly published: false;
430
+ readonly error: Error;
431
+ };
432
+ /** The result of {@link CloudRoaring.materializeMany}. */
433
+ export interface MaterializeManyRun {
434
+ /** One per output, in the order given: one output's refusal or error never stops another. */
435
+ readonly outputs: MaterializeManyOutcome[];
436
+ readonly stats: MaterializeManyStats;
437
+ }
300
438
  /** What an `*Into` verb wrote: the new generation of the destination, and whether it became current. */
301
439
  export interface MaterializeResult {
302
440
  /**
@@ -350,6 +488,13 @@ export declare class CloudRoaring {
350
488
  private readonly requireEncryption;
351
489
  /** Resolved store-level per-op budget (null = disabled); the admin scans use it, with a per-op override. */
352
490
  private readonly budget;
491
+ /**
492
+ * Moved at the start and at the end of each `eraseSubject` this store runs. A `materializeMany` call with a feed records
493
+ * it when it begins and is refused once it has moved: it reads caller memory, which no erasure can reach.
494
+ */
495
+ private erasureEpoch;
496
+ /** How many `eraseSubject` calls of this store are running: while any is, every check of the counter counts as moved. */
497
+ private erasuresRunning;
353
498
  /**
354
499
  * Refuse any option key the store does not take, at the top level or inside a group, naming each one.
355
500
  *
@@ -408,6 +553,147 @@ export declare class CloudRoaring {
408
553
  * **And by default it collects nothing**, unlike `load()`. See the `keep` default below.
409
554
  */
410
555
  private materialize;
556
+ /**
557
+ * **Hold a set of ids in memory**, to be an operand of {@link CloudRoaring.materializeMany} and of no other verb.
558
+ *
559
+ * ```ts
560
+ * const vip = await store.memory([7, 42, 99_001]);
561
+ * await store.materializeMany({ operands: { vip, engaged: store.segment('cond-engaged') }, outputs: [...], keep: 12 });
562
+ * vip.release();
563
+ * ```
564
+ *
565
+ * It takes what {@link CloudRoaring.load} takes as input (ids as a sync or async iterable or a typed array, `{ bitmap }`
566
+ * or `{ serialized }`) and checks it the same way, with the same errors: the size cap, the structure, the safe
567
+ * deserializer and each id's range. **Only a real `Uint32Array` skips the per-id range check**; any other typed array goes
568
+ * through it, so `Int32Array([-1])` and `Float64Array([1.5])` throw {@link ValidationError}. An object with an `ascending`
569
+ * key is refused: ids that arrive in key order are a `feed`. An empty set is accepted here, and refused by
570
+ * `materializeMany` unless its name is in `mayBeEmpty`.
571
+ *
572
+ * The ids are held as the chunks a stored generation would hold, until {@link MemoryOperand.release}; between calls that
573
+ * memory is the caller's, and during a call it counts against `maxBufferedBytes`. There is no size option: the load's
574
+ * own cap applies to a serialized input.
575
+ *
576
+ * **Erasure.** Only `eraseSubject` moves the store's counter (at its start and at its end). One that started in this store
577
+ * since the handle began to be made, or is still running, makes a call that reads the handle fail the outputs that read it with
578
+ * {@link StaleOperandError} (`reason: 'erased'`), before any request and again just before each publish. An erasure in
579
+ * another store or another process is not seen, nor are `dropSegment`, `eraseNamespace`, `destroySegment` and the free
580
+ * function `eraseIdFromSegment`, which do not move it: the handle is the caller's copy, so build it from the source of truth at
581
+ * the start of each refresh and release it at the end.
582
+ *
583
+ * @throws {ValidationError} where a load of the same input would.
584
+ */
585
+ memory(input: LoadInput): Promise<MemoryOperand>;
586
+ /**
587
+ * **Many outputs, each operand read once per group.** Each output is an expression over named stored operands (`and`, `or`, `andNot`, nested
588
+ * to depth 64) published as a new generation of its own `dest`, exactly as an `*Into` would publish it, but computed
589
+ * together: each operand's chunks are read once for all the outputs that use them, instead of once per output and
590
+ * once more for every scratch segment a nested expression would otherwise need.
591
+ *
592
+ * ```ts
593
+ * const run = await store.materializeMany({
594
+ * operands: { us: store.segment('us'), engaged: store.segment('engaged'), optOut: store.segment('opt-out') },
595
+ * outputs: [
596
+ * { dest: store.segment('send-1'), expr: { and: ['us', 'engaged'] }, exclude: ['optOut'] },
597
+ * { dest: store.segment('send-2'), expr: { andNot: ['us', 'engaged'] } },
598
+ * ],
599
+ * keep: 3,
600
+ * });
601
+ * run.outputs[0]; // a MaterializeResult, or { published: false, error }
602
+ * ```
603
+ *
604
+ * **Results.** `run.outputs[i]` is index-aligned with `outputs` and is exactly what output `i`'s `*Into` would have
605
+ * returned (a {@link MaterializeResult}: published, or refused with its `reason`), or `{ published: false, error }` for
606
+ * what it would have thrown. One output's refusal or error never stops another. The call itself throws only for bad
607
+ * input ({@link ValidationError} naming the output index and path, before any request), an operand that names no
608
+ * segment (unless `allowAbsentOperands`), and a `budget` its own plan exceeds, the last two before any chunk is read.
609
+ *
610
+ * **One generation per operand.** By default every stored operand is pinned for the call: read at the generation it had
611
+ * when the call started, however long the call runs and whatever loads land meanwhile. That is one generation per
612
+ * operand, not one instant across operands (two pins are two registry reads). `pin: false` reads live, as a combine does.
613
+ *
614
+ * **Every pinned operand an output subtracts is re-checked.** An output subtracts an operand when it is in its `exclude`
615
+ * or under any entry of an `andNot` after the first, at any depth. Immediately before the publishes the call re-reads the
616
+ * registry row of each such operand and compares its generation and incarnation with what was pinned (a name deleted and
617
+ * created again is a replacement though it is generation 0 again). If one was replaced, each output that subtracts it is
618
+ * not published and carries {@link StaleOperandError}; the others publish. It costs one registry read per such operand per
619
+ * group, and `stats.operands` carries every operand's pinned, start and end generation and whether it moved.
620
+ *
621
+ * **Memory.** Everything the pass holds resident is counted against `maxBufferedBytes` (default 256 MiB): each output's
622
+ * buffered chunks, the operand streams' ranges, the chunks being evaluated, and the plan itself, and a call whose plan alone
623
+ * passes it is refused before any chunk is read. When the outputs do not fit, they run in groups, each reading its operands
624
+ * once (so the operand reads grow with the total output size over the budget), and a group's buffers are released only as
625
+ * its publishes settle. A chunk count the index understated does not grow a group past the budget: the largest buffer is
626
+ * dropped and that output re-runs alone. An output that cannot fit alone has `BudgetExceededError` as its result. The count
627
+ * is the pass's own: process memory is more, measured at 1.6 times the ledger's high water on Linux and 2.5 to 5 times on
628
+ * macOS on the in-memory backend (the guide has the shapes and what is not counted).
629
+ *
630
+ * **Untrusted bytes.** Every operand chunk is decoded through the checks on untrusted tier data. A key an operand's index
631
+ * lists whose bytes are missing is an error for the outputs that read the operand, never an empty chunk.
632
+ *
633
+ * **Handles.** An expired or released handle anywhere in the call (an operand or a `dest`) is refused before any request,
634
+ * as on the `*Into` verbs. A leased operand's lease, and an operand's deadline, are checked before each chunk key
635
+ * is read: a lapse fails only the outputs that read that operand, never reads empty. Each `dest`'s lease and deadline
636
+ * are checked again just before its publish.
637
+ *
638
+ * **The chunk cache is bypassed:** the pass reads through the storage source and never writes the decoded-chunk cache, so
639
+ * a batch does not evict other readers' hot chunks. Each publish still invalidates its `dest` as an `*Into` does.
640
+ *
641
+ * **A limit of the form.** An output cannot name another output of the same call: a `dest` that is also an operand is
642
+ * refused. Chain two calls.
643
+ *
644
+ * **Held operands.** An operand may be a {@link MemoryOperand} from {@link CloudRoaring.memory}: ids the caller holds,
645
+ * read beside the stored ones and giving the same output byte for byte. It is read from memory (no request, and the
646
+ * shared chunk cache never sees it), counts against `maxBufferedBytes` while the call runs, and is refused when empty
647
+ * unless its name is in `mayBeEmpty`. A handle from another store, or a released one, is a {@link ValidationError}. An
648
+ * erasure in this store since the handle was made fails the outputs that read it with {@link StaleOperandError}
649
+ * (`reason: 'erased'`).
650
+ *
651
+ * **A feed.** `feed: { names, records, counts }` adds operands that arrive as records in chunk-key order, for conditions
652
+ * too many to hold or store. Each record is checked before the pass sees it (see {@link MaterializeManyFeedRecord}): a bad
653
+ * feed is refused, never read as fewer members, and **a fed output is published only after the whole feed was read and every
654
+ * end-of-feed check passed**, so a bad feed refuses every fed output and publishes none of them; outputs that name only
655
+ * stored operands are unaffected. At the end of the feed `counts` must equal the ids the feed held for each name, and a
656
+ * name that appeared in no record is refused unless it is in `mayBeEmpty`. A feed is read once, so the call runs its outputs
657
+ * as one group and **`maxBufferedBytes` is required**: each record is converted to compressed bitmaps as it arrives, the key
658
+ * in hand and its fed outputs are counted against the budget, and the call fails as soon as the count passes it, with
659
+ * {@link BudgetExceededError} for every fed output. **Erasure:** a fed call is refused at its next record, and before each
660
+ * fed publish, once `eraseSubject` has started in this store ({@link StaleOperandError}, `reason: 'erased'`).
661
+ *
662
+ * ```ts
663
+ * await store.materializeMany({
664
+ * operands: { unsub: store.segment('global-unsub') },
665
+ * feed: {
666
+ * names: ['us', 'engaged'],
667
+ * records: warehouseRecords(), // yields { key, operands: { us: Uint32Array, engaged: Uint32Array } } by key
668
+ * counts: () => warehouseCounts(),
669
+ * },
670
+ * maxBufferedBytes: 512 * 1024 * 1024,
671
+ * outputs: [{ dest: store.segment('send-1'), expr: { and: ['us', 'engaged'] }, exclude: ['unsub'] }],
672
+ * keep: 12,
673
+ * });
674
+ * ```
675
+ */
676
+ materializeMany(options: MaterializeManyOptions): Promise<MaterializeManyRun>;
677
+ private epochNow;
678
+ /**
679
+ * Whether an erasure has started in this store since `at` was read, or was running when it was: one that is still
680
+ * running is seen by the count of those running, and one that has ended since by the counter, which its end moved.
681
+ * The one check every call that reads caller memory uses.
682
+ */
683
+ private epochMoved;
684
+ /** A call's feed with the store's erasure counter: read once here, checked by the combine before any record is read. */
685
+ private feedOf;
686
+ /**
687
+ * The registry row of `ref`, read straight from the registry, not from a snapshot: a re-check must see a move. With
688
+ * `fingerprint`, the current generation's object is opened too (one tail read, as a pin of it makes), and its
689
+ * fingerprint is returned beside the row: a generation's number is identified by its row and its object, since a number
690
+ * can be taken again once its object is gone.
691
+ */
692
+ private currentRowOf;
693
+ /** Throw what a read of `handle` must throw now: its lease error, or a refusal for a deadline that has passed. */
694
+ private assertReadable;
695
+ /** {@link assertReadable} for a destination, named by where it sits in the call. */
696
+ private assertWritable;
411
697
  /**
412
698
  * **Subject access (GDPR Art. 15 / CCPA right-to-know): which segments is this id a member of?**
413
699
  *
@@ -498,6 +784,7 @@ export declare class CloudRoaring {
498
784
  concurrency?: number;
499
785
  budget?: BudgetOption;
500
786
  }): Promise<EraseSubjectResult>;
787
+ private eraseSubjectRun;
501
788
  /**
502
789
  * Every generation still in the bucket for this segment, ascending, with the current one marked.
503
790
  *
@@ -657,24 +944,26 @@ export declare class CloudRoaring {
657
944
  * A refused load deletes the object it wrote before returning — it sits above `currentGen`, where generation
658
945
  * collection deliberately never looks — but only while the segment's registry row is unchanged or gone. Once
659
946
  * another write has changed the row, the generation number it holds may name another incarnation's live object,
660
- * so it leaves the orphan rather than risk deleting live data. The orphan is an ordinary generation once a later
661
- * one is current above it, and collection counts it within `keep`.
947
+ * so it leaves the orphan rather than risk deleting live data. No row's list names it, so it takes no place in a
948
+ * window, and a listing deletes it once a later generation is current above it.
662
949
  *
663
- * **Collection is by name for the default `keep`.** With `keep` of 0 or 1, a load that found nothing above the
664
- * pointer deletes the one generation its publish pushed out of the window and lists nothing; it lists the segment's
665
- * objects on every sixteenth generation, and on any load that met an object above the pointer or whose check found
666
- * the current generation's object gone, to take what the name-only passes leave, such as the generations an
667
- * earlier, wider `keep` held. `keep` of 2 or more lists on every load. {@link LoadResult.collected} then names what
668
- * the pass deleted by name, and that generation may have been gone already.
950
+ * **Collection is by name at any `keep` up to 64.** The segment's row records the generations a load keeps, so a load
951
+ * that found nothing above the pointer deletes the generations its publish pushed out of the window and lists
952
+ * nothing. It lists the segment's objects on every sixteenth generation, and on any load that met an object above the
953
+ * pointer or whose check found the current generation's object gone, to take what the name-only passes leave, such as
954
+ * an object a crashed load left below the pointer. A `keep` above 64 records no list and lists on every load, and so
955
+ * does the first load of a row that records none, which then records it. {@link LoadResult.collected} then names
956
+ * what the pass deleted by name, and one name may have been gone already.
669
957
  *
670
958
  * What it **throws** for is a fault rather than an outcome: invalid options or ids, and a crypto-shredded
671
959
  * segment (`ValidationError`); a key the keystore cannot provide (`KeyUnavailableError`); a current generation
672
960
  * 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
673
- * (`IntegrityError`); a driver failure; and a collection pass
674
- * by listing that could not prove the segment was still the same one (`WriteConflictError`). That one, and a
675
- * failure in the collection's own reads or deletes, can be raised **after** the publish already landed, so a throw
676
- * does not by itself mean the load did not take effect — re-read the pointer rather than assuming. A collection by
677
- * name that finds the segment changed returns an empty `collected` instead.
961
+ * (`IntegrityError`); a driver failure; and a fault in the collection's own reads, listing or deletes, or in the write
962
+ * that records the window (anything but a lost race). A collection fault can be raised **after** the publish already
963
+ * landed, so a throw does not by itself mean the load did not take effect — re-read the pointer rather than assuming.
964
+ * A collection, by name or by listing, that finds another writer has moved the row since the publish (a load, a
965
+ * rollback, a purge) stops and returns what it deleted in `collected`: a load that took effect does not throw for a
966
+ * race it won.
678
967
  *
679
968
  * **A `TransientError` from the registry write can leave the publish unsettled, and deletes nothing.** The
680
969
  * generation's object is sent again after a throttle where the backend allows it (a write id tells a first send
@@ -840,6 +1129,55 @@ export declare class CloudRoaring {
840
1129
  retireExpired(options?: Omit<RetireExpiredOptions, 'now'> & {
841
1130
  now?: number;
842
1131
  }): Promise<RetireExpiredResult>;
1132
+ /**
1133
+ * **Remove the registry rows that releases before 0.12.0 left as `deleted: true` envelopes.** Those releases never
1134
+ * removed a row they deleted, and no later call removes it either: it is invisible to every read, and every full
1135
+ * listing of the registry still reads it (one GET each). This is the one call that removes them. It is an admin call,
1136
+ * not a sweep, and nothing schedules it: run it once after the last process on a release before 0.12.0 is gone.
1137
+ *
1138
+ * ```ts
1139
+ * const preview = await store.reapRegistryTombstones({ dryRun: true });
1140
+ * console.log(`would remove ${preview.wouldReap} of ${preview.examined} rows read`);
1141
+ *
1142
+ * const done = await store.reapRegistryTombstones({ confirmNoLegacyWriters: true });
1143
+ * if (done.limited) runAgain(); // `limit` (default 1,000) stopped the run with rows unread
1144
+ * ```
1145
+ *
1146
+ * **What it removes.** Only a row that is `deleted: true` and whose token carries no incarnation id, which is what a
1147
+ * release before 0.12.0 left, or a 0.12.0 or later release left by deleting a row born before 0.12.0 (both are safe to
1148
+ * remove), whatever its `status`. It never touches a live row, a `destroyed` row that is not `deleted` (a crypto-shred's
1149
+ * or a `dropSegment`'s tombstone: the attestation of an erasure, which it cannot tell from another), or a deleted row
1150
+ * with an incarnation id, and it never touches a generation. **So it does not clean a bucket completely**: the
1151
+ * `destroyed` tombstones `dropSegment` leaves stay, so do live rows written before 0.12.0, and so does a tombstone a
1152
+ * 0.12.0 or later release wrote while `conditionalDelete` was off (GCS by default, S3 on a custom endpoint): every full
1153
+ * listing still reads it, and this call neither removes it nor runs on such a store.
1154
+ * It also covers the library's own bookkeeping rows when `namespace` is not set, by the same rule. The default
1155
+ * namespace cannot be scoped to: leave `namespace` out to cover it.
1156
+ *
1157
+ * **The two things it asks.** `confirmNoLegacyWriters: true` is required for a real run (without it, a
1158
+ * `ValidationError`, and nothing is requested): your statement that no process on a release before 0.12.0 writes
1159
+ * this registry, since one that did would, re-creating a removed name, issue the removed row's tokens again. And the
1160
+ * registry must apply a delete under a precondition: where its `conditionalDelete` capability is off (GCS by default,
1161
+ * S3 on a custom endpoint) it throws `CapabilityError` before any request, dry run included. A registry that keeps
1162
+ * no such rows (the in-memory and local-filesystem ones) throws `UnsupportedError`.
1163
+ *
1164
+ * **Each removal is fenced** on the version it read, so a `create` that lands over the envelope first wins, the
1165
+ * delete is refused, and the row is counted as `skipped.raced` (which also counts a row already gone). That holds where
1166
+ * the backend applies the precondition, which auto-detection checks (S3 only on an AWS host). On an endpoint that
1167
+ * ignores `If-Match` (MinIO, fake-gcs-server), a `conditionalDelete: true` you set makes this an unfenced delete: run
1168
+ * it with every writer stopped. On a versioned bucket a delete leaves a delete marker and the earlier version, record
1169
+ * included, so the name is not erased from the bucket. A `create` that had already read the envelope and
1170
+ * whose write meets the removal throws `WriteConflictError`, which this library does not retry: retry the create, and
1171
+ * it finds no row and writes one.
1172
+ *
1173
+ * **Cost** over R rows read and E removed: `ceil(R / 1000)` LIST requests, R GETs and E DELETEs (a dry run, no
1174
+ * DELETEs). `limit` bounds the removals, and the run stops listing once it is spent. **A run is not resumable**: each
1175
+ * lists and reads from the start (R GETs whatever the `limit`), a dry run with a `limit` reports the same first rows
1176
+ * every time, so `wouldReap` is a total only when `limited` is `false`, and `limited` is `true` whenever the limit was
1177
+ * spent with keys left, removable or not. An unreadable object stops the run with an error naming its key, and
1178
+ * removes nothing more: it may be a newer release's row. Scope the run with `namespace` to get past one.
1179
+ */
1180
+ reapRegistryTombstones(options?: ReapRegistryTombstonesOptions): Promise<ReapRegistryTombstonesResult>;
843
1181
  /**
844
1182
  * **Forget everything this store has cached about a segment.** Use it after destroying or retiring a segment
845
1183
  * through a path this store did not perform itself.
@@ -883,6 +1221,8 @@ export declare class CloudRoaring {
883
1221
  * those of a live read that fetched across a publish (see {@link PinnedStorageChunkSource.currentVersion}).
884
1222
  */
885
1223
  private pinSegment;
1224
+ /** 16 hex digits from the store's `Rng`, which names the holder of one lease: unique, not secret. */
1225
+ private mintHolder;
886
1226
  /**
887
1227
  * An engine reading every segment in `pins` at its pinned generation and everything else live, with the store's
888
1228
  * retries, so a transient fault on a pinned read is retried as it would be on a live one.
@@ -1004,8 +1344,9 @@ export interface BaseCombineOptions extends IdRange {
1004
1344
  */
1005
1345
  export interface CombineOptions extends BaseCombineOptions {
1006
1346
  /**
1007
- * Segments whose ids are subtracted from the result. An **expired** handle here excludes nothing, in every
1008
- * combine — it is skipped without being read, exactly as in {@link Segment.andNot}.
1347
+ * Segments whose ids are subtracted from the result. An **expired** handle here is refused, in every combine:
1348
+ * the stream throws {@link ValidationError} naming it, before any request is made, exactly as in
1349
+ * {@link Segment.andNot}.
1009
1350
  */
1010
1351
  readonly exclude?: Segment[];
1011
1352
  }
@@ -1053,7 +1394,7 @@ export interface MaterializeOptions extends CombineOptions {
1053
1394
  * recovery story can depend on a materialisation collecting nothing: `rollbackSegment` refuses a target that
1054
1395
  * has been collected. Pass a number to collect on the way through; `0` keeps only the
1055
1396
  * generation this call publishes. It collects by listing the destination, so it clears every generation below the
1056
- * new one beyond `keep`, however many earlier calls kept, where a `load()` deletes by name the one generation its
1397
+ * new one beyond `keep`, however many earlier calls kept, where a `load()` deletes by name the generations its
1057
1398
  * publish pushes out of the window.
1058
1399
  */
1059
1400
  readonly keep?: number;
@@ -1096,6 +1437,32 @@ export interface IdStream extends AsyncIterable<number> {
1096
1437
  * solves the same problem the same way, by taking {@link BaseCombineOptions}.
1097
1438
  */
1098
1439
  export type AndNotIntoOptions = Omit<MaterializeOptions, 'exclude'>;
1440
+ /** What {@link Segment.pin} and {@link Segment.pinAt} take besides the generation. */
1441
+ export interface PinOptions {
1442
+ /**
1443
+ * Hold the pinned generation until this instant: epoch-**milliseconds**, after now and at most
1444
+ * {@link MAX_LEASE_MS} (14 days) from it. A load's collection leaves a leased generation in the bucket until the lease
1445
+ * ends, and a read of the handle after it throws {@link LeaseExpiredError}, never empty.
1446
+ */
1447
+ readonly leaseUntil?: number;
1448
+ }
1449
+ /** The lease a pinned handle holds, as {@link Segment.lease} reports it. */
1450
+ export interface Lease {
1451
+ /** 16 hex digits naming this lease in the segment's registry row. */
1452
+ readonly holder: string;
1453
+ /** Epoch-ms the lease runs to on this store's clock. */
1454
+ readonly until: number;
1455
+ }
1456
+ /**
1457
+ * The generation {@link Segment.pinAt} reopens: what an earlier pin recorded in {@link Segment.pinnedAt}. The
1458
+ * fingerprint is required, since a generation number is taken again after a purge and re-create.
1459
+ */
1460
+ export interface PinAt {
1461
+ /** The generation number an earlier pin held. */
1462
+ readonly generation: number;
1463
+ /** The fingerprint of that generation's object, from the earlier pin's `pinnedAt.fingerprint`. */
1464
+ readonly fingerprint: string;
1465
+ }
1099
1466
  /** What {@link Segment.stat} answers: the generation a handle reads, its id count and its metadata. */
1100
1467
  export interface SegmentStat {
1101
1468
  /** The generation read, or `null` when the segment has none. */
@@ -1104,6 +1471,26 @@ export interface SegmentStat {
1104
1471
  /** The metadata the generation was loaded with; absent when it has none. */
1105
1472
  readonly metadata?: GenerationMetadata;
1106
1473
  }
1474
+ /**
1475
+ * A set of ids held in memory by {@link CloudRoaring.memory}, to be an operand of {@link CloudRoaring.materializeMany}
1476
+ * and of no other verb. The bytes are the caller's until {@link MemoryOperand.release}.
1477
+ *
1478
+ * It belongs to the store that made it. `eraseSubject` in that store, started after the handle began to be made or still
1479
+ * running when it was, fails the outputs of a `materializeMany` call that read it with {@link StaleOperandError}
1480
+ * (`reason: 'erased'`): the call does not throw, and its outputs that do not read the handle publish. Nothing else is
1481
+ * seen: an erasure in another store or process, and
1482
+ * `dropSegment`, `eraseNamespace`, `destroySegment` and the free function `eraseIdFromSegment`, do not move the counter.
1483
+ */
1484
+ export declare class MemoryOperand {
1485
+ private readonly view;
1486
+ /** Not constructible: a handle comes from {@link CloudRoaring.memory}. */
1487
+ private constructor();
1488
+ /**
1489
+ * Zero and drop the bytes it holds. Any later use throws {@link ValidationError}; releasing twice does nothing. A call
1490
+ * still reading it fails the outputs that read it.
1491
+ */
1492
+ release(): void;
1493
+ }
1107
1494
  /**
1108
1495
  * A handle bound to one segment — the read verbs, plus the three `*Into` verbs that write a **new generation**
1109
1496
  * of another segment.
@@ -1130,6 +1517,13 @@ export declare class Segment {
1130
1517
  * pinned handle passed as an **operand** is still read at its pin rather than live.
1131
1518
  */
1132
1519
  readonly pinnedAt?: PinnedAt;
1520
+ /**
1521
+ * The lease this handle holds, when it came from `pin({ leaseUntil })`; `undefined` otherwise. It reports what was
1522
+ * taken and does not change when the lease ends: a read after `lease.until` (or after {@link Segment.release}) throws
1523
+ * {@link LeaseExpiredError}.
1524
+ */
1525
+ readonly lease?: Lease;
1526
+ private readonly leaseHold?;
1133
1527
  /**
1134
1528
  * Not constructible: a handle comes from {@link CloudRoaring.segment}, which wires it to the store's engine,
1135
1529
  * caches and write path. The constructor takes those internals, so it is not part of the surface.
@@ -1161,11 +1555,20 @@ export declare class Segment {
1161
1555
  * 0, so a pin of the old segment never reads the new one: what it has already read still answers, as the
1162
1556
  * instant it pinned, and anything it would have to fetch fails with `NotFoundError`, as a swept pin's does.
1163
1557
  *
1164
- * **It is a hold, not a lease.** Nothing here stops a collection deleting the generation underneath
1165
- * you: a pinned read deliberately does **not** heal forward, because silently serving a different generation
1166
- * is the one thing a pin exists to prevent, so it fails instead. Size `keep` to cover your longest pinned
1167
- * job — see [Generations and `keep`](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/loading.md#generations-and-keep) — or take the pin on a segment
1168
- * you are not collecting.
1558
+ * **It is a hold, and `pin({ leaseUntil })` makes it a bounded one.** Without a lease nothing stops a collection
1559
+ * deleting the generation underneath you: a pinned read deliberately does **not** heal forward, because silently
1560
+ * serving a different generation is the one thing a pin exists to prevent, so it fails instead. Size `keep` to cover
1561
+ * your longest pinned job — see [Generations and `keep`](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/loading.md#generations-and-keep) — take the pin on a segment
1562
+ * you are not collecting, or lease it.
1563
+ *
1564
+ * **A lease** (`leaseUntil`: epoch **milliseconds**, after now and at most 14 days out) keeps the generation out of
1565
+ * a load's collection until then, from any process, and the pin is taken with it: the lease is written to the segment's
1566
+ * row before the object is opened. A read of the handle after the lease, or after {@link Segment.release}, throws
1567
+ * {@link LeaseExpiredError} at every read site, including when the handle is an operand or an `exclude` of a combine
1568
+ * or the target of an `*Into`, and never reads empty. A stream checks the lease each time it reads a chunk. A segment
1569
+ * holds at most 64 live leases ({@link LeaseLimitError}). A collector holds a lease 60 seconds past its end, which covers
1570
+ * clocks that differ by that much. Erasure, shred, drop and retention expiry ignore a lease. See
1571
+ * [Hold a generation for a job](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/reading.md#hold-a-generation-for-a-job-a-lease).
1169
1572
  *
1170
1573
  * A segment with no current generation pins nothing and reads empty, exactly as it would unpinned. A pinned
1171
1574
  * segment whose row is later dropped or destroyed fails once it must open its object again, rather than go empty
@@ -1205,7 +1608,55 @@ export declare class Segment {
1205
1608
  * Needs a store built on the `.crbm` storage source (the default when you pass a backend or a raw driver). Throws
1206
1609
  * {@link UnsupportedError} on a store wired with a pre-built source that cannot pin.
1207
1610
  */
1208
- pin(): Promise<Segment>;
1611
+ pin(options?: PinOptions): Promise<Segment>;
1612
+ /**
1613
+ * Hold what this pin names until `leaseUntil`, or `undefined` for a pin with no lease. Refuses an option it does not
1614
+ * know, a value that is not epoch-milliseconds in the future, one past the longest lease, and one past the handle's own
1615
+ * `expiresAt`, before any request is made.
1616
+ */
1617
+ private leaseUntilOf;
1618
+ /**
1619
+ * End this handle's lease now, so a load's collection may take its generation. Idempotent: a handle with no lease, or
1620
+ * one already released, does nothing and makes no request. Every read of the handle after it throws
1621
+ * {@link LeaseExpiredError} (reason `'released'`), including one already under way at its next chunk. A lease that has
1622
+ * ended by its own time needs no release: this then makes no request either, and the registry row's entry is pruned
1623
+ * by a later write. Otherwise it is one registry read and one write; a failure of either is thrown as the registry's own
1624
+ * typed error, and the entry then holds until its time ends, `release()` being safe to call again.
1625
+ */
1626
+ release(): Promise<void>;
1627
+ /**
1628
+ * The error a read of this handle must throw, or `undefined` when it holds no lease or the lease is live. One property
1629
+ * read on a handle with no lease. It is not {@link Segment.expired}: that rule reads empty, and a lease must never.
1630
+ */
1631
+ private leaseError;
1632
+ /**
1633
+ * Throw the first lease error among `handles`: this handle and every operand and exclude of a call. It runs before
1634
+ * any `expiresAt` rule and before the engine, so no operand, exclude or cached reader can answer past a lease.
1635
+ */
1636
+ private assertLeases;
1637
+ /**
1638
+ * Reopen a generation a pin named earlier, as a pinned handle like the one {@link Segment.pin} returns, held at
1639
+ * `at.generation` instead of the current one. Pass what the earlier pin recorded, `{ generation, fingerprint }`
1640
+ * from its {@link Segment.pinnedAt}. A generation number alone is not an identity, since a purged and re-created
1641
+ * name starts again at 0, so the fingerprint is required: a bare number, a malformed fingerprint or a key other
1642
+ * than `generation` and `fingerprint` is a {@link ValidationError}. Identify a pin by its `generation` and
1643
+ * `fingerprint`: the handle's `pinnedAt.version` can differ from the earlier pin's.
1644
+ *
1645
+ * Throws {@link NotFoundError} when the generation is gone (collected, purged, or on a crypto-shredded segment),
1646
+ * is above the row's pointer, or is another object than the fingerprint names, and never reads empty. A rollback
1647
+ * deletes nothing, so once a later load moves the pointer past a generation it rolled back from, that one can be
1648
+ * reopened while its object is stored. Nothing keeps a generation alive unless you lease it: a later load's
1649
+ * collection can delete it, which is sized by `keep` on every writer, or held by `options.leaseUntil` (see
1650
+ * {@link PinOptions} and {@link Segment.pin}). A leased `pinAt` writes the lease before it verifies the object, and
1651
+ * releases it if the verify fails; for a generation below the pointer, a collector that read the row before the lease
1652
+ * landed can still delete the generation inside its own round trip, and the handle then fails with `NotFoundError`
1653
+ * like any swept pin. A leased handle is checked at every read site as a leased `pin()` is. With a registry it costs one row read and one tail read,
1654
+ * and a leased `pinAt` one conditional write to the row besides; the handle
1655
+ * then reads as one from `pin()` does, including its failure on a chunk it has not cached once the generation is
1656
+ * swept. Works for an encrypted segment. The first argument is an object so that fields can join it later; the lease
1657
+ * is in `options`, the second.
1658
+ */
1659
+ pinAt(at: PinAt, options?: PinOptions): Promise<Segment>;
1209
1660
  /**
1210
1661
  * An opaque string that names this handle's segment, namespace included: two handles of one segment have the
1211
1662
  * same key, a live handle and its pins among them. Use it as a `Map` key or a log field when you track handles
@@ -1244,6 +1695,8 @@ export declare class Segment {
1244
1695
  * awaiting.
1245
1696
  */
1246
1697
  private refuseIfExpired;
1698
+ /** `result` as is, or, when a handle of the call holds a lease, checking that lease each time it reads a chunk. */
1699
+ private guarded;
1247
1700
  /**
1248
1701
  * Time an op with the injected clock and emit an `op` metric on completion (success or throw). Skipped
1249
1702
  * entirely when no sink is wired, so the default path pays nothing.
@@ -1301,9 +1754,40 @@ export declare class Segment {
1301
1754
  * without checking its options, as every read of one does.
1302
1755
  */
1303
1756
  iterate(options?: IdRange): IdStream;
1757
+ /**
1758
+ * **Pinned handles only.** The ids at 1-based ranks `n`, `2n`, `3n` …, counted over the ids in `(after, through]`
1759
+ * when a range is given, ascending: `everyNth(1000)` yields the 1,000th id, then the 2,000th, and so on. A last
1760
+ * partial window yields nothing, so a caller cutting a send into windows of `n` appends its own final end.
1761
+ *
1762
+ * It places each boundary from the per-chunk counts the pin already holds and reads only the chunks that hold one,
1763
+ * each once however many boundaries it holds, so it reads at most one chunk per boundary and never more chunks
1764
+ * than the object has. A range with `after` inside a chunk reads that chunk too, to count the ids at or below
1765
+ * `after`, even when fewer than `n` ids remain above it; when the chunks in range hold fewer than `n` ids in all,
1766
+ * nothing is fetched. `through` stops the read and no chunk past it is read, though a `through` inside a chunk may read that last chunk and
1767
+ * yield nothing from it. The reads and the read-ahead window are
1768
+ * those of {@link Segment.iterate}, and the per-op budget is charged before the first fetch, with an upper bound on
1769
+ * the chunks the read can take. A pinned read after the generation is swept throws {@link NotFoundError}.
1770
+ *
1771
+ * The ranks come from the index's counts, which are trusted as {@link Segment.count} trusts them. A chunk that is
1772
+ * read must hold the number of ids its index says, or the read throws {@link IntegrityError}; a chunk that is not
1773
+ * read is not checked, so an index that is wrong about one, and, on an unencrypted object, still adds up to the footer's
1774
+ * total, moves the ids after it to the wrong ranks. An encrypted object's index is authenticated and bound to the
1775
+ * object, so a wrong count there needs the key.
1776
+ *
1777
+ * A live handle is refused with {@link UnsupportedError} at the first read, because its counts and its chunks could
1778
+ * come from two generations and name the wrong id. `n` that is not a positive integer throws
1779
+ * {@link ValidationError} at the first read, as does a bad bound. An expired handle reads empty.
1780
+ *
1781
+ * ```ts
1782
+ * const audience = await store.segment('audience').pin();
1783
+ * const ends: number[] = [];
1784
+ * for await (const id of audience.everyNth(1_000)) ends.push(id); // the end of each window of 1,000
1785
+ * ```
1786
+ */
1787
+ everyNth(n: number, options?: IdRange): AsyncIterable<number>;
1304
1788
  /**
1305
1789
  * The options a combine hands the engine, read once, when it is called ({@link readOptions}), with the `exclude`
1306
- * it was given, already reduced to its live handles ({@link liveExcludes}) and mapped down to the plain refs
1790
+ * it was given ({@link excludesOf}) and mapped down to the plain refs
1307
1791
  * `core` takes. A method rather than a module function because `ref` is class-private — the encapsulation is
1308
1792
  * worth more than the free function.
1309
1793
  *
@@ -1313,15 +1797,23 @@ export declare class Segment {
1313
1797
  */
1314
1798
  private refsIn;
1315
1799
  /**
1316
- * The handles in `options.exclude` that have not expired. **An expired exclusion excludes nothing**, in every
1317
- * combine, so it is dropped here, before the engine is asked for anything: it is never fetched, never
1318
- * checked for absence, and never counted in the pin-consistency check. `exclude` is read once, so a getter sees
1319
- * one call.
1800
+ * The handles in `options.exclude`, read once so a getter sees one call. With none this is one property read and
1801
+ * a shared empty list; otherwise it is the caller's own array, so the common case allocates nothing.
1802
+ */
1803
+ private excludesOf;
1804
+ /**
1805
+ * The refusal for a combine whose **exclusion** has expired, or `undefined` when none has. Unlike an expired
1806
+ * operand, which reads empty, an exclusion is a suppression or opt-out list: left out silently, the result
1807
+ * would include the very ids it was passed to remove. So it is refused with a {@link ValidationError} naming each
1808
+ * expired exclusion, as the `*Into` verbs refuse an expired handle.
1320
1809
  *
1321
- * With no `exclude` this is one property read and a shared empty list, and with none expired it returns the
1322
- * caller's own array, so the common case allocates nothing.
1810
+ * It is judged against the injected clock when the combine is called, before the engine is asked for anything:
1811
+ * no request is made, and an expired exclusion that names a segment that does not exist is refused as expired,
1812
+ * not as an absent operand. It runs ahead of the expired-operand shortcuts, so a combine that would read empty
1813
+ * anyway still refuses a lapsed exclusion. A stream already being read is not re-checked when an exclusion
1814
+ * expires part-way, as an operand expiring part-way does not stop it either.
1323
1815
  */
1324
- private liveExcludes;
1816
+ private expiredExcludes;
1325
1817
  /**
1326
1818
  * Chunk-skipping intersection: stream the ids in **this** segment AND every segment in `others`, ascending.
1327
1819
  * Fetches only the Storage chunks present in *all* operands (a key absent from any operand contributes nothing
@@ -1376,9 +1868,9 @@ export declare class Segment {
1376
1868
  * suppression list: at most one read per surviving key of `this`, so subtracting a 61,000-chunk global
1377
1869
  * opt-out list from a 40-chunk audience costs at most 40 reads, not 61,000.
1378
1870
  *
1379
- * An expired handle in `excludes` excludes nothing, and is skipped without being read; if every one has
1380
- * expired the result is `this` whole. An expired `this` is empty. `exclude` on `intersect` and `union` follows
1381
- * the same rule.
1871
+ * An expired handle in `excludes` is refused, not skipped: the stream throws {@link ValidationError} naming it,
1872
+ * before any request is made, because a lapsed suppression list would otherwise let through the ids it was
1873
+ * passed to remove. An expired `this` is empty. `exclude` on `intersect` and `union` follows the same rule.
1382
1874
  *
1383
1875
  * To filter the *result of an intersection*, do not chain — pass `exclude` to {@link intersect} instead, so
1384
1876
  * the suppression folds into the same pass rather than materializing an intermediate segment first.
@@ -1399,5 +1891,6 @@ export declare class Segment {
1399
1891
  workload?: Workload;
1400
1892
  }): Promise<CostReport>;
1401
1893
  }
1402
- export { MemoryStorage, LocalFsStorage, CrbmStorageChunkSource, destroySegment, eraseNamespace, excludingReservedRows, readRetentionPolicy, MIN_EXPIRES_AT_MS, InProcessKeystore, NodeAead, aadFor, CloudRoaringError, ValidationError, WriteConflictError, IntegrityError, NotFoundError, UnsupportedError, CapabilityError, TransientError, KeyUnavailableError, BudgetExceededError, isCloudRoaringError, isWriteConflictError, isTransientError, isNotFoundError, isIntegrityError, isValidationError, CrbmReader, BufferReader, CountingMetricsSink, RecordingAuditSink, DEFAULT_RETRY_POLICY, AWS_US_EAST_1_ONDEMAND, ELASTICACHE_REDIS_US_EAST_1_ONDEMAND, ONE_REDIS_HA_CLUSTER, } from '@cloudbitmaps/core';
1403
- export type { Aead, AeadSealed, AuditEvent, BlobReader, BlobSink, Budget, BudgetOption, ChunkRef, Clock, CodecBitmap, CodecInterface, ConsistencyErrorEntry, ConsistencyIssue, ConsistencyReport, CostReport, CrbmCrypto, CrbmReaderOptions, CrbmStorageChunkSourceOptions, DestroyResult, ClearRegistrySummary, DropResult, EncodedChunk, EraseDeps, EstimateInput, ExportFailure, ExportFormat, ExportManifest, ExportOptions, ExportSink, ExportWriter, ExportedSegment, GenKey, GenerationEntry, GenerationMetadata, GovernanceMeta, IAuditSink, IKeystore, IMetricsSink, IRegistryDriver, IStorageDriver, IdRange, InProcessKeystoreOptions, LoadGuard, LoadInput, LoadOptions, LoadRefusal, LoadResult, LocalFsStorageOptions, MemoryStorageOptions, MetricEvent, MetricOpName, MetricsSnapshot, NewRegistryRecord, PinnedAt, PinnedObject, PortableBitmap, PricingProfile, RedisNodeType, RedisSizing, RegCaps, RegistryPatch, RegistryRecord, RegistryStatus, RegistrySummary, RegistryWriteOptions, RetentionPolicy, RetireEntry, RetireExpiredOptions, RetireExpiredResult, RetryPolicy, Rng, RollbackResult, SealedRegistrySummary, SegmentInfo, SegmentRef, SegmentSize, SegmentSizing, SetRetentionResult, StorageBackend, StorageCaps, StorageChunkSource, Token, Workload, WrappedDek, } from '@cloudbitmaps/core';
1894
+ export { deserializePortable };
1895
+ export { MemoryStorage, LocalFsStorage, CrbmStorageChunkSource, destroySegment, eraseNamespace, excludingReservedRows, readRetentionPolicy, MIN_EXPIRES_AT_MS, LEASE_SKEW_MS, MAX_LEASE_MS, MAX_LEASES_PER_SEGMENT, InProcessKeystore, NodeAead, aadFor, CloudRoaringError, ValidationError, WriteConflictError, IntegrityError, NotFoundError, UnsupportedError, CapabilityError, TransientError, KeyUnavailableError, BudgetExceededError, LeaseExpiredError, LeaseLimitError, StaleOperandError, isCloudRoaringError, isWriteConflictError, isTransientError, isNotFoundError, isIntegrityError, isValidationError, isLeaseExpiredError, isLeaseLimitError, isStaleOperandError, CrbmReader, BufferReader, CountingMetricsSink, RecordingAuditSink, DEFAULT_RETRY_POLICY, AWS_US_EAST_1_ONDEMAND, ELASTICACHE_REDIS_US_EAST_1_ONDEMAND, ONE_REDIS_HA_CLUSTER, } from '@cloudbitmaps/core';
1896
+ export type { Aead, AeadSealed, AuditEvent, BlobReader, BlobSink, Budget, BudgetOption, ChunkRef, Clock, CodecBitmap, CodecInterface, ConsistencyErrorEntry, ConsistencyIssue, ConsistencyReport, CostReport, CrbmCrypto, CrbmReaderOptions, CrbmStorageChunkSourceOptions, DestroyResult, ClearRegistrySummary, DropResult, EncodedChunk, EraseDeps, EstimateInput, ExportFailure, ExportFormat, ExportManifest, ExportOptions, ExportSink, ExportWriter, ExportedSegment, GenKey, GenerationEntry, GenerationMetadata, GovernanceMeta, IAuditSink, IKeystore, IMetricsSink, IRegistryDriver, IStorageDriver, IdRange, InProcessKeystoreOptions, LoadGuard, LoadInput, LoadOptions, LoadRefusal, LoadResult, LocalFsStorageOptions, MemoryStorageOptions, MetricEvent, MetricOpName, MetricsSnapshot, NewRegistryRecord, PinnedAt, PinnedObject, PortableBitmap, PricingProfile, RedisNodeType, RedisSizing, RegCaps, RegistryPatch, RegistryRecord, RegistryStatus, RegistrySummary, ReapRegistryTombstonesOptions, ReapRegistryTombstonesResult, RegistryWriteOptions, LeaseEntry, RetentionPolicy, RetireEntry, RetireExpiredOptions, RetireExpiredResult, RetryPolicy, Rng, RollbackResult, SealedRegistrySummary, SegmentInfo, SegmentRef, SegmentSize, SegmentSizing, SetRetentionResult, StorageBackend, StorageCaps, StorageChunkSource, Token, Workload, WrappedDek, } from '@cloudbitmaps/core';