@cloudbitmaps/core 0.19.1 → 0.20.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 (38) hide show
  1. package/dist/{chunk-RSLVTRJY.js → chunk-YSC7NJWV.js} +314 -216
  2. package/dist/chunk-YSC7NJWV.js.map +7 -0
  3. package/dist/core/audit.d.ts +56 -3
  4. package/dist/core/combine-many.d.ts +9 -6
  5. package/dist/core/consistency.d.ts +10 -4
  6. package/dist/core/crbm/fingerprint.d.ts +18 -0
  7. package/dist/core/crbm/format.d.ts +2 -4
  8. package/dist/core/crbm/frame.d.ts +10 -0
  9. package/dist/core/crbm/reader.d.ts +1 -8
  10. package/dist/core/crbm-storage-source.d.ts +229 -89
  11. package/dist/core/engine.d.ts +17 -14
  12. package/dist/core/erase-id.d.ts +62 -27
  13. package/dist/core/generation-gc.d.ts +2 -1
  14. package/dist/core/leases.d.ts +7 -0
  15. package/dist/core/load.d.ts +5 -3
  16. package/dist/core/metrics.d.ts +5 -5
  17. package/dist/core/pinned-storage-source.d.ts +3 -3
  18. package/dist/core/pointer-id.d.ts +19 -0
  19. package/dist/core/ports.d.ts +127 -35
  20. package/dist/core/reader-defaults.d.ts +14 -3
  21. package/dist/core/rollback.d.ts +2 -1
  22. package/dist/core/summary.d.ts +38 -21
  23. package/dist/core/token.d.ts +13 -8
  24. package/dist/driver-kit.d.ts +2 -1
  25. package/dist/driver-kit.js +11 -4
  26. package/dist/driver-kit.js.map +2 -2
  27. package/dist/drivers/_shared/object-registry.d.ts +1 -1
  28. package/dist/drivers/_shared/registry.d.ts +26 -17
  29. package/dist/drivers/localfs/storage.d.ts +7 -6
  30. package/dist/drivers/memory.d.ts +8 -6
  31. package/dist/drivers/retry/retrying-chunk-source.d.ts +1 -5
  32. package/dist/export/index.d.ts +16 -0
  33. package/dist/index.d.ts +2 -5
  34. package/dist/index.js +1169 -1041
  35. package/dist/index.js.map +4 -4
  36. package/package.json +1 -1
  37. package/dist/chunk-RSLVTRJY.js.map +0 -7
  38. package/dist/core/cost.d.ts +0 -351
@@ -53,7 +53,9 @@ export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
53
53
  * Hard ceiling on how many segments' readers (each holding a fully-parsed `.crbm` index) are cached at once
54
54
  * (default 1024) — a memory bound for a long-running server. Past it, the least-recently-used
55
55
  * segment's reader is evicted; the next read of an evicted segment re-opens it (one cheap tail GET, since
56
- * generations are immutable). Raise it for a big cache working set of small segments.
56
+ * generations are immutable). Raise it for a big cache working set of small segments. A source with a timed refresh
57
+ * also keeps up to 8 times this many segments' resolutions apart from their readers, for the TTL: the re-open then
58
+ * reads no row, and comes only when a read needs the reader, for the index or for a chunk.
57
59
  */
58
60
  readonly maxOpenSegments?: number;
59
61
  /**
@@ -67,7 +69,8 @@ export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
67
69
  * that region is at most this ceiling divided by {@link maxOpenSegments} (64 KiB by default) — its fair share — so a small generation's
68
70
  * chunks are served from memory with no further request, and a bigger one is read by range. Only a source with a timed
69
71
  * pointer refresh ({@link currentGenTtlMs} above 0, a registry and a clock) keeps chunk bytes. Lower it for memory-tight deployments with wide segments; a single
70
- * segment whose index alone exceeds the budget is still cached (it can't be shrunk) but nothing else alongside.
72
+ * segment whose index alone exceeds the budget is still cached (it can't be shrunk) but nothing else alongside. A
73
+ * sixteenth of it bounds the resolutions a source with a timed refresh keeps besides (4 MiB by default).
71
74
  */
72
75
  readonly maxOpenIndexBytes?: number;
73
76
  }
@@ -81,12 +84,12 @@ export interface PinnedObject {
81
84
  readonly fingerprint?: string;
82
85
  }
83
86
  /**
84
- * What a live version's resolution named, without the object's fingerprint: `<generation>:<row token>`, or
87
+ * What a live version's resolution named, without the object's fingerprint: `<generation>:<pointerId>`, or
85
88
  * `<generation>` with no registry. For a caller that compares a live version, from
86
- * {@link CrbmStorageChunkSource.currentVersion} or a chunk of its `getChunks`, with a row it read, which names no
87
- * object. A live version always ends with the suffix, which holds no `#`, so the match starts at its last `#` and a row
88
- * token holding `#` of its own is kept whole. Known limit: a string with no suffix, such as a pin's version or one
89
- * from another source, loses its end when its row token itself ends in `#<base 36>.<base 36>`; no caller passes one.
89
+ * {@link CrbmStorageChunkSource.currentVersion} or a chunk of its `getChunks`, with a row it read. A live version always
90
+ * ends with the suffix, which holds no `#`, so the match starts at its last `#` and a `pointerId` holding `#` of its own
91
+ * is kept whole. Known limit: a string with no suffix, such as a pin's version or one from another source, loses its end
92
+ * when its `pointerId` itself ends in `#<base 36>.<base 36>`; no caller passes one.
90
93
  */
