@cloudbitmaps/core 0.19.0 → 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 (39) hide show
  1. package/dist/{chunk-TNWTIAON.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 +13 -4
  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 -3
  10. package/dist/core/crbm-storage-source.d.ts +249 -96
  11. package/dist/core/engine.d.ts +22 -14
  12. package/dist/core/erase-id.d.ts +62 -26
  13. package/dist/core/generation-gc.d.ts +3 -2
  14. package/dist/core/keys.d.ts +28 -0
  15. package/dist/core/leases.d.ts +7 -0
  16. package/dist/core/load.d.ts +11 -7
  17. package/dist/core/metrics.d.ts +5 -5
  18. package/dist/core/pinned-storage-source.d.ts +3 -3
  19. package/dist/core/pointer-id.d.ts +19 -0
  20. package/dist/core/ports.d.ts +131 -35
  21. package/dist/core/reader-defaults.d.ts +14 -3
  22. package/dist/core/rollback.d.ts +2 -1
  23. package/dist/core/summary.d.ts +38 -21
  24. package/dist/core/token.d.ts +13 -8
  25. package/dist/driver-kit.d.ts +2 -1
  26. package/dist/driver-kit.js +11 -4
  27. package/dist/driver-kit.js.map +2 -2
  28. package/dist/drivers/_shared/object-registry.d.ts +1 -1
  29. package/dist/drivers/_shared/registry.d.ts +26 -17
  30. package/dist/drivers/localfs/storage.d.ts +7 -6
  31. package/dist/drivers/memory.d.ts +8 -6
  32. package/dist/drivers/retry/retrying-chunk-source.d.ts +1 -5
  33. package/dist/export/index.d.ts +16 -0
  34. package/dist/index.d.ts +2 -5
  35. package/dist/index.js +1288 -1080
  36. package/dist/index.js.map +4 -4
  37. package/package.json +1 -1
  38. package/dist/chunk-TNWTIAON.js.map +0 -7
  39. package/dist/core/cost.d.ts +0 -351
@@ -38,8 +38,9 @@ export interface GenKey extends SegmentRef {
38
38
  readonly generation: number;
39
39
  }
40
40
  /**
41
- * A segment's grounded on-disk footprint — the current generation's Storage object bytes, read cheaply from the
42
- * `.crbm` footer/index (no payload reads). Powers the grounded `costReport()`.
41
+ * A segment's on-disk footprint — the current generation's Storage object bytes, read cheaply from the `.crbm`
42
+ * footer/index (no payload reads). It is the `sizeBytes` a segment's `stat()` reports, and what bounds the bytes a combine
43
+ * holds for an operand's stream.
43
44
  */
44
45
  export interface SegmentSize {
45
46
  readonly sizeBytes: number;
@@ -119,16 +120,10 @@ export interface StorageChunkSource {
119
120
  */
120
121
  getChunks?(ref: SegmentRef, keys: readonly number[], options?: ReadChunksOptions): AsyncIterable<ChunkRead>;
121
122
  /**
122
- * Optional: the current generation's grounded size, cheaply (from the already-parsed `.crbm` index — no
123
- * payload reads), or `null` if the segment has no Storage generation. Powers the grounded `costReport()`.
123
+ * Optional: the current generation's size, cheaply (from the already-parsed `.crbm` index — no payload reads), or
124
+ * `null` if the segment has no Storage generation.
124
125
  */
125
126
  sizeOf?(ref: SegmentRef): Promise<SegmentSize | null>;
126
- /**
127
- * Optional: how often this source re-reads a segment's pointer while the segment is being read, in ms, or 0 if
128
- * it never does. The grounded `costReport()` prices the pointer refresh at this interval; a source that omits it
129
- * is priced at the store's default.
130
- */
131
- readonly pointerRefreshMs?: number;
132
127
  /**
133
128
  * Optional: the current generation's **per-chunk cardinality** (`chunkKey → count`), read from the
134
129
  * already-parsed `.crbm` index with **no payload reads**, or `null` if the segment has no Storage generation.
@@ -144,6 +139,17 @@ export interface StorageChunkSource {
144
139
  * summary omits this, and `count()` keeps its other paths.
145
140
  */
146
141
  summary?(ref: SegmentRef): Promise<GenerationSummary | null>;
142
+ /**
143
+ * Optional: what {@link summary} answers, with the size of the generation's object, both from **one** resolution of
144
+ * the segment, so the size is the size of the generation described; `null` if the segment has no Storage
145
+ * generation. The `.crbm` source answers from the registry row's summary when it can use it, whose fingerprint
146
+ * carries the object's size: one registry read and no read of the object. When it cannot, it opens the generation
147
+ * (one tail read of the object when it is not open already, and a range read for an index longer than the tail) and
148
+ * answers from its footer and index, with no payload read. For a source that omits it, `stat()` asks
149
+ * {@link summary} and {@link sizeOf}, two resolutions that can straddle a publish, and reports no size when
150
+ * there is no `sizeOf` either.
151
+ */
152
+ stat?(ref: SegmentRef): Promise<(GenerationSummary & SegmentSize) | null>;
147
153
  /**
148
154
  * Optional: the segment's **current generation number** as this source resolves it right now (registry
149
155
  * `currentGen`, or the highest storage generation), or `null` if the segment has no Storage generation. The engine
@@ -194,6 +200,10 @@ export interface StorageChunkSource {
194
200
  * Note that putting the incarnation in the *object key* would not fix this: the new incarnation still starts
195
201
  * at generation 0, so the cache key collides either way. The identity has to reach the cache.
196
202
  *
203
+ * Within one incarnation a number can be taken again once its object is deleted, so two objects must never share
204
+ * a version either, however long after its pointer read a source opens the object: the `.crbm` source's version
205
+ * ends with the fingerprint of the object it opened.
206
+ *
197
207
  * Returns `null` when the segment resolves to no generation. A source that only ever serves one immutable
198
208
  * generation may omit this, and the engine falls back to the generation alone.
199
209
  */
@@ -207,6 +217,46 @@ export interface StorageCaps {
207
217
  readonly maxObjectBytes: number;
208
218
  /** Optional: enables the pure-object `LATEST`-pointer registry variant (S3 conditional put). */
209
219
  readonly conditionalPut?: boolean;
220
+ /**
221
+ * Optional: `true` when `delete` applies {@link StorageDeleteOptions.ifVersion}. The driver then removes an object
222
+ * only while it is still the one that version names, by a check the backend applies in the same step as the removal
223
+ * (a precondition on the request, or a check and a removal with nothing between them), refuses another object under
224
+ * the key with {@link WriteConflictError}, and reports a `version` on every {@link IStorageDriver.getTail} of an
225
+ * object. `false` or absent: `delete` ignores `ifVersion` and removes whatever is under the key, and a caller that
226
+ * decided to delete one object can remove another stored under the key since.
227
+ *
228
+ * A driver that learns this from its client may report `false` until its first delete given `ifVersion`; what the
229
+ * delete does is what it states. The S3 driver does: it reads where the client sends its requests, and whether the
230
+ * SDK sends the header, on its first such delete, and reports `false` for good when that cannot be found out.
231
+ */
232
+ readonly conditionalDelete?: boolean;
233
+ }
234
+ /** What {@link IStorageDriver.getTail} read. */
235
+ export interface TailRead {
236
+ /** The last `min(maxBytes, size)` bytes of the object. */
237
+ readonly bytes: Uint8Array;
238
+ /** The object's true total size, not the number of bytes returned. */
239
+ readonly size: number;
240
+ /**
241
+ * Optional: which object the read saw, as the backend names it (an S3 or Azure Blob ETag, a GCS object generation),
242
+ * taken from the response that carried `bytes`, or from the metadata read when no bytes were asked for. Opaque, and
243
+ * compared by equality only, by the driver that reported it. It names the object, not the key: two objects stored
244
+ * one after the other under one key carry different versions, except where the backend's version is computed from
245
+ * the bytes (an S3 ETag of an object stored without SSE-KMS or SSE-C is), where two objects with the same bytes, each
246
+ * stored whole in one request, or each in parts of the same sizes, share one. A driver that reports
247
+ * `conditionalDelete: true` reports it on every read; another may omit it.
248
+ */
249
+ readonly version?: string;
250
+ }
251
+ /** Options of {@link IStorageDriver.delete}. */
252
+ export interface StorageDeleteOptions {
253
+ /**
254
+ * Delete the object only while it is the one this version names: the `version` a {@link IStorageDriver.getTail} of
255
+ * it reported. Pass the one read when the decision to delete was made, never one read after it, or the condition
256
+ * names whatever is under the key by then. Applied only by a driver that reports
257
+ * {@link StorageCaps.conditionalDelete}; ignored by any other, and absent, the delete is unconditional.
258
+ */
259
+ readonly ifVersion?: string;
210
260
  }
211
261
  /**
212
262
  * Immutable object storage for `.crbm` generations. A "dumb byte mover": it
@@ -235,9 +285,27 @@ export interface StorageCaps {
235
285
  * negative or non-integer offset or length. Never a clamped, short or adjacent read. A pin whose object
236
286
  * was purged and loaded again as a smaller one asks for a range past its end, and this error is what sends it
237
287
  * to check that the object is still the one it pinned.
238
- * - **`getTail` reports the object's true total size**, not the number of bytes it returned.
288
+ * - **`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.
239
290
  * - **`delete` is idempotent.** Deleting an absent key is a no-op, not an error: collection passes race each
240
- * other and retry.
291
+ * other and retry. That holds with `ifVersion` too: an absent object is a no-op whatever version is given.
292
+ * - **`delete` with `ifVersion` does what {@link StorageCaps.conditionalDelete} says.** A driver that reports `true`
293
+ * removes the object only while it is the one the version names, by a check the backend applies in the same step as
294
+ * the removal (S3 `If-Match`, GCS `ifGenerationMatch`, Azure Blob `ifMatch`; in memory, the check and the removal with
295
+ * no await between them), and for another object under the key throws {@link WriteConflictError} and leaves it. A
296
+ * check made by reading the object first and deleting after is not one step, and does not qualify. Where the service
297
+ * answers a failed precondition for a key with no object, the driver looks again before it reports a conflict, so an
298
+ * absent object stays a no-op. A driver that reports `false`, or omits it, ignores `ifVersion` and deletes whatever is
299
+ * under the key. A number can be taken again once its object is deleted, so this is what keeps a delete decided from
300
+ * one read, and delayed, from removing an object stored under the number since: the erasure's delete of a holder
301
+ * 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.
303
+ * - **A driver that wraps another forwards `ifVersion` and the `version` of a tail read, or reports
304
+ * `conditionalDelete: false`.** A wrapper that hands `delete` the key alone, and reports the inner driver's
305
+ * capability, makes every conditional delete through it unconditional without a word.
306
+ * - **A conditional delete may be sent again**, by the backend client's retry, after a lost response: a copy that meets
307
+ * its own landed delete finds the object absent, which is its success, and one that meets an object stored since is
308
+ * refused. It can remove nothing the first copy could not.
241
309
  * - **`list` is strongly consistent, read-after-delete.** Once `delete` has resolved, `list` no longer yields
242
310
  * that generation, and once `putImmutable` has resolved it does. The erasure's re-check for a generation still
243
311
  * holding the id, `dropSegment`'s `generationsRemaining`, the retention sweep's check that a tombstone's storage
@@ -272,15 +340,16 @@ export interface IStorageDriver {
272
340
  */
273
341
  getRange(key: GenKey, offset: number, length: number): Promise<Uint8Array>;
274
342
  /**
275
- * Speculative tail read: the last `min(maxBytes, size)` bytes + the object's **true total size**. A missing
276
- * object throws {@link NotFoundError}.
343
+ * 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}.
277
345
  */
278
- getTail(key: GenKey, maxBytes: number): Promise<{
279
- bytes: Uint8Array;
280
- size: number;
281
- }>;
282
- /** Idempotent: deleting an absent key is a no-op, not an error. */
283
- delete(key: GenKey): Promise<void>;
346
+ getTail(key: GenKey, maxBytes: number): Promise<TailRead>;
347
+ /**
348
+ * Idempotent: deleting an absent key is a no-op, not an error. With `options.ifVersion`, on a driver that reports
349
+ * {@link StorageCaps.conditionalDelete}, the object is removed only while it is the one that version names, and
350
+ * another object under the key is left and the delete throws {@link WriteConflictError}; any other driver ignores it.
351
+ */
352
+ delete(key: GenKey, options?: StorageDeleteOptions): Promise<void>;
284
353
  /**
285
354
  * Enumerate the generations present for a segment (orphan sweep / latest-gen resolution). Strongly
286
355
  * consistent, read-after-delete and read-after-put.
@@ -303,10 +372,12 @@ export type RegistryStatus = 'active' | 'destroyed';
303
372
  */
304
373
  export type GovernanceMeta = Record<string, unknown>;
305
374
  /**
306
- * A cached description of one generation, carried on the registry row: how many ids it holds, and the metadata it
307
- * was loaded with. The generation's `.crbm` object stays the truth; this is a copy, written by the same write that
308
- * moves the pointer, and it names the generation it describes so a copy left behind by a writer that did not
309
- * carry it can never be taken for another generation's.
375
+ * A cached description of one generation, carried on the registry row: how many ids it holds, the metadata it was
376
+ * loaded with, and the fingerprint of its object (its size and footer checksum). The generation's `.crbm` object stays
377
+ * the truth; this is a copy, written by the same write that moves the pointer, and it names the generation it describes
378
+ * so a copy left behind by a writer that did not carry it can never be taken for another generation's. The fingerprint
379
+ * names the object too: a reader checks every object it opens for the row's generation against it, and refuses one
380
+ * that is another object under that number.
310
381
  *
311
382
  * Two shapes. On a cleartext segment the values are in the clear. On an encrypted segment (a row with
312
383
  * `wrappedDeks`) they are sealed under the segment's data key, as the generation's index is, so the row reveals
@@ -333,6 +404,12 @@ export interface ClearRegistrySummary {
333
404
  readonly generation: number;
334
405
  /** How many ids the generation holds: an integer from 0 to 2^32. */
335
406
  readonly cardinality: number;
407
+ /**
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.
411
+ */
412
+ readonly fingerprint: string;
336
413
  /** The generation's metadata, when it has any. Never the empty object. */
337
414
  readonly metadata?: GenerationMetadata;
338
415
  }
@@ -341,8 +418,9 @@ export interface SealedRegistrySummary {
341
418
  /** The generation this describes. A non-negative safe integer. */
342
419
  readonly generation: number;
343
420
  /**
344
- * Base64 of `nonce(12) ‖ ciphertext ‖ tag(16)`, sealing the cardinality as a little-endian u64 followed by the
345
- * metadata's canonical JSON, if any. The count is fixed-width, so the length reveals only the metadata's size.
421
+ * 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.
346
424
  */
347
425
  readonly sealed: string;
348
426
  }
@@ -426,12 +504,23 @@ export interface RegistryRecord extends SegmentRef {
426
504
  * restored from a backup. It holds with overwhelming probability rather than by construction, since the token carries random parts: a
427
505
  * 128-bit incarnation id drawn when the row is created, and a 64-bit part drawn for each write. Two incarnations of one
428
506
  * name meet with probability 2^-128 for any pair (about n^2 / 2^129 among n of them), and two writes at one counter,
429
- * after a restore, with probability 2^-64. A bare decimal token, on a row no 0.12 or later registry has written,
430
- * carries neither part.
507
+ * after a restore, with probability 2^-64.
431
508
  */
432
509
  readonly token: Token;
510
+ /**
511
+ * 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
+ * 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`
514
+ * leaves it. The registry sets it; a caller never does. Compared by equality only, and never reused under one name,
515
+ * 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
+ * warm reader's cache standing, and a write that changes the resolution never does.
517
+ */
518
+ readonly pointerId: Token;
433
519
  }
434
- /** The caller-settable fields at {@link IRegistryDriver.create} (audit + token are driver-managed). */
520
+ /**
521
+ * The caller-settable fields at {@link IRegistryDriver.create} (audit, `token` and `pointerId` are driver-managed: the new
522
+ * row's `pointerId` is the token the create returns).
523
+ */
435
524
  export interface NewRegistryRecord {
436
525
  /** `null` ⇒ the segment has no Storage generation yet — see {@link RegistryRecord.currentGen}. */
437
526
  readonly currentGen: number | null;
@@ -447,13 +536,18 @@ export interface NewRegistryRecord {
447
536
  readonly keptGens?: readonly number[];
448
537
  }
449
538
  /**
450
- * Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity + audit + token are off-limits).
539
+ * Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity, audit, `token` and `pointerId` are
540
+ * off-limits).
451
541
  *
452
542
  * Presence-based: a field the patch does not mention is left as it was. `summary` is the one exception, because it
453
543
  * describes the current generation: a patch that moves `currentGen` and does not mention `summary` drops the old
454
544
  * one rather than keep a description of another generation. A `summary` the patch gives must name the
455
545
  * `currentGen` the row will have. `keptGens` follows the pointer the same way: a patch that moves `currentGen` and does
456
546
  * not mention it drops it, and one that gives it must name only generations below the `currentGen` the row will have.
547
+ * "Moves" means changes value: a patch that names `currentGen` at the value it has keeps both.
548
+ *
549
+ * A patch that names a resolved field (`currentGen`, `status`, `wrappedDeks`, `keyId`, `summary`), by presence and at any
550
+ * value, sets the row's {@link RegistryRecord.pointerId} to the token the write returns; any other patch leaves it.
457
551
  */
458
552
  export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency' | 'summary' | 'keptGens' | 'leases'>>;
459
553
  /**
@@ -488,9 +582,7 @@ export interface RegCaps {
488
582
  * `true` when this registry's `delete` removes a row from its backend for good, so that no later `list` reads it, and
489
583
  * removes it only while it is still the exact version the delete read: by a delete the backend applies under a
490
584
  * precondition (an S3 or Azure Blob ETag, a GCS object generation), or under a lock no other writer of the backend
491
- * can take. Only a row whose token carries an incarnation id is removed so. A row first written by a release before
492
- * 0.12 has a bare decimal token, and a delete still tombstones it: a process on that release, re-creating the name
493
- * over nothing, would issue those counters again from 0, so its row could not be told apart from the deleted one.
585
+ * can take.
494
586
  *
495
587
  * `false` or absent: every `delete` leaves a tombstone, which every later full `list` still reads. A shipped registry
496
588
  * says which it is.
@@ -609,6 +701,10 @@ export declare function isStorageBackend(value: unknown): value is StorageBacken
609
701
  * keeps going; where nothing of the earlier row is left, or the row is restored from a backup to an older counter, the
610
702
  * random parts alone keep the tokens apart: with probability 1 - 2^-128 per pair of incarnations, and 1 - 2^-64 per
611
703
  * pair of writes at one counter.
704
+ * - **Every row carries its `pointerId`** ({@link RegistryRecord.pointerId}): `create` sets it to the token it returns,
705
+ * a `compareAndSwap` whose patch names a resolved field (`renewsPointer` in `@cloudbitmaps/core/driver-kit`) sets it
706
+ * to the token that write returns, and every other write leaves it. `get` and `list` return it. A caller's write
707
+ * never sets it. A row returned without one fails every read of the segment with {@link UnsupportedError}.
612
708
  * - **`delete` is idempotent** without an `expected` token: deleting an absent row is a no-op, not an error.
613
709
  * **With `expected` it is fenced**: it lands only while the row still carries that token, and otherwise throws
614
710
  * {@link WriteConflictError} and leaves the row.
@@ -662,8 +758,8 @@ export interface IRegistryDriver {
662
758
  */
663
759
  list(namespace?: string): AsyncIterable<RegistryRecord>;
664
760
  /**
665
- * Remove the row. A registry whose {@link RegCaps.conditionalDelete} is `true` removes a row whose token carries an
666
- * incarnation id from its backend; every other row is tombstoned. Either way a later `create` gets a token no
761
+ * Remove the row. A registry whose {@link RegCaps.conditionalDelete} is `true` removes it from its backend; any other
762
+ * tombstones it. Either way a later `create` gets a token no
667
763
  * earlier incarnation of the name held, but for a collision of probability 2^-128 per pair of incarnations.
668
764
  *
669
765
  * Without `expected`, idempotent: deleting an absent row is a no-op.
@@ -1,7 +1,6 @@
1
1
  /**
2
- * The storage reader's defaults, stated once for the two modules that need them: the reader, which applies them,
3
- * and the cost model, which prices what they cause. The cost model once imported them from the reader itself,
4
- * which pulled the whole read path into a module that is otherwise arithmetic.
2
+ * The storage reader's defaults. `@cloudbitmaps/tools` prices what two of them cause and keeps its own copies, which
3
+ * `tests/tools/store-defaults.test.ts` holds equal to these.
5
4
  */
6
5
  /** Default TTL (ms) for re-resolving a segment's `currentGen` — the bound on post-publish read staleness. */
7
6
  export declare const DEFAULT_CURRENT_GEN_TTL_MS = 2000;
@@ -16,3 +15,15 @@ export declare const REFRESH_RETRY_MS = 500;
16
15
  export declare const DEFAULT_MAX_OPEN_SEGMENTS = 1024;
17
16
  /** Default aggregate ceiling (bytes) on resident parsed indices in the reader cache — the steady-state byte bound. */
18
17
  export declare const DEFAULT_MAX_OPEN_INDEX_BYTES: number;
18
+ /**
19
+ * How many segments' resolutions a store keeps per reader it keeps open: the resolution cache's count bound is this
20
+ * times `cache.readerMax` (8,192 by default). A resolution is the few hundred bytes of a row a read resolves through,
21
+ * kept for `cache.genTtlMs` whether or not the segment's reader is still open, so a store reading more segments at once
22
+ * than its readers hold keeps more of them than it keeps readers.
23
+ */
24
+ export declare const RESOLUTIONS_PER_OPEN_SEGMENT = 8;
25
+ /**
26
+ * The resolution cache's byte bound is `cache.readerMaxBytes` divided by this (4 MiB by default): what it holds is a
27
+ * sixteenth of what the readers may.
28
+ */
29
+ export declare const RESOLUTION_BYTES_DIVISOR = 16;
@@ -95,7 +95,8 @@ export interface RollbackResult {
95
95
  * `currentGen`, where generation collection never looks. They stay until loads pass them (the first load whose
96
96
  * number one of them holds numbers above them all, and collection then keeps the newest `keep` of what is below its
97
97
  * pointer), `dropSegment` deletes them, or a subject erasure
98
- * deletes them: all of those present when it rewrites, and only those holding the id when the current generation does not.
98
+ * deletes them: all of those present when it rewrites, and, when the current generation does not hold the id, only those
99
+ * holding it and any no read of the segment can open, whatever the id.
99
100
  * Not collecting them is a deliberate trade: an operator who has just undone a bad load should not have the
100
101
  * evidence collected out from under them, while a rollback target that still holds erased data would make the
101
102
  * erasure undoable by an ordinary operator action.
@@ -2,15 +2,17 @@
2
2
  * A generation's summary on its registry row: how a writer builds it, how a reader opens it, and how it is held
3
3
  * against the object it describes.
4
4
  *
5
- * The row carries a cached description of the current generation, `{ generation, cardinality, metadata? }`, written by
6
- * the same compare-and-swap that moves the pointer. On a cleartext segment the values are in the clear. On an
7
- * encrypted one the row holds `{ generation, sealed }` instead: the cardinality as a fixed-width little-endian u64,
8
- * then the metadata's canonical JSON if there is any, sealed under the segment's data key with the summary scope of
9
- * the associated data, which binds it to its namespace, segment and generation. The count is fixed-width so the
10
- * ciphertext's length reveals only the metadata's size, never how many digits the count has. Pure: no I/O.
5
+ * The row carries a cached description of the current generation, `{ generation, cardinality, fingerprint, metadata? }`,
6
+ * written by the same compare-and-swap that moves the pointer. The fingerprint names the generation's object, by its
7
+ * size and footer checksum, as `CrbmReader.fingerprint` does. On a cleartext segment the values are in the clear. On an
8
+ * encrypted one the row holds `{ generation, sealed }` instead: the cardinality as a fixed-width little-endian u64, the
9
+ * object's size as a u64 and its footer checksum as a u32, then the metadata's canonical JSON if there is any, sealed
10
+ * under the segment's data key with the summary scope of the associated data, which binds it to its namespace, segment
11
+ * and generation. The numbers are fixed-width so the ciphertext's length reveals only the metadata's size, never how
12
+ * many digits the count or the size has. Pure: no I/O.
11
13
  *
12
14
  * The generation's `.crbm` object stays the truth, and the summary is a copy of it. {@link summaryAgrees} is the
13
- * comparison of one with the other.
15
+ * comparison of one with the other, and the fingerprint is what a reader holds every object it opens against.
14
16
  */
15
17
  import type { Aead } from './crypto.js';
16
18
  import type { ClearRegistrySummary, GenerationMetadata, RegistryRecord, RegistrySummary, SealedRegistrySummary, SegmentRef } from './ports.js';
@@ -19,28 +21,39 @@ export interface GenerationDescription {
19
21
  readonly cardinality: number;
20
22
  readonly metadata?: GenerationMetadata | undefined;
21
23
  }
22
- /** The clear summary of a generation on a cleartext segment. `metadata` is left off when there is none. */
23
- export declare function clearSummary(generation: number, cardinality: number, metadata?: GenerationMetadata): ClearRegistrySummary;
24
+ /** What a row's summary says: the generation's description, and the fingerprint of the object it describes. */
25
+ export interface SummaryDescription extends GenerationDescription {
26
+ readonly fingerprint: string;
27
+ }
28
+ /** The size of the object a fingerprint names, in bytes: what `stat()` reports from a row's summary. */
29
+ export declare function sizeOfFingerprint(fingerprint: string): number;
30
+ /**
31
+ * The clear summary of a generation on a cleartext segment, naming its object by `fingerprint`. `metadata` is left off
32
+ * when there is none.
33
+ */
34
+ export declare function clearSummary(generation: number, cardinality: number, fingerprint: string, metadata?: GenerationMetadata): ClearRegistrySummary;
24
35
  /**
25
- * The sealed summary of a generation on an encrypted segment: the cardinality as a u64, then the metadata's canonical
26
- * JSON, sealed under `aead` and bound to `ref` and `generation`.
36
+ * The sealed summary of a generation on an encrypted segment: the cardinality as a u64, the object's size as a u64 and
37
+ * its footer checksum as a u32, then the metadata's canonical JSON, sealed under `aead` and bound to `ref` and
38
+ * `generation`.
27
39
  */
28
- export declare function sealSummary(aead: Aead, ref: SegmentRef, generation: number, cardinality: number, metadata?: GenerationMetadata): SealedRegistrySummary;
40
+ export declare function sealSummary(aead: Aead, ref: SegmentRef, generation: number, cardinality: number, fingerprint: string, metadata?: GenerationMetadata): SealedRegistrySummary;
29
41
  /**
30
42
  * What a sealed summary says, opened with `aead` under the summary scope of `ref` and the summary's own generation.
31
43
  * The bytes are untrusted: a blob that is not base64, is the wrong size, fails its authentication (tampered, or moved
32
- * from another generation's row), or holds a count outside 0 to 2^32 or metadata that breaks the rules, is an
33
- * {@link IntegrityError}.
44
+ * from another generation's row), or holds a count outside 0 to 2^32, a size no object can have, or metadata that
45
+ * breaks the rules, is an {@link IntegrityError}.
34
46
  */
35
- export declare function openSummary(aead: Aead, ref: SegmentRef, summary: SealedRegistrySummary): GenerationDescription;
47
+ export declare function openSummary(aead: Aead, ref: SegmentRef, summary: SealedRegistrySummary): SummaryDescription;
36
48
  /**
37
49
  * What a row's summary says of its current generation, or `undefined` when it says nothing a reader may use. It is
38
50
  * used only for the generation it names, on an active row, and in the shape the row's keys call for: clear on a row
39
51
  * with no wrapped keys, sealed on a row with them. A sealed one is opened with `aead`, and is unusable without it,
40
- * or when it does not open, so a summary a tamperer moved or rewrote is never believed. Never throws for a bad
41
- * summary: the row's copy is a cache, and a copy that cannot be used is a copy that is not there.
52
+ * or when it does not open, so a summary a tamperer moved or rewrote is never believed; a clear one whose fingerprint
53
+ * names no object is unusable too. Never throws for a bad summary: the row's copy is a cache, and a copy that cannot be
54
+ * used is a copy that is not there.
42
55
  */
43
- export declare function usableSummary(ref: SegmentRef, row: RegistryRecord, aead: Aead | undefined): GenerationDescription | undefined;
56
+ export declare function usableSummary(ref: SegmentRef, row: Pick<RegistryRecord, 'summary' | 'status' | 'currentGen' | 'wrappedDeks'>, aead: Aead | undefined): SummaryDescription | undefined;
44
57
  /**
45
58
  * Whether a summary's description of a generation is what its object holds: the same id count and the same metadata,
46
59
  * compared in canonical form, so key order never matters. A summary with metadata over an object with none, and the
@@ -54,8 +67,12 @@ export declare function summaryAgrees(summary: GenerationDescription, object: Ge
54
67
  * metadata block is not authenticated, so a block stripped from one reads as none, and the sealed summary on the row is
55
68
  * the one copy that says there was a block; carrying it keeps the rewrite from erasing the record of what it dropped.
56
69
  * Pass `authenticated` only for a summary that was opened (a clear one has no more authority than the block it sits
57
- * beside). Never refuses: a rewrite over a stripped block is still an erasure.
70
+ * beside). Never refuses. The erasure reaches this with a stripped block only past a fingerprint forged to match the
71
+ * row's, since stripping the block changes the object's size and the open is held to the row's fingerprint.
58
72
  */
59
73
  export declare function metadataToCarry(object: GenerationMetadata | undefined, authenticated: GenerationDescription | undefined): GenerationMetadata | undefined;
60
- /** The summary of a generation of `ref`, clear or sealed under `aead` as the segment's keys call for. */
61
- export declare function summaryOf(ref: SegmentRef, generation: number, description: GenerationDescription, aead: Aead | undefined): RegistrySummary;
74
+ /**
75
+ * The summary of a generation of `ref`, describing the object `description.fingerprint` names, clear or sealed under
76
+ * `aead` as the segment's keys call for.
77
+ */
78
+ export declare function summaryOf(ref: SegmentRef, generation: number, description: SummaryDescription, aead: Aead | undefined): RegistrySummary;
@@ -4,8 +4,8 @@
4
4
  * A shipped registry gives a row it creates a token `<incarnation>.<counter>.<write>`: 32 lowercase hex digits drawn
5
5
  * from injected entropy when the row is created, a decimal counter, and 16 hex digits drawn for each write. Every write
6
6
  * of the row keeps the incarnation, and a name created again draws a new one, so two incarnations are told apart by it
7
- * even when their rows were created in the same millisecond or one is gone. A row a 0.11 build wrote has no incarnation
8
- * for as long as it lives (`"7"`, then `"8.<write>"`), and a registry of someone else's issues tokens in any form.
7
+ * even when their rows were created in the same millisecond or one is gone. A registry of someone else's issues tokens
8
+ * in any form, and one whose tokens carry no incarnation id is told apart only by the row's creation stamp.
9
9
  *
10
10
  * This lives in `core/` so the publish can read it, and the registry drivers, which build the tokens, import it from
11
11
  * here: the form has one definition.
@@ -14,18 +14,23 @@ import type { Token } from './ports.js';
14
14
  /** An incarnation-form token: the incarnation id, the counter and a write part, `.`-separated, in lowercase hex. */
15
15
  export declare const INCARNATION_TOKEN: RegExp;
16
16
  /**
17
- * The incarnation id in a token, or `undefined` for one that has none: a schema-1-born row's, or any token another
18
- * driver issues. Two tokens with the same incarnation are writes of one incarnation of a row.
17
+ * The incarnation id in a token, or `undefined` for one that has none: a token a registry of someone else's issues in
18
+ * another form. Two tokens with the same incarnation are writes of one incarnation of a row.
19
19
  */
20
20
  export declare function incarnationOf(token: Token): string | undefined;
21
+ /** An audit event's `incarnation`, from the token of the row it is about: absent for a token that carries none. */
22
+ export declare function incarnationField(token: Token | undefined): {
23
+ readonly incarnation?: string;
24
+ };
21
25
  /**
22
26
  * Whether two reads of one name's row are one incarnation of it.
23
27
  *
24
28
  * - Both tokens carry an incarnation id: the ids decide, and the clock stamps do not enter.
25
- * - Only one does: different incarnations. A name created again is always given an incarnation id, and a write never
26
- * takes one away, so a row without an id never becomes a row with one, nor the reverse.
27
- * - Neither does (a row 0.11 wrote, or a registry of someone else's): there is no id to compare, and the creation stamp
28
- * is the only signal left. It is a clock reading, so a caller that acts on this answer proves its own object too.
29
+ * - Only one does: different incarnations. A registry gives its rows one form of token, so a row without an id never
30
+ * becomes a row with one, nor the reverse.
31
+ * - Neither does (a registry of someone else's whose tokens carry none): there is no id to compare, and the creation
32
+ * stamp is the only signal left. It is a clock reading, so a caller that acts on this answer proves its own object
33
+ * too.
29
34
  */
30
35
  export declare function sameIncarnation(a: {
31
36
  readonly token: Token;
@@ -1,7 +1,8 @@
1
- export type { GenKey, IRegistryDriver, IStorageDriver, SegmentRef, StorageBackend, StorageCaps, } from './core/ports.js';
1
+ export type { GenKey, IRegistryDriver, IStorageDriver, SegmentRef, StorageBackend, StorageCaps, StorageDeleteOptions, TailRead, } from './core/ports.js';
2
2
  export { brandAsBackend, STORAGE_BACKEND } from './core/ports.js';
3
3
  export type { IMetricsSink, MetricEvent } from './core/metrics.js';
4
4
  export { IntegrityError, isIntegrityError, isNotFoundError, isTransientError, isValidationError, isWriteConflictError, NotFoundError, TransientError, ValidationError, WriteConflictError, } from './core/errors.js';
5
+ export { RESOLVED_FIELDS, pointerIdOf, renewPointer, renewsPointer } from './core/pointer-id.js';
5
6
  export { validateSegmentRef } from './core/validate.js';
6
7
  export type { BlobSink } from './core/blob.js';
7
8
  export { MAX_ROW_BYTES, ObjectStoreRegistry, ObjectVersionRaced, } from './drivers/_shared/object-registry.js';
@@ -2,6 +2,7 @@ import {
2
2
  DEFAULT_NAMESPACE,
3
3
  IntegrityError,
4
4
  NotFoundError,
5
+ RESOLVED_FIELDS,
5
6
  STORAGE_BACKEND,
6
7
  TransientError,
7
8
  ValidationError,
@@ -12,7 +13,6 @@ import {
12
13
  encodeNameForKey,
13
14
  encodeNameForPath,
14
15
  entropyIsAvailable,
15
- incarnationOf,
16
16
  isIntegrityError,
17
17
  isNotFoundError,
18
18
  isTransientError,
@@ -24,13 +24,16 @@ import {
24
24
  newIncarnationToken,
25
25
  nextRegistryToken,
26
26
  parseRegistryEnvelope,
27
+ pointerIdOf,
27
28
  recordFromNew,
29
+ renewPointer,
30
+ renewsPointer,
28
31
  serializeRegistryEnvelope,
29
32
  validateNewRegistryRecord,
30
33
  validateRegistryPatch,
31
34
  validateSegmentRef,
32
35
  webCryptoEntropy
33
- } from "./chunk-TNWTIAON.js";
36
+ } from "./chunk-YSC7NJWV.js";
34
37
 
35
38
  // src/drivers/_shared/object-registry-keys.ts
36
39
  var REGISTRY_SUFFIX = ".reg";
@@ -137,7 +140,7 @@ var ObjectStoreRegistry = class {
137
140
  const conditionalDelete = this.removesRows();
138
141
  return entropyIsAvailable(this.entropy) ? { strongRead: true, conditionalDelete } : { strongRead: true, canWrite: false, conditionalDelete };
139
142
  }
140
- /** Whether a delete removes a row born with an incarnation id: the store has a delete, and vouches for it. */
143
+ /** Whether a delete removes a row: the store has a delete, and vouches for it. */
141
144
  removesRows() {
142
145
  return this.store.conditionalDelete === true && typeof this.store.delete === "function";
143
146
  }
@@ -242,7 +245,7 @@ var ObjectStoreRegistry = class {
242
245
  }
243
246
  try {
244
247
  const remove = this.removesRows() ? this.store.delete : void 0;
245
- if (remove !== void 0 && incarnationOf(current.env.record.token) !== void 0) {
248
+ if (remove !== void 0) {
246
249
  await remove.call(this.store, key, { version: current.version });
247
250
  return;
248
251
  }
@@ -316,6 +319,7 @@ export {
316
319
  NotFoundError,
317
320
  ObjectStoreRegistry,
318
321
  ObjectVersionRaced,
322
+ RESOLVED_FIELDS,
319
323
  STORAGE_BACKEND,
320
324
  TransientError,
321
325
  ValidationError,
@@ -331,7 +335,10 @@ export {
331
335
  namespaceKeyPart,
332
336
  namespacePathPart,
333
337
  normalizeObjectPrefix,
338
+ pointerIdOf,
334
339
  prefixPart,
340
+ renewPointer,
341
+ renewsPointer,
335
342
  validateSegmentRef
336
343
  };
337
344
  //# sourceMappingURL=driver-kit.js.map