@cloudbitmaps/core 0.20.0 → 0.21.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.
Files changed (75) hide show
  1. package/README.md +85 -0
  2. package/dist/{chunk-YSC7NJWV.js → chunk-IGLIZBR7.js} +740 -137
  3. package/dist/chunk-IGLIZBR7.js.map +7 -0
  4. package/dist/core/absent-operands.d.ts +8 -0
  5. package/dist/core/audit.d.ts +12 -2
  6. package/dist/core/bit-route.d.ts +2 -1
  7. package/dist/core/blob.d.ts +12 -1
  8. package/dist/core/bulk-load.d.ts +148 -0
  9. package/dist/core/cas-retry.d.ts +27 -0
  10. package/dist/core/check.d.ts +22 -0
  11. package/dist/core/chunk-checks.d.ts +6 -1
  12. package/dist/core/chunk-window.d.ts +11 -4
  13. package/dist/core/combine-expr.d.ts +1 -3
  14. package/dist/core/combine-feed.d.ts +2 -16
  15. package/dist/core/combine-many.d.ts +12 -9
  16. package/dist/core/concurrency.d.ts +8 -0
  17. package/dist/core/consistency.d.ts +69 -17
  18. package/dist/core/crbm/fingerprint.d.ts +5 -0
  19. package/dist/core/crbm/format.d.ts +0 -1
  20. package/dist/core/crbm/plan-reads.d.ts +9 -3
  21. package/dist/core/crbm/reader.d.ts +52 -6
  22. package/dist/core/crbm/varint.d.ts +2 -1
  23. package/dist/core/crbm/writer.d.ts +5 -1
  24. package/dist/core/crbm-open.d.ts +154 -0
  25. package/dist/core/crbm-storage-source.d.ts +65 -452
  26. package/dist/core/crbm-write.d.ts +75 -0
  27. package/dist/core/crypto.d.ts +6 -1
  28. package/dist/core/engine.d.ts +222 -33
  29. package/dist/core/erase-id.d.ts +42 -7
  30. package/dist/core/erasure.d.ts +4 -3
  31. package/dist/core/errors.d.ts +8 -2
  32. package/dist/core/generation-gc.d.ts +129 -15
  33. package/dist/core/generation-list.d.ts +11 -0
  34. package/dist/core/id-window.d.ts +46 -0
  35. package/dist/core/invalidation-scope.d.ts +77 -0
  36. package/dist/core/known-generations-shape.d.ts +13 -0
  37. package/dist/core/known-generations.d.ts +147 -0
  38. package/dist/core/leases.d.ts +4 -3
  39. package/dist/core/ledger-seed.d.ts +17 -0
  40. package/dist/core/load.d.ts +17 -38
  41. package/dist/core/lru.d.ts +2 -15
  42. package/dist/core/metrics.d.ts +4 -2
  43. package/dist/core/order.d.ts +2 -0
  44. package/dist/core/pinned-storage-source.d.ts +4 -4
  45. package/dist/core/ports.d.ts +89 -16
  46. package/dist/core/publish.d.ts +207 -0
  47. package/dist/core/registry-scan.d.ts +0 -2
  48. package/dist/core/retention-sweep.d.ts +69 -65
  49. package/dist/core/retention.d.ts +1 -1
  50. package/dist/core/retry.d.ts +12 -19
  51. package/dist/core/rollback.d.ts +60 -31
  52. package/dist/core/summary.d.ts +50 -18
  53. package/dist/core/token.d.ts +2 -2
  54. package/dist/core/validate.d.ts +2 -0
  55. package/dist/driver-kit.d.ts +4 -0
  56. package/dist/driver-kit.js +80 -7
  57. package/dist/driver-kit.js.map +3 -3
  58. package/dist/drivers/_shared/bytes.d.ts +6 -0
  59. package/dist/drivers/_shared/faults.d.ts +15 -0
  60. package/dist/drivers/_shared/object-keys.d.ts +18 -0
  61. package/dist/drivers/_shared/object-registry-keys.d.ts +0 -2
  62. package/dist/drivers/_shared/object-registry.d.ts +2 -0
  63. package/dist/drivers/_shared/options.d.ts +19 -0
  64. package/dist/drivers/_shared/registry.d.ts +13 -10
  65. package/dist/drivers/localfs/exact-case.d.ts +0 -3
  66. package/dist/drivers/localfs/fs-util.d.ts +22 -0
  67. package/dist/drivers/localfs/registry.d.ts +10 -4
  68. package/dist/drivers/localfs/storage.d.ts +9 -2
  69. package/dist/drivers/memory.d.ts +7 -4
  70. package/dist/index.d.ts +1 -1
  71. package/dist/index.js +3647 -2410
  72. package/dist/index.js.map +4 -4
  73. package/package.json +1 -1
  74. package/dist/chunk-YSC7NJWV.js.map +0 -7
  75. package/dist/core/kept-generations.d.ts +0 -40
