@cloudbitmaps/core 0.20.0 → 0.22.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 (77) hide show
  1. package/README.md +101 -5
  2. package/dist/{chunk-YSC7NJWV.js → chunk-OSU7W2Y3.js} +811 -164
  3. package/dist/chunk-OSU7W2Y3.js.map +7 -0
  4. package/dist/core/audit.d.ts +12 -2
  5. package/dist/core/bit-route.d.ts +9 -1
  6. package/dist/core/blob.d.ts +12 -1
  7. package/dist/core/codec.d.ts +1 -1
  8. package/dist/core/combine-expr.d.ts +10 -8
  9. package/dist/core/combine-feed.d.ts +12 -18
  10. package/dist/core/combine-many.d.ts +29 -22
  11. package/dist/core/concurrency.d.ts +8 -0
  12. package/dist/core/consistency.d.ts +69 -17
  13. package/dist/core/crbm/reader.d.ts +52 -6
  14. package/dist/core/crbm-storage-source.d.ts +65 -452
  15. package/dist/core/crypto.d.ts +6 -1
  16. package/dist/core/determinism.d.ts +2 -1
  17. package/dist/core/engine.d.ts +222 -33
  18. package/dist/core/erase-id.d.ts +60 -15
  19. package/dist/core/erasure.d.ts +5 -4
  20. package/dist/core/errors.d.ts +61 -11
  21. package/dist/core/leases.d.ts +4 -3
  22. package/dist/core/load.d.ts +58 -45
  23. package/dist/core/lru.d.ts +2 -15
  24. package/dist/core/metrics.d.ts +5 -3
  25. package/dist/core/pinned-storage-source.d.ts +4 -4
  26. package/dist/core/ports.d.ts +96 -22
  27. package/dist/core/registry-scan.d.ts +0 -7
  28. package/dist/core/retention-sweep.d.ts +70 -66
  29. package/dist/core/retention.d.ts +2 -2
  30. package/dist/core/retry.d.ts +15 -18
  31. package/dist/core/rollback.d.ts +61 -31
  32. package/dist/core/validate.d.ts +2 -0
  33. package/dist/driver-kit.d.ts +11 -2
  34. package/dist/driver-kit.js +84 -16
  35. package/dist/driver-kit.js.map +3 -3
  36. package/dist/drivers/_shared/bytes.d.ts +6 -0
  37. package/dist/drivers/_shared/faults.d.ts +15 -0
  38. package/dist/drivers/_shared/object-keys.d.ts +18 -0
  39. package/dist/drivers/_shared/object-registry-keys.d.ts +0 -2
  40. package/dist/drivers/_shared/object-registry.d.ts +2 -0
  41. package/dist/drivers/_shared/options.d.ts +19 -0
  42. package/dist/drivers/backends.d.ts +2 -0
  43. package/dist/drivers/crypto.d.ts +1 -1
  44. package/dist/export/index.d.ts +9 -8
  45. package/dist/index.d.ts +11 -10
  46. package/dist/index.js +4202 -2833
  47. package/dist/index.js.map +4 -4
  48. package/package.json +7 -2
  49. package/dist/chunk-YSC7NJWV.js.map +0 -7
  50. package/dist/core/chunk-checks.d.ts +0 -34
  51. package/dist/core/chunk-stream.d.ts +0 -22
  52. package/dist/core/chunk-window.d.ts +0 -30
  53. package/dist/core/combine-ledger.d.ts +0 -43
  54. package/dist/core/crbm/crc32c.d.ts +0 -10
  55. package/dist/core/crbm/fingerprint.d.ts +0 -18
  56. package/dist/core/crbm/format.d.ts +0 -151
  57. package/dist/core/crbm/frame.d.ts +0 -10
  58. package/dist/core/crbm/plan-reads.d.ts +0 -44
  59. package/dist/core/crbm/varint.d.ts +0 -13
  60. package/dist/core/crbm/writer.d.ts +0 -68
  61. package/dist/core/due-index.d.ts +0 -53
  62. package/dist/core/generation-gc.d.ts +0 -179
  63. package/dist/core/item-pull.d.ts +0 -29
  64. package/dist/core/kept-generations.d.ts +0 -40
  65. package/dist/core/metadata.d.ts +0 -42
  66. package/dist/core/reader-defaults.d.ts +0 -29
  67. package/dist/core/summary.d.ts +0 -78
  68. package/dist/core/token.d.ts +0 -41
  69. package/dist/drivers/_shared/entropy.d.ts +0 -5
  70. package/dist/drivers/_shared/registry.d.ts +0 -104
  71. package/dist/drivers/localfs/exact-case.d.ts +0 -37
  72. package/dist/drivers/localfs/fs-util.d.ts +0 -33
  73. package/dist/drivers/localfs/paths.d.ts +0 -40
  74. package/dist/drivers/localfs/registry.d.ts +0 -44
  75. package/dist/drivers/localfs/storage.d.ts +0 -21
  76. package/dist/drivers/memory.d.ts +0 -61
  77. package/dist/testing/fuzz-core.d.ts +0 -15
@@ -15,20 +15,26 @@
15
15
  * does not match — and silently, because a `catch` that stops matching just falls through, defeating
16
16
  * transient retry and the publish path's conflict handling with no error of its own.
17
17
  *
18
- * `Symbol.for` is identity-stable across copies, bundles and realms, so classify errors with the exported
19
- * predicates below (never `instanceof`) anywhere an error may cross that boundary.
18
+ * `Symbol.for` is identity-stable across copies, bundles and realms, so classify errors with `errorKind`
19
+ * (never `instanceof`) anywhere an error may cross that boundary.
20
20
  */
21
21
  declare const ERROR_BRAND: unique symbol;
22
22
  declare const TRANSIENT_BRAND: unique symbol;
23
23
  /** Base class for every error CloudRoaring throws. */
24
24
  export declare class CloudRoaringError extends Error {
25
- /** Cross-bundle brand — see the predicates ({@link isCloudRoaringError}, …). Non-enumerable-ish (symbol key ⇒ not in JSON). */
25
+ /** Cross-bundle brand — see {@link errorKind}. Non-enumerable-ish (symbol key ⇒ not in JSON). */
26
26
  readonly [ERROR_BRAND]: true;
27
27
  constructor(message: string);
28
28
  }