91
94
  export declare function rowVersionOf(version: string): string;
92
95
  /** What a pin that leases its generation does between reading the row and opening the object. */
@@ -112,6 +115,21 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
112
115
  * steady-state memory bound; re-opening an evicted segment is one cheap tail GET.
113
116
  */
114
117
  private readonly snapshots;
118
+ /**
119
+ * Each segment's resolution, kept apart from its reader, by segment: what a read of its row found, for the TTL from
120
+ * the instant that read was sent, whether or not the reader cache still holds the segment's reader. So letting a reader
121
+ * go moves nothing: the next read builds a snapshot on the resolution, reads no row, and opens the object when it needs
122
+ * the index or a chunk the chunk cache does not hold (a version, a count and a stat from the row's summary need
123
+ * neither). It holds the resolved fields only ({@link Resolved}): the wrapped keys, never an unwrapped one, so a
124
+ * snapshot built on it unwraps the key again through the keystore. It never holds a resolution that found no
125
+ * generation: the next read of such a segment reads the row.
126
+ *
127
+ * Only a source with a timed refresh has one: without it, letting a reader go is one of the few things that ever moves
128
+ * a read on, and keeping the resolution would keep it forever. **Bounded** by count and by bytes, both derived from
129
+ * the reader cache's: {@link RESOLUTIONS_PER_OPEN_SEGMENT} times its count bound, and its byte bound over
130
+ * {@link RESOLUTION_BYTES_DIVISOR}. An entry heavier than the byte bound is kept alone, as the reader cache keeps one.
131
+ */
132
+ private readonly resolutions;
115
133
  /**
116
134
  * Pins ({@link heldKey}) whose object has been found replaced: another object is under its generation's key. Kept,
117
135
  * and bounded as the readers are, so that a pin's later chunk reads and any reopen fail at once rather than pay for
@@ -141,6 +159,13 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
141
159
  * the disagreement again.
142
160
  */
143
161
  private readonly distrusted;
162
+ /**
163
+ * The readers held to the row's summary: by a live open ({@link openLive}), or by a live pin that shared one. A pin
164
+ * that finds one memoised under its version shares it as it is: the version names the row's `pointerId`, and every
165
+ * write of the summary renews it, so the summary it was held to is the one the row has now. Any other memoised reader
166
+ * (a `pinAt`'s, held to the fingerprint its caller named) is held to the row's summary before a live pin shares it.
167
+ */
168
+ private readonly rowChecked;
144
169
  private readonly registry;
145
170
  private readonly keystore;
146
171
  private readonly requireEncryption;
@@ -151,8 +176,8 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
151
176
  private readonly currentGenTtlMs;
152
177
  /**
153
178
  * Whether the segment's pointer is re-read on a timer: it needs a clock (the TTL), a registry (the cheap row read; with
154
- * none, resolving is a listing of the bucket) and a TTL above 0. It decides the keeping of chunk bytes, the TTL lapse
155
- * and the refresh interval the cost report prices, so it is decided once.
179
+ * none, resolving is a listing of the bucket) and a TTL above 0. It decides the keeping of chunk bytes and the TTL
180
+ * lapse, so it is decided once.
156
181
  */
157
182
  private readonly timedRefresh;
158
183
  constructor(driver: IStorageDriver, options?: CrbmStorageChunkSourceOptions);
@@ -164,63 +189,90 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
164
189
  private readonly keys;
165
190
  private keyOf;
166
191
  /**
167
- * The segment's live snapshot, refreshing on the TTL. Cheap within the TTL window. The snapshot holds the resolved
168
- * target, and its reader once a read has asked for one.
192
+ * The segment's live snapshot, refreshing on the TTL. Cheap within the TTL window: the snapshot the reader cache holds,
193
+ * while its resolution is inside the TTL. With a timed refresh, a segment whose reader the cache let go is served a
194
+ * snapshot built on its kept resolution, while that is inside the TTL: no row is read, and the reader opens only when a
195
+ * read needs one. Otherwise the pointer is read now. The snapshot holds the resolved target, and its reader once a read
196
+ * has asked for one.
169
197
  */
170
198
  private liveSnapshot;
171
- /** The current resolved reader for a segment, refreshing on the TTL. */
172
- private resolvedReader;
173
199
  /**
174
- * A refresh failed transiently and the prior reader keeps serving: make this snapshot lapse after
175
- * {@link REFRESH_RETRY_MS} (never longer than the TTL) rather than after a whole TTL, so the next read after
176
- * that asks the registry again. It costs no read of its own; it only changes when the next refresh happens.
200
+ * Read the segment's pointer now. The read is sent before this returns, and the TTL counts from this instant: a read
201
+ * that answers late does not lengthen the life of what it found. The row read is always fresh, so what a snapshot on
202
+ * it says of the generation (its summary included) is what the row says now, never `prior`'s.
203
+ *
204
+ * A read that fails with a transient fault answers what `prior` found, when it found a generation, and is asked again
205
+ * soon ({@link retrySoon}): `prior` is the resolution of the snapshot the reader cache still holds, so an outage of the
206
+ * registry keeps that snapshot's generation, and its reader and unwrapped key where it has them; one resolved without
207
+ * an open, for a count or a generation lookup, unwraps the key from `prior`'s wrapped keys when a read first needs
208
+ * it. The outage ends shortly after the registry answers.
209
+ * That answer is the snapshot's alone ({@link Resolution.rodeOut}): it is not kept as the segment's resolution, and a
210
+ * snapshot built on it after the reader cache let that one go fails with the fault. With no `prior` the fault
211
+ * fails the read, as a cold resolve's does. If `prior` found none or failed, the pointer is read once more rather than
212
+ * a dead resolution re-armed (else the segment reads empty for a whole TTL window). Anything else — an access denial, a corrupt row, a
213
+ * registry that answers NotFound — fails the read exactly as a cold resolve of the segment would, and the resolution
214
+ * and the snapshot on it are forgotten, so a reader and the key it unwrapped do not outlive a refresh that could not
215
+ * be trusted.
216
+ */
217
+ private resolveNow;
218
+ /**
219
+ * A refresh failed transiently and the prior resolution keeps serving: make it lapse after {@link REFRESH_RETRY_MS}
220
+ * (never longer than the TTL) rather than after a whole TTL, so the next read after that asks the registry again. It
221
+ * costs no read of its own; it only changes when the next refresh happens.
177
222
  */
