@cloudbitmaps/core 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.
@@ -0,0 +1,119 @@
1
+ import type { Clock, Rng } from './determinism.js';
2
+ import type { IRegistryDriver, LeaseEntry, RegistryRecord, SegmentRef, Token } from './ports.js';
3
+ /** The longest lease: a `leaseUntil` further than this from the clock is refused. */
4
+ export declare const MAX_LEASE_MS: number;
5
+ /**
6
+ * How much longer than `until` a collector holds a lease, and the most a collector will believe a lease runs past
7
+ * its own clock. It is the tolerated difference between the holder's clock and the collector's, in either direction.
8
+ */
9
+ export declare const LEASE_SKEW_MS = 60000;
10
+ /** The most live leases a writer records on one segment; the next one is refused with {@link LeaseLimitError}. */
11
+ export declare const MAX_LEASES_PER_SEGMENT = 64;
12
+ /** The most entries a stored row may hold: four times what a writer writes, so a release can raise the cap. */
13
+ export declare const MAX_STORED_LEASES = 256;
14
+ /**
15
+ * Read-modify-write attempts of one take or release before it reports contention. Takers of one row serialise: each
16
+ * round exactly one write lands, so the slowest of `k` simultaneous takers needs `k` attempts. The bound is the cap
17
+ * plus a few, so that the most a segment can hold, all arriving at once, can all land.
18
+ */
19
+ export declare const LEASE_CAS_ATTEMPTS: number;
20
+ /**
21
+ * Whether an entry holds its generation against a collector reading `now`: it has not ended (`until` plus the margin
22
+ * is ahead), and it does not run further past `now` than any honest holder with a clock inside the margin could have
23
+ * written (`until` at most the longest lease plus the margin ahead). The second clause bounds a corrupt or forged
24
+ * `until` without refusing the row: an entry past it is read and not honoured.
25
+ */
26
+ export declare function isLive(entry: LeaseEntry, now: number): boolean;
27
+ /** The generations a collector reading `now` must spare, from the row it holds. */
28
+ export declare function heldGenerations(record: RegistryRecord | null, now: number): ReadonlySet<number>;
29
+ /** The entries of a row that are live at `now`, in the row's order. */
30
+ export declare function liveLeases(record: RegistryRecord | null, now: number): readonly LeaseEntry[];
31
+ /**
32
+ * What a writer that fences on the row's token needs to wait out lease churn: a sleep, and the rng that spreads it.
33
+ * Both are optional; without a sleep a retry follows at once.
34
+ */
35
+ export interface ChurnDeps {
36
+ readonly clock?: {
37
+ readonly sleep?: (ms: number) => Promise<void>;
38
+ } | undefined;
39
+ readonly rng?: Rng | undefined;
40
+ }
41
+ /**
42
+ * How many times one writer retries after meeting a row that differs from the one it read only in its leases. Takers of
43
+ * one row serialise: each round exactly one write lands, so a writer racing `k` holders needs up to `k + 1` rounds, and a
44
+ * holder can take and release inside one job, so the bound covers a take and a release by every holder the row can
45
+ * hold, with a few to spare: 2 * {@link MAX_LEASES_PER_SEGMENT} + 8. At waits of up to 25 ms growing to up to 400 ms it is a
46
+ * minute at the very most before the writer reports the conflict it had, and a lease writer that goes on beyond
47
+ * that has stopped being a lease writer and become a flood.
48
+ */
49
+ export declare const LEASE_ONLY_RETRIES: number;
50
+ /**
51
+ * Whether `now` is `held` with nothing changed but its leases: the same incarnation, and every other field equal,
52
+ * `updatedAt` and the token (which every write moves) aside. `alsoIgnore` names fields the caller's own earlier write
53
+ * changed, so a writer comparing against a row it has itself written does not take its own change for another's.
54
+ *
55
+ * This is the one test that lets a writer fenced on the row's token go on after a lease write: a lease is written by
56
+ * readers, in numbers no operator controls, and it says nothing about the content the writer derived, the pointer it
57
+ * judged or the key it holds. Any other difference is another writer's, and the fence refuses as it always has.
58
+ */
59
+ export declare function onlyLeasesDiffer(held: RegistryRecord, now: RegistryRecord, alsoIgnore?: readonly (keyof RegistryRecord)[]): boolean;
60
+ /** A writer's count of the lease-only changes it has waited out, and the wait. One per call of the writer. */
61
+ export interface LeaseChurn {
62
+ /**
63
+ * When `now` is `held` changed only in its leases and the bound is not spent: wait, then read the row again with
64
+ * `reread`, and return what it found. The wait comes before the read, so the writer's next write is made against a row
65
+ * as fresh as one round trip, not one as stale as the wait. The caller checks that row against `held` once more (a
66
+ * write may have landed during the wait) and goes on against it, without redoing its work. `undefined` otherwise, and
67
+ * the caller does what it always did.
68
+ *
69
+ * The wait is uniform in `[0, bound)`, the bound starting at 25 ms and doubling to 400 ms, taken on the injected
70
+ * clock's `sleep` and spread by its `rng`. With no `sleep` there is no wait, and the retries follow one another at
71
+ * once; with no `rng` the wait is exactly the bound, so contenders stay in step. The store always supplies both.
72
+ */
73
+ settle(held: RegistryRecord | null | undefined, now: RegistryRecord | null, reread: () => Promise<RegistryRecord | null>, alsoIgnore?: readonly (keyof RegistryRecord)[]): Promise<{
74
+ readonly row: RegistryRecord | null;
75
+ } | undefined>;
76
+ }
77
+ export declare function leaseChurn(deps: ChurnDeps): LeaseChurn;
78
+ /** What the lease writers need: the registry, the clock that judges liveness and waits, and the rng that spreads waits. */
79
+ export interface LeaseDeps {
80
+ readonly registry: IRegistryDriver;
81
+ readonly clock: Clock;
82
+ readonly rng?: Rng;
83
+ }
84
+ /** What a take asks for. */
85
+ export interface LeaseTake {
86
+ /** Drawn once per pin call, outside any retry, so a write that landed unseen is found again and never duplicated. */
87
+ readonly holder: string;
88
+ /** The generation to hold. */
89
+ readonly generation: number;
90
+ /** Epoch-ms, already checked against the caller's clock. */
91
+ readonly until: number;
92
+ /** The row the caller read; the first write is made against it, with no row read of its own. */
93
+ readonly row: RegistryRecord;
94
+ /** The generation must still be the row's pointer on every attempt: a pin of the current generation. */
95
+ readonly current: boolean;
96
+ }
97
+ /** A lease that landed: the token its write returned, and the row that write was made against. */
98
+ export interface TakenLease {
99
+ readonly token: Token;
100
+ readonly row: RegistryRecord;
101
+ }
102
+ /**
103
+ * Write a lease entry for `take.holder`, fenced on the row it was made against, so a lease that lands proves the row
104
+ * was unchanged since the caller read it. Returns `'moved'` when `take.current` and the pointer is no longer at the
105
+ * generation (the caller starts over from a fresh read). Throws {@link NotFoundError} for a row that is gone,
106
+ * destroyed or has no such published generation, {@link LeaseLimitError} when the segment already has
107
+ * {@link MAX_LEASES_PER_SEGMENT} live leases (nothing is written), `WriteConflictError` after
108
+ * {@link LEASE_CAS_ATTEMPTS} lost races, and the registry's own `TransientError` for a write that stayed unanswered.
109
+ *
110
+ * Idempotent by holder: an entry for the holder is replaced, so a retry of a whole pin re-takes without duplicating,
111
+ * and a write that got no answer is settled by reading the row for the holder's entry.
112
+ */
113
+ export declare function takeLease(ref: SegmentRef, deps: LeaseDeps, take: LeaseTake): Promise<TakenLease | 'moved'>;
114
+ /**
115
+ * Remove `holder`'s entry, and every entry that has ended, in one write. Idempotent: a row that is gone, destroyed or
116
+ * has no entry for the holder is already released. Throws `WriteConflictError` after {@link LEASE_CAS_ATTEMPTS} lost
117
+ * races and the registry's `TransientError` for a write that stayed unanswered over an unchanged row.
118
+ */
119
+ export declare function releaseLease(ref: SegmentRef, deps: LeaseDeps, holder: string): Promise<void>;
@@ -57,8 +57,21 @@ export declare class ChunkLoadInput {
57
57
  * Check a load's input and decode a bitmap input, before the load makes any request.
58
58
  *
59
59
  * @returns ids unchanged, to be consumed lazily by the write, or the decoded bitmap.
60
+ * @param subject names what takes the input in a refusal's message.
60
61
  * @throws {ValidationError} for a byte array passed as ids; bytes that are malformed, over the cap, or followed by
61
62
  * more bytes; a `{ bitmap }` with no `serialize` method, or whose `serialize('portable')` returns no `Uint8Array`;
62
63
  * and anything that is none of the three inputs. An error the caller's own `serialize` throws propagates as it is.
63
64
  */
64
- export declare function prepareLoadInput(input: LoadInput, codec: CodecInterface): Iterable<number> | AsyncIterable<number> | DecodedLoadInput;
65
+ export declare function prepareLoadInput(input: LoadInput, codec: CodecInterface, subject?: string): Iterable<number> | AsyncIterable<number> | DecodedLoadInput;
66
+ /**
67
+ * Portable bytes a caller holds, checked and decoded by the codec's safe deserializer: the one check every bitmap
68
+ * input of a load goes through, and what a flavor offers its users to decode bytes they hold themselves.
69
+ *
70
+ * The bytes must be a `Uint8Array` (a Node `Buffer` is one), at most {@link MAX_SERIALIZED_LOAD_BYTES}, one whole
71
+ * bitmap and structurally sound. A view is read over the bytes it really holds, and bytes in a `SharedArrayBuffer`
72
+ * are copied first. An empty or detached buffer is the empty bitmap.
73
+ *
74
+ * @param what names the input in a refusal's message.
75
+ * @throws {ValidationError} for anything that is not such bytes; nothing reaches the codec's native decoder then.
76
+ */
77
+ export declare function decodeSerialized(bytes: unknown, codec: CodecInterface, what?: string): CodecBitmap;
@@ -47,10 +47,10 @@ export interface LoadDeps {
47
47
  */
48
48
  readonly readRetry?: ReadRetry;
49
49
  /**
50
- * Collect by listing the segment's objects after the publish, whatever `keep` is. Absent, a load that keeps at
51
- * most one generation and found nothing above its pointer deletes by name the one generation its publish pushed
52
- * out of the window, and lists every sixteenth generation to take what that leaves; the generations older than
53
- * the window wait for that listing. Set it for a caller whose `keep` promises every generation below the new
50
+ * Collect by listing the segment's objects after the publish, whatever `keep` is. Absent, a load whose row records
51
+ * the generations it keeps and that found nothing above its pointer deletes by name the generations its publish
52
+ * pushed out of the window, and lists every sixteenth generation to take what that leaves; the generations no list
53
+ * names wait for that listing. Set it for a caller whose `keep` promises every generation below the new
54
54
  * one beyond the window is gone when the call returns: the `*Into` verbs, whose `keep` is how an operator clears
55
55
  * a destination that earlier materialisations kept in full.
56
56
  */
@@ -95,12 +95,14 @@ export interface LoadOptions {
95
95
  * longest pinned job runs, on every writer that loads the segment — see "Generations and `keep`" in the loading
96
96
  * guide.
97
97
  *
98
- * With `keep` of 0 or 1, a load that found nothing above the pointer collects without listing: it deletes the one
99
- * generation its publish pushed out of the window, and lists the segment's objects on every sixteenth generation to
100
- * take whatever that pass leaves, such as the generations an earlier, wider `keep` held. With `keep` of 2 or more
101
- * it lists on every load, and so does one whose check found the current generation's object gone: the guard's read of
102
- * it, or, when the guard took the size from the row's summary, one zero-byte read made before a `keep` of 1 takes a
103
- * name. A `keep` at least the generation published collects nothing and asks for nothing.
98
+ * The segment's row records the generations a load keeps, up to 64, so a load that found nothing above the pointer
99
+ * collects without listing: it deletes the generations its publish pushed out of the window, and lists the segment's
100
+ * objects on every sixteenth generation to take whatever that pass leaves, such as an object a crashed load left
101
+ * below the pointer. A `keep` above 64 records no list and lists on every load, and so does the first load of a
102
+ * row that records none, which then records it; so does a load whose check found the current generation's object
103
+ * gone: the guard's read of it, or, when the guard took the size from the row's summary, one zero-byte read made
104
+ * before a `keep` of 1 or more takes a name. A `keep` at least the generation published collects nothing and asks
105
+ * for nothing.
104
106
  */
105
107
  readonly keep?: number;
106
108
  /**
@@ -154,7 +156,7 @@ export interface LoadResult {
154
156
  /**
155
157
  * The superseded generations collected after publishing. Empty when nothing was published.
156
158
  *
157
- * When collection deleted by name (see {@link LoadOptions.keep}) this is the name it deleted, and that generation
159
+ * When collection deleted by name (see {@link LoadOptions.keep}) these are the names it deleted, and one
158
160
  * may have been gone already: a delete of an absent object succeeds on every backend and does not say so, so the
159
161
  * list names what the pass asked the bucket to delete, not what it found there. A listing pass names the
160
162
  * generations it found and deleted. Neither is a receipt, since a concurrent collector may have taken a generation
@@ -92,6 +92,30 @@ export type MetricEvent = {
92
92
  readonly kind: 'op';
93
93
  readonly name: MetricOpName;
94
94
  readonly ms: number;
95
+ } | {
96
+ /**
97
+ * A once-only note that a setting looks too small for how the store is used. It is not a fault: nothing failed.
98
+ * Emitted by a backend that can read the setting, never on a request's path after the first, and silent for
99
+ * anything the backend cannot read, so no event does not mean the setting is large enough.
100
+ *
101
+ * `'socket-pool-below-window'`: the S3 client's pool is smaller than `threshold` sockets, twice `concurrency`.
102
+ * `concurrency` is the default window of a combine (32), since a combine's own `concurrency` is chosen per call
103
+ * and the backend cannot see it: a store that always passes a lower one can ignore the event. `maxSockets` is
104
+ * the `maxSockets` of the agent for the scheme of the client's endpoint, or, when the client does not expose an
105
+ * endpoint, the larger of the agents its handler has made (the event fires only when all are below `threshold`).
106
+ * `driver` and `bucket` say which backend the pool
107
+ * belongs to; they never carry a credential, an endpoint or a key.
108
+ *
109
+ * A sink that switches on `kind` with an exhaustive `never` check stops compiling at this variant: add a case
110
+ * for `'advisory'` (ignoring it is fine). A sink with a `default` branch needs no change.
111
+ */
112
+ readonly kind: 'advisory';
113
+ readonly code: 'socket-pool-below-window';
114
+ readonly driver: string;
115
+ readonly bucket: string;
116
+ readonly maxSockets: number;
117
+ readonly threshold: number;
118
+ readonly concurrency: number;
95
119
  };
96
120
  /**
97
121
  * The sink you plug in. One method, synchronous, fire-and-forget. It must never throw back into the
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import type { BlobSink } from './blob.js';
11
11
  import type { WrappedDek } from './crypto.js';
12
+ import type { IMetricsSink } from './metrics.js';
12
13
  /** Opaque optimistic-concurrency token — unique per write, compared by equality only. */
13
14
  export type Token = string;
14
15
  /**
@@ -334,6 +335,20 @@ export interface SealedRegistrySummary {
334
335
  */
335
336
  readonly sealed: string;
336
337
  }
338
+ /**
339
+ * One lease on a generation. It names no fingerprint: a collector spares a generation by number and cannot compare an
340
+ * object without reading it, and a purge and re-create drops the whole list with the row. A pin's own fingerprint,
341
+ * which its reads check, stops a number retaken after a rollback being read as the leased object; the entry then spares a
342
+ * different object from collection until it ends, at most the longest lease.
343
+ */
344
+ export interface LeaseEntry {
345
+ /** 16 lowercase hex digits drawn once per pin call; names the holder, so a retried write is idempotent. */
346
+ readonly holder: string;
347
+ /** The generation held. */
348
+ readonly generation: number;
349
+ /** Epoch-ms the lease runs to on the holder's clock; a collector holds it for the skew margin longer. */
350
+ readonly until: number;
351
+ }
337
352
  /**
338
353
  * One registry row — the authoritative per-segment record. Exactly one per segment.
339
354
  */
@@ -376,6 +391,21 @@ export interface RegistryRecord extends SegmentRef {
376
391
  * correct, and a reader then opens the generation instead. Trusted only for the generation it names.
377
392
  */
378
393
  readonly summary?: RegistrySummary;
394
+ /**
395
+ * The generations below `currentGen` that the last publish kept as its grace window, ascending. Optional: absent
396
+ * means the row does not know which generations are kept, and a load then lists the bucket to collect and records
397
+ * them; an empty list means none are. Trusted only while every entry is below `currentGen`. A patch that moves
398
+ * `currentGen` without naming it drops it.
399
+ */
400
+ readonly keptGens?: readonly number[];
401
+ /**
402
+ * The leases on this segment's generations: each keeps one generation out of a load's collection until it ends.
403
+ * Optional; absent means none. Unlike {@link keptGens} it does not follow the pointer: a patch that moves
404
+ * `currentGen` leaves it as it was, and only a patch that names it, or the row going away, changes it. Entries
405
+ * past their end are ignored by every collector and pruned by the next write that names the list. A row's identity
406
+ * is its token, so a purged and re-created segment starts with none.
407
+ */
408
+ readonly leases?: readonly LeaseEntry[];
379
409
  /** Epoch-ms of creation / last mutation (from the driver's injected clock). */
380
410
  readonly createdAt: number;
381
411
  readonly updatedAt: number;
@@ -402,6 +432,8 @@ export interface NewRegistryRecord {
402
432
  readonly residency?: GovernanceMeta;
403
433
  /** Must name `currentGen` when given. */
404
434
  readonly summary?: RegistrySummary;
435
+ /** Every entry below `currentGen`, ascending; see {@link RegistryRecord.keptGens}. */
436
+ readonly keptGens?: readonly number[];
405
437
  }
406
438
  /**
407
439
  * Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity + audit + token are off-limits).
@@ -409,9 +441,10 @@ export interface NewRegistryRecord {
409
441
  * Presence-based: a field the patch does not mention is left as it was. `summary` is the one exception, because it
410
442
  * describes the current generation: a patch that moves `currentGen` and does not mention `summary` drops the old
411
443
  * one rather than keep a description of another generation. A `summary` the patch gives must name the
412
- * `currentGen` the row will have.
444
+ * `currentGen` the row will have. `keptGens` follows the pointer the same way: a patch that moves `currentGen` and does
445
+ * not mention it drops it, and one that gives it must name only generations below the `currentGen` the row will have.
413
446
  */
414
- export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency' | 'summary'>>;
447
+ export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency' | 'summary' | 'keptGens' | 'leases'>>;
415
448
  /**
416
449
  * What a caller passes {@link IRegistryDriver.create} or {@link IRegistryDriver.compareAndSwap} besides the write itself.
417
450
  */
@@ -453,6 +486,47 @@ export interface RegCaps {
453
486
  */
454
487
  readonly conditionalDelete?: boolean;
455
488
  }
489
+ /** What {@link IRegistryDriver.reapLegacyTombstones} is asked to do. */
490
+ export interface ReapLegacyTombstonesOptions {
491
+ /** Only rows in this namespace; every namespace, the library's bookkeeping namespaces included, when absent. */
492
+ readonly namespace?: string;
493
+ /** Count what would be removed and remove nothing. */
494
+ readonly dryRun: boolean;
495
+ /** The most rows to remove (or, in a dry run, to count as removable) in this call. */
496
+ readonly limit: number;
497
+ }
498
+ /** What a reap of the tombstones a registry holds found. Every count is of rows, not requests. */
499
+ export interface ReapLegacyTombstonesResult {
500
+ /** The call was a dry run: nothing was removed, and `wouldReap` says what a real run removes. */
501
+ readonly dryRun: boolean;
502
+ /** Rows read. Rows the `limit` stopped the run before reading are not counted. */
503
+ readonly examined: number;
504
+ /** Rows removed. Always 0 in a dry run. */
505
+ readonly reaped: number;
506
+ /** Rows a dry run found removable. Always 0 in a real run. */
507
+ readonly wouldReap: number;
508
+ /**
509
+ * The `limit` was spent and the listing had more keys: rows were left unread, and they may or may not be removable,
510
+ * so run again. A run is not resumable: each lists and reads from the start, so a dry run with a `limit` reports the
511
+ * same first rows every time, and `wouldReap` is a total only when this is `false`.
512
+ */
513
+ readonly limited: boolean;
514
+ /** Rows read and left, by why. */
515
+ readonly skipped: {
516
+ /** A live (`active`, not deleted) row. */
517
+ readonly live: number;
518
+ /** A `destroyed` row that is not `deleted`: a crypto-shred's or a drop's tombstone, kept as the attestation of an erasure. */
519
+ readonly destroyed: number;
520
+ /**
521
+ * A deleted row that carries an incarnation id, written by a 0.12 or later release. On a registry that removes rows,
522
+ * `delete` and the sweep remove such a row themselves; one left while the registry could not remove rows (GCS by
523
+ * default, S3 on a custom endpoint) stays, and this call, which refuses on such a registry, does not remove it.
524
+ */
525
+ readonly incarnated: number;
526
+ /** A row that changed, or was already gone (a 404), between its read and its delete, so the delete (fenced on the version read) refused. */
527
+ readonly raced: number;
528
+ };
529
+ }
456
530
  /**
457
531
  * Refuse, with `UnsupportedError`, a write that would write an object before its row when the registry reports it
458
532
  * cannot write a row (see {@link RegCaps.canWrite}). Called before the write's first request.
@@ -496,6 +570,14 @@ export interface StorageBackend {
496
570
  readonly storage: IStorageDriver;
497
571
  /** Where the `currentGen` pointer, the discovery index and the wrapped DEKs live. */
498
572
  readonly registry: IRegistryDriver;
573
+ /**
574
+ * Optional. A store with a metrics sink calls this once, as it is built, with that sink (already safe to call: it
575
+ * never throws back). A backend that has something to say about its own setup, such as a pool it can read and finds
576
+ * small, keeps the sink and emits an `advisory` event to it later. A backend without it is simply never told. One backend
577
+ * may be handed to several stores, and is called once by each, with each store's own sink. It must not throw: an
578
+ * exception propagates from the store's constructor, so a backend's bug fails fast rather than going unseen.
579
+ */
580
+ attachMetrics?(sink: IMetricsSink): void;
499
581
  }
500
582
  /**
501
583
  * The brand key, for the type-only `declare` field a backend class carries.
@@ -619,5 +701,20 @@ export interface IRegistryDriver {
619
701
  * created after the decision. A third-party driver that ignores `expected` keeps the unfenced behaviour.
620
702
  */
621
703
  delete(ref: SegmentRef, expected?: Token): Promise<void>;
704
+ /**
705
+ * Optional: remove the `deleted: true` rows with no incarnation id, which a release before 0.12 left, or a 0.12 or
706
+ * later release left by deleting a row born before 0.12, so that no later full `list` reads them. Only a registry that
707
+ * keeps such rows in its backend implements it; the object registries do.
708
+ *
709
+ * It removes a row only if it is `deleted: true` with no incarnation id (whatever its `status`), and only by a delete
710
+ * the backend applies under the version it read (see {@link RegCaps.conditionalDelete}), so a `create` that lands over
711
+ * the row first wins. A registry that cannot delete under that precondition throws `CapabilityError` before its first
712
+ * request. It never touches a live row, a `destroyed` row that is not `deleted`, or a deleted row with an incarnation
713
+ * id, so it does not remove every row a full `list` reads. If `conditionalDelete` was set on a backend that ignores the
714
+ * precondition, the delete is unfenced.
715
+ *
716
+ * Not resumable: every call lists and reads from the start (R reads for R rows, whatever the `limit`).
717
+ */
718
+ reapLegacyTombstones?(options: ReapLegacyTombstonesOptions): Promise<ReapLegacyTombstonesResult>;
622
719
  }