29
- /** Invalid caller input (bad id, segment name, options). Raised before any storage call. */
29
+ /**
30
+ * Invalid input, which the same call fails on again. Most is refused before any storage call (a bad id, segment name
31
+ * or option); some is found by the call itself: a read past the end of an object, or a bucket or container a cloud
32
+ * driver finds does not exist.
33
+ */
30
34
  export declare class ValidationError extends CloudRoaringError {
31
35
  }
36
+ /** What a contention {@link WriteConflictError} ends with: the call is safe to run again. */
37
+ export declare const CONTENTION_NEXT_STEP = "; another writer changed the segment's row while this call ran. Re-run the call: it reads the row afresh";
32
38
  /** An OCC conditional write/delete lost the race — the row changed since it was read. */
33
39
  export declare class WriteConflictError extends CloudRoaringError {
34
40
  }
@@ -93,14 +99,46 @@ export declare class UnsupportedError extends CloudRoaringError {
93
99
  export declare class CapabilityError extends CloudRoaringError {
94
100
  }
95
101
  /**
96
- * An operation would exceed its per-op **denial-of-wallet budget** — too many backend requests for a single
97
- * `count`/`iterate`/`intersect`/`subjectReport`/`eraseSubject` call — so it is refused **before** fanning out
98
- * (hard invariant 6). Default-on but generous (normal ops never hit it); tune it
99
- * per store (`budget`) or per op, or disable with `budget: false`. Deterministic (never retried): the op is too
100
- * big by policy, not by luck. Each request's bytes are separately capped (the safe-deserialize ceiling), so
101
- * bounding the request count transitively bounds bytes. Carries the projected count + the limit, never data.
102
+ * An operation would exceed one of its per-op **denial-of-wallet bounds** — too many backend requests
103
+ * (`budget.maxRequests`), too many bytes held at once by a combine (`maxBufferedBytes`), or too many registry rows
104
+ * in a scan (`maxScanSegments`) — so it is refused, before the fan-out where it can be known beforehand (hard
105
+ * invariant 6). Default-on but generous (normal ops never hit it); tune each per store or per op, or disable the
106
+ * request budget with `budget: false`. Deterministic (never retried): the op is too big by policy, not by luck.
107
+ * Each request's bytes are separately capped (the safe-deserialize ceiling), so bounding the request count
108
+ * transitively bounds bytes.
109
+ *
110
+ * Carries an option name and two numbers, and no data: `option` names the option to raise, `limit` is the bound in
111
+ * force, and `spent` is what the call had reached when it was refused. What `spent` measures depends on `option`:
112
+ *
113
+ * - `'budget.maxRequests'`: the requests the call needs, which raising the option to it lets through. For a plan, it
114
+ * is the planned reads (with the reads already taken, when a run plans again); an enumeration that
115
+ * stops at the bound reports `limit + 1`, a lower bound.
116
+ * - `'maxBufferedBytes'`: a **lower bound** on the bytes the call needs resident: what it held plus what it asked
117
+ * for at the moment it was refused. Raising the limit to `spent` can be refused again, further on, because the
118
+ * call had not finished growing. The one figure that lets an output through is the per-output refusal's, which
119
+ * the message gives as "raise maxBufferedBytes to at least N". Otherwise raise the limit with room to spare, or
120
+ * split the outputs across calls.
121
+ * - `'maxScanSegments'`: `limit + 1`, a lower bound: the scan stops at the first row past the bound (for the retention
122
+ * sweep's due-index scan, the first due-index pointer, unread, which may be stale), so it never learns the fleet's
123
+ * size.
124
+ *
125
+ * The message states the same limit.
102
126
  */
103
127
  export declare class BudgetExceededError extends CloudRoaringError {
128
+ /** The option to raise to let the call through. */
129
+ readonly option: 'budget.maxRequests' | 'maxBufferedBytes' | 'maxScanSegments';
130
+ /** The bound in force: requests, bytes or rows, by `option`. */
131
+ readonly limit: number;
132
+ /**
133
+ * What the call had reached when refused. By `option`: the requests the call needs (`'budget.maxRequests'`), a
134
+ * lower bound on the resident bytes it needs (`'maxBufferedBytes'`), or `limit + 1` (`'maxScanSegments'`).
135
+ */
136
+ readonly spent: number;
137
+ constructor(message: string, details: {
138
+ readonly option: 'budget.maxRequests' | 'maxBufferedBytes' | 'maxScanSegments';
139
+ readonly limit: number;
140
+ readonly spent: number;
141
+ });
104
142
  }
105
143
  /**
106
144
  * An encrypted segment's data key (DEK) cannot be unwrapped because the keystore holds none of the
@@ -148,7 +186,9 @@ export declare function isLeaseLimitError(err: unknown): err is LeaseLimitError;
148
186
  export declare function isNotFoundError(err: unknown): err is NotFoundError;
149
187
  /** Corrupt/oversized/failed-checksum bytes from a tier. */
150
188
  export declare function isIntegrityError(err: unknown): err is IntegrityError;
151
- /** Invalid caller input. */
189
+ /**
190
+ * Invalid caller input; also the error a cloud driver raises for a bucket or container it finds does not exist.
191
+ */
152
192
  export declare function isValidationError(err: unknown): err is ValidationError;
153
193
  /** This build or this store's wiring cannot do what was asked, though nothing is malformed. */
154
194
  export declare function isUnsupportedError(err: unknown): err is UnsupportedError;
@@ -158,4 +198,14 @@ export declare function isCapabilityError(err: unknown): err is CapabilityError;
158
198
  export declare function isBudgetExceededError(err: unknown): err is BudgetExceededError;
159
199
  /** No key the keystore holds can unwrap an encrypted segment's data key. */
160
200
  export declare function isKeyUnavailableError(err: unknown): err is KeyUnavailableError;
201
+ /** The name of one of the library's error classes, as `errorKind` returns it. */
202
+ export type ErrorKind = 'CloudRoaringError' | 'ValidationError' | 'WriteConflictError' | 'IntegrityError' | 'NotFoundError' | 'UnsupportedError' | 'CapabilityError' | 'TransientError' | 'KeyUnavailableError' | 'BudgetExceededError' | 'LeaseExpiredError' | 'LeaseLimitError' | 'StaleOperandError';
203
+ /**
204
+ * Which of the library's error classes `err` is, or `undefined` when it is none of them. Works on every copy of
205
+ * the package, every bundle and every realm, where `instanceof` can silently stop matching: it reads the
206
+ * `Symbol.for` brands and the error's `name`. An application's own subclass of one class returns
207
+ * `'CloudRoaringError'`, except a subclass of `TransientError`, which returns `'TransientError'`; an error
208
+ * cloned out of a worker (the brand is gone) returns `undefined`.
209
+ */
210
+ export declare function errorKind(err: unknown): ErrorKind | undefined;
161
211
  export {};
@@ -18,8 +18,8 @@ export declare const MAX_STORED_LEASES = 256;
18
18
  */
19
19
  export declare const LEASE_CAS_ATTEMPTS: number;
20
20
  /**
21
- * Fresh writes sent after one that got no answer and left the row as it was, by the lease writers and by an erasure's
22
- * renewal of the row.
21
+ * Fresh writes sent after one that got no answer and left the row as it was, by the lease writers, by an erasure's
22
+ * renewal of the row and by a publish: four sends of the row write in all.
23
23
  */
24
24
  export declare const UNANSWERED_RESENDS = 3;
25
25
  /** The upper bound of the first of those waits, in ms; each next one doubles it. */
@@ -65,7 +65,7 @@ export declare const LEASE_ONLY_RETRIES: number;
65
65
  */
66
66
  export declare function onlyLeasesDiffer(held: RegistryRecord, now: RegistryRecord, alsoIgnore?: readonly (keyof RegistryRecord)[]): boolean;
67
67
  /** A writer's count of the lease-only changes it has waited out, and the wait. One per call of the writer. */
68
- export interface LeaseChurn {
68
+ interface LeaseChurn {
69
69
  /**
70
70
  * When `now` is `held` changed only in its leases and the bound is not spent: wait, then read the row again with
71
71
  * `reread`, and return what it found. The wait comes before the read, so the writer's next write is made against a row
@@ -124,3 +124,4 @@ export declare function takeLease(ref: SegmentRef, deps: LeaseDeps, take: LeaseT
124
124
  * races and the registry's `TransientError` for a write that stayed unanswered over an unchanged row.
125
125
  */
126
126
  export declare function releaseLease(ref: SegmentRef, deps: LeaseDeps, holder: string): Promise<void>;
127
+ export {};
@@ -1,29 +1,6 @@
1
- /**
2
- * `loadSegment` — the loaded store's primary write path, as one call.
3
- *
4
- * A load is always the same four steps: take the next generation number, write one immutable object, move the
5
- * pointer, collect what the move superseded. Inside the library each is its own internal function, and the
6
- * step a hand-composed load leaves out is the last one, so a store that composed it would accumulate superseded
7
- * generations it keeps paying for.
8
- * Composing it here, once, is the point.
9
- *
10
- * The other reason it is one call is the **guard**. A load REPLACES a segment: whatever the stream contains is
11
- * what the segment contains afterwards. That makes an upstream query returning fewer rows than usual — or none —
12
- * a silent, successful-looking wipe, and nothing downstream can tell it from a legitimate shrink. The guard is
13
- * the place to say what a plausible result looks like, and it has to sit **between the write and the publish**,
14
- * because that is the only moment where the new content is known and the old one is still authoritative.
15
- *
16
- * Refusing therefore has to clean up after itself. The object is already durable at that point, and it sits
17
- * ABOVE `currentGen`, where generation collection deliberately never looks (it deletes strictly below the
18
- * pointer), so a refused load deletes its own object before returning, while the segment's row is unchanged (or
19
- * differs only in its leases), gone or `destroyed`, or has key material when the load wrote cleartext. Under a row
20
- * that is gone or keyed, a read of the object's footer must prove it the load's own first. Once another write has
21
- * otherwise changed the row, the object's number may name a re-created segment's live object, so it stays, and
22
- * collection takes it like any other generation once one above it is current.
23
- */
24
1
  import { type IAuditSink } from './audit.js';
25
2
  import { type CodecBitmap, type CodecInterface } from './codec.js';
26
- import type { Clock, Rng } from './determinism.js';
3
+ import type { Clock, Entropy, Rng } from './determinism.js';
27
4
  import type { IKeystore } from './crypto.js';
28
5
  import { type LoadInput } from './load-input.js';
29
6
  import { type ReadRetry } from './retry.js';
@@ -41,6 +18,14 @@ export interface LoadDeps {
41
18
  * bound). Absent, the read retry's source is used if it has one, and otherwise each wait is its bound.
42
19
  */
43
20
  readonly rng?: Rng;
21
+ /**
22
+ * The source of the write's identity: 12 random bytes the new generation's footer carries (for a cleartext generation;
23
+ * an encrypted one carries its index's AEAD nonce), so two loads of the same ids at the same number are two objects
24
+ * and every check that a footer is "mine" names this write. Required: a load without one (a JavaScript caller can
25
+ * leave it out) is refused with {@link ValidationError} before any read, keystore call or write.
26
+ * `@cloudbitmaps/roaring` supplies Web Crypto's by default.
27
+ */
28
+ readonly entropy: Entropy;
44
29
  /**
45
30
  * The store's read retry, for the read the guard makes of the current generation: a transient fault there is run
46
31
  * again under it rather than failing the load. Absent, the read is made once. It does not govern the write: a
@@ -98,12 +83,20 @@ export interface LoadGuard {
98
83
  * `'outputs[3].'`).
99
84
  */
100
85
  export declare function checkedGuard(guard: LoadGuard | undefined, where: string): LoadGuard | undefined;
86
+ /**
87
+ * Options of `store.load` and of core's `loadSegment`: what a load may publish (`allowEmpty`, `guard`), what it keeps
88
+ * (`keep`) and what it records (`metadata`, `audit`).
89
+ */
101
90
  export interface LoadOptions {
102
91
  /**
103
92
  * Publish an empty generation over a non-empty one. Off by default: an empty result is far more often an
104
93
  * upstream failure than an intent, and the failure is indistinguishable from success once it lands.
105
94
  */
106
95
  readonly allowEmpty?: boolean;
96
+ /**
97
+ * Bounds a plausible result must meet, judged before the pointer moves; a load outside them is refused
98
+ * (`published: false`), not thrown. See {@link LoadGuard}.
99
+ */
107
100
  readonly guard?: LoadGuard;
108
101
  /**
109
102
  * Generations to keep below the new pointer as a grace window (default 1). A non-negative integer: anything
@@ -116,14 +109,21 @@ export interface LoadOptions {
116
109
  * longest pinned job runs, on every writer that loads the segment — see "Generations and `keep`" in the loading
117
110
  * guide.
118
111
  *
119
- * The segment's row records the generations a load keeps, up to 64, so a load that found nothing above the pointer
120
- * collects without listing: it deletes the generations its publish pushed out of the window, and lists the segment's
121
- * objects on every sixteenth generation to take whatever that pass leaves, such as an object a crashed load left
122
- * below the pointer. A `keep` above 64 records no list and lists on every load, and so does the first load of a
123
- * row that records none, which then records it; so does a load whose check found the current generation's object
124
- * gone: the guard's read of it, or, when the guard took the size from the row's summary, one zero-byte read made
125
- * before a `keep` of 1 or more takes a name. A `keep` at least the generation published collects nothing and asks
126
- * for nothing.
112
+ * The window is the `keep` most recently current generations below the one the load publishes, so the generation the
113
+ * load superseded comes first. After a rollback that is the one the rollback restored, and it stays in the window of
114
+ * every load after it, ahead of the generations the rollback moved away from, for as many loads as `keep` says.
115
+ *
116
+ * The segment's row records the generations a load keeps, up to 127.
117
+ *
118
+ * With a `keep` of at most 64, a load whose number one existence check found free deletes by name the generations
119
+ * its publish pushed out of the window, and lists the segment's objects on every sixteenth generation to take
120
+ * whatever that pass leaves, such as an object a crashed load left below the pointer.
121
+ *
122
+ * A load lists instead on every load when `keep` is above 64, when the row has a pointer and no usable record (the
123
+ * load starts one from the bucket), when the row's record names more than 64 generations below the pointer that no
124
+ * lease holds (as after a `keep` above 64, or `materializeMany`), or when the guard's read, or the zero-byte read
125
+ * before a delete by name, finds the current generation's object missing. A publish that a read of the row settles
126
+ * after its last attempt lists too. A `keep` at least the generation published collects nothing and asks for nothing.
127
127
  */
128
128
  readonly keep?: number;
129
129
  /**
@@ -138,15 +138,11 @@ export interface LoadOptions {
138
138
  * over as it is. Sealed under the segment's key when the segment is encrypted.
139
139
  */
140
140
  readonly metadata?: GenerationMetadata;
141
+ /** Audit sink for this load: it receives `segment.publish`, or `segment.load-refused` when the load is refused. */
141
142
  readonly audit?: IAuditSink;
142
143
  }
143
144
  /** The bounds a guard can refuse a generation for: every {@link LoadRefusal} but a lost race. */
144
145
  export type GuardRefusal = Exclude<LoadRefusal, 'superseded'>;
145
- /**
146
- * The bound a generation of `cardinality` ids breaks over a current one of `before` (`null`: none, or not read), in the
147
- * order a load checks them, or `undefined`. Pure: the one judgement a load and a dry run share.
148
- */
149
- export declare function guardRefusal(cardinality: number, before: number | null, guard: LoadGuard | undefined, allowEmpty: boolean | undefined): GuardRefusal | undefined;
150
146
  /** What a dry run learns about one write: the size it would replace, and the bound that would refuse it. */
151
147
  export interface LoadJudgement {
152
148
  /**
@@ -165,7 +161,7 @@ export interface LoadJudgement {
165
161
  * would, and an encrypted one with no keystore the `KeyUnavailableError`. What it says holds for now only: a load
166
162
  * made later is judged against the segment as it is then.
167
163
  */
168
- export declare function judgeLoad(ref: SegmentRef, cardinality: number, deps: LoadDeps, options?: Pick<LoadOptions, 'allowEmpty' | 'guard'>): Promise<LoadJudgement>;
164
+ export declare function judgeLoad(ref: SegmentRef, cardinality: number, deps: Omit<LoadDeps, 'entropy'>, options?: Pick<LoadOptions, 'allowEmpty' | 'guard'>): Promise<LoadJudgement>;
169
165
  /** Why a load did not become current. */
170
166
  export type LoadRefusal = 'empty' | 'min-cardinality' | 'min-retained' | 'max-growth'
171
167
  /**
@@ -173,16 +169,23 @@ export type LoadRefusal = 'empty' | 'min-cardinality' | 'min-retained' | 'max-gr
173
169
  * put refused this one and it wrote nothing (`size: 0`); or the segment's registry row changed while the load was
174
170
  * writing: another load published, a retention change, a rollback or an erasure wrote the row, the row was
175
171
  * deleted, or a row appeared where the load found none. Once another write has changed the row, the object stays in
176
- * the bucket, still billed, until a later load's collection deletes it (at the latest the listing a load runs
177
- * every sixteenth generation) or the segment is dropped, because its generation number may by then name another
178
- * incarnation's live object; once the row is `destroyed` (dropped or crypto-shredded), the object is deleted, and
179
- * once the row is gone, so is an object its footer proves this load's own.
172
+ * the bucket, still billed, until a later load's collection deletes it or the segment is dropped, because its
173
+ * generation number may by then name another writer's object; until it goes, a rollback onto it needs `allowUnknown`, as onto any generation the row does not know. Once the row is
174
+ * `destroyed` (dropped or crypto-shredded), the object is deleted; once the row is gone, so is an object whose footer
175
+ * matches what this load wrote (another load's object of the same ids is not a match: the footer carries each write's
176
+ * own identity), and so is a cleartext one under a row with key material.
180
177
  */
181
178
  | 'superseded';
179
+ /**
180
+ * What a load did: the generation it wrote, whether that is now current (`published`, and `reason` when not), the
181
+ * object it wrote and what it collected.
182
+ */
182
183
  export interface LoadResult {
183
184
  /**
184
185
  * The generation written. Present even when refused: a refusal deletes that object while the segment's row is
185
- * unchanged or `destroyed`, or gone once its footer proves it this load's own, and leaves it in the bucket once another write has changed the row. A load that lost its
186
+ * unchanged or `destroyed`, and, once one read of its footer matches what the load wrote, when the row is gone or,
187
+ * for a cleartext object, has key material. Under any other row another write changed, the object stays in the
188
+ * bucket, an orphan a later load's collection takes, and until then a rollback onto it needs `allowUnknown`. A load that lost its
186
189
  * generation number to another wrote nothing, and reports `size: 0`.
187
190
  */
188
191
  readonly generation: number;
@@ -190,10 +193,19 @@ export interface LoadResult {
190
193
  readonly published: boolean;
191
194
  /** Set only when `published` is false. */
192
195
  readonly reason?: LoadRefusal;
196
+ /**
197
+ * Bytes of the generation's object as stored, or 0 when another load took the generation number first and
198
+ * nothing was written.
199
+ */
193
200
  readonly size: number;
201
+ /**
202
+ * SHA-256 of the object as stored, or `''` when another load took the generation number first. The library neither
203
+ * keeps nor checks it; reads are checked with CRC32C.
204
+ */
194
205
  readonly sha256: string;
206
+ /** Non-empty chunks in the generation written (at most 65,536), or 0 when nothing was written. */
195
207
  readonly chunkCount: number;
196
- /** Distinct ids in the generation that was written, post-dedup. */
208
+ /** Distinct ids in the generation that was written, post-dedup, or 0 when nothing was written. */
197
209
  readonly cardinality: number;
198
210
  /**
199
211
  * What the segment held when the guard judged it — `null` when there was no current generation, or when no
@@ -218,7 +230,8 @@ export interface LoadResult {
218
230
  *
219
231
  * `input` is ids, or a whole bitmap as `{ serialized }` portable Roaring bytes or a `{ bitmap }` with
220
232
  * `serialize('portable')` ({@link LoadInput}). A bitmap input is checked, decoded and serialized before the first
221
- * request, and is written from its own chunks; the generation is byte for byte the one its ids would write.
233
+ * request, and is written from its own chunks; the generation holds chunk for chunk what its ids would write, and carries its own write
234
+ * identity in the footer like any other.
222
235
  *
223
236
  * Returns what was written and whether it became current. A refusal is **not** an exception: `published: false`
224
237
  * with a `reason` is a normal outcome a caller branches on, in the same shape as a successful load, because the
@@ -1,19 +1,5 @@
1
- /**
2
- * Bounded LRU cache with optional TTL — the cache tier's memory ceiling.
3
- *
4
- * Time comes from an injected `Clock` (never `Date.now()`) so the cache is deterministic
5
- * under simulation.
6
- *
7
- * The cache can be bounded by entry **count** (`maxEntries`, always) and, optionally, by an aggregate
8
- * **byte weight** (`maxBytes`). The byte bound exists for values whose footprint is large and uneven — the
9
- * storage reader cache, where one entry pins a fully-parsed `.crbm` index that can be ~6 MB for a wide segment
10
- * yet a few hundred bytes for a small one (a count-only bound lets 1024 wide indices blow a Lambda's
11
- * memory even though the count is "in bounds"). Weights arrive **after** insertion (a cached reader is a
12
- * pending promise; its size is known only once it resolves), so weight is reported via {@link setWeight} and
13
- * eviction re-runs then.
14
- */
15
1
  import type { Clock } from './determinism.js';
16
- export interface LruOptions {
2
+ interface LruOptions {
17
3
  /** Hard ceiling on entries; the least-recently-used is evicted past it. */
18
4
  readonly maxEntries: number;
19
5
  /**
@@ -67,3 +53,4 @@ export declare class BoundedLru<K, V> {
67
53
  /** Current aggregate reported weight (bytes) across all entries. */
68
54
  get weightBytes(): number;
69
55
  }
56
+ export {};
@@ -49,8 +49,10 @@ export type MetricEvent = {
49
49
  readonly ms: number;
50
50
  } | {
51
51
  /**
52
- * One decoded-chunk cache lookup (emitted only when a cache is configured). `hit: false` means the lookup found
53
- * no cached chunk. A combine or `iterate` looks up every chunk it needs when it opens its streams. A caller that
52
+ * One decoded-chunk cache lookup, or one chunk counted a miss without a lookup (emitted only when a cache is
53
+ * configured). `hit: false` means the cache did not hold the chunk. A combine or `iterate` counts every chunk it
54
+ * needs when it opens its streams: a lookup each, except that on a cache that holds nothing each chunk after the
55
+ * one a stream starts at is counted a miss without one. A caller that
54
56
  * missed while another caller's read of the same chunk was open waits on that read and adds no `storage.get`, and a
55
57
  * range carries several chunks, so the misses can outnumber the `storage.get` events; the `storage.get` count is
56
58
  * the number of requests for chunks. `materializeMany` never looks up the cache, so it emits none.
@@ -131,7 +133,7 @@ export type MetricEvent = {
131
133
  };
132
134
  /**
133
135
  * The sink you plug in. One method, synchronous, fire-and-forget. It must never throw back into the
134
- * caller — `CloudRoaring` wraps a user sink with {@link safeMetrics} so a buggy sink can't break I/O.
136
+ * caller — `CloudRoaring` wraps a user sink so that a buggy sink can't break I/O.
135
137
  */
136
138
  export interface IMetricsSink {
137
139
  onEvent(event: MetricEvent): void;
@@ -27,14 +27,14 @@ import type { CrbmStorageChunkSource } from './crbm-storage-source.js';
27
27
  * What a pin holds for one segment: the generation, the version identifying those exact bytes, and the pinned
28
28
  * object's fingerprint, so a read can tell that the generation it opens is still that object.
29
29
  *
30
- * `fingerprint` is optional so that a pin built by hand, before there was one, still compiles. Nothing checks the
31
- * object such a pin reads, so it cannot tell a name purged and loaded again from the segment it pinned.
32
- * `seg.pin()` always records it.
30
+ * `fingerprint` is required for a pin of a generation (`null` only when `generation` is `null`): without it a read
31
+ * could not tell a name purged and loaded again from the segment it pinned. `seg.pin()` always records it, and a
32
+ * pin of a generation without a string `version` and `fingerprint` is refused with a `ValidationError`.
33
33
  */
34
34
  export interface PinnedAt {
35
35
  readonly generation: number | null;
36
36
  readonly version: string | null;
37
- readonly fingerprint?: string | null;
37
+ readonly fingerprint: string | null;
38
38
  }
39
39
  export declare class PinnedStorageChunkSource implements StorageChunkSource {
40
40
  private readonly inner;
@@ -281,12 +281,18 @@ export interface StorageDeleteOptions {
281
281
  * `getRange` may answer empty without reaching the backend, so it proves neither that the object exists nor
282
282
  * that the offset lies inside it: the cloud drivers answer it empty, the memory and local-filesystem drivers
283
283
  * throw for a missing object or an offset past the end.)
284
+ * - **A deleted object is missing to the next read**: once `delete(key)` has resolved, `getTail` and a non-empty `getRange`
285
+ * of that key throw {@link NotFoundError} (a zero-length `getRange` is the exemption above), with no stale answer from a
286
+ * cache or a replica. A rollback reads its target's footer once
287
+ * after moving the pointer, in place of listing the bucket, and takes a read that succeeds as proof that the object is
288
+ * there.
284
289
  * - **An out-of-range read throws {@link ValidationError}**: a range that runs past the end of the object, or a
285
290
  * negative or non-integer offset or length. Never a clamped, short or adjacent read. A pin whose object
286
291
  * was purged and loaded again as a smaller one asks for a range past its end, and this error is what sends it
287
292
  * to check that the object is still the one it pinned.
288
293
  * - **`getTail` reports the object's true total size**, not the number of bytes it returned, and, where the driver has
289
- * one, the object's `version` ({@link TailRead.version}) from the same response as the bytes.
294
+ * one, the object's `version` ({@link TailRead.version}) from the same response as the bytes. Its length is a safe
295
+ * integer, zero or less for an empty tail; any other length throws {@link ValidationError} before anything is read.
290
296
  * - **`delete` is idempotent.** Deleting an absent key is a no-op, not an error: collection passes race each
291
297
  * other and retry. That holds with `ifVersion` too: an absent object is a no-op whatever version is given.
292
298
  * - **`delete` with `ifVersion` does what {@link StorageCaps.conditionalDelete} says.** A driver that reports `true`
@@ -299,7 +305,7 @@ export interface StorageDeleteOptions {
299
305
  * under the key. A number can be taken again once its object is deleted, so this is what keeps a delete decided from
300
306
  * one read, and delayed, from removing an object stored under the number since: the erasure's delete of a holder
301
307
  * above the pointer passes the version it read when it searched that object, and a refused load's delete of an
302
- * object it proved its own by a footer read passes the version of that read.
308
+ * object whose footer, read once, matched what it wrote passes the version of that read.
303
309
  * - **A driver that wraps another forwards `ifVersion` and the `version` of a tail read, or reports
304
310
  * `conditionalDelete: false`.** A wrapper that hands `delete` the key alone, and reports the inner driver's
305
311
  * capability, makes every conditional delete through it unconditional without a word.
@@ -328,7 +334,9 @@ export interface IStorageDriver {
328
334
  /**
329
335
  * Stream a new immutable generation. The driver opens a destination, hands `write` a {@link BlobSink},
330
336
  * then atomically commits (and computes the content hash). Throws {@link WriteConflictError} if the key
331
- * already exists (write-once).
337
+ * already exists (write-once). Call `write` once: the object committed is the bytes that one call wrote, because
338
+ * the caller records the write identity of that call's object (its footer) for every later proof; a driver that
339
+ * runs `write` again after a failed attempt must commit only the last run's bytes.
332
340
  */
333
341
  putImmutable(key: GenKey, write: (sink: BlobSink) => Promise<void>): Promise<{
334
342
  size: number;
@@ -341,7 +349,10 @@ export interface IStorageDriver {
341
349
  getRange(key: GenKey, offset: number, length: number): Promise<Uint8Array>;
342
350
  /**
343
351
  * Speculative tail read: the last `min(maxBytes, size)` bytes + the object's **true total size**, and its `version`
344
- * where the driver has one ({@link TailRead}). A missing object throws {@link NotFoundError}.
352
+ * where the driver has one ({@link TailRead}). `maxBytes` must be a safe integer: zero or less answers an empty tail
353
+ * with the size, and one past the size the whole object. Any other `maxBytes` (`NaN`, a fraction, a negative one
354
+ * included, an infinity of either sign, a whole number past the safe range (2^53 or more, or -2^53 or less)) throws
355
+ * {@link ValidationError} before anything is read. A missing object throws {@link NotFoundError}.
345
356
  */
346
357
  getTail(key: GenKey, maxBytes: number): Promise<TailRead>;
347
358
  /**
@@ -405,11 +416,16 @@ export interface ClearRegistrySummary {
405
416
  /** How many ids the generation holds: an integer from 0 to 2^32. */
406
417
  readonly cardinality: number;
407
418
  /**
408
- * The fingerprint of the generation's object, `<size>:<footer checksum>` in decimal, as `CrbmReader.fingerprint` and
409
- * a pin's `pinnedAt.fingerprint` spell it: the object's size in bytes, which `stat()` reports, and the CRC32C its
410
- * footer stores.
419
+ * The fingerprint of the generation's object, `<size>:<footer checksum>` in decimal, as a pin's `pinnedAt.fingerprint`
420
+ * spells it: the object's size in bytes, which `stat()` reports, and the CRC32C its footer stores.
411
421
  */
412
422
  readonly fingerprint: string;
423
+ /**
424
+ * The bytes from the end of the last chunk payload to the end of the object: the extension block, if any, the index and
425
+ * the footer. A cold open reads this many bytes from the object's end to open it in one request. An integer from 104,
426
+ * the footer's size, to the object's size less its 8-byte preamble, and at most 8,392,820.
427
+ */
428
+ readonly tailBytes: number;
413
429
  /** The generation's metadata, when it has any. Never the empty object. */
414
430
  readonly metadata?: GenerationMetadata;
415
431
  }
@@ -419,11 +435,24 @@ export interface SealedRegistrySummary {
419
435
  readonly generation: number;
420
436
  /**
421
437
  * Base64 of `nonce(12) ‖ ciphertext ‖ tag(16)`, sealing the cardinality as a little-endian u64, the object's size as a
422
- * little-endian u64 and its footer checksum as a little-endian u32 (its fingerprint), then the metadata's canonical
423
- * JSON, if any. The numbers are fixed-width, so the length reveals only the metadata's size.
438
+ * little-endian u64 and its footer checksum as a little-endian u32 (its fingerprint), its `tailBytes` as a
439
+ * little-endian u32, then the metadata's canonical JSON, if any. The numbers are fixed-width, so the length reveals only
440
+ * the metadata's size.
424
441
  */
425
442
  readonly sealed: string;
426
443
  }
444
+ /**
445
+ * A generation a row knows it published: its number, and the checksum its object's footer stores. A rollback moves only
446
+ * onto a generation the row knows, and an object whose footer carries another checksum is another object under that
447
+ * number. The footer's checksum covers the index's checksum, which covers each chunk's, so it commits to the object's ids
448
+ * and shape; for a cleartext object it does not cover the extension block's bytes.
449
+ */
450
+ export interface KnownGeneration {
451
+ /** A non-negative safe integer. */
452
+ readonly generation: number;
453
+ /** The object's footer CRC32C (the second half of its fingerprint `<size>:<crc>`), an integer from 0 to 2^32 - 1. */
454
+ readonly footerCrc: number;
455
+ }
427
456
  /**
428
457
  * One lease on a generation. It names no fingerprint: a collector spares a generation by number and cannot compare an
429
458
  * object without reading it, and a purge and re-create drops the whole list with the row. A pin's own fingerprint,
@@ -481,15 +510,28 @@ export interface RegistryRecord extends SegmentRef {
481
510
  */
482
511
  readonly summary?: RegistrySummary;
483
512
  /**
484
- * The generations below `currentGen` that the last publish kept as its grace window, ascending. Optional: absent
485
- * means the row does not know which generations are kept, and a load then lists the bucket to collect and records
486
- * them; an empty list means none are. Trusted only while every entry is below `currentGen`. A patch that moves
513
+ * The generations this row knows and has not decided to delete, the current one included, with each object's footer
514
+ * checksum: the ones it made current, in the order they were made current, oldest first and the current one last, and,
515
+ * when a publish over a row with no usable ledger seeds one, the ones a count by `keep` keeps in the bucket, which can
516
+ * include an object no load published (an orphan or a stray). An entry says the library knows the generation and its
517
+ * footer checksum, not that a load published it. A generation appears once,
518
+ * at its most recent time current (a rollback onto it moves it to the end). The entries before the last are the kept
519
+ * window, the generations a live lease holds, and what a rollback left; those numbered above `currentGen` are what a
520
+ * rollback left. In the clear on every
521
+ * row, an encrypted one included: a footer checksum of an encrypted object covers a random nonce and says nothing about
522
+ * its count, size or content, but the entries above the pointer show anyone who reads the registry that a rollback
523
+ * happened, and from which generation.
524
+ *
525
+ * Optional: absent means the row does not know, and a load then lists the bucket to collect. A shipped registry stores
526
+ * the list in the order given, at most 192 entries written and 384 read, and refuses a list that repeats a
527
+ * generation, one whose last entry is not the row's `currentGen`, and one whose current entry's checksum disagrees with
528
+ * a clear `summary`'s fingerprint. A stored list that disagrees with its row is read and not used. A patch that moves
487
529
  * `currentGen` without naming it drops it.
488
530
  */
489
- readonly keptGens?: readonly number[];
531
+ readonly knownGens?: readonly KnownGeneration[];
490
532
  /**
491
533
  * The leases on this segment's generations: each keeps one generation out of a load's collection until it ends.
492
- * Optional; absent means none. Unlike {@link keptGens} it does not follow the pointer: a patch that moves
534
+ * Optional; absent means none. Unlike {@link knownGens} it does not follow the pointer: a patch that moves
493
535
  * `currentGen` leaves it as it was, and only a patch that names it, or the row going away, changes it. Entries
494
536
  * past their end are ignored by every collector and pruned by the next write that names the list. A row's identity
495
537
  * is its token, so a purged and re-created segment starts with none.
@@ -510,7 +552,7 @@ export interface RegistryRecord extends SegmentRef {
510
552
  /**
511
553
  * The token of the most recent write that named a field a read resolves through: the row's create, or a compare-and-swap
512
554
  * whose patch named `currentGen`, `status`, `wrappedDeks`, `keyId` or `summary` (the resolved fields), at any value,
513
- * the one the field already had included. A write that names only `leases`, `retention`, `residency` or `keptGens`
555
+ * the one the field already had included. A write that names only `leases`, `retention`, `residency` or `knownGens`
514
556
  * leaves it. The registry sets it; a caller never does. Compared by equality only, and never reused under one name,
515
557
  * since a token is not: a reader keys what it caches on a generation with it, so a lease or a policy write leaves a
516
558
  * warm reader's cache standing, and a write that changes the resolution never does.
@@ -532,8 +574,8 @@ export interface NewRegistryRecord {
532
574
  readonly residency?: GovernanceMeta;
533
575
  /** Must name `currentGen` when given. */
534
576
  readonly summary?: RegistrySummary;
535
- /** Every entry below `currentGen`, ascending; see {@link RegistryRecord.keptGens}. */
536
- readonly keptGens?: readonly number[];
577
+ /** In the order made current, the last entry the new row's `currentGen`; see {@link RegistryRecord.knownGens}. */
578
+ readonly knownGens?: readonly KnownGeneration[];
537
579
  }
538
580
  /**
539
581
  * Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity, audit, `token` and `pointerId` are
@@ -542,14 +584,14 @@ export interface NewRegistryRecord {
542
584
  * Presence-based: a field the patch does not mention is left as it was. `summary` is the one exception, because it
543
585
  * describes the current generation: a patch that moves `currentGen` and does not mention `summary` drops the old
544
586
  * one rather than keep a description of another generation. A `summary` the patch gives must name the
545
- * `currentGen` the row will have. `keptGens` follows the pointer the same way: a patch that moves `currentGen` and does
546
- * not mention it drops it, and one that gives it must name only generations below the `currentGen` the row will have.
587
+ * `currentGen` the row will have. `knownGens` follows the pointer the same way: a patch that moves `currentGen` and does
588
+ * not mention it drops it, and one that gives it must end with the `currentGen` the row will have.
547
589
  * "Moves" means changes value: a patch that names `currentGen` at the value it has keeps both.
548
590
  *
549
591
  * A patch that names a resolved field (`currentGen`, `status`, `wrappedDeks`, `keyId`, `summary`), by presence and at any
550
592
  * value, sets the row's {@link RegistryRecord.pointerId} to the token the write returns; any other patch leaves it.
551
593
  */
552
- export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency' | 'summary' | 'keptGens' | 'leases'>>;
594
+ export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency' | 'summary' | 'knownGens' | 'leases'>>;
553
595
  /**
554
596
  * What a caller passes {@link IRegistryDriver.create} or {@link IRegistryDriver.compareAndSwap} besides the write itself.
555
597
  */
@@ -566,6 +608,25 @@ export interface RegistryWriteOptions {
566
608
  * settled by reading the row, never by this record. A driver that ignores the option reads the row.
567
609
  */
568
610
  readonly held?: RegistryRecord | null;
611
+ /**
612
+ * Called with the token this write carries, before the write is sent. A write sent again with a new token reports
613
+ * it; one that repeats the same body repeats the token already reported. The token is the one the write records:
614
+ * when the write lands, the row's {@link RegistryRecord.token} is it, and so is its {@link RegistryRecord.pointerId}
615
+ * when the write names a resolved field. A write that does not land leaves no row carrying it. A call refused before
616
+ * it has a token to send, on a conflict its own read finds, reports none.
617
+ *
618
+ * The token is unique to that write attempt, also against another writer racing the same version: two writers that
619
+ * send a write against the same row version never carry the same token, so a version counter alone is not enough.
620
+ * Otherwise a loser's write would pass for the winner's.
621
+ *
622
+ * It tells a writer which row writes are its own, whatever became of its request: a write that failed, or got no
623
+ * answer, may still have landed, and the row's `pointerId` names its writer when no later write has renewed it: it is
624
+ * then one of the tokens the writer was told. Only a registry that reports {@link IRegistryDriver.reportsWriteTokens}
625
+ * calls it; one that does not ignores it, and a writer then settles such a write by reading the object it published.
626
+ * A registry that wraps or extends another and overrides `create` or `compareAndSwap` passes this option through to
627
+ * the write it forwards, or sets the flag to `false`.
628
+ */
629
+ readonly onToken?: (token: Token) => void;
569
630
  }
570
631
  /** Capabilities a registry driver advertises; validated fail-fast at wiring time. */
571
632
  export interface RegCaps {
@@ -610,8 +671,8 @@ declare const STORAGE_BACKEND_BRAND: unique symbol;
610
671
  * Where a store keeps everything: the generations, and the pointer saying which one is current.
611
672
  *
612
673
  * **Branded on purpose.** The two fields alone are not enough to qualify, and that is the whole point. This
613
- * shape is also {@link LoadDeps}, so before the brand any `{ storage, registry }`
614
- * object literal satisfied it — including one assembling halves from two *unrelated* stores, which the store
674
+ * same two fields are the heart of the deps core's standalone functions take, so without the brand any `{ storage, registry }`
675
+ * object literal would satisfy it — including one assembling halves from two *unrelated* stores, which the store
615
676
  * accepted and then answered **empty** for a segment that holds data, because it read a pointer from a place
616
677
  * nothing had ever been written. That is the exact silent-empty failure one-class-per-backend exists to
617
678
  * remove, and it was reachable in five lines.
@@ -723,6 +784,19 @@ export declare function isStorageBackend(value: unknown): value is StorageBacken
723
784
  */
724
785
  export interface IRegistryDriver {
725
786
  capabilities(): RegCaps;
787
+ /**
788
+ * `true` when every `create` and `compareAndSwap` of this registry reports the token its write carries to
789
+ * {@link RegistryWriteOptions.onToken} before sending it, a resend included. A load, an erasure's rewrite and every
790
+ * other publish then knows whether a write that conflicted or got no answer landed by whether the row's `pointerId`
791
+ * is a token it was told, which names the writer and not only the bytes. Absent or `false`: the registry reports
792
+ * none, and such a write is settled by reading the object the publish wrote. A shipped registry says which it is.
793
+ *
794
+ * The flag may be a getter or a field. A registry that wraps or extends another and overrides `create` or
795
+ * `compareAndSwap` either passes `options` through to the write it forwards, or sets `reportsWriteTokens` to
796
+ * `false`. One that does neither reads every write of its own that landed after an error or a lost answer as not its
797
+ * own (`superseded`), and deletes nothing.
798
+ */
799
+ readonly reportsWriteTokens?: boolean;
726
800
  /**
727
801
  * The segment's record, or `null` if it doesn't exist (or was deleted). Throw {@link TransientError} for a fault
728
802
  * worth riding out (throttling, a 5xx, a dropped connection): a reader whose pointer refresh meets one keeps