178
223
  private retrySoon;
179
224
  /**
180
- * TTL elapsed: cheaply re-resolve the pointer. The row read is always fresh, so what the new snapshot says of the
181
- * generation (its summary included) is what the row says now, never the prior snapshot's. The reader is reopened
182
- * only if the generation or the row changed, and only when a read asks for it.
225
+ * Keep `resolution` as the segment's, from the instant its read was sent, so the reads that come while it is in flight
226
+ * share it, and a read after the reader cache let the segment go builds on it. Its weight is reported once it lands,
227
+ * and one that finds no generation, or fails, is forgotten. Both are identity-guarded, as the reader cache's are, and
228
+ * nothing is installed when the read lands: an `invalidate` while it is in flight, or a newer resolution, is never
229
+ * undone by its answer.
183
230
  */
184
- private refreshedTarget;
231
+ private remember;
185
232
  /**
186
- * Memoize a snapshot as the segment's at `now()`, forgetting it later if it resolves to null / throws
187
- * (identity-guarded, so a stale cleanup never clobbers a fresher snapshot). Installing the (pending) snapshot
188
- * **synchronously** is what lets concurrent callers coalesce onto one in-flight resolve.
233
+ * A live snapshot on `resolution`. `before` is the snapshot it replaces, while the reader cache still holds one: its
234
+ * reader is kept if the resolution finds the same generation, `pointerId` and object ({@link Snapshot.reuse}), and its
235
+ * unwrapped key if the same wrapped keys. A snapshot built after the reader cache let the segment go has neither, so
236
+ * its open asks the keystore again: a key is held only while a reader of the segment is.
189
237
  */
190
- private install;
191
- private now;
238
+ private snapshotOn;
239
+ /** What the snapshot on `resolution` that replaces `before` reads: see {@link snapshotOn}. */
240
+ private targetOn;
192
241
  /**
193
- * How often this source re-reads a segment's pointer while the segment is being read, in ms: its TTL, or 0 when
194
- * it has no timed refresh (no clock, no registry, or a TTL of 0). The grounded cost report prices the refresh at
195
- * this.
242
+ * Memoize a snapshot as the segment's, forgetting it later if it resolves to null / throws (identity-guarded, so a
243
+ * stale cleanup never clobbers a fresher snapshot). Installing the (pending) snapshot **synchronously** is what lets
244
+ * concurrent callers coalesce onto one in-flight resolve.
196
245
  */
197
- get pointerRefreshMs(): number;
246
+ private install;
247
+ private now;
198
248
  /**
199
249
  * Whether a stream reading from `snap` must resolve its segment again before its next chunk, as a read of one chunk
200
- * would: the snapshot's TTL has lapsed, an invalidation has happened since `epoch`, or the snapshot is no longer the
201
- * one the reader cache holds for the segment (it let it go, or another read installed a newer one). Else nothing can
202
- * have moved it, and the check costs a compare and a lookup.
250
+ * would. In this order: an invalidation has happened since `epoch` (what the store that erased stops on, at once);
251
+ * the TTL of the snapshot's resolution has lapsed; or that resolution is no longer the segment's: another read
252
+ * installed a newer one (a refresh, or a heal), or the resolution cache let it go. With no resolution cache (no timed
253
+ * refresh) the last test is of the snapshot itself, so there the reader cache letting it go is a move, as it is one of
254
+ * the few things that ever moves such a store's read on. With one, it is not. Else nothing can have moved it, and the
255
+ * check costs a compare and a lookup in each cache.
203
256
  */
204
257
  private mayHaveMoved;
205
258
  private expired;