623
720
  export {};
@@ -0,0 +1,22 @@
1
+ import type { IRegistryDriver, ReapLegacyTombstonesResult } from './ports.js';
2
+ /** Rows one call removes when no `limit` says otherwise. */
3
+ export declare const DEFAULT_REAP_LIMIT = 1000;
4
+ /** The keys `reapRegistryTombstones` takes. */
5
+ export interface ReapRegistryTombstonesOptions {
6
+ /** Only rows in this namespace. Omit to cover every namespace, the library's own bookkeeping rows included. */
7
+ readonly namespace?: string;
8
+ /** Count what a real run would remove, and remove nothing. Needs no `confirmNoLegacyWriters`. */
9
+ readonly dryRun?: boolean;
10
+ /** Required `true` for a real run: no process on a release before 0.12 writes this registry. */
11
+ readonly confirmNoLegacyWriters?: boolean;
12
+ /** The most rows to remove in this call (default {@link DEFAULT_REAP_LIMIT}); a run that stops on it says `limited`. */
13
+ readonly limit?: number;
14
+ }
15
+ /** What a reap found and did: see {@link ReapLegacyTombstonesResult}. */
16
+ export type ReapRegistryTombstonesResult = ReapLegacyTombstonesResult;
17
+ /**
18
+ * Remove the `deleted: true` rows with no incarnation id from `registry`, or count them (`dryRun`).
19
+ *
20
+ * Cost over R rows read and E removed: `ceil(R / 1000)` LIST requests, R GETs and E DELETEs.
21
+ */
22
+ export declare function reapRegistryTombstones(registry: IRegistryDriver, options?: ReapRegistryTombstonesOptions): Promise<ReapRegistryTombstonesResult>;
@@ -1,3 +1,4 @@
1
+ import { type ChurnDeps } from './leases.js';
1
2
  import type { GovernanceMeta, IRegistryDriver, SegmentRef } from './ports.js';
