@cloudbitmaps/core 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,13 @@
1
1
  import { type IAuditSink } from './audit.js';
2
2
  import type { Yielder } from './cooperative.js';
3
3
  import type { Clock, Rng } from './determinism.js';
4
+ import type { PublishedKept } from './generation-gc.js';
4
5
  import type { Aead, CrbmCrypto, IKeystore, WrappedDek } from './crypto.js';
5
6
  import type { ChunkRef, ChunkRead, ReadChunksOptions, StorageChunkSource, GenerationMetadata, GenerationSummary, GenKey, IStorageDriver, IRegistryDriver, RegistryRecord, RegistrySummary, SegmentRef, SegmentSize, Token } from './ports.js';
6
7
  import { CrbmReader } from './crbm/reader.js';
7
8
  import type { CrbmReaderOptions } from './crbm/reader.js';
8
9
  import type { CodecBitmap, CodecInterface } from './codec.js';
9
- import { DecodedLoadInput } from './load-input.js';
10
+ import { ChunkLoadInput, DecodedLoadInput } from './load-input.js';
10
11
  export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
11
12
  /**
12
13
  * Optional {@link IRegistryDriver}. When provided, the current generation is resolved via the registry's
@@ -78,6 +79,18 @@ export interface PinnedObject {
78
79
  readonly version: string;
79
80
  readonly fingerprint?: string;
80
81
  }
82
+ /**
83
+ * What one resolution of a segment's pointer found: the target, and what its row says of the generation, each
84
+ * computed only when asked for. The key is unwrapped once however many ask, and a failure is not remembered, so a
85
+ * transient fault in the keystore is asked again.
86
+ */
87
+ /** What a pin that leases its generation does between reading the row and opening the object. */
88
+ export interface PinLease {
89
+ /** Write the lease against the row the pin read: its token, or `'moved'` when the pointer is no longer at the generation. */
90
+ take(row: RegistryRecord): Promise<{
91
+ readonly token: Token;
92
+ } | 'moved'>;
93
+ }
81
94
  export declare class CrbmStorageChunkSource implements StorageChunkSource {
82
95
  private readonly driver;
83
96
  /**
@@ -216,9 +229,27 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
216
229
  * and a `keep: 0` sweep can land in that gap, and nothing has been handed out yet, so the generation current
217
230
  * once the swept one is gone is exactly what a pin taken now should hold. One retry, then propagate.
218
231
  */
219
- pinGeneration(ref: SegmentRef): Promise<({
232
+ pinGeneration(ref: SegmentRef, lease?: PinLease): Promise<({
220
233
  generation: number;
221
234
  } & Required<PinnedObject>) | null>;
235
+ /**
236
+ * Open one named generation and verify it is the object `at.fingerprint` names, for a pin the caller reopens. One
237
+ * row read and one tail read with a registry (none of the row for a store without one): the open is the one a
238
+ * pinned read makes ({@link readerAt}), memoised under the version the pin will read by, so the pin's first read
239
+ * finds it open. A generation that is gone, above the row's pointer, on a destroyed or absent row, or another
240
+ * object than the fingerprint names, throws `NotFoundError`; nothing here reads empty.
241
+ *
242
+ * A generation counts as published while the pointer is at or above it. A rollback deletes nothing, so after one
243
+ * and a later load, a generation it rolled back from can be reopened while its object is still stored and the
244
+ * fingerprint matches. Like `pinGeneration`'s pinned reads, this opens the object without the live open's check
245
+ * of the row's summary against it, since that check is made where the row's own generation is read.
246
+ */
247
+ pinGenerationAt(ref: SegmentRef, at: {
248
+ readonly generation: number;
249
+ readonly fingerprint: string;
250
+ }, lease?: PinLease): Promise<{
251
+ generation: number;
252
+ } & Required<PinnedObject>>;
222
253
  private pinOnce;
223
254
  /**
224
255
  * Where a pinned reader of one object is memoised. With a registry that is its version, which names the object:
@@ -460,72 +491,17 @@ export declare function writeCrbmGenerationStream(driver: IStorageDriver, key: G
460
491
  clock?: Yielder;
461
492
  metadata?: GenerationMetadata;
462
493
  }): Promise<StreamWriteResult>;
463
- /**
464
- * Point a segment's registry `currentGen` at `key.generation` — the publish step that makes a freshly-written
465
- * generation the authoritative latest (so registry-aware readers see it). **Forward-only and idempotent:**
466
- * if the registry has no row it creates one; if it's already at/ahead of `key.generation` it's a no-op (an
467
- * out-of-order/duplicate publish never regresses the pointer); otherwise it advances via compare-and-swap,
468
- * retrying a few times under contention. Separated from the Storage write so callers can publish atomically
469
- * after the immutable object is durable (write-then-publish).
470
- *
471
- * **Throws** rather than returning, in two cases, because each would otherwise create a state no reader can
472
- * recover from: a `destroyed` (crypto-shredded) row, where advancing the pointer would name an object encrypted
473
- * under a shredded key; and an advance carrying **new** `wrappedDeks`, which would either strand that key or make
474
- * the row advertise encryption over cleartext generations. Contention that never clears throws
475
- * {@link WriteConflictError} after the retries.
476
- *
477
- * **`expectFrom` turns forward-only into read-modify-write.** Forward-only is the right rule for a writer whose
478
- * content does not depend on what was current — a load computes its ids upstream, so publishing over a newer
479
- * generation loses nothing the loader knew about. It is the WRONG rule for a writer that *derived* its content
480
- * from a particular generation: the erasure rewrite streams generation `from` and clears one bit, so publishing
481
- * it over a newer generation silently discards whatever that generation added. Passing `expectFrom` makes the
482
- * publish land only while the pointer is still exactly there, and return `false` otherwise, so the caller can
483
- * report `superseded` and re-derive. The compare-and-swap itself carries the token read in the same iteration,
484
- * so the check and the write see the same row.
485
- *
486
- * **`expectToken` fences the LINEAGE, and a derived writer needs both.** A generation number identifies a
487
- * generation only within one incarnation of a name: the numbering restarts at `0` once the row is purged and
488
- * the bucket empty, so a name that is retired and re-created has a *different* segment wearing the *same*
489
- * `currentGen`. `expectFrom` alone matched it — and an erasure rewrite then published one incarnation's content
490
- * over another's, deleted the live objects with its `keep: 0` collection, and returned `erased: true`. The row's
491
- * OCC token is the identity that survives this: a shipped registry's later `create` gets a token never issued before
492
- * under that name, but for a collision of probability 2^-128 per pair of incarnations, so no token is reused across them.
493
- *
494
- * Pass the token read alongside `expectFrom` and the publish lands only on the same row it was derived from.
495
- * The check is deliberately **conservative**: the token also changes on writes that are not supersessions at
496
- * all (a `setRetention`, a due-index reindex), so such a write makes a derived publish report `superseded` and
497
- * the caller re-derive. That costs a re-run on a rare, unrelated write; the alternative costs a segment.
498
- *
499
- * **The driver sends a registry write once, and an unanswered one is reconciled by its effect.** A `create` or
500
- * `compareAndSwap` that fails with {@link TransientError} (a throttle, a lost response, a timeout) may or may not
501
- * have landed, and may still be in flight. The publish reads the row once more and decides from what it holds:
502
- *
503
- * - the pointer names `key.generation`, on the incarnation the write was made against (the row's `createdAt`), and
504
- * `holdsOwnObject` proves the object under it the caller's: the write landed, and the publish returns `true`. Only
505
- * the writer of a write-once object publishes its number, and a number is never taken while an object holds it;
506
- * - the row is still the one the write was made against (the same token, or still no row): the write may land yet. The
507
- * publish then waits, on the injected clock, and sends a **new** write from the row it just read: a request of its
508
- * own, not a replay of the driver's, carrying the same version, so under the registry's fence at most one of the two
509
- * lands and the other meets a conflict, which the next read settles. At most {@link UNANSWERED_RESENDS} follow, after
510
- * a wait under 500 ms, then 1 s, then 2 s (full jitter when an `rng` is given); each is settled the same way. Still
511
- * unanswered, or with no clock to wait on, this throws the registry's own `TransientError` and writes nothing more.
512
- * A caller must not delete its object on it;
513
- * - anything else: the row has moved past the state the write was conditioned on, so that write can never land, and
514
- * the publish goes on as after a lost race, from the row it just read. A pointer at `key.generation` that is not over
515
- * the caller's own object, or is on another incarnation, is a lost race too and answers `false`, never an already
516
- * current re-publish.
517
- *
518
- * A failed read throws `TransientError` too. The same check of the pointer runs after a {@link WriteConflictError},
519
- * so a write that landed and then met itself (a registry whose client re-sent it) is not taken for a lost race.
520
- *
521
- * **A `summary` goes in the same write that moves the pointer**, so a reader that sees this generation as current
522
- * sees the count and metadata that describe it, and every attempt sends the same one. It must be the shape the row's
523
- * keys call for: sealed for an object written with a key, clear for one written without.
524
- */
525
- export declare function publishGeneration(registry: IRegistryDriver, key: GenKey, options?: {
494
+ export declare function publishGenerationKept(registry: IRegistryDriver, key: GenKey, options?: {
526
495
  wrappedDeks?: readonly WrappedDek[];
527
496
  expectFrom?: number;
528
497
  expectToken?: Token;
498
+ /**
499
+ * The row `expectToken` names, as the caller read it. With it, a row whose token has moved but which differs from
500
+ * this one only in its leases (a reader took or released one) does not refuse the publish: it goes on against the
501
+ * fresh row, after a jittered wait and without the caller redoing its work, up to a bound. Any other difference
502
+ * refuses as ever. Without it, a moved token refuses.
503
+ */
504
+ expectRow?: RegistryRecord | null;
529
505
  /**
530
506
  * Publish only while the segment still has **no registry row**.
531
507
  *
@@ -584,7 +560,39 @@ export declare function publishGeneration(registry: IRegistryDriver, key: GenKey
584
560
  * held its generation for a while before another writer moved on.
585
561
  */
586
562
  onUnanswered?: () => void;
587
- }): Promise<boolean>;
563
+ /**
564
+ * The clock that judges whether a lease has ended. With it, a publish over a row that holds ended leases drops
565
+ * them in the write that moves the pointer, which rewrites the row anyway. Without it, leases are left as they are.
566
+ */
567
+ leasesNow?: () => number;
568
+ /**
569
+ * Clear the row's leases in the write that moves the pointer: the caller's collection deletes every generation
570
+ * below the new pointer whatever a lease says, as a subject erasure's does.
571
+ */
572
+ clearLeases?: boolean;
573
+ /**
574
+ * How many generations below the new pointer the caller keeps. With it, the write that moves the pointer also
575
+ * records the window in the row (`keptGens`: the newest `keep` of the row's own list and the generation that was
576
+ * current), derived from the very row the write is made against, and the result names the generations the window
577
+ * dropped. Without it, or above the most a row records, the row records none, and a collection lists.
578
+ */
579
+ keep?: number;
580
+ }): Promise<PublishResult>;
581
+ /** What a publish did: whether the pointer is at the generation, and what the write recorded in the row. */
582
+ export interface PublishResult {
583
+ readonly published: boolean;
584
+ /**
585
+ * What the write that moved the pointer recorded: the list, the generations the window dropped, and the new row's
586
+ * token. Absent when no write of this call is known to have landed (the pointer was already there, or the outcome
587
+ * was settled without knowing which write it was), and a collection then lists.
588
+ */
589
+ readonly kept?: PublishedKept;
590
+ }
591
+ /**
592
+ * {@link publishGenerationKept}'s answer to "did the pointer move", for a caller with no use for what it recorded:
593
+ * a publish with no `keep` leaves the row recording no list.
594
+ */
595
+ export declare function publishGeneration(registry: IRegistryDriver, key: GenKey, options?: Parameters<typeof publishGenerationKept>[2]): Promise<boolean>;
588
596
  /**
589
597
  * Whether the object under `key` is the one `fingerprint` names, from one read of its footer: false when it is another
590
598
  * object or gone. Unlike {@link holdsObject}, a read that cannot tell throws, for a caller that must not take "could
@@ -655,7 +663,7 @@ export interface BulkLoadResult {
655
663
  * {@link WriteConflictError} (write-once). Without a registry a `StorageChunkSource` serves the **highest**
656
664
  * generation present, so a too-high number silently shadows real data; with one, `publishGeneration` decides.
657
665
  */
658
- export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenKey, ids: Iterable<number> | AsyncIterable<number> | DecodedLoadInput, options?: {
666
+ export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenKey, ids: Iterable<number> | AsyncIterable<number> | DecodedLoadInput | ChunkLoadInput, options?: {
659
667
  registry?: IRegistryDriver;
660
668
  keystore?: IKeystore;
661
669
  requireEncryption?: boolean;
@@ -724,7 +732,7 @@ export interface LoadAhead {
724
732
  * {@link bulkLoadCrbmGeneration} for a load whose number, and perhaps its key, are still being fetched: the same
725
733
  * options, with the same meaning, and the same writes. The number is joined only where the object is written.
726
734
  */
727
- export declare function bulkLoadAhead(driver: IStorageDriver, ref: SegmentRef, ahead: LoadAhead, ids: Iterable<number> | AsyncIterable<number> | DecodedLoadInput, options?: NonNullable<Parameters<typeof bulkLoadCrbmGeneration>[3]>): Promise<BulkLoadResult>;
735
+ export declare function bulkLoadAhead(driver: IStorageDriver, ref: SegmentRef, ahead: LoadAhead, ids: Parameters<typeof bulkLoadCrbmGeneration>[2], options?: NonNullable<Parameters<typeof bulkLoadCrbmGeneration>[3]>): Promise<BulkLoadResult>;
728
736
  /**
729
737
  * Open a {@link CrbmReader} on one generation over the storage driver's range/tail reads (decrypting if `crypto`).
730
738
  * The reader the write paths use to re-read what they wrote, and the erasure rewrite uses to stream the old
@@ -78,6 +78,11 @@ export interface CombineOptions extends IdRange {
78
78
  */
79
79
  readonly allowAbsentOperands?: boolean;
80
80
  }
81
+ /** One chunk of a combine's result: the bitmap of its remainders, owned by whoever reads it. */
82
+ export interface CombinedChunk {
83
+ readonly chunkKey: number;
84
+ readonly bitmap: CodecBitmap;
85
+ }
81
86
  export declare class SegmentEngine {
82
87
  private readonly storage;
83
88
  private readonly cache;
@@ -99,6 +104,8 @@ export declare class SegmentEngine {
99
104
  * they deliver, as a read whose entry an invalidation dropped does not (streams hold no `openReads` entry).
100
105
  */
101
106
  private readonly openStreams;
107
+ /** How many times {@link invalidate} has been called: a read compares it with the count it began under. */
108
+ private invalidations;
102
109
  constructor(deps: EngineDeps);
103
110
  /** Membership: one chunk lookup — the cache, else one Storage fetch of that chunk. */
104
111
  has(seg: SegmentRef, id: number): Promise<boolean>;
@@ -159,6 +166,29 @@ export declare class SegmentEngine {
159
166
  * the caller's to keep.
160
167
  */
161
168
  iterateBatches(seg: SegmentRef, range?: IdRange): AsyncGenerator<Uint32Array>;
169
+ /**
170
+ * The ids at 1-based ranks `n`, `2n`, `3n` … counted over the ids in `range`, ascending. The rank of each boundary
171
+ * is placed from the index's per-chunk counts, so only the chunks that hold a boundary are read, each once however
172
+ * many boundaries it holds, through the same coalesced stream, window and budget unit as {@link iterate}. A range with
173
+ * `after` inside a chunk reads that chunk too, to count the ids at or below `after`. A last partial window yields
174
+ * nothing.
175
+ *
176
+ * The counts place ids, and are trusted as `count()` trusts them: a chunk that is read must decode to the size its
177
+ * index says, or the read throws {@link IntegrityError} (invariant 5), but a chunk that is not read is not checked, so
178
+ * an index that lies consistently about one (on an unencrypted object, the sum still matches the footer's total,
179
+ * which opening the object checks before any boundary is placed) shifts the ranks after it. An encrypted object's
180
+ * index is authenticated and bound to the object, so a wrong count there needs the key. The index and the chunks must come from one
181
+ * generation (invariant 3): the caller reads through a source that holds one, a pinned one, and a source with no
182
+ * per-chunk counts is refused.
183
+ *
184
+ * Nothing is fetched when the chunks in range hold fewer than `n` ids. The budget is charged before the first fetch
185
+ * with an upper bound, the cut chunk plus one chunk per boundary the counts allow (capped by the chunks in range),
186
+ * which can exceed the chunks actually read. A range that cuts its first chunk reads it even when the ids above
187
+ * `after` turn out to be fewer than `n`, and one that cuts its last chunk may read it and yield nothing.
188
+ */
189
+ everyNth(seg: SegmentRef, n: number, range?: IdRange): AsyncGenerator<number>;
190
+ /** A chunk's remainders ascending, once its size is confirmed against the index's count for it (invariant 5). */
191
+ private chunkRemainders;
162
192
  /**
163
193
  * `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
164
194
  *
@@ -215,6 +245,20 @@ export declare class SegmentEngine {
215
245
  unionBatches(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<Uint32Array>;
216
246
  /** {@link andNot}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
217
247
  andNotBatches(seg: SegmentRef, excludes: readonly SegmentRef[], options?: Omit<CombineOptions, 'exclude'>): AsyncGenerator<Uint32Array>;
248
+ /**
249
+ * {@link intersect}, as the chunks the result is made of: `{ chunkKey, bitmap }` in ascending key order, each a
250
+ * non-empty bitmap of the chunk's 16-bit remainders, cut to the read's range. What a materialisation writes
251
+ * straight into its new generation, with no id built on the way.
252
+ *
253
+ * **Each bitmap belongs to the caller, who may change it:** it is the combine's own accumulator or a bitmap built
254
+ * for the range's edge, never a chunk the cache holds or another operand's. The reads, the budget, the metric and the
255
+ * errors are the id reads' own.
256
+ */
257
+ intersectChunks(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<CombinedChunk>;
258
+ /** {@link union}, as chunks: see {@link intersectChunks}. */
259
+ unionChunks(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<CombinedChunk>;
260
+ /** {@link andNot}, as chunks: see {@link intersectChunks}. */
261
+ andNotChunks(seg: SegmentRef, excludes: readonly SegmentRef[], options?: Omit<CombineOptions, 'exclude'>): AsyncGenerator<CombinedChunk>;
218
262
  /**
219
263
  * Everything a combine decides before it reads a chunk: validation, each operand's keys and generation, the
220
264
  * absent-operand refusal, key alignment, the budget and the metric. `null` when the range is empty. The per-id
@@ -223,6 +267,10 @@ export declare class SegmentEngine {
223
267
  private combinePlan;
224
268
  private combine;
225
269
  private combineBatches;
270
+ /** The chunk read: the plan every combine shares, then each key's result, cut to the range at its edges. */
271
+ private combineChunks;
272
+ /** An edge chunk's bitmap cut to the window, as a new bitmap: the remainders `edgeIds` would yield. */
273
+ private cutToWindow;
226
274
  /**
227
275
  * The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
228
276
  * Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
@@ -239,16 +287,21 @@ export declare class SegmentEngine {
239
287
  */
240
288
  private operandChunk;
241
289
  /**
242
- * Open `keys` of `seg` as a stream of coalesced ranges, or `undefined` when the read is per key: a source with no
243
- * `getChunks`, a segment with no generation, or nothing to read. The chunks already decoded in the cache are taken out
244
- * of the key list first and served from there, so a repeat read asks the source for nothing it holds. Nothing is
245
- * requested until the first chunk is asked for.
290
+ * `keys` of `seg` as a stream of coalesced ranges, or `undefined` when the read is per key: a source with no
291
+ * `getChunks`, a segment with no generation, or nothing to read. Nothing is requested, and no cache lookup made, until
292
+ * the first chunk is asked for (see {@link StreamedChunks}).
246
293
  */
247
294
  private openStreamed;
295
+ /**
296
+ * Open the stream of `streamed`, at its first uncached chunk `from`: over that chunk and the later keys the cache does
297
+ * not hold. Each later key is counted as a cache hit or miss here, so a key is counted once whichever way it is served.
298
+ */
299
+ private startStream;
248
300
  private closeStreamed;
249
301
  /**
250
- * The decoded chunk at `chunkKey` of a streamed read: the next chunk of its stream, or, for a chunk the cache held
251
- * when the read opened, the cached instance while it is still there, and a read of that chunk alone once it is not.
302
+ * The decoded chunk at `chunkKey` of a streamed read: from the cache while the stream is unopened and the cache holds
303
+ * it; else the next chunk of its stream, or, for a chunk the cache held when the stream opened, the cached instance
304
+ * while it is still there, and a read of that chunk alone once it is not.
252
305
  */
253
306
  private streamedChunk;
254
307
  /**
@@ -13,8 +13,9 @@
13
13
  * operation — `eraseIdFromSegment` rewrites the generation without it.
14
14
  */
15
15
  import { type IAuditSink } from './audit.js';
16
+ import { type ChurnDeps } from './leases.js';
16
17
  import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
17
- export interface EraseDeps {
18
+ export interface EraseDeps extends ChurnDeps {
18
19
  readonly registry: IRegistryDriver;
19
20
  }
20
21
  export interface DestroyResult {
@@ -42,6 +42,25 @@ export declare class IntegrityError extends CloudRoaringError {
42
42
  */
43
43
  export declare class NotFoundError extends CloudRoaringError {
44
44
  }
45
+ /**
46
+ * A read of a leased pin came after its lease ended, by `until` or by `release()`. Thrown at every read site of the
47
+ * handle, including a leased handle used as an operand or an exclude of a combine, and never answered empty: a
48
+ * handle that read empty here would be an opt-out list that suppresses nobody. Carries the instant the lease ended
49
+ * and why; never an id or a key.
50
+ */
51
+ export declare class LeaseExpiredError extends CloudRoaringError {
52
+ /** Epoch-ms the lease was taken until. */
53
+ readonly until: number;
54
+ /** `'expired'` when the clock reached `until`, `'released'` after `release()`. */
55
+ readonly reason: 'expired' | 'released';
56
+ constructor(message: string, until: number, reason: 'expired' | 'released');
57
+ }
58
+ /**
59
+ * A segment already has as many live leases as one row records, so another cannot be taken. Nothing was written.
60
+ * Release one, wait for one to end, or share one lease across the tasks of a job.
61
+ */
62
+ export declare class LeaseLimitError extends CloudRoaringError {
63
+ }
45
64
  /**
46
65
  * This build/configuration cannot perform the requested operation, though nothing is malformed. Two uses:
47
66
  * (1) **format** — the bytes are well-formed but unreadable here (an unknown `.crbm` major version) —
@@ -104,6 +123,10 @@ export declare function isCloudRoaringError(err: unknown): err is CloudRoaringEr
104
123
  export declare function isWriteConflictError(err: unknown): err is WriteConflictError;
105
124
  /** A retryable transient infrastructure fault. The retry layer keys on this. */
106
125
  export declare function isTransientError(err: unknown): err is TransientError;
126
+ /** A read came after its pin's lease ended. */
127
+ export declare function isLeaseExpiredError(err: unknown): err is LeaseExpiredError;
128
+ /** A segment has no room for another live lease. */
129
+ export declare function isLeaseLimitError(err: unknown): err is LeaseLimitError;
107
130
  /** A requested object/row does not exist. */
108
131
  export declare function isNotFoundError(err: unknown): err is NotFoundError;
109
132
  /** Corrupt/oversized/failed-checksum bytes from a tier. */
@@ -1,4 +1,4 @@
1
- import type { GenKey, IStorageDriver, IRegistryDriver, RegistryRecord, SegmentRef } from './ports.js';
1
+ import type { GenKey, IStorageDriver, IRegistryDriver, RegistryRecord, SegmentRef, Token } from './ports.js';
2
2
  /** The two ports generation bookkeeping needs: the objects, and the pointer that says which one is current. */
3
3
  export interface GenerationDeps {
4
4
  readonly storage: IStorageDriver;
@@ -51,9 +51,9 @@ export interface LoadNumber {
51
51
  export declare function nextLoadGeneration(ref: SegmentRef, deps: GenerationDeps, row: RegistryRecord | null): Promise<LoadNumber>;
52
52
  /**
53
53
  * How often a load that could collect by name lists instead: every generation divisible by this. A name-only pass
54
- * takes one generation a load and never looks at the rest, so what it misses (a refused load's object that stayed
55
- * because the row had changed, extras left by a `keep` that shrank, an object a rollback or an erasure left
56
- * stranded below the pointer) waits for a listing. The number is a generation's, not a clock's, so the choice needs
54
+ * takes the names its publish pushed out of the row's window and never looks at the rest, so what it misses (a crashed
55
+ * or refused load's object, a delete that failed, extras a fault left after a `keep` that shrank, an object a rollback
56
+ * or an erasure left stranded below the pointer) waits for a listing. The number is a generation's, not a clock's, so the choice needs
57
57
  * no state and no time, and a segment that loads cleanly lists once in this many loads at most. The bound is in
58
58
  * generation numbers: a rollback moves the pointer down, and the loads that then take the numbers above it count
59
59
  * from there.
@@ -97,58 +97,82 @@ export declare function gcOrphanGenerations(ref: SegmentRef, deps: GenerationDep
97
97
  keep?: number;
98
98
  }): Promise<number[]>;
99
99
  /**
100
- * Collect after a load's publish: by name when `byName` (the load numbered its generation with one existence check,
101
- * the object its row named was there when the guard looked, and its caller does not ask for a listing) and `keep` is
102
- * at most one, and by listing otherwise ({@link gcOrphanGenerations}). A load lists in three cases: `keep` of 2 or
103
- * more, which a name cannot serve because the window then counts generations that may be absent (a refused
104
- * neighbour's number is a gap, and by name would take one the window promised to keep); a number the listing chose,
105
- * so something sits above the pointer or the check could not answer, or a current object the load's check found gone; and
106
- * every {@link LIST_COLLECTION_CADENCE}th generation, which collects what a name-only pass leaves. It makes no
107
- * request when `keep` is at least the generation published, since no more generations than that exist below it. A
108
- * load onto a `destroyed` row is refused at its publish and never reaches here; a drop that lands after the publish
109
- * leaves this pass collecting the tombstone's generations, one by name or all of them by listing, and the drop's own
110
- * sweep takes the rest.
100
+ * What makes a collection spare leased generations: the clock that says whether a lease has ended. Only a load's
101
+ * collection passes one, and a load with no clock passes none: it cannot tell a live lease from an ended one, so it
102
+ * reads none. A pass without a guard never reads the row's leases, which is what keeps erasure, shred and drop deleting
103
+ * what a lease names.
104
+ */
105
+ export interface LeaseGuard {
106
+ readonly now: () => number;
107
+ }
108
+ /** What a load's publish recorded in the row, as `publishGeneration` reports it. */
109
+ export interface PublishedKept {
110
+ /** The list the publish wrote, or `undefined` when it wrote none (the row then does not know which are kept). */
111
+ readonly list: readonly number[] | undefined;
112
+ /** The names the publish pushed out of the window, ascending. */
113
+ readonly evict: readonly number[];
114
+ /** The token of the row the publish wrote, when it is known: a write settled by reading the row has none. */
115
+ readonly token: Token | undefined;
116
+ }
117
+ /**
118
+ * Collect after a load's publish. The row records the generations the load keeps, so the pass deletes by name what the
119
+ * publish pushed out of that window ({@link deleteEvicted}) and lists the segment ({@link gcOrphanGenerations}'s
120
+ * pass) only when it cannot rely on the row or must catch what a name does not:
121
+ *
122
+ * - the row recorded no list (a `keep` above {@link MAX_KEPT_GENERATIONS}, a row an earlier schema wrote, one a
123
+ * rollback or an erasure moved, or a publish whose write was settled by reading the row): the pass keeps the
124
+ * newest `keep` generations present below the pointer, and records them with one compare-and-swap, fenced on the
125
+ * publish's own token, so the next load collects by name;
126
+ * - the guard found the current generation's object gone, or, for a `keep` of 1 or more when the guard took the size
127
+ * from the row's summary and opened nothing, one zero-byte read of it does not find it (`proveCurrent`): the
128
+ * window then counts a generation that is not there, so the same pass keeps what is;
129
+ * - a number the listing chose (something sits above the pointer, or the existence check could not answer), a
130
+ * caller that asks for a listing, and every {@link LIST_COLLECTION_CADENCE}th generation: the pass deletes every
131
+ * generation below the pointer the row does not name, which collects what a name-only pass leaves.
132
+ *
133
+ * It makes no request when `keep` is at least the generation published, since no more generations than that exist
134
+ * below it. A load onto a `destroyed` row is refused at its publish and never reaches here; a drop that lands after
135
+ * the publish leaves this pass collecting the tombstone's generations, and the drop's own sweep takes the rest.
136
+ *
137
+ * The seed's compare-and-swap moves the row's token, so a derived writer in flight on the row (an erasure rewrite)
138
+ * meets a lost fence and re-derives: it happens once per row that recorded no list. A `WriteConflictError` from it is
139
+ * a lost race, and the load took effect, so it is not raised; the next load records the list. Any other fault is
140
+ * raised, as every fault of a collection after a landed publish is.
111
141
  */
112
142
  export declare function collectAfterLoad(ref: SegmentRef, deps: GenerationDeps, options: {
113
143
  generation: number;
114
144
  keep: number;
145
+ /** The load's number was proved free by one existence check, and no caller asked for a listing. */
115
146
  byName: boolean;
147
+ /** The guard opened the current generation's object and found it gone. */
148
+ currentGone?: boolean;
149
+ kept: PublishedKept | undefined;
116
150
  /**
117
- * The object the load's row named as current, which the caller did not open. Before it collects by name a generation
118
- * the window keeps (a `keep` of 1), the pass looks for this object with one zero-byte read, and lists instead
119
- * unless that finds it: a name-only pass is safe only while the object the row named is in the bucket.
151
+ * The object the load's row named as current, which the caller did not open. Before it collects by name, the pass
152
+ * looks for this object with one zero-byte read, and lists instead unless that finds it: a name-only pass is
153
+ * safe only while the object the row named is in the bucket.
120
154
  */
121
155
  proveCurrent?: GenKey;
156
+ /** Spare the generations the row's live leases hold. Absent, the pass ignores leases. */
157
+ leases?: LeaseGuard;
122
158
  }): Promise<number[]>;
123
159
  /**
124
- * Collect, with no listing, the one generation a publish of `generation` pushed out of a grace window of `keep`
125
- * (0 or 1): `generation - keep - 1`, when that is a number at all. Returns that name, whether or not an object was
126
- * there: a delete of an absent object succeeds on every backend and says nothing, so the name returned is the one
127
- * asked for, not one found, and a generation a concurrent pass, an erasure or a lifecycle rule had already taken is
128
- * listed all the same. A list is not a receipt (invariant 4), and neither is this.
160
+ * Delete, with no listing, the generations a publish of `generation` pushed out of its row's window, `evict`, taken
161
+ * from the list the publish was written against. Returns the names asked for, whether or not an object was there: a
162
+ * delete of an absent object succeeds on every backend and says nothing, so a generation a concurrent pass, an erasure
163
+ * or a lifecycle rule had already taken is listed all the same. A list is not a receipt (invariant 4), and neither is
164
+ * this.
129
165
  *
130
- * The caller has published `generation` having numbered it with one existence check, so the row named
131
- * `generation - 1` and every generation the window newly evicts is below it. With `keep` of 0 or 1 that name is
132
- * the one a listing pass would also take, or one it left to a later pass: never a generation the listing keeps.
133
- * That rests on the object the row named being in the bucket, and a fault can break it (a lifecycle rule or a
134
- * partial restore removing it, or an erasure deleting the object of a load whose publish then landed;
135
- * `checkConsistency` reports the state). The caller, which learns that from its guard's read of the current
136
- * generation or, when the guard took the size from the row's summary and opened nothing, from one zero-byte read of
137
- * it (see {@link collectAfterLoad}), lists instead; a load that made neither (`allowEmpty` without `minRetained`)
138
- * cannot tell, and deletes by name the older generation a listing would have kept as the window. `keep` of 2 or more is refused,
139
- * because there the window counts the generations that exist, which a name cannot know.
140
- *
141
- * Collection never touches the current generation, and the row is re-proved before the delete as every pass does.
142
- * A row that is gone, or a pointer that has fallen below `generation` (a rollback, or a name purged and re-created
143
- * with fewer loads), deletes nothing and returns `[]`: the publish already landed, nothing here protects it or
144
- * needs the result, and a throw would hand the caller a failure for a load that took effect. A pointer that has
145
- * moved above `generation` does not stop it: a publish landing in the meantime leaves routine collection working.
146
- * A fault, a registry read or a delete that throws, still propagates, with the pointer at `generation`. The bound
147
- * is `generation`, the pointer this load's own publish set, so the lower-of-two-pointers rule of a listing pass
148
- * holds with one observation in place of two. Nothing is deleted when `generation` has nothing below its window,
149
- * and no request is made.
166
+ * Each delete re-reads the row first (invariant 4) and stops the pass, deleting nothing more and raising nothing, unless
167
+ * the row still has a pointer at or above `generation` (a rollback, or a name purged and re-created with fewer loads,
168
+ * has moved it below: the publish already landed, nothing here protects it or needs the result), the name is below
169
+ * that pointer, and the row's own list is usable and does not name it. The last condition is what keeps a late delete
170
+ * from taking a generation that a later publish, after a rollback, put in the window. A pointer that has moved above
171
+ * `generation` does not stop it: a publish landing in the meantime leaves routine collection working. A fault, a
172
+ * registry read or a delete that throws, propagates, with the pointer at `generation`.
150
173
  */
151
- export declare function collectByName(ref: SegmentRef, deps: GenerationDeps, options: {
174
+ export declare function deleteEvicted(ref: SegmentRef, deps: GenerationDeps, options: {
152
175
  generation: number;
153
- keep: number;
176
+ evict: readonly number[];
177
+ leases?: LeaseGuard;
154
178
  }): Promise<number[]>;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The generations a segment's row records as kept: the grace window below its pointer, so a load collects what its
3
+ * publish pushed out of the window by name instead of listing the bucket.
4
+ *
5
+ * The record is a cache of what a listing would keep, never an inventory of the bucket. Absent means the row does not
6
+ * know, and the load lists; an empty list means nothing below the pointer is kept. A patch that moves the pointer
7
+ * without naming the list drops it (see `applyRegistryPatch`), so a writer that forgets the list leaves a row that
8
+ * lists, never one that deletes a generation it should keep. Pure: no I/O, time or randomness.
9
+ */
10
+ import type { RegistryRecord } from './ports.js';
11
+ /**
12
+ * The most generations a writer records. A `keep` above it records none, and the load lists on every publish. A
13
+ * reader accepts up to {@link MAX_STORED_KEPT_GENERATIONS}, so a release can write more than this without a new row
14
+ * schema.
15
+ */
16
+ export declare const MAX_KEPT_GENERATIONS = 64;
17
+ /** The most entries a stored row may hold: four times what a writer writes. */
18
+ export declare const MAX_STORED_KEPT_GENERATIONS = 256;
19
+ /**
20
+ * The row's list when a collection may rely on it: present, on a row with a pointer, and every entry below that
21
+ * pointer. A well-formed list that disagrees with the pointer is read but not used (the load lists instead), as a
22
+ * summary that names another generation is.
23
+ */
24
+ export declare function usableKeptGens(record: RegistryRecord | null): readonly number[] | undefined;
25
+ /** What a publish of a generation writes into the row, and which names it pushes out of the window. */
26
+ export interface KeptAfter {
27
+ /** The list to write, or `undefined` to write none (the row then does not know, and the next load lists). */
28
+ readonly list: readonly number[] | undefined;
29
+ /** The generations the window no longer keeps, ascending: the names to delete. */
30
+ readonly evict: readonly number[];
31
+ }
32
+ /**
33
+ * The list a publish of `generation` over `prev` writes: the newest `keep` of the row's list and the generation that
34
+ * was current, and the names that fell out of it. Only generations that were current are named, so a number a refused
35
+ * or crashed load took is never in it.
36
+ *
37
+ * No list is written when the row has no usable list to extend (except for generation 0, which has nothing below it),
38
+ * when `keep` is above {@link MAX_KEPT_GENERATIONS}, or when no `keep` is given.
39
+ */
40
+ export declare function nextKept(prev: RegistryRecord | null, generation: number, keep: number | undefined): KeptAfter;