206
259
  /**
207
- * The segment's current generation number — the engine keys its chunk cache by this so a generation bump
208
- * is observed instead of serving a stale decoded chunk. Served from the (TTL-refreshed)
209
- * snapshot, so no extra backend read within the TTL window. `null` if the segment has no committed generation.
260
+ * The segment's current generation number, as its snapshot's resolution found it: no backend read within the TTL
261
+ * window, and no open of the object, ever. `null` if the segment has no committed generation. A generation swept from
262
+ * under the resolution is met by the read that fetches from it, which heals ({@link withFreshSnapshot}). Since it
263
+ * opens nothing, the only error it meets is the resolution's own, and it fails with that error at once: there is
264
+ * nothing to heal. The reads that can open the object (`listChunkKeys`, `summary`, `currentVersion` and the rest)
265
+ * resolve once more on any `NotFoundError`, a registry's among them, since theirs can be a swept generation.
210
266
  *
211
- * Heals a swept generation exactly like {@link withFreshSnapshot}: a caller that resolves the generation once
212
- * per op, before any chunk fetch, would otherwise fail the whole operation rather than one chunk. The engine
213
- * keys by {@link currentVersion} instead, which heals the same way, and falls back to this for a source that
214
- * cannot report a version. It is spelled out rather than delegated because a lookup like this runs once per
215
- * operand of every `has`/`count`/`iterate`/`intersect`, almost always served from the cached snapshot with
216
- * no backend call at all. Routing it through the generic helper cost ~115 ns/op on that path (a second async
217
- * frame, a per-call closure, and an `await` on a plain number) for a race that fires only during a
218
- * concurrent sweep. One retry, then propagate — same contract, same eviction rule.
267
+ * It is spelled out rather than delegated because a lookup like this runs once per operand of every
268
+ * `has`/`count`/`iterate`/`intersect`, almost always served from the cached snapshot with no backend call at all.
269
+ * Routing it through the generic helper cost ~115 ns/op on that path (a second async frame, a per-call closure, and
270
+ * an `await` on a plain number).
219
271
  */
220
272
  currentGeneration(ref: SegmentRef): Promise<number | null>;
221
273
  /**
222
- * Forget everything derived from `ref`: the resolved snapshot and the open reader behind it (and with the
223
- * reader, the DEK it unwrapped at open time), and what its pins' objects were found to be. The next read
274
+ * Forget everything derived from `ref`: its resolution, the resolved snapshot and the open reader behind it (and with
275
+ * the reader, the DEK it unwrapped at open time), and what its pins' objects were found to be. The next read
224
276
  * resolves the segment again.
225
277
  *
226
278
  * Needed because the TTL refresh only ever answers "has the pointer moved?", and every *destructive* verb —
@@ -231,24 +283,34 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
231
283
  */
232
284
  invalidate(ref: SegmentRef): void;
233
285
  /**
234
- * `<generation>#<object>` for a registry-less source, or `<generation>:<row token>#<object>` with one: the generation
235
- * the segment's snapshot resolved, and the object its reader opened, by that object's size and footer checksum in
236
- * base 36, which the open read anyway. The token moves on every row write, so an unrelated write (a `setRetention`)
237
- * costs the segment's decoded chunks once — bounded, and on an admin path. Two objects do not share a version
238
- * string, as far as their sizes and footer checksums tell them apart, whichever way a number came to name a second
239
- * one: a name purged and loaded again (a new row, or with no registry the same bare number), a number taken again
240
- * within one row once its object was deleted, or an object put back under its key from outside the library. That
241
- * holds however long after its row read a reader opens: a snapshot that a `count()` resolved from the row opens its
242
- * reader only when a later read needs it, and the object under the key may have changed in between. The version is
243
- * opaque: compare two for equality.
286
+ * `<generation>#<object>` for a registry-less source, or `<generation>:<pointerId>#<object>` with one: the generation
287
+ * the segment's snapshot resolved, and the object a reader of it opens, by that object's size and footer checksum in
288
+ * base 36. The row's `pointerId` moves only with a write that names a field a read resolves through, at any value, so
289
+ * a lease or a policy write (a `setRetention`) leaves the version, the open reader and the decoded chunks as they were.
290
+ * Two objects do not share a version string, as far as their sizes and footer checksums tell them apart, whichever way
291
+ * a number came to name a second one: a name purged and loaded again (a new row, or with no registry the same bare
292
+ * number), a number taken again within one row once its object was deleted, or an object put back under its key from
293
+ * outside the library. The version is opaque: compare two for equality.
244
294
  *
245
- * This is the call the engine makes **once per operand of every read**, before any chunk fetch, to key its
246
- * chunk cache, so it heals a swept generation exactly as {@link currentGeneration} does: unhealed, a cold read
247
- * racing a publish and a `keep: 0` sweep failed the whole operation with `NotFoundError`, where the same race
248
- * on a chunk fetch heals. Spelled out rather than delegated for the reason `currentGeneration` gives: it is
249
- * almost always served from the cached snapshot with no backend call at all. One retry, then propagate.
295
+ * Where the row's summary names the object, the version is answered from the resolution, with no open: every live
296
+ * open of the generation is held to that object ({@link openLive}), refused as a move otherwise, so the one object any
297
+ * reader of this resolution can name is the one the summary names, and a version named from it is the one that
298
+ * reader's would be. So a `has()` whose chunk is in the chunk cache opens nothing, after the reader cache let
299
+ * the segment go included (a read that needs the index opens the object for it, and on an encrypted segment the
300
+ * key is unwrapped to read the row's sealed summary); an object under the number that is not the row's is met by the read that fetches from it,
301
+ * which refuses it and heals. Where the row has no summary it can use, or there is no registry, nothing names the
302
+ * object but the object: the reader is opened, and the version names what it opened, however long after its row read
303
+ * that is.
304
+ *
305
+ * This is the call the engine makes **once per operand of every read**, before any chunk fetch, to key its chunk
306
+ * cache, so an open it makes heals a swept generation as a read does: unhealed, a cold read racing a publish and a
307
+ * `keep: 0` sweep failed the whole operation with `NotFoundError`, where the same race on a chunk fetch heals. Spelled
308
+ * out rather than delegated for the reason `currentGeneration` gives: it is almost always served from the cached
309
+ * snapshot with no backend call at all. One retry, then propagate.
250
310
  */