2
3
  /**
3
4
  * The smallest accepted `expiresAt`: 2001-09-09, the point below which an epoch-**seconds** value would land.
@@ -18,7 +19,7 @@ export interface RetentionPolicy {
18
19
  */
19
20
  readonly expiresAt: number;
20
21
  }
21
- export interface RetentionDeps {
22
+ export interface RetentionDeps extends ChurnDeps {
22
23
  readonly registry: IRegistryDriver;
23
24
  }
24
25
  export interface SetRetentionResult {
@@ -18,9 +18,10 @@
18
18
  */
19
19
  import { type IAuditSink } from './audit.js';
20
20
  import type { IKeystore } from './crypto.js';
21
+ import { type ChurnDeps } from './leases.js';
21
22
  import type { IStorageDriver, GenerationMetadata, IRegistryDriver, SegmentRef } from './ports.js';
22
23
  /** What the generation helpers need: the objects, and the pointer that says which one is current. */
23
- export interface GenerationListDeps {
24
+ export interface GenerationListDeps extends ChurnDeps {
24
25
  readonly storage: IStorageDriver;
25
26
  readonly registry: IRegistryDriver;
26
27
  /**
@@ -1,5 +1,6 @@
1
1
  export type { GenKey, IRegistryDriver, IStorageDriver, SegmentRef, StorageBackend, StorageCaps, } from './core/ports.js';
2
2
  export { brandAsBackend, STORAGE_BACKEND } from './core/ports.js';
3
+ export type { IMetricsSink, MetricEvent } from './core/metrics.js';
3
4
  export { IntegrityError, isNotFoundError, isValidationError, isWriteConflictError, NotFoundError, TransientError, ValidationError, WriteConflictError, } from './core/errors.js';
4
5
  export { validateSegmentRef } from './core/validate.js';
5
6
  export type { BlobSink } from './core/blob.js';
@@ -1,4 +1,5 @@
1
1
  import {
2
+ CapabilityError,
2
3
  DEFAULT_NAMESPACE,
3
4
  IntegrityError,
4
5
  NotFoundError,
@@ -28,7 +29,7 @@ import {
28
29
  validateRegistryPatch,
29
30
  validateSegmentRef,
30
31
  webCryptoEntropy
31
- } from "./chunk-LFUNPMXZ.js";
32
+ } from "./chunk-CCP4U32U.js";
32
33
 
33
34
  // src/drivers/_shared/object-registry-keys.ts
34
35
  var REGISTRY_SUFFIX = ".reg";
@@ -253,6 +254,104 @@ var ObjectStoreRegistry = class {
253
254
  }
254
255
  throw new WriteConflictError(`registry delete: contention deleting "${ref.segment}" \u2014 retry`);
255
256
  }
257
+ /**
258
+ * Remove the `deleted: true` rows with no incarnation id (the envelopes a release before 0.12 left), or count them.
259
+ * One listing, a read per key and a conditional delete per envelope, so a row that a create wrote over since it was
260
+ * read keeps its new version and the delete is refused. Refuses with {@link CapabilityError}, before any request,
261
+ * where the store's delete is not known to apply its precondition.
262
+ *
263
+ * An object that cannot be read or parsed stops the run with the error that names its key, and removes nothing
264
+ * more: it may be a newer release's row. Rows removed before the stop stay removed, and a re-run is safe.
265
+ *
266
+ * Not resumable: it lists and reads from the start on every call. A delete refused as a conflict, including one for a
267
+ * row already gone, is counted as `raced`.
268
+ */
269
+ async reapLegacyTombstones(options) {
270
+ await this.store.resolveCapabilities?.();
271
+ const remove = this.removesRows() ? this.store.delete : void 0;
272
+ if (remove === void 0) {
273
+ throw new CapabilityError(
274
+ `reapRegistryTombstones: this registry's delete is not conditional (capabilities().conditionalDelete is false), so a removal could take a row a create has just written over the envelope. Nothing was requested`
275
+ );
276
+ }
277
+ const { dryRun, limit } = options;
278
+ const counts = {
279
+ examined: 0,
280
+ reaped: 0,
281
+ wouldReap: 0,
282
+ live: 0,
283
+ destroyed: 0,
284
+ incarnated: 0,
285
+ raced: 0
286
+ };
287
+ let limited = false;
288
+ const removable = () => counts.reaped + counts.wouldReap;
289
+ const handle = async (keys) => {
290
+ const rows = await mapWithConcurrency(
291
+ keys,
292
+ LIST_READ_CONCURRENCY,
293
+ (key) => this.readRow(key).then((row) => ({ key, row }))
294
+ );
295
+ const found = [];
296
+ for (const { key, row } of rows) {
297
+ if (row === null) continue;
298
+ counts.examined += 1;
299
+ const { deleted, record } = row.env;
300
+ if (!deleted) {
301
+ if (record.status === "destroyed") counts.destroyed += 1;
302
+ else counts.live += 1;
303
+ } else if (incarnationOf(record.token) !== void 0) {
304
+ counts.incarnated += 1;
305
+ } else if (removable() + found.length >= limit) {
306
+ limited = true;
307
+ } else {
308
+ found.push({ key, version: row.version });
309
+ }
310
+ }
311
+ if (dryRun) {
312
+ counts.wouldReap += found.length;
313
+ return;
314
+ }
315
+ await mapWithConcurrency(found, LIST_READ_CONCURRENCY, async ({ key, version }) => {
316
+ try {
317
+ await remove.call(this.store, key, { version });
318
+ counts.reaped += 1;
319
+ } catch (err) {
320
+ if (!isWriteConflictError(err)) throw err;
321
+ counts.raced += 1;
322
+ }
323
+ });
324
+ };
325
+ let page = [];
326
+ for await (const key of this.store.listKeys(
327
+ registryListPrefix(this.prefix, options.namespace)
328
+ )) {
329
+ if (parseRegistryKey(this.prefix, key) === null) continue;
330
+ if (removable() >= limit) {
331
+ limited = true;
332
+ break;
333
+ }
334
+ page.push(key);
335
+ if (page.length >= LIST_READ_CONCURRENCY) {
336
+ await handle(page);
337
+ page = [];
338
+ }
339
+ }
340
+ if (page.length > 0) await handle(page);
341
+ return {
342
+ dryRun,
343
+ examined: counts.examined,
344
+ reaped: counts.reaped,
345
+ wouldReap: counts.wouldReap,
346
+ limited,
347
+ skipped: {
348
+ live: counts.live,
349
+ destroyed: counts.destroyed,
350
+ incarnated: counts.incarnated,
351
+ raced: counts.raced
352
+ }
353
+ };
354
+ }
256
355
  /**
257
356
  * Read + parse a registry object, returning its envelope and version fence, or null if absent.
258
357
  *