@@ -1,69 +1,7 @@
1
- /**
2
- * The retention **sweep** — the thing that acts on the policies `setSegmentRetention` records.
3
- *
4
- * `retireExpired` enumerates the registry, selects the segments whose `expiresAt` has passed, and retires each
5
- * one through {@link dropSegment}. It deliberately **delegates rather than reimplements**: the registry → Storage
6
- * ordering, the re-sweep for an object a load was still writing, and the `generationsRemaining` report are all
7
- * load-bearing and already live there. A sweep that open-coded the deletions would be a second
8
- * implementation of the most dangerous ordering in the library.
9
- *
10
- * **This is a call, not a daemon.** Nothing here schedules itself. You run it from whatever heartbeat your
11
- * deployment already has — an EventBridge rule, a Kubernetes CronJob, a queue consumer, the job that runs your
12
- * loads — and the library stays a library: it has to behave identically in a Lambda, an edge isolate and a
13
- * long-lived server, and a timer that only works in one of those is worse than none.
14
- *
15
- * Three properties make it safe to point at a fleet:
16
- *
17
- * - **`dryRun` at the sweep level.** `dropSegment`'s `confirmSegment` guard is vacuous in a loop (it is the same
18
- * variable twice), so the meaningful preview is here: it reports what it *would* retire, per segment, and
19
- * touches nothing.
20
- * - **A per-cycle `limit`.** Clock skew, a bad backfill, or a namespace-wide policy mistake should cost one
21
- * bounded batch, not the fleet. When the limit bites, the result says so (`limited: true`) rather than looking
22
- * like a completed sweep.
23
- * - **A ledger, never an exception.** A per-segment fault is recorded and the sweep continues — the caller's
24
- * load-bearing question is *which* segments were retired, and a throw from the middle of the loop answers it
25
- * for none of them while having already retired some. Same posture as `eraseNamespace`.
26
- *
27
- * It also **purges the tombstones its own retirements leave behind**. A retired segment that had a registry row
28
- * gets a `destroyed` row, and one dead row per retired daily bucket — or per retired dedup wave — is exactly the
29
- * registry litter `dropSegment` already refuses to create for a row-less accumulator. Purging is narrow on
30
- * purpose: only a tombstone carrying the **stamp this sweep writes on its own retirements** (so it is attributably
31
- * ours: a crypto-shred's tombstone does not carry it, unless the shred lands between this sweep's re-read of the
32
- * row and its drop, which then reports the tombstone dropped and stamps it), only after a grace period, and only
33
- * once Storage is provably empty for it —
34
- * because deleting the row is what makes the name reusable and takes the segment out of reach of
35
- * the generation collection.
36
- *
37
- * **On a registry that reports `conditionalDelete`, the purge removes the row for good**, so a full scan stops paying a
38
- * read for every name a namespace ever held: the scan costs what is live and what is inside its grace. The delete is
39
- * fenced on the token the purge judged and applied by the store only to that version of the row. Each retirement
40
- * files a pointer in the due index under the day its tombstone's grace ends, so `scan: 'index'` purges as well as
41
- * retires; nothing on the row records that day, which the purge derives from the stamp. The purge removes every pointer
42
- * it read to the row, and a pointer that outlives its row anyway (a purge that ran with another grace, a delete that
43
- * landed and lost its response, a pointer older than an index scan's lookback) is removed by the next scan that reads
44
- * it: an index scan in its buckets, and an unscoped fleet scan, which reads every pointer, in all of them. Where a
45
- * delete only rewrites the row as a tombstone, none of this is done: nothing is removed for good, so a pointer would
46
- * only add a row for every scan to read.
47
- */
48
1
  import { type IAuditSink } from './audit.js';
49
2
  import type { DropDeps, DropResult } from './erasure.js';
50
3
  /** Default cap on retirements per sweep — a bounded batch, so a policy mistake costs one batch, not the fleet. */
51
4
  export declare const DEFAULT_RETIRE_LIMIT = 100;