251
311
  currentVersion(ref: SegmentRef): Promise<string | null>;
312
+ /** {@link currentVersion} of one snapshot: named by the row's summary when it can be, else by the reader, opened. */
313
+ private versionOn;
252
314
  /**
253
315
  * Resolve the segment **once** and report what a pin should hold: the generation, and the version that
254
316
  * identifies those exact bytes. `null` when the segment resolves to no generation — there is nothing to pin,
@@ -282,7 +344,7 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
282
344
  private pinOnce;
283
345
  /**
284
346
  * Where a pinned reader of one object is memoised. With a registry that is its version, which names the object:
285
- * a name purged and loaded again gets a new row, and with it a new token. Without one the version is the bare
347
+ * a name purged and loaded again gets a new row, and with it a new `pointerId`. Without one the version is the bare
286
348
  * generation number, which two incarnations share, so the object's fingerprint is added to tell them apart.
287
349
  */
288
350
  private pinnedKey;
@@ -314,9 +376,10 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
314
376
  * **And the object is checked**, when the caller says which one it pinned. A generation number is not an
315
377
  * identity (invariant 1): once a name is purged and loaded again, its new segment starts again at generation 0,
316
378
  * so a pin of the old one would open the new one's object as its own, and read it beside the chunks it had
317
- * already cached from the old one. The row's token cannot tell them apart, since it moves on every write, a
318
- * publish included; the object's fingerprint can, and a pin whose object has been replaced fails, as one whose
319
- * generation has been swept does. The memo is keyed so that two pins of one generation number in two
379
+ * already cached from the old one. The row's `pointerId` cannot tell them apart, since it moves with every write
380
+ * that changes what the row resolves to, a publish included, while the pinned object stays what it was; the
381
+ * object's fingerprint can, and a pin whose object has been replaced fails, as one whose generation has been swept
382
+ * does. The memo is keyed so that two pins of one generation number in two
320
383
  * incarnations never share a reader, with a registry or without one ({@link pinnedKey}).
321
384
  */
322
385
  private readerAt;
@@ -343,7 +406,7 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
343
406
  * for {@link getChunkAt}.
344
407
  */
345
408
  cardinalitiesAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<ReadonlyMap<number, number> | null>;
346
- /** Grounded size of a specific generation. `held` as for {@link getChunkAt}. */
409
+ /** Size of a specific generation. `held` as for {@link getChunkAt}. */
347
410
  sizeOfAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<SegmentSize | null>;
348
411
  /**
349
412
  * Whether the segment is a registered name at all. With a registry that is exactly "a row exists" — including
@@ -356,30 +419,48 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
356
419
  */
357
420
  exists(ref: SegmentRef): Promise<boolean>;
358
421
  /**
359
- * Evict a snapshot we just failed to read from — matched by the snapshot itself, so a concurrent call's
360
- * fresher snapshot is never clobbered. The identity guard is the whole point: `install` replaces the entry
361
- * wholesale, so comparing anything else would drop a good snapshot on the floor.
422
+ * Evict a snapshot we just failed to read from, and the resolution it was built on — each matched by identity, so a
423
+ * concurrent call's fresher snapshot or resolution is never clobbered. The identity guard is the whole point: `install`
424
+ * and `remember` replace an entry wholesale, so comparing anything else would drop a good one on the floor. The
425
+ * resolution goes too, or the read's retry would build its snapshot on it again and reopen the generation it just
426
+ * found gone; and so does a snapshot the reader cache holds on that same resolution, which another read built once the
427
+ * cache had let `snap` go, or the retry would open that one.
362
428
  */
363
429
  private dropStale;
364
430
  /** Open a {@link CrbmReader} for an already-resolved generation target (decrypting if the segment is encrypted). */
365
431
  private openForTarget;
366
432
  private openGeneration;
367
433
  /**
368
- * Open the generation a resolution found, and hold the row's summary against the object while it is open anyway:
369
- * the same count and metadata, or the summary is not believed again in this process ({@link distrusted}). This
370
- * costs no request; a mismatch is not an error, since the object is the truth and this read has it.
434
+ * Open the generation a resolution found, and hold the row's summary against the object: the object must be the one
435
+ * the summary names by its fingerprint, and is refused before its index is opened otherwise, as another object under
436
+ * the generation's number ({@link NotFoundError}, so the read resolves the row again, as for a swept generation); and
437
+ * it should hold the same count and metadata, or the summary is not believed again in this process
438
+ * ({@link distrusted}). Neither check costs a request. A row with no summary it can use names no object, and its
439
+ * generation's object is opened as it is.
371
440
  */
372
441
  private openLive;
373
442
  private distrustKey;
374
443
  /** What the row says of the generation, unless it is a summary this process has found to disagree with its object. */
375
444
  private trustedSummary;
376
445
  /**
377
- * The current generation + its DEK wrappings: the registry's authoritative record (one strong read), or a
378
- * `list` scan for the max generation when there's no registry (a registry-less source can only read cleartext
379
- * — there's nowhere a wrapped DEK could live). The row's summary rides along, to be used or not by whoever
380
- * asks.
446
+ * What the segment resolves to now: the fields of the registry's authoritative record a read resolves through (one
447
+ * strong read), or the highest generation a `list` of the bucket finds when there is no registry (a registry-less
448
+ * source can only read cleartext: there is nowhere a wrapped DEK could live). `null` for no generation.
449
+ */
450
+ private resolve;
451
+ /**
452
+ * The segment resolved afresh for a pin, with the row it read, which a lease is written against. Never a kept
453
+ * resolution, and kept nowhere: the generation current right now is the whole promise of a pin.
381
454
  */
382
455
  private resolveLive;
456
+ /**
457
+ * What a snapshot, or a pin, makes of a resolution: its decryption context and what the row's summary says of the
458
+ * generation, each made only when asked for. A snapshot that replaces `prior` with the same wrapped keys keeps the key
459
+ * `prior` unwrapped, so a count polling an encrypted segment asks the keystore once, not once each `genTtlMs`.
460
+ * Different keys (a shred, a re-created name) unwrap afresh, and so does a snapshot with no prior: the resolution
461
+ * holds the wrapped keys, never an unwrapped one.
462
+ */
463
+ private liveOf;
383
464
  /** Build the per-generation decryption context for an encrypted segment (undefined for cleartext). */
384
465
  private cryptoForRead;
385
466
  getChunk(ref: ChunkRef): Promise<Uint8Array | null>;
@@ -407,16 +488,35 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
407
488
  * it can: the row's summary of the generation it names, on an active row, in the shape the row's keys call for
408
489
  * (a sealed one is opened with the segment's key), and one this process has not found to disagree with its
409
490
  * object. That is one registry read when the snapshot is cold, none when it is warm, and no read of the object.
410
- * With no summary it can use (an older row, one for another generation, one that does not open), it opens the
411
- * generation and answers from the object. A row that names an object that is gone still answers, while a read
412
- * of the object throws: the number is true of the generation the row names.
491
+ * With no summary it can use (a row written without one, one for another generation, one that does not open), it
492
+ * opens the generation and answers from the object. A row that names an object that is gone, or another object
493
+ * under the number, still answers, while a read of the object throws: the number is true of the generation the row
494
+ * names.
413
495
  *
414
496
  * What the row says is not confirmed here: a summary edited to agree with itself by whoever can write the row is
415
497
  * believed until the next time the object is opened, which holds it against the summary.
416
498
  */
417
499
  summary(ref: SegmentRef): Promise<GenerationSummary | null>;
500
+ /**
501
+ * What one resolution of the segment says of its generation: from the row's summary when this process may use it,
502
+ * else from the generation's object, opened. `null` for no generation. One heal, as a read of the object has: a
503
+ * generation swept from under the snapshot, or replaced under its number, is resolved again once.
504
+ */
505
+ private described;
418
506
  /** {@link summary} of a specific generation, from the object a pin holds. `held` as for {@link getChunkAt}. */
419
507
  summaryAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<GenerationSummary | null>;
508
+ /**
509
+ * The current generation's number, id count and metadata, with its object's size, all from one resolution. The answer
510
+ * comes from the row when it can, as {@link summary}'s does: the row's summary of the generation it names, whose
511
+ * fingerprint carries the object's size. That is one registry read when the snapshot is cold, none when it is warm,
512
+ * and no read of the object; on an encrypted segment the key is unwrapped to open the summary, as for a count. With no
513
+ * summary it can use, it opens the generation and answers from its footer and index, with no payload read: a tail read
514
+ * more when cold. A row that names an object that is gone, or another object, still answers from the row: the figures
515
+ * are the generation's the row names, and `checkConsistency` is what finds such a row.
516
+ */
517
+ stat(ref: SegmentRef): Promise<(GenerationSummary & SegmentSize) | null>;
518
+ /** {@link stat} of a specific generation, from the object a pin holds. `held` as for {@link getChunkAt}. */
519
+ statAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<(GenerationSummary & SegmentSize) | null>;
420
520
  /**
421
521
  * Run `read` against the pinned snapshot, healing the torn-read window generation GC can open: if the
422
522
  * generation we resolved was superseded *and* swept (the grace window elapsed), the Storage driver throws
@@ -424,8 +524,8 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
424
524
  * caller-visible error, so instead we drop the stale snapshot,
425
525
  * re-resolve `currentGen`, and retry once — the read then serves the newer (committed, immutable) generation,
426
526
  * a monotonic move forward — within one incarnation of the row. A name that was retired and re-created is a
427
- * different segment and can resolve to a LOWER generation, which is why the snapshot carries the row's token
428
- * as its lineage rather than trusting the generation number (invariant 1).
527
+ * different segment and can resolve to a LOWER generation, which is why the snapshot carries the row's
528
+ * `pointerId` as its lineage rather than trusting the generation number (invariants 1 and 2).
429
529
  *
430
530
  * **Resolution and open are inside the retry, not before it.** Resolving `currentGen` and opening that
431
531
  * generation's object are two backend round trips with a gap between them, so the object can be swept after
@@ -475,13 +575,27 @@ export declare function writeCrbmGeneration(driver: IStorageDriver, key: GenKey,
475
575
  fingerprint: string;
476
576
  }>;
477
577
  /**
478
- * Whether the object under `key` is provably the one `fingerprint` names ({@link CrbmReader.fingerprint}), from one
479
- * read of its footer. False when it is another object, when it is gone, and when the read fails in any way: a caller
480
- * that deletes on `true` deletes nothing it cannot prove is its own.
578
+ * The object under `key`, when it is provably the one `fingerprint` names ({@link CrbmReader.fingerprint}), from one
579
+ * read of its footer, with the version the driver reported on that read (`undefined` from a driver that reports none).
580
+ * `null` when it is another object, when it is gone, and when the read fails in any way: a caller that deletes what
581
+ * this proves deletes nothing it cannot prove is its own, and passes the version as the delete's `ifVersion`, so a
582
+ * number taken again between the read and the delete keeps the object put under it since.
583
+ */
584
+ export declare function provenObject(storage: IStorageDriver, key: GenKey, fingerprint: string): Promise<{
585
+ readonly version: string | undefined;
586
+ } | null>;
587
+ /**
588
+ * Whether the object under `key` is still the one `fingerprint` names ({@link CrbmReader.fingerprint}), from one read
589
+ * of its footer. Unlike {@link provenObject}, a fault is not an answer: `NotFoundError` when the key holds no object,
590
+ * and any other failure of the read, thrown as it is.
591
+ */
592
+ export declare function sameObjectUnder(storage: IStorageDriver, key: GenKey, fingerprint: string): Promise<boolean>;
593
+ /**
594
+ * Whether the object under `key` says it is encrypted, from one read of its footer, with no key. `noteVersion` is shown
595
+ * the version the driver reported on that read, when it reported one: a caller that deletes the object for what its
596
+ * footer says passes it as the delete's `ifVersion`.
481
597
  */
482
- export declare function holdsObject(storage: IStorageDriver, key: GenKey, fingerprint: string): Promise<boolean>;
483
- /** Whether the object under `key` says it is encrypted, from one read of its footer, with no key. */
484
- export declare function objectIsEncrypted(storage: IStorageDriver, key: GenKey): Promise<boolean>;
598
+ export declare function objectIsEncrypted(storage: IStorageDriver, key: GenKey, noteVersion?: (version: string) => void): Promise<boolean>;
485
599
  /**
486
600
  * What one tail read of the object under `key` tells a rollback about it: whether the footer says it is encrypted, and,
487
601
  * when the object and the key agree (an encrypted object opened with `crypto`, a cleartext one with none), the reader
@@ -678,6 +792,12 @@ export interface PublishResult {
678
792
  * was settled without knowing which write it was), and a collection then lists.
679
793
  */
680
794
  readonly kept?: PublishedKept;
795
+ /**
796
+ * A token of the row whose pointer is at the generation, when it is published: the one the write was given, or the
797
+ * row read that showed the pointer there. Every write of a row keeps its incarnation, so this names the incarnation
798
+ * the publish landed in, for the audit event.
799
+ */
800
+ readonly token?: Token;
681
801
  }