52
- /**
53
- * How many purges in a row the registry may refuse in one call before the sweep stops purging for the rest of it. A
54
- * blanket refusal (a policy that denies delete) costs this many attempts and no more; a refusal particular to one row
55
- * (a blob with a snapshot, an object under a legal hold) does not hold the purges behind it, since a purge that succeeds
56
- * starts the count again.
57
- */
58
- export declare const MAX_CONSECUTIVE_PURGE_FAULTS = 3;
59
- /**
60
- * Default delay before a retirement's own tombstone row is purged: 24 h.
61
- *
62
- * The row is a fence — while it exists, a load refuses the segment, so
63
- * a load that was mid-write when the drop landed cannot resurrect it. That window is
64
- * seconds to minutes in practice; a day of margin costs one tiny row and removes any need to reason about it.
65
- */
66
- export declare const DEFAULT_TOMBSTONE_GRACE_MS = 86400000;
67
5
  export interface RetireExpiredOptions {
68
6
  /** Scope the sweep to one namespace. Omit to sweep every namespace the registry knows. */
69
7
  readonly namespace?: string;
@@ -74,8 +12,9 @@ export interface RetireExpiredOptions {
74
12
  readonly now: number;
75
13
  /**
76
14
  * Maximum segments to retire in this cycle (default 100). It also bounds the purges attempted (a purge the registry
77
- * refuses is not charged; {@link RetireExpiredResult.purgeFaults} says how purging stops after refusals) and the
78
- * due-index pointers to nothing one call removes.
15
+ * refuses is not charged; {@link RetireExpiredResult.purgeFaults} says how purging stops after refusals), the
16
+ * due-index pointers to nothing one call removes, and, counted apart from the retirements, the tombstones this sweep
17
+ * did not write whose pointers one call removes.
79
18
  */
80
19
  readonly limit?: number;
81
20
  /** Report what would be retired and change nothing. */
@@ -209,7 +148,10 @@ export interface RetireExpiredResult {
209
148
  readonly wouldRetire: number;
210
149
  /** Tombstone rows actually deleted. Zero under `dryRun`; the `would-purge-tombstone` entries carry the preview. */
211
150
  readonly tombstonesPurged: number;
212
- /** True when `limit` cut the cycle short — **more segments are still eligible**. Re-run. */
151
+ /**
152
+ * True when `limit` cut the cycle short — **more segments are still eligible**. Re-run. The cap on removing pointers
153
+ * of tombstones this sweep did not write leaves it false.
154
+ */
213
155
  readonly limited: boolean;
214
156
  /**
215
157
  * Deletes this sweep attempted that were **refused for a reason other than a lost race**: a tombstone's purge
@@ -238,6 +180,67 @@ export interface RetireExpiredResult {
238
180
  * or a paused schedule recovers on its own, short enough that a cycle is eight list calls rather than hundreds.
239
181
  */
240
182
  export declare const DEFAULT_LOOKBACK_BUCKETS = 7;
183
+ /** The options of a sweep, validated, with every default filled in. */
184
+ interface ResolvedRetireOptions {
185
+ readonly audit: IAuditSink | undefined;
186
+ readonly namespace: string | undefined;
187
+ readonly now: number;
188
+ readonly limit: number;
189
+ readonly purgeTombstones: boolean;
190
+ readonly grace: number;
191
+ readonly maxScanSegments: number;
192
+ readonly dryRun: boolean;
193
+ readonly scan: 'fleet' | 'index';
194
+ readonly lookbackBuckets: number;
195
+ readonly shards: readonly number[] | undefined;
196
+ readonly totalShards: number;
197
+ }
198
+ /**
199
+ * Check a sweep's options and fill in the defaults. Pure: it reads nothing but `options`, and throws
200
+ * `ValidationError` for the first bad one, in the order the checks below run.
201
+ */
202
+ export declare function resolveRetireOptions(options: RetireExpiredOptions): ResolvedRetireOptions;
203
+ /**
204
+ * The counters and the ledger of one sweep, and the two budgets that bound it.
205
+ *
206
+ * The budget is charged on ATTEMPT, not on success, and that distinction is the whole guard. `dropSegment`
207
+ * writes the tombstone BEFORE sweeping Storage, so a fault in the Storage phase is a segment that is
208
+ * already retired. Counting only successes would let a partial storage outage march through the entire fleet
209
+ * with the cap never engaging, reporting `retired: 0, limited: false` (a "completed sweep that retired nothing")
210
+ * while every segment in the namespace is tombstoned. So a step charges `attempted` before the call that can delete
211
+ * (the drop, or the purge's collection), after any re-read that finds nothing due, and gives the charge back in two
212
+ * places only: a purge the registry or storage refused (`refusedPurge`), and a drop that wrote and deleted nothing
213
+ * because the row was no longer the sweep's to retire (`dropSegmentFor` returned `null`).
214
+ */
215
+ export declare class SweepTally {
216
+ readonly entries: RetireEntry[];
217
+ eligible: number;
218
+ retired: number;
219
+ wouldRetire: number;
220
+ tombstonesPurged: number;
221
+ limited: boolean;
222
+ /** Deletes the registry refused for a reason other than a lost race. */
223
+ purgeFaults: number;
224
+ /** The first one's reason. A lost race is the row or pointer changing under a fenced delete, which is the fence working. */
225
+ firstPurgeFault: `failed: ${string}` | undefined;
226
+ /** Retirements and tombstone purges begun, charged against `limit`. */
227
+ attempted: number;
228
+ /** Tombstones the sweep did not write whose pointers it worked, bounded by `limit` apart from `attempted`. */
229
+ attemptedForeign: number;
230
+ /**
231
+ * Purging stops after this many refused purges in a row, and a purge that succeeds starts the count again. Each
232
+ * refused attempt costs reads before it fails, and a blanket refusal would fail them all; one row's refusal must
233
+ * not hold the rest.
234
+ */
235
+ purgeRun: number;
236
+ purging: boolean;
237
+ /** Record a step's ledger entry; `null` adds nothing (a step with nothing to report, or one that recorded its own). */
238
+ add(entry: RetireEntry | null): void;
239
+ /** Count a delete the registry refused, unless it was a lost race. */
240
+ readonly noteFault: (err: unknown) => void;
241
+ /** A purge the registry refused is not charged to `limit`, and after enough in a row purging stops. */
242
+ refusedPurge(): void;
243
+ }
241
244
  /**
242
245
  * Retire every segment whose retention policy has expired, and clean up the tombstones earlier sweeps left.
243
246
  *
@@ -246,3 +249,4 @@ export declare const DEFAULT_LOOKBACK_BUCKETS = 7;
246
249
  * condition, not a partial result to be mistaken for a complete sweep.
247
250
  */
248
251
  export declare function retireExpired(deps: DropDeps, options: RetireExpiredOptions): Promise<RetireExpiredResult>;
252
+ export {};
@@ -1,4 +1,4 @@
1
- import { type ChurnDeps } from './leases.js';
1
+ import type { ChurnDeps } from './leases.js';
2
2
  import type { GovernanceMeta, IRegistryDriver, SegmentRef } from './ports.js';
3
3
  /**
4
4
  * The smallest accepted `expiresAt`: 2001-09-09, the point below which an epoch-**seconds** value would land.
@@ -1,21 +1,3 @@
1
- /**
2
- * `withRetry` — the one retry/backoff primitive.
3
- *
4
- * A pure, storage-agnostic helper: it knows nothing about HTTP, AWS, or any SDK. It retries an operation
5
- * while the operation keeps failing with a **retryable** error (by default {@link TransientError}), waiting
6
- * a bounded, jittered, exponentially-growing delay between attempts. Time and randomness are **injected**
7
- * (the determinism seam) — production wires a `setTimeout`-backed clock + a real RNG; the
8
- * tests wire virtual ones — so a backoff schedule is replayable from a seed and unit tests run with no
9
- * real sleeping.
10
- *
11
- * It deliberately does **not** retry deterministic failures ({@link WriteConflictError},
12
- * {@link ValidationError}, {@link IntegrityError}, {@link NotFoundError}, …): retrying those either can't
13
- * help or would be incorrect. A pointer conflict is retried by a *separate* loop inside
14
- * the publish, because each attempt must re-read the row and re-decide — a blind replay of the same
15
- * compare-and-swap would either fail again on a stale token or, worse, advance a pointer whose state has
16
- * changed underneath it. A write-once object collision is never replayed at all: the generation number is
17
- * taken, so the caller has to pick a new one.
18
- */
19
1
  import type { Clock, Rng } from './determinism.js';
20
2
  export interface RetryPolicy {
21
3
  /** Total attempts including the first (so `maxAttempts: 4` = 1 try + 3 retries). Must be ≥ 1. */
@@ -61,6 +43,15 @@ export interface RetryDeps {
61
43
  export declare function backoffDelayMs(policy: RetryPolicy, attempt: number): number;
62
44
  /** Apply the policy's jitter to a computed delay using the injected RNG. */
63
45
  export declare function applyJitter(policy: RetryPolicy, delayMs: number, rng: Rng): number;
46
+ /** A clock that can wait: the one member {@link jitteredWait} uses. */
47
+ interface Sleeper {
48
+ readonly sleep?: ((ms: number) => Promise<void>) | undefined;
49
+ }
50
+ /**
51
+ * Wait a uniformly spread share of `boundMs`, whole milliseconds, by the clock's `sleep`. With no `rng` it waits the
52
+ * whole bound; with no `sleep` it does not wait.
53
+ */
54
+ export declare function jitteredWait(clock: Sleeper | undefined, rng: Rng | undefined, boundMs: number): Promise<void>;
64
55
  /**
65
56
  * The store's read retry, as a write path takes it for the reads it makes along the way: a policy (the default when
66
57
  * absent) and what {@link withRetry} needs to run it.
@@ -78,7 +69,8 @@ export declare function retryRead<T>(op: () => Promise<T>, retry: ReadRetry | un
78
69
  /**
79
70
  * `policy`, checked when it is wired rather than at the first retry: a delay or factor that is not a finite number, a
80
71
  * negative delay, a factor below 1 or a jitter mode it does not know would make the backoff `NaN`, which a timer reads
81
- * as 1 ms, so bounded backoff silently became a hot retry loop.
72
+ * as 1 ms, so bounded backoff silently became a hot retry loop. `maxAttempts` is a whole number, and `maxDelayMs` is not
73
+ * below `baseDelayMs`, which it would otherwise cap every delay under.
82
74
  */
83
75
  export declare function checkedRetryPolicy(policy: RetryPolicy): RetryPolicy;
84
76
  /**
@@ -87,3 +79,4 @@ export declare function checkedRetryPolicy(policy: RetryPolicy): RetryPolicy;
87
79
  * the *operation's* error — never a wrapper — so callers keep their typed-error branching.
88
80
  */
89
81
  export declare function withRetry<T>(op: () => Promise<T>, policy: RetryPolicy, deps: RetryDeps): Promise<T>;
82
+ export {};
@@ -1,21 +1,3 @@
1
- /**
2
- * `listGenerations` and `rollbackSegment` — see what a segment has been, and put it back.
3
- *
4
- * Immutable generations mean the previous version of a segment is usually still sitting in the bucket: a load
5
- * that replaced it did not overwrite anything, it wrote a new object and moved a pointer. So recovering from a
6
- * bad load is, in principle, moving the pointer back, which no load, publish or sweep can do, because every other
7
- * write path in the library only ever advances the pointer, and refuses a move that would lower it.
8
- *
9
- * That refusal is right for a *writer*: a load whose ids came from upstream loses nothing by being out-raced, and
10
- * letting it regress the pointer would let a slow loader silently undo a fast one. It is wrong for an *operator*,
11
- * who has looked at the segment, decided the current generation is wrong, and knows which one they want. So
12
- * rollback is the one call that moves the pointer backwards, and it is deliberately not reachable from any
13
- * automatic path — no sweep, no retry, no reconciliation calls it.
14
- *
15
- * It is audited for the same reason. Every other pointer move can be reconstructed from "a load happened"; this
16
- * one is a human overriding the ordering rule the rest of the system relies on, which is exactly the event an
17
- * Art. 30 record or an incident review wants to find.
18
- */
19
1
  import { type IAuditSink } from './audit.js';
20
2
  import type { IKeystore } from './crypto.js';
21
3
  import { type ChurnDeps } from './leases.js';
@@ -26,8 +8,10 @@ export interface GenerationListDeps extends ChurnDeps {
26
8
  readonly registry: IRegistryDriver;
27
9
  /**
28
10
  * The key of an encrypted segment, so a rollback can read its target's count and metadata and seal them into the
29
- * row. Without it, a rollback of an encrypted segment still works and leaves the row with no summary of the
30
- * generation it moves to, which a reader then takes from the object.
11
+ * row. Without it, a rollback of an encrypted segment onto a generation the row knows still works and leaves the
12
+ * row with no summary of the generation it moves to, which a reader then takes from the object; `allowUnknown` needs
13
+ * it, and is refused with `KeyUnavailableError`, or throws `TransientError` when the key service did not answer in
14
+ * time (the keystore's own, or one whose `cause` is the timeout it threw).
31
15
  */
32
16
  readonly keystore?: IKeystore;
33
17
  }
@@ -36,6 +20,15 @@ export interface GenerationEntry {
36
20
  readonly generation: number;
37
21
  /** Whether the registry pointer currently names this generation. */
38
22
  readonly current: boolean;
23
+ /**
24
+ * Whether the row's record of the generations it published, `knownGens`, names this generation. A rollback onto a known
25
+ * generation needs no `allowUnknown`; an unknown one needs it, except below the pointer in the two cases the pointer
26
+ * alone decides (a row with no usable `knownGens`, and a target below a full one's bound), where it needs none. The
27
+ * row's word, read with the listing's one registry read: the object is not opened, so
28
+ * its checksum is not compared. `false` for every generation of a row whose record is missing or unusable, and for a
29
+ * segment with no row. The current generation of a row with a usable record is always known.
30
+ */
31
+ readonly known: boolean;
39
32
  /**
40
33
  * The current generation's id count, from its registry row, with no read of the object. Present only on the current
41
34
  * entry, and only when the row's summary can be used: for an encrypted segment that takes a keystore that opens the
@@ -65,11 +58,20 @@ export interface RollbackResult {
65
58
  readonly generation: number;
66
59
  }
67
60
  /**
68
- * Move a segment's pointer **back** to a generation that is still in the bucket.
61
+ * Move a segment's pointer to a generation that is still in the bucket and that the segment's row knows it published.
69
62
  *
70
63
  * The only non-forward-only publish in the library, and the only one an operator has to ask for by name. It
71
64
  * refuses rather than guesses:
72
65
  *
66
+ * - a generation the **row does not know** is refused with `ValidationError`, before any request to storage. The row
67
+ * records, in `knownGens`, the generations its loads and rollbacks made current, with the footer checksum of each
68
+ * object; a target outside that record is where objects live that no row ever published: a load that wrote its object
69
+ * and died before the publish, a refused load's object, an object sealed under a key no row holds, a load published
70
+ * after the point a registry was restored to. {@link rollbackSegment}'s `allowUnknown` is the one explicit override;
71
+ * - a known generation whose object is **not the one the row recorded** (its footer checksum differs: another object was
72
+ * stored under the number) is refused with `IntegrityError`, with or without a key, since the footer is in the clear.
73
+ * The check runs before the target is opened, so an object sealed under another key reports as another object, not as a
74
+ * failed decryption. `allowUnknown` moves onto it anyway;
73
75
  * - a generation whose object is **not in the bucket** — collected, or never written — is refused with
74
76
  * `NotFoundError` rather than pointed at, because a pointer naming a missing object is the one state the whole
75
77
  * design exists to avoid;
@@ -78,23 +80,50 @@ export interface RollbackResult {
78
80
  * - a target that is **not what the row says the segment is** is refused with `IntegrityError`: a cleartext object
79
81
  * under a row with keys, or an encrypted one under a row with none, either of which every read would then refuse.
80
82
  * One tail read of the target decides it, as does a footer that fails its own checks;
81
- * - rolling to the generation **already current** is a no-op that reports itself, not an error.
83
+ * - `allowUnknown` on an **encrypted** segment, outside the two cases the pointer alone decides (below), needs the key,
84
+ * and is refused with `KeyUnavailableError` when the store has
85
+ * no keystore, or its keystore holds none of the key's KEKs or one that does not unwrap the key: an object the row
86
+ * does not vouch for and that nothing opens may be one sealed under a key no row holds. A key service that did not
87
+ * answer in time is thrown as `TransientError`, to retry;
88
+ * - rolling to the generation **already current** is a no-op that reports itself, not an error, once one footer read finds
89
+ * its object in the bucket and, when the row knows it, with the checksum recorded. A pointer that names a missing
90
+ * object is refused with `NotFoundError`, as any missing target is, and one that names another object than the row
91
+ * recorded with `IntegrityError`.
92
+ *
93
+ * Which generations are known: the current one, the ones a load's `keep` keeps below it, those a live lease holds, and
94
+ * the ones a rollback left above the pointer, until a load numbers above them. A known target needs no flag and, for
95
+ * the same reason, no key: moving onto an object the row published, forward or back. Two cases are decided by the
96
+ * pointer alone, so a target below it needs no flag and no key, and are audited with `unknown: true`: a row with no
97
+ * usable `knownGens` (a registry that drops the field, or a row whose pointer was moved by hand); and a target below the
98
+ * bound of a full record (128 entries or more, leased ones included), which is its least recently current entry that no
99
+ * lease entry names. Neither gets the checks above. A generation a publish with a smaller `keep` no longer names but that
100
+ * stays in the bucket (a lease spared it, or its delete failed) needs `allowUnknown` until a later listing collects it.
82
101
  *
83
102
  * The same tail read opens the target when the segment's key is at hand, and the rollback writes the target's own id
84
103
  * count and metadata into the row, in the compare-and-swap that moves the pointer: a reader that sees the target as
85
104
  * current sees what describes it. (One tail read, and a range read as well when the target's index is longer than the
86
- * tail read: a target of very many chunks.) A store with no keystore, or one that cannot open the segment's key for any
87
- * reason, a keystore that is unreachable or timed out included, still rolls an encrypted segment back and leaves the
88
- * row with no summary of the target. The undo of a rollback whose target was collected meanwhile puts back the summary
89
- * the row had, and so does the undo of an `allowForward` rollback whose target was replaced meanwhile: an object above
90
- * the pointer can be deleted by an erasure and its number taken by a load, between the read and the swap, so after the
91
- * swap such a target is read once more, and the pointer put back unless it is the object whose summary was written.
105
+ * tail read: a target of very many chunks.) A store with no keystore, or whose keystore cannot open the segment's key
106
+ * (it holds none of the key's KEKs, one it holds does not unwrap it, or it throws `TransientError` or times out as
107
+ * `AbortSignal.timeout()` and the AWS SDK report a timeout), still rolls an encrypted segment onto a known generation, or
108
+ * below the pointer in the two judged-as-before cases, and leaves the row with no summary of the target. Any other
109
+ * failure of the keystore is thrown. After the swap the target's footer is read once more, and the pointer put back,
110
+ * restoring the summary and the record the row had, unless it is still the object that was read: collection can delete a
111
+ * target below the old pointer between the read and the swap, and an erasure can delete one above it and a load take its
112
+ * number. A swap that gets no answer is settled by reading the row: one the row shows landed, the row being the one the
113
+ * swap leaves, is checked as any swap is, and otherwise its `TransientError` is thrown, since it may have landed or may
114
+ * still land, and a re-run finds the pointer where it is. A write by another writer is not taken for it, unless it leaves
115
+ * the very row this swap would. On a cleartext segment two writes can: another rollback onto the same target, whatever
116
+ * row it read, and a load that wrote under that number the very object the rollback read and carried the ledger the
117
+ * rollback would (a load that carries a usable ledger, as one with a `keep` above 64 does; over a row with no usable
118
+ * ledger the rollback writes none and a load seeds one, so those rows differ). On an encrypted segment a rollback with no
119
+ * key at hand writes no summary, so another such rollback onto the same target can too. That call then reports and
120
+ * audits the move as its own; the pointer is where it asked for it.
92
121
  *
93
122
  * What it does **not** do is delete anything. The generations above the new pointer stay in the bucket, which is
94
123
  * what makes the rollback reversible — roll forward again by naming one of them. They are also then *above*
95
124
  * `currentGen`, where generation collection never looks. They stay until loads pass them (the first load whose
96
- * number one of them holds numbers above them all, and collection then keeps the newest `keep` of what is below its
97
- * pointer), `dropSegment` deletes them, or a subject erasure
125
+ * number one of them holds numbers above them all, and collection then keeps `keep` of what is below its pointer, the
126
+ * generation that load superseded first), `dropSegment` deletes them, or a subject erasure
98
127
  * deletes them: all of those present when it rewrites, and, when the current generation does not hold the id, only those
99
128
  * holding it and any no read of the segment can open, whatever the id.
100
129
  * Not collecting them is a deliberate trade: an operator who has just undone a bad load should not have the
@@ -103,5 +132,5 @@ export interface RollbackResult {
103
132
  */
104
133
  export declare function rollbackSegment(ref: SegmentRef, toGeneration: number, deps: GenerationListDeps, options?: {
105
134
  audit?: IAuditSink;
106
- allowForward?: boolean;
135
+ allowUnknown?: boolean;
107
136
  }): Promise<RollbackResult>;
@@ -2,28 +2,50 @@
2
2
  * A generation's summary on its registry row: how a writer builds it, how a reader opens it, and how it is held
3
3
  * against the object it describes.
4
4
  *
5
- * The row carries a cached description of the current generation, `{ generation, cardinality, fingerprint, metadata? }`,
6
- * written by the same compare-and-swap that moves the pointer. The fingerprint names the generation's object, by its
7
- * size and footer checksum, as `CrbmReader.fingerprint` does. On a cleartext segment the values are in the clear. On an
8
- * encrypted one the row holds `{ generation, sealed }` instead: the cardinality as a fixed-width little-endian u64, the
9
- * object's size as a u64 and its footer checksum as a u32, then the metadata's canonical JSON if there is any, sealed
5
+ * The row carries a cached description of the current generation, `{ generation, cardinality, fingerprint, tailBytes,
6
+ * metadata? }`, written by the same compare-and-swap that moves the pointer. The fingerprint names the generation's
7
+ * object, by its size and footer checksum, as `CrbmReader.fingerprint` does, and `tailBytes` is how much of the object's
8
+ * end one open reads. On a cleartext segment the values are in the clear. On an encrypted one the row holds
9
+ * `{ generation, sealed }` instead: the cardinality as a fixed-width little-endian u64, the object's size as a u64 and
10
+ * its footer checksum as a u32, then `tailBytes` as a u32, then the metadata's canonical JSON if there is any, sealed
10
11
  * under the segment's data key with the summary scope of the associated data, which binds it to its namespace, segment
11
12
  * and generation. The numbers are fixed-width so the ciphertext's length reveals only the metadata's size, never how
12
- * many digits the count or the size has. Pure: no I/O.
13
+ * many digits the count or the size has, and `tailBytes`, which tracks the number of chunks, is sealed for the same
14
+ * reason the size is. Pure: no I/O.
13
15
  *
14
16
  * The generation's `.crbm` object stays the truth, and the summary is a copy of it. {@link summaryAgrees} is the
15
17
  * comparison of one with the other, and the fingerprint is what a reader holds every object it opens against.
16
18
  */
17
19
  import type { Aead } from './crypto.js';
18
20
  import type { ClearRegistrySummary, GenerationMetadata, RegistryRecord, RegistrySummary, SealedRegistrySummary, SegmentRef } from './ports.js';
19
- /** What a summary says about a generation: its id count, and its metadata when it has any. */
21
+ /** A generation holds at most every 32-bit id. */
22
+ export declare const MAX_SUMMARY_CARDINALITY: number;
23
+ /**
24
+ * The most a summary's `tailBytes` can be: the reader's own default caps on what an open fetches, an index of at most
25
+ * 8 MiB, an extension block of at most its cap and its trailer, and the footer. The library's writer cannot reach it. A
26
+ * reader can report more for an object it did not write, since it accepts bytes between the index and the footer. A
27
+ * literal, so the registry validators that use it do not import the format module; a test holds it to the sum of the
28
+ * format's constants.
29
+ */
30
+ export declare const MAX_TAIL_BYTES = 8392820;
31
+ /** Whether `tailBytes` is one a summary of an object of `size` bytes can carry. */
32
+ export declare function tailBytesFit(tailBytes: unknown, size: number): boolean;
33
+ /**
34
+ * What a summary says about a generation: its id count, and its metadata when it has any. `tailBytes`, when given, is
35
+ * the bytes of the object's end one open reads.
36
+ */
20
37
  export interface GenerationDescription {
21
38
  readonly cardinality: number;
22
39
  readonly metadata?: GenerationMetadata | undefined;
40
+ readonly tailBytes?: number | undefined;
23
41
  }
24
- /** What a row's summary says: the generation's description, and the fingerprint of the object it describes. */
42
+ /**
43
+ * What a row's summary says: the generation's description, the fingerprint of the object it describes and the
44
+ * bytes of that object's end one open reads.
45
+ */
25
46
  export interface SummaryDescription extends GenerationDescription {
26
47
  readonly fingerprint: string;
48
+ readonly tailBytes: number;
27
49
  }
28
50
  /** The size of the object a fingerprint names, in bytes: what `stat()` reports from a row's summary. */
29
51
  export declare function sizeOfFingerprint(fingerprint: string): number;
@@ -31,13 +53,13 @@ export declare function sizeOfFingerprint(fingerprint: string): number;
31
53
  * The clear summary of a generation on a cleartext segment, naming its object by `fingerprint`. `metadata` is left off
32
54
  * when there is none.
33
55
  */
34
- export declare function clearSummary(generation: number, cardinality: number, fingerprint: string, metadata?: GenerationMetadata): ClearRegistrySummary;
56
+ export declare function clearSummary(generation: number, cardinality: number, fingerprint: string, tailBytes: number, metadata?: GenerationMetadata): ClearRegistrySummary;
35
57
  /**
36
- * The sealed summary of a generation on an encrypted segment: the cardinality as a u64, the object's size as a u64 and
37
- * its footer checksum as a u32, then the metadata's canonical JSON, sealed under `aead` and bound to `ref` and
38
- * `generation`.
58
+ * The sealed summary of a generation on an encrypted segment: the cardinality as a u64, the object's size as a u64,
59
+ * its footer checksum as a u32 and its `tailBytes` as a u32, then the metadata's canonical JSON, sealed under `aead` and
60
+ * bound to `ref` and `generation`.
39
61
  */
40
- export declare function sealSummary(aead: Aead, ref: SegmentRef, generation: number, cardinality: number, fingerprint: string, metadata?: GenerationMetadata): SealedRegistrySummary;
62
+ export declare function sealSummary(aead: Aead, ref: SegmentRef, generation: number, cardinality: number, fingerprint: string, tailBytes: number, metadata?: GenerationMetadata): SealedRegistrySummary;
41
63
  /**
42
64
  * What a sealed summary says, opened with `aead` under the summary scope of `ref` and the summary's own generation.
43
65
  * The bytes are untrusted: a blob that is not base64, is the wrong size, fails its authentication (tampered, or moved
@@ -56,9 +78,10 @@ export declare function openSummary(aead: Aead, ref: SegmentRef, summary: Sealed
56
78
  export declare function usableSummary(ref: SegmentRef, row: Pick<RegistryRecord, 'summary' | 'status' | 'currentGen' | 'wrappedDeks'>, aead: Aead | undefined): SummaryDescription | undefined;
57
79
  /**
58
80
  * Whether a summary's description of a generation is what its object holds: the same id count and the same metadata,
59
- * compared in canonical form, so key order never matters. A summary with metadata over an object with none, and the
60
- * reverse, disagree: whether an encrypted object's metadata block is present is not authenticated (stripping it needs
61
- * no key), so the row's sealed copy is what a missing block is held against.
81
+ * compared in canonical form, so key order never matters, and the same `tailBytes` when both say one (a cold open sizes
82
+ * its tail read from the row's, so a number the object does not bear out is not believed again). A summary with
83
+ * metadata over an object with none, and the reverse, disagree: whether an encrypted object's metadata block is present
84
+ * is not authenticated (stripping it needs no key), so the row's sealed copy is what a missing block is held against.
62
85
  */
63
86
  export declare function summaryAgrees(summary: GenerationDescription, object: GenerationDescription): boolean;
64
87
  /**
@@ -73,6 +96,15 @@ export declare function summaryAgrees(summary: GenerationDescription, object: Ge
73
96
  export declare function metadataToCarry(object: GenerationMetadata | undefined, authenticated: GenerationDescription | undefined): GenerationMetadata | undefined;
74
97
  /**
75
98
  * The summary of a generation of `ref`, describing the object `description.fingerprint` names, clear or sealed under
76
- * `aead` as the segment's keys call for.
99
+ * `aead` as the segment's keys call for. `undefined` when the object's `tailBytes` is above {@link MAX_TAIL_BYTES}: a
100
+ * reader accepts bytes between an object's index and its footer, so an object this library did not write can report a
101
+ * tail the row cannot record, and a row is legal without a summary (a read then opens the object with the default tail).
102
+ * The description's size, footer checksum and `tailBytes` must all come from the one object.
103
+ */
104
+ export declare function summaryOf(ref: SegmentRef, generation: number, description: SummaryDescription, aead: Aead | undefined): RegistrySummary | undefined;
105
+ /**
106
+ * {@link summaryOf} for an object this library wrote, whose tail is within {@link MAX_TAIL_BYTES} by construction (its
107
+ * index holds at most 65,536 entries and its extension block at most 4 KiB). Throws {@link IntegrityError} for a tail
108
+ * that is not, which the writer cannot produce.
77
109
  */
78
- export declare function summaryOf(ref: SegmentRef, generation: number, description: SummaryDescription, aead: Aead | undefined): RegistrySummary;
110
+ export declare function ownSummary(ref: SegmentRef, generation: number, description: SummaryDescription, aead: Aead | undefined): RegistrySummary;
@@ -29,8 +29,8 @@ export declare function incarnationField(token: Token | undefined): {
29
29
  * - Only one does: different incarnations. A registry gives its rows one form of token, so a row without an id never
30
30
  * becomes a row with one, nor the reverse.
31
31
  * - Neither does (a registry of someone else's whose tokens carry none): there is no id to compare, and the creation
32
- * stamp is the only signal left. It is a clock reading, so a caller that acts on this answer proves its own object
33
- * too.
32
+ * stamp is the only signal left. It is a clock reading, so `landedHere` also checks the object under the number by
33
+ * its footer, and `onlyLeasesDiffer` also requires every field but the leases to match.
34
34
  */
35
35
  export declare function sameIncarnation(a: {
36
36
  readonly token: Token;
@@ -49,3 +49,5 @@ export declare function validateUserRef(ref: SegmentRef): void;
49
49
  export declare function validateChunkKeyOrder(keys: readonly number[]): void;
50
50
  /** Validate a chunk ref: the segment/namespace rules plus `chunkKey` ∈ `[0, 65535]` (a u16). A driver-boundary check. */
51
51
  export declare function validateChunkRef(ref: ChunkRef): void;
52
+ /** The chunk-key half of {@link validateChunkRef}, for a caller that has already validated the segment: an integer in `[0, 65535]`. */
53
+ export declare function validateChunkKey(chunkKey: number): void;
@@ -9,3 +9,7 @@ export { MAX_ROW_BYTES, ObjectStoreRegistry, ObjectVersionRaced, } from './drive
9
9
  export type { ObjectRegistryStore, ObjectRow } from './drivers/_shared/object-registry.js';
10
10
  export { encodeNameForKey, encodeNameForPath, namespaceKeyPart, namespacePathPart, } from './drivers/_shared/keys.js';
11
11
  export { normalizeObjectPrefix, prefixPart } from './drivers/_shared/object-registry-keys.js';
12
+ export { parseGenerationFromKey, segmentObjectPrefix, storageObjectKey, } from './drivers/_shared/object-keys.js';
13
+ export { refuseUnknownOptions, requireBool, requirePositiveInt } from './drivers/_shared/options.js';
14
+ export { isTransportFault, toTransient } from './drivers/_shared/faults.js';
15
+ export { concatBytes } from './drivers/_shared/bytes.js';