682
802
  /**
683
803
  * {@link publishGenerationKept}'s answer to "did the pointer move", for a caller with no use for what it recorded:
@@ -686,7 +806,7 @@ export interface PublishResult {
686
806
  export declare function publishGeneration(registry: IRegistryDriver, key: GenKey, options?: Parameters<typeof publishGenerationKept>[2]): Promise<boolean>;
687
807
  /**
688
808
  * Whether the object under `key` is the one `fingerprint` names, from one read of its footer: false when it is another
689
- * object or gone. Unlike {@link holdsObject}, a read that cannot tell throws, for a caller that must not take "could
809
+ * object or gone. Unlike {@link provenObject}, a read that cannot tell throws, for a caller that must not take "could
690
810
  * not look" for "not this object".
691
811
  */
692
812
  export declare function provesOwnObject(storage: IStorageDriver, key: GenKey, fingerprint: string): Promise<boolean>;
@@ -829,9 +949,29 @@ export declare function bulkLoadAhead(driver: IStorageDriver, ref: SegmentRef, a
829
949
  /**
830
950
  * Open a {@link CrbmReader} on one generation over the storage driver's range/tail reads (decrypting if `crypto`).
831
951
  * The reader the write paths use to re-read what they wrote, and the erasure rewrite uses to stream the old
832
- * generation; the engine's read path goes through {@link CrbmStorageChunkSource} instead, which caches these.
952
+ * generation; the engine's read path goes through {@link CrbmStorageChunkSource} instead, which caches these. With
953
+ * `named`, the fingerprint the row's summary records, the object must be that one: another is {@link NotFoundError}.
954
+ * The version of the object it opened, from the tail read the open made, is what {@link objectVersionOf} answers.
955
+ */
956
+ export declare function openGenerationReader(storage: IStorageDriver, key: GenKey, crypto: CrbmCrypto | undefined, options?: Omit<CrbmReaderOptions, 'crypto'>, named?: string): Promise<CrbmReader>;
957
+ /**
958
+ * {@link openGenerationReader}, with `noteVersion` shown the version {@link objectVersionOf} would answer as soon as the
959
+ * tail read that carries it returns, before the open goes on: a caller whose open then fails, on an object sealed under
960
+ * another key, say, still knows which object it met, and can condition a delete of it on that read.
961
+ */
962
+ export declare function openGenerationReaderNoting(storage: IStorageDriver, key: GenKey, crypto: CrbmCrypto | undefined, noteVersion: (version: string) => void): Promise<CrbmReader>;
963
+ /**
964
+ * The version of the object `reader` was opened on, as the driver reported it on the tail read the open made, for a
965
+ * delete conditioned on that object ({@link StorageDeleteOptions.ifVersion}); `undefined` for a reader
966
+ * {@link openGenerationReader} did not open, or a driver that reports none. Held beside the reader rather than on it, so
967
+ * nothing is added to the reader's public surface, and no read the engine makes pays for it.
968
+ */
969
+ export declare function objectVersionOf(reader: CrbmReader): string | undefined;
970
+ /**
971
+ * {@link openGenerationReader}, of the object `fingerprint` names: `undefined` when the object under the key is another
972
+ * one, found from its footer before its index is opened, so an object sealed under another key is told apart as well.
833
973
  */
834
- export declare function openGenerationReader(storage: IStorageDriver, key: GenKey, crypto: CrbmCrypto | undefined, options?: Omit<CrbmReaderOptions, 'crypto'>): Promise<CrbmReader>;
974
+ export declare function openNamedGeneration(storage: IStorageDriver, key: GenKey, crypto: CrbmCrypto | undefined, fingerprint: string): Promise<CrbmReader | undefined>;
835
975
  /**
836
976
  * Re-open a freshly written generation and assert it round-trips exactly what was streamed into it: the same
837
977
  * per-chunk key set *and* the same total cardinality (on top of the codec's own per-chunk CRC + footer checks), and the
@@ -3,7 +3,7 @@ import type { Budget, BudgetOption } from './budget.js';
3
3
  import type { Clock } from './determinism.js';
4
4
  import type { BoundedLru } from './lru.js';
5
5
  import type { IMetricsSink } from './metrics.js';
6
- import type { GenerationMetadata, StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
6
+ import type { GenerationMetadata, StorageChunkSource, SegmentRef } from './ports.js';
7
7
  /**
8
8
  * Range requests a stream holds ahead of a combine or `iterate` by default (chunk keys, on a source that reads chunk by
9
9
  * chunk, and for `count`'s fallback) — bounds memory and concurrent reads (invariant 6). A read that does not fit
@@ -128,20 +128,21 @@ export declare class SegmentEngine {
128
128
  */
129
129
  count(seg: SegmentRef): Promise<number>;
130
130
  /**
131
- * What the segment's current generation is: its number, its id count and its metadata, from one resolution, so the
132
- * three cannot straddle a publish. A source with no summary answers from `count()` and the generation it resolves.
131
+ * What the segment's current generation is: its number, its id count, its metadata and the size of its object in
132
+ * bytes. A source with a `stat` (the `.crbm` source) answers all four from one resolution, so they cannot straddle
133
+ * a publish: from the registry row's summary when it can use it, else from the opened generation's footer and
134
+ * index, with no payload read. A source without one answers from its other methods, each its own resolution: its
135
+ * summary when it has one, else `count()` and the generation it resolves, and its `sizeOf`, or `sizeBytes: null`
136
+ * when it has none. A segment with no generation answers `{ generation: null, cardinality: 0, sizeBytes: null }`.
133
137
  */
134
138
  stat(seg: SegmentRef): Promise<{
135
139
  generation: number | null;
136
140
  cardinality: number;
137
141
  metadata?: GenerationMetadata;
142
+ sizeBytes: number | null;
138
143
  }>;
139
- /** Whether the Storage source can measure segment size (for grounded cost); false ⇒ storage isn't grounded. */
140
- get supportsStorageSize(): boolean;
141
- /** How often the Storage source re-reads a pointer while reading (cost reporting); undefined if it does not say. */
142
- get pointerRefreshMs(): number | undefined;
143
- /** Grounded Storage size of a segment's current generation (cost reporting), or null if it has no generation. */
144
- segmentSize(seg: SegmentRef): Promise<SegmentSize | null>;
144
+ /** The size of the segment's current generation from the source's `sizeOf`, or `null` when it cannot say. */
145
+ private sizeOf;
145
146
  /** The chunks of `seg` at `chunkKeys`, read through a window of {@link DEFAULT_INTERSECT_CONCURRENCY}. */
146
147
  private chunkWindow;
147
148
  /**
@@ -219,11 +220,13 @@ export declare class SegmentEngine {
219
220
  * the result — every chunk read is a whole, checksum-verified, immutable generation. The edge a *long* call can
220
221
  * hit is invariant 3's: if it straddles a mid-call `cache.genTtlMs` boundary and a load has published, an
221
222
  * operand's not-yet-requested chunks may re-resolve forward to the newer generation (a generation hop within one long
222
- * call) — the call never crashes or returns a torn object, but may mix generations. Three things hop it without
223
- * waiting for the TTL, so a shorter call can meet them too: **the reader cache evicting an operand mid-call**
224
- * (`maxOpenSegments`), whose re-read re-resolves fresh; a sweep deleting the generation it was reading, which
225
- * heals the read forward; and an invalidation, which this store's own `load`, `rollback` and `eraseSubject` make
226
- * and `invalidate()` makes on request. Still whole/immutable per read, never torn. An operand's stream resolves its
223
+ * call) — the call never crashes or returns a torn object, but may mix generations. Some things hop it without
224
+ * waiting for the TTL, so a shorter call can meet them too: **the reader cache evicting an operand mid-call** on a
225
+ * store with no timed refresh (`maxOpenSegments`), whose re-read re-resolves fresh, and on a store with one the
226
+ * resolution cache letting the operand's resolution go; a sweep deleting the generation it was reading, or another
227
+ * object under its number, met by this read or another, which heals the read forward, as another read's failed open
228
+ * of the operand does; and an invalidation, which this store's own `load`, `rollback` and `eraseSubject` make and
229
+ * `invalidate()` makes on request. Still whole/immutable per read, never torn. An operand's stream resolves its
227
230
  * segment again before it serves each chunk, so each of those moves a running read as it would a read of one chunk, and
228
231
  * the ranges it had requested of the earlier generation are dropped, not served.
229
232
  */