@cloudbitmaps/core 0.19.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +85 -0
  2. package/dist/{chunk-RSLVTRJY.js → chunk-IGLIZBR7.js} +982 -281
  3. package/dist/chunk-IGLIZBR7.js.map +7 -0
  4. package/dist/core/absent-operands.d.ts +8 -0
  5. package/dist/core/audit.d.ts +67 -4
  6. package/dist/core/bit-route.d.ts +2 -1
  7. package/dist/core/blob.d.ts +12 -1
  8. package/dist/core/bulk-load.d.ts +148 -0
  9. package/dist/core/cas-retry.d.ts +27 -0
  10. package/dist/core/check.d.ts +22 -0
  11. package/dist/core/chunk-checks.d.ts +6 -1
  12. package/dist/core/chunk-window.d.ts +11 -4
  13. package/dist/core/combine-expr.d.ts +1 -3
  14. package/dist/core/combine-feed.d.ts +2 -16
  15. package/dist/core/combine-many.d.ts +21 -15
  16. package/dist/core/concurrency.d.ts +8 -0
  17. package/dist/core/consistency.d.ts +76 -18
  18. package/dist/core/crbm/fingerprint.d.ts +23 -0
  19. package/dist/core/crbm/format.d.ts +2 -5
  20. package/dist/core/crbm/frame.d.ts +10 -0
  21. package/dist/core/crbm/plan-reads.d.ts +9 -3
  22. package/dist/core/crbm/reader.d.ts +51 -12
  23. package/dist/core/crbm/varint.d.ts +2 -1
  24. package/dist/core/crbm/writer.d.ts +5 -1
  25. package/dist/core/crbm-open.d.ts +154 -0
  26. package/dist/core/crbm-storage-source.d.ts +243 -490
  27. package/dist/core/crbm-write.d.ts +75 -0
  28. package/dist/core/crypto.d.ts +6 -1
  29. package/dist/core/engine.d.ts +239 -47
  30. package/dist/core/erase-id.d.ts +97 -27
  31. package/dist/core/erasure.d.ts +4 -3
  32. package/dist/core/errors.d.ts +8 -2
  33. package/dist/core/generation-gc.d.ts +131 -16
  34. package/dist/core/generation-list.d.ts +11 -0
  35. package/dist/core/id-window.d.ts +46 -0
  36. package/dist/core/invalidation-scope.d.ts +77 -0
  37. package/dist/core/known-generations-shape.d.ts +13 -0
  38. package/dist/core/known-generations.d.ts +147 -0
  39. package/dist/core/leases.d.ts +9 -1
  40. package/dist/core/ledger-seed.d.ts +17 -0
  41. package/dist/core/load.d.ts +17 -36
  42. package/dist/core/lru.d.ts +2 -15
  43. package/dist/core/metrics.d.ts +9 -7
  44. package/dist/core/order.d.ts +2 -0
  45. package/dist/core/pinned-storage-source.d.ts +7 -7
  46. package/dist/core/pointer-id.d.ts +19 -0
  47. package/dist/core/ports.d.ts +210 -45
  48. package/dist/core/publish.d.ts +207 -0
  49. package/dist/core/reader-defaults.d.ts +14 -3
  50. package/dist/core/registry-scan.d.ts +0 -2
  51. package/dist/core/retention-sweep.d.ts +69 -65
  52. package/dist/core/retention.d.ts +1 -1
  53. package/dist/core/retry.d.ts +12 -19
  54. package/dist/core/rollback.d.ts +62 -32
  55. package/dist/core/summary.d.ts +74 -25
  56. package/dist/core/token.d.ts +13 -8
  57. package/dist/core/validate.d.ts +2 -0
  58. package/dist/driver-kit.d.ts +6 -1
  59. package/dist/driver-kit.js +90 -10
  60. package/dist/driver-kit.js.map +3 -3
  61. package/dist/drivers/_shared/bytes.d.ts +6 -0
  62. package/dist/drivers/_shared/faults.d.ts +15 -0
  63. package/dist/drivers/_shared/object-keys.d.ts +18 -0
  64. package/dist/drivers/_shared/object-registry-keys.d.ts +0 -2
  65. package/dist/drivers/_shared/object-registry.d.ts +3 -1
  66. package/dist/drivers/_shared/options.d.ts +19 -0
  67. package/dist/drivers/_shared/registry.d.ts +31 -19
  68. package/dist/drivers/localfs/exact-case.d.ts +0 -3
  69. package/dist/drivers/localfs/fs-util.d.ts +22 -0
  70. package/dist/drivers/localfs/registry.d.ts +10 -4
  71. package/dist/drivers/localfs/storage.d.ts +16 -8
  72. package/dist/drivers/memory.d.ts +14 -9
  73. package/dist/drivers/retry/retrying-chunk-source.d.ts +1 -5
  74. package/dist/export/index.d.ts +16 -0
  75. package/dist/index.d.ts +2 -5
  76. package/dist/index.js +4303 -2938
  77. package/dist/index.js.map +4 -4
  78. package/package.json +1 -1
  79. package/dist/chunk-RSLVTRJY.js.map +0 -7
  80. package/dist/core/cost.d.ts +0 -351
  81. package/dist/core/kept-generations.d.ts +0 -40
@@ -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
@@ -211,6 +217,46 @@ export interface StorageCaps {
211
217
  readonly maxObjectBytes: number;
212
218
  /** Optional: enables the pure-object `LATEST`-pointer registry variant (S3 conditional put). */
213
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;
214
260
  }
215
261
  /**
216
262
  * Immutable object storage for `.crbm` generations. A "dumb byte mover": it
@@ -235,13 +281,37 @@ export interface StorageCaps {
235
281
  * `getRange` may answer empty without reaching the backend, so it proves neither that the object exists nor
236
282
  * that the offset lies inside it: the cloud drivers answer it empty, the memory and local-filesystem drivers
237
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.
238
289
  * - **An out-of-range read throws {@link ValidationError}**: a range that runs past the end of the object, or a
239
290
  * negative or non-integer offset or length. Never a clamped, short or adjacent read. A pin whose object
240
291
  * was purged and loaded again as a smaller one asks for a range past its end, and this error is what sends it
241
292
  * to check that the object is still the one it pinned.
242
- * - **`getTail` reports the object's true total size**, not the number of bytes it returned.
293
+ * - **`getTail` reports the object's true total size**, not the number of bytes it returned, and, where the driver has
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.
243
296
  * - **`delete` is idempotent.** Deleting an absent key is a no-op, not an error: collection passes race each
244
- * other and retry.
297
+ * other and retry. That holds with `ifVersion` too: an absent object is a no-op whatever version is given.
298
+ * - **`delete` with `ifVersion` does what {@link StorageCaps.conditionalDelete} says.** A driver that reports `true`
299
+ * removes the object only while it is the one the version names, by a check the backend applies in the same step as
300
+ * the removal (S3 `If-Match`, GCS `ifGenerationMatch`, Azure Blob `ifMatch`; in memory, the check and the removal with
301
+ * no await between them), and for another object under the key throws {@link WriteConflictError} and leaves it. A
302
+ * check made by reading the object first and deleting after is not one step, and does not qualify. Where the service
303
+ * answers a failed precondition for a key with no object, the driver looks again before it reports a conflict, so an
304
+ * absent object stays a no-op. A driver that reports `false`, or omits it, ignores `ifVersion` and deletes whatever is
305
+ * under the key. A number can be taken again once its object is deleted, so this is what keeps a delete decided from
306
+ * one read, and delayed, from removing an object stored under the number since: the erasure's delete of a holder
307
+ * above the pointer passes the version it read when it searched that object, and a refused load's delete of an
308
+ * object whose footer, read once, matched what it wrote passes the version of that read.
309
+ * - **A driver that wraps another forwards `ifVersion` and the `version` of a tail read, or reports
310
+ * `conditionalDelete: false`.** A wrapper that hands `delete` the key alone, and reports the inner driver's
311
+ * capability, makes every conditional delete through it unconditional without a word.
312
+ * - **A conditional delete may be sent again**, by the backend client's retry, after a lost response: a copy that meets
313
+ * its own landed delete finds the object absent, which is its success, and one that meets an object stored since is
314
+ * refused. It can remove nothing the first copy could not.
245
315
  * - **`list` is strongly consistent, read-after-delete.** Once `delete` has resolved, `list` no longer yields
246
316
  * that generation, and once `putImmutable` has resolved it does. The erasure's re-check for a generation still
247
317
  * holding the id, `dropSegment`'s `generationsRemaining`, the retention sweep's check that a tombstone's storage
@@ -276,15 +346,19 @@ export interface IStorageDriver {
276
346
  */
277
347
  getRange(key: GenKey, offset: number, length: number): Promise<Uint8Array>;
278
348
  /**
279
- * Speculative tail read: the last `min(maxBytes, size)` bytes + the object's **true total size**. A missing
280
- * object throws {@link NotFoundError}.
349
+ * Speculative tail read: the last `min(maxBytes, size)` bytes + the object's **true total size**, and its `version`
350
+ * where the driver has one ({@link TailRead}). `maxBytes` must be a safe integer: zero or less answers an empty tail
351
+ * with the size, and one past the size the whole object. Any other `maxBytes` (`NaN`, a fraction, a negative one
352
+ * included, an infinity of either sign, a whole number past the safe range (2^53 or more, or -2^53 or less)) throws
353
+ * {@link ValidationError} before anything is read. A missing object throws {@link NotFoundError}.
281
354
  */
282
- getTail(key: GenKey, maxBytes: number): Promise<{
283
- bytes: Uint8Array;
284
- size: number;
285
- }>;
286
- /** Idempotent: deleting an absent key is a no-op, not an error. */
287
- delete(key: GenKey): Promise<void>;
355
+ getTail(key: GenKey, maxBytes: number): Promise<TailRead>;
356
+ /**
357
+ * Idempotent: deleting an absent key is a no-op, not an error. With `options.ifVersion`, on a driver that reports
358
+ * {@link StorageCaps.conditionalDelete}, the object is removed only while it is the one that version names, and
359
+ * another object under the key is left and the delete throws {@link WriteConflictError}; any other driver ignores it.
360
+ */
361
+ delete(key: GenKey, options?: StorageDeleteOptions): Promise<void>;
288
362
  /**
289
363
  * Enumerate the generations present for a segment (orphan sweep / latest-gen resolution). Strongly
290
364
  * consistent, read-after-delete and read-after-put.
@@ -307,10 +381,12 @@ export type RegistryStatus = 'active' | 'destroyed';
307
381
  */
308
382
  export type GovernanceMeta = Record<string, unknown>;
309
383
  /**
310
- * A cached description of one generation, carried on the registry row: how many ids it holds, and the metadata it
311
- * was loaded with. The generation's `.crbm` object stays the truth; this is a copy, written by the same write that
312
- * moves the pointer, and it names the generation it describes so a copy left behind by a writer that did not
313
- * carry it can never be taken for another generation's.
384
+ * A cached description of one generation, carried on the registry row: how many ids it holds, the metadata it was
385
+ * loaded with, and the fingerprint of its object (its size and footer checksum). The generation's `.crbm` object stays
386
+ * the truth; this is a copy, written by the same write that moves the pointer, and it names the generation it describes
387
+ * so a copy left behind by a writer that did not carry it can never be taken for another generation's. The fingerprint
388
+ * names the object too: a reader checks every object it opens for the row's generation against it, and refuses one
389
+ * that is another object under that number.
314
390
  *
315
391
  * Two shapes. On a cleartext segment the values are in the clear. On an encrypted segment (a row with
316
392
  * `wrappedDeks`) they are sealed under the segment's data key, as the generation's index is, so the row reveals
@@ -337,6 +413,18 @@ export interface ClearRegistrySummary {
337
413
  readonly generation: number;
338
414
  /** How many ids the generation holds: an integer from 0 to 2^32. */
339
415
  readonly cardinality: number;
416
+ /**
417
+ * The fingerprint of the generation's object, `<size>:<footer checksum>` in decimal, as `CrbmReader.fingerprint` and
418
+ * a pin's `pinnedAt.fingerprint` spell it: the object's size in bytes, which `stat()` reports, and the CRC32C its
419
+ * footer stores.
420
+ */
421
+ readonly fingerprint: string;
422
+ /**
423
+ * The bytes from the end of the last chunk payload to the end of the object: the extension block, if any, the index and
424
+ * the footer. A cold open reads this many bytes from the object's end to open it in one request. An integer from 104,
425
+ * the footer's size, to the object's size less its 8-byte preamble, and at most 8,392,820.
426
+ */
427
+ readonly tailBytes: number;
340
428
  /** The generation's metadata, when it has any. Never the empty object. */
341
429
  readonly metadata?: GenerationMetadata;
342
430
  }
@@ -345,11 +433,25 @@ export interface SealedRegistrySummary {
345
433
  /** The generation this describes. A non-negative safe integer. */
346
434
  readonly generation: number;
347
435
  /**
348
- * Base64 of `nonce(12) ‖ ciphertext ‖ tag(16)`, sealing the cardinality as a little-endian u64 followed by the
349
- * metadata's canonical JSON, if any. The count is fixed-width, so the length reveals only the metadata's size.
436
+ * Base64 of `nonce(12) ‖ ciphertext ‖ tag(16)`, sealing the cardinality as a little-endian u64, the object's size as a
437
+ * little-endian u64 and its footer checksum as a little-endian u32 (its fingerprint), its `tailBytes` as a
438
+ * little-endian u32, then the metadata's canonical JSON, if any. The numbers are fixed-width, so the length reveals only
439
+ * the metadata's size.
350
440
  */
351
441
  readonly sealed: string;
352
442
  }
443
+ /**
444
+ * A generation a row knows it published: its number, and the checksum its object's footer stores. A rollback moves only
445
+ * onto a generation the row knows, and an object whose footer carries another checksum is another object under that
446
+ * number. The footer's checksum covers the index's checksum, which covers each chunk's, so it commits to the object's ids
447
+ * and shape; for a cleartext object it does not cover the extension block's bytes.
448
+ */
449
+ export interface KnownGeneration {
450
+ /** A non-negative safe integer. */
451
+ readonly generation: number;
452
+ /** The object's footer CRC32C (the second half of its fingerprint `<size>:<crc>`), an integer from 0 to 2^32 - 1. */
453
+ readonly footerCrc: number;
454
+ }
353
455
  /**
354
456
  * One lease on a generation. It names no fingerprint: a collector spares a generation by number and cannot compare an
355
457
  * object without reading it, and a purge and re-create drops the whole list with the row. A pin's own fingerprint,
@@ -407,15 +509,28 @@ export interface RegistryRecord extends SegmentRef {
407
509
  */
408
510
  readonly summary?: RegistrySummary;
409
511
  /**
410
- * The generations below `currentGen` that the last publish kept as its grace window, ascending. Optional: absent
411
- * means the row does not know which generations are kept, and a load then lists the bucket to collect and records
412
- * them; an empty list means none are. Trusted only while every entry is below `currentGen`. A patch that moves
512
+ * The generations this row knows and has not decided to delete, the current one included, with each object's footer
513
+ * checksum: the ones it made current, in the order they were made current, oldest first and the current one last, and,
514
+ * when a publish over a row with no usable ledger seeds one, the ones a count by `keep` keeps in the bucket, which can
515
+ * include an object no load published (an orphan or a stray). An entry says the library knows the generation and its
516
+ * footer checksum, not that a load published it. A generation appears once,
517
+ * at its most recent time current (a rollback onto it moves it to the end). The entries before the last are the kept
518
+ * window, the generations a live lease holds, and what a rollback left; those numbered above `currentGen` are what a
519
+ * rollback left. In the clear on every
520
+ * row, an encrypted one included: a footer checksum of an encrypted object covers a random nonce and says nothing about
521
+ * its count, size or content, but the entries above the pointer show anyone who reads the registry that a rollback
522
+ * happened, and from which generation.
523
+ *
524
+ * Optional: absent means the row does not know, and a load then lists the bucket to collect. A shipped registry stores
525
+ * the list in the order given, at most 192 entries written and 384 read, and refuses a list that repeats a
526
+ * generation, one whose last entry is not the row's `currentGen`, and one whose current entry's checksum disagrees with
527
+ * a clear `summary`'s fingerprint. A stored list that disagrees with its row is read and not used. A patch that moves
413
528
  * `currentGen` without naming it drops it.
414
529
  */
415
- readonly keptGens?: readonly number[];
530
+ readonly knownGens?: readonly KnownGeneration[];
416
531
  /**
417
532
  * The leases on this segment's generations: each keeps one generation out of a load's collection until it ends.
418
- * Optional; absent means none. Unlike {@link keptGens} it does not follow the pointer: a patch that moves
533
+ * Optional; absent means none. Unlike {@link knownGens} it does not follow the pointer: a patch that moves
419
534
  * `currentGen` leaves it as it was, and only a patch that names it, or the row going away, changes it. Entries
420
535
  * past their end are ignored by every collector and pruned by the next write that names the list. A row's identity
421
536
  * is its token, so a purged and re-created segment starts with none.
@@ -430,12 +545,23 @@ export interface RegistryRecord extends SegmentRef {
430
545
  * restored from a backup. It holds with overwhelming probability rather than by construction, since the token carries random parts: a
431
546
  * 128-bit incarnation id drawn when the row is created, and a 64-bit part drawn for each write. Two incarnations of one
432
547
  * name meet with probability 2^-128 for any pair (about n^2 / 2^129 among n of them), and two writes at one counter,
433
- * after a restore, with probability 2^-64. A bare decimal token, on a row no 0.12 or later registry has written,
434
- * carries neither part.
548
+ * after a restore, with probability 2^-64.
435
549
  */
436
550
  readonly token: Token;
551
+ /**
552
+ * The token of the most recent write that named a field a read resolves through: the row's create, or a compare-and-swap
553
+ * whose patch named `currentGen`, `status`, `wrappedDeks`, `keyId` or `summary` (the resolved fields), at any value,
554
+ * the one the field already had included. A write that names only `leases`, `retention`, `residency` or `knownGens`
555
+ * leaves it. The registry sets it; a caller never does. Compared by equality only, and never reused under one name,
556
+ * 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
557
+ * warm reader's cache standing, and a write that changes the resolution never does.
558
+ */
559
+ readonly pointerId: Token;
437
560
  }
438
- /** The caller-settable fields at {@link IRegistryDriver.create} (audit + token are driver-managed). */
561
+ /**
562
+ * The caller-settable fields at {@link IRegistryDriver.create} (audit, `token` and `pointerId` are driver-managed: the new
563
+ * row's `pointerId` is the token the create returns).
564
+ */
439
565
  export interface NewRegistryRecord {
440
566
  /** `null` ⇒ the segment has no Storage generation yet — see {@link RegistryRecord.currentGen}. */
441
567
  readonly currentGen: number | null;
@@ -447,19 +573,24 @@ export interface NewRegistryRecord {
447
573
  readonly residency?: GovernanceMeta;
448
574
  /** Must name `currentGen` when given. */
449
575
  readonly summary?: RegistrySummary;
450
- /** Every entry below `currentGen`, ascending; see {@link RegistryRecord.keptGens}. */
451
- readonly keptGens?: readonly number[];
576
+ /** In the order made current, the last entry the new row's `currentGen`; see {@link RegistryRecord.knownGens}. */
577
+ readonly knownGens?: readonly KnownGeneration[];
452
578
  }
453
579
  /**
454
- * Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity + audit + token are off-limits).
580
+ * Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity, audit, `token` and `pointerId` are
581
+ * off-limits).
455
582
  *
456
583
  * Presence-based: a field the patch does not mention is left as it was. `summary` is the one exception, because it
457
584
  * describes the current generation: a patch that moves `currentGen` and does not mention `summary` drops the old
458
585
  * one rather than keep a description of another generation. A `summary` the patch gives must name the
459
- * `currentGen` the row will have. `keptGens` follows the pointer the same way: a patch that moves `currentGen` and does
460
- * not mention it drops it, and one that gives it must name only generations below the `currentGen` the row will have.
586
+ * `currentGen` the row will have. `knownGens` follows the pointer the same way: a patch that moves `currentGen` and does
587
+ * not mention it drops it, and one that gives it must end with the `currentGen` the row will have.
588
+ * "Moves" means changes value: a patch that names `currentGen` at the value it has keeps both.
589
+ *
590
+ * A patch that names a resolved field (`currentGen`, `status`, `wrappedDeks`, `keyId`, `summary`), by presence and at any
591
+ * value, sets the row's {@link RegistryRecord.pointerId} to the token the write returns; any other patch leaves it.
461
592
  */
462
- export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency' | 'summary' | 'keptGens' | 'leases'>>;
593
+ export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency' | 'summary' | 'knownGens' | 'leases'>>;
463
594
  /**
464
595
  * What a caller passes {@link IRegistryDriver.create} or {@link IRegistryDriver.compareAndSwap} besides the write itself.
465
596
  */
@@ -476,6 +607,25 @@ export interface RegistryWriteOptions {
476
607
  * settled by reading the row, never by this record. A driver that ignores the option reads the row.
477
608
  */
478
609
  readonly held?: RegistryRecord | null;
610
+ /**
611
+ * Called with the token this write carries, before the write is sent. A write sent again with a new token reports
612
+ * it; one that repeats the same body repeats the token already reported. The token is the one the write records:
613
+ * when the write lands, the row's {@link RegistryRecord.token} is it, and so is its {@link RegistryRecord.pointerId}
614
+ * when the write names a resolved field. A write that does not land leaves no row carrying it. A call refused before
615
+ * it has a token to send, on a conflict its own read finds, reports none.
616
+ *
617
+ * The token is unique to that write attempt, also against another writer racing the same version: two writers that
618
+ * send a write against the same row version never carry the same token, so a version counter alone is not enough.
619
+ * Otherwise a loser's write would pass for the winner's.
620
+ *
621
+ * It tells a writer which row writes are its own, whatever became of its request: a write that failed, or got no
622
+ * answer, may still have landed, and the row's `pointerId` names its writer when no later write has renewed it: it is
623
+ * then one of the tokens the writer was told. Only a registry that reports {@link IRegistryDriver.reportsWriteTokens}
624
+ * calls it; one that does not ignores it, and a writer then settles such a write by reading the object it published.
625
+ * A registry that wraps or extends another and overrides `create` or `compareAndSwap` passes this option through to
626
+ * the write it forwards, or sets the flag to `false`.
627
+ */
628
+ readonly onToken?: (token: Token) => void;
479
629
  }
480
630
  /** Capabilities a registry driver advertises; validated fail-fast at wiring time. */
481
631
  export interface RegCaps {
@@ -492,9 +642,7 @@ export interface RegCaps {
492
642
  * `true` when this registry's `delete` removes a row from its backend for good, so that no later `list` reads it, and
493
643
  * removes it only while it is still the exact version the delete read: by a delete the backend applies under a
494
644
  * precondition (an S3 or Azure Blob ETag, a GCS object generation), or under a lock no other writer of the backend
495
- * can take. Only a row whose token carries an incarnation id is removed so. A row first written by a release before
496
- * 0.12 has a bare decimal token, and a delete still tombstones it: a process on that release, re-creating the name
497
- * over nothing, would issue those counters again from 0, so its row could not be told apart from the deleted one.
645
+ * can take.
498
646
  *
499
647
  * `false` or absent: every `delete` leaves a tombstone, which every later full `list` still reads. A shipped registry
500
648
  * says which it is.
@@ -613,6 +761,10 @@ export declare function isStorageBackend(value: unknown): value is StorageBacken
613
761
  * keeps going; where nothing of the earlier row is left, or the row is restored from a backup to an older counter, the
614
762
  * random parts alone keep the tokens apart: with probability 1 - 2^-128 per pair of incarnations, and 1 - 2^-64 per
615
763
  * pair of writes at one counter.
764
+ * - **Every row carries its `pointerId`** ({@link RegistryRecord.pointerId}): `create` sets it to the token it returns,
765
+ * a `compareAndSwap` whose patch names a resolved field (`renewsPointer` in `@cloudbitmaps/core/driver-kit`) sets it
766
+ * to the token that write returns, and every other write leaves it. `get` and `list` return it. A caller's write
767
+ * never sets it. A row returned without one fails every read of the segment with {@link UnsupportedError}.
616
768
  * - **`delete` is idempotent** without an `expected` token: deleting an absent row is a no-op, not an error.
617
769
  * **With `expected` it is fenced**: it lands only while the row still carries that token, and otherwise throws
618
770
  * {@link WriteConflictError} and leaves the row.
@@ -631,6 +783,19 @@ export declare function isStorageBackend(value: unknown): value is StorageBacken
631
783
  */
632
784
  export interface IRegistryDriver {
633
785
  capabilities(): RegCaps;
786
+ /**
787
+ * `true` when every `create` and `compareAndSwap` of this registry reports the token its write carries to
788
+ * {@link RegistryWriteOptions.onToken} before sending it, a resend included. A load, an erasure's rewrite and every
789
+ * other publish then knows whether a write that conflicted or got no answer landed by whether the row's `pointerId`
790
+ * is a token it was told, which names the writer and not only the bytes. Absent or `false`: the registry reports
791
+ * none, and such a write is settled by reading the object the publish wrote. A shipped registry says which it is.
792
+ *
793
+ * The flag may be a getter or a field. A registry that wraps or extends another and overrides `create` or
794
+ * `compareAndSwap` either passes `options` through to the write it forwards, or sets `reportsWriteTokens` to
795
+ * `false`. One that does neither reads every write of its own that landed after an error or a lost answer as not its
796
+ * own (`superseded`), and deletes nothing.
797
+ */
798
+ readonly reportsWriteTokens?: boolean;
634
799
  /**
635
800
  * The segment's record, or `null` if it doesn't exist (or was deleted). Throw {@link TransientError} for a fault
636
801
  * worth riding out (throttling, a 5xx, a dropped connection): a reader whose pointer refresh meets one keeps
@@ -666,8 +831,8 @@ export interface IRegistryDriver {
666
831
  */
667
832
  list(namespace?: string): AsyncIterable<RegistryRecord>;
668
833
  /**
669
- * Remove the row. A registry whose {@link RegCaps.conditionalDelete} is `true` removes a row whose token carries an
670
- * incarnation id from its backend; every other row is tombstoned. Either way a later `create` gets a token no
834
+ * Remove the row. A registry whose {@link RegCaps.conditionalDelete} is `true` removes it from its backend; any other
835
+ * tombstones it. Either way a later `create` gets a token no
671
836
  * earlier incarnation of the name held, but for a collision of probability 2^-128 per pair of incarnations.
672
837
  *
673
838
  * Without `expected`, idempotent: deleting an absent row is a no-op.
@@ -0,0 +1,207 @@
1
+ import type { Yielder } from './cooperative.js';
2
+ import type { Rng } from './determinism.js';
3
+ import type { PublishedKept } from './generation-gc.js';
4
+ import type { WrappedDek } from './crypto.js';
5
+ import type { GenKey, IRegistryDriver, KnownGeneration, RegistryRecord, RegistrySummary, Token } from './ports.js';
6
+ /** What a publish is told, beyond the registry and the generation key. */
7
+ interface PublishOptions {
8
+ wrappedDeks?: readonly WrappedDek[];
9
+ expectFrom?: number;
10
+ expectToken?: Token;
11
+ /**
12
+ * The row `expectToken` names, as the caller read it. With it, a row whose token has moved but which differs from
13
+ * this one only in its leases (a reader took or released one) does not refuse the publish: it goes on against the
14
+ * fresh row, after a jittered wait and without the caller redoing its work, up to a bound. Any other difference
15
+ * refuses as ever. Without it, a moved token refuses.
16
+ */
17
+ expectRow?: RegistryRecord | null;
18
+ /**
19
+ * Fields of `expectRow` that a row may also differ in, beside its leases, without the fence refusing: a write that
20
+ * changed only those did not touch what the caller derived its content from. An erasure's rewrite passes `knownGens`,
21
+ * since another erasure's removal of generations from the ledger leaves the generation it streamed alone. A write that
22
+ * also renewed the row's `pointerId` still refuses.
23
+ */
24
+ expectRowIgnores?: readonly (keyof RegistryRecord)[];
25
+ /**
26
+ * Publish only while the segment still has **no registry row**.
27
+ *
28
+ * The fence for a caller whose decision rests on the row being ABSENT: every load that found no row sets it,
29
+ * guarded or not. `expectFrom` and `expectToken` cannot express it: both compare against a value read from a
30
+ * row, so when there was no row there is nothing to compare and both are simply omitted — leaving the publish a
31
+ * bare forward-only advance that happily lands over whatever appeared in the meantime.
32
+ *
33
+ * Without it, a guarded load into a segment that did not exist yet would judge "no row", pass its empty and
34
+ * `minRetained` bounds vacuously, and land an EMPTY generation over the ids a concurrent writer had just loaded,
35
+ * reporting `published: true`. An unguarded load would move the pointer over another first load's generation or
36
+ * onto a row `setRetention` made, and, where an erasure had deleted its object above that row's pointer, onto an
37
+ * object that is gone. Materialising into a destination that does not exist yet is the ordinary first run of a
38
+ * pipeline, so the `*Into` verbs reach this as often as `loadSegment` does.
39
+ */
40
+ expectAbsent?: boolean;
41
+ /**
42
+ * The generation's summary (see {@link RegistrySummary}), written by the write that moves the pointer to it. It
43
+ * names `key.generation`. Absent, the row is left with none for this generation.
44
+ */
45
+ summary?: RegistrySummary;
46
+ /**
47
+ * The segment's row as the caller already read it (`null`: it found none). The first attempt acts on it
48
+ * instead of reading the row again, which is sound because that attempt writes only under the row's own
49
+ * fence: its compare-and-swap carries this row's token, and with no row it creates create-only, so a row
50
+ * that changed or appeared since makes the write lose and the next attempt reads the row afresh. Only the
51
+ * idempotent "already current" answer writes nothing, so a reused row never gives it.
52
+ */
53
+ row?: RegistryRecord | null;
54
+ /**
55
+ * The object was written without encryption. Its publish is refused, with {@link KeyUnavailableError}, onto a row
56
+ * that carries key material: a reader of that segment opens every generation with its key, and a later
57
+ * `destroySegment` would attest that shredding it made a readable object unreadable. A caller decides its key
58
+ * from the row it read before the write, so this closes the window in which another writer creates the
59
+ * segment encrypted meanwhile.
60
+ */
61
+ cleartext?: boolean;
62
+ /**
63
+ * Whether the object under `key` is the one the caller wrote, and throws when it cannot tell. Asked only when a
64
+ * registry write failed and the row then names `key.generation`, on a registry that does not report its writes'
65
+ * tokens ({@link IRegistryDriver.reportsWriteTokens}; one that does settles it by the row's `pointerId` and never
66
+ * asks), to tell this publish's write from another writer's at the same number. Every caller that writes the object
67
+ * passes it. Without it, such a write that ended without an answer over a row that names `key.generation` cannot be
68
+ * settled: the publish throws {@link TransientError} and the caller keeps its object.
69
+ */
70
+ holdsOwnObject?: () => Promise<boolean>;
71
+ /**
72
+ * What a publish waits on before it sends a fresh compare-and-swap, after a write that ended without an answer and
73
+ * left the row as it was. Without a `sleep`, such a write throws at once instead of being sent again.
74
+ */
75
+ clock?: Yielder;
76
+ /** Spreads that wait (full jitter) so concurrent publishers do not retry together. Without it the wait is the bound. */
77
+ rng?: Rng;
78
+ /**
79
+ * Called once for each registry write that ended without an answer, before the row is read back. A caller whose
80
+ * publish then answers `false` can say that it cannot tell whether its own write landed first: the row may have
81
+ * held its generation for a while before another writer moved on.
82
+ */
83
+ onUnanswered?: () => void;
84
+ /**
85
+ * The clock that judges whether a lease has ended. With it, a publish over a row that holds ended leases drops
86
+ * them in the write that moves the pointer, which rewrites the row anyway. Without it, leases are left as they are.
87
+ */
88
+ leasesNow?: () => number;
89
+ /**
90
+ * Clear the row's leases in the write that moves the pointer: the caller's collection deletes every generation
91
+ * below the new pointer whatever a lease says, as a subject erasure's does.
92
+ */
93
+ clearLeases?: boolean;
94
+ /**
95
+ * How many generations below the new pointer the caller keeps. With it, the write that moves the pointer also
96
+ * records the window in the row (the entries of `knownGens`: the `keep` most recently current of the generations
97
+ * below the new one, and the ones a live lease holds), derived from the very row the write is made against, and the
98
+ * result names the generations the window dropped. Without it, or above the most a row collects by name, the result
99
+ * names no window, and a collection lists.
100
+ */
101
+ keep?: number;
102
+ /**
103
+ * The footer checksum of the object under `key` (the second half of its fingerprint), which the write that moves the
104
+ * pointer records in the row's `knownGens` beside the generation. It comes from the object the caller wrote or opened,
105
+ * the same one its `summary` describes.
106
+ */
107
+ footerCrc: number;
108
+ /**
109
+ * The window a ledger starts with, over a row that has no usable one ({@link readSeed}): the generations a count by
110
+ * `keep` keeps, which the write records as the ledger's entries before the new generation.
111
+ */
112
+ seed?: readonly KnownGeneration[];
113
+ }
114
+ /**
115
+ * Point a segment's registry `currentGen` at `key.generation` — the publish step that makes a freshly-written
116
+ * generation the authoritative latest (so registry-aware readers see it). **Forward-only and idempotent:**
117
+ * if the registry has no row it creates one; if it's already at/ahead of `key.generation` it's a no-op (an
118
+ * out-of-order/duplicate publish never regresses the pointer); otherwise it advances via compare-and-swap,
119
+ * retrying a few times under contention. Separated from the Storage write so callers can publish atomically
120
+ * after the immutable object is durable (write-then-publish).
121
+ *
122
+ * **Throws** rather than returning, in two cases, because each would otherwise create a state no reader can
123
+ * recover from: a `destroyed` (crypto-shredded) row, where advancing the pointer would name an object encrypted
124
+ * under a shredded key; and an advance carrying **new** `wrappedDeks`, which would either strand that key or make
125
+ * the row advertise encryption over cleartext generations. Contention that never clears throws
126
+ * {@link WriteConflictError} after the retries.
127
+ *
128
+ * **`expectFrom` turns forward-only into read-modify-write.** Forward-only is the right rule for a writer whose
129
+ * content does not depend on what was current — a load computes its ids upstream, so publishing over a newer
130
+ * generation loses nothing the loader knew about. It is the WRONG rule for a writer that *derived* its content
131
+ * from a particular generation: the erasure rewrite streams generation `from` and clears one bit, so publishing
132
+ * it over a newer generation silently discards whatever that generation added. Passing `expectFrom` makes the
133
+ * publish land only while the pointer is still exactly there, and return `false` otherwise, so the caller can
134
+ * report `superseded` and re-derive. The compare-and-swap itself carries the token read in the same iteration,
135
+ * so the check and the write see the same row.
136
+ *
137
+ * **`expectToken` fences the LINEAGE, and a derived writer needs both.** A generation number identifies a
138
+ * generation only within one incarnation of a name: the numbering restarts at `0` once the row is purged and
139
+ * the bucket empty, so a name that is retired and re-created has a *different* segment wearing the *same*
140
+ * `currentGen`. `expectFrom` alone matched it — and an erasure rewrite then published one incarnation's content
141
+ * over another's, deleted the live objects with its `keep: 0` collection, and returned `erased: true`. The row's
142
+ * OCC token is the identity that survives this: a shipped registry's later `create` gets a token never issued before
143
+ * under that name, but for a collision of probability 2^-128 per pair of incarnations, so no token is reused across them.
144
+ *
145
+ * Pass the token read alongside `expectFrom` and the publish lands only on the same row it was derived from.
146
+ * The check is deliberately **conservative**: the token also changes on writes that are not supersessions at
147
+ * all (a `setRetention`, a due-index reindex), so such a write makes a derived publish report `superseded` and
148
+ * the caller re-derive. That costs a re-run on a rare, unrelated write; the alternative costs a segment.
149
+ *
150
+ * **The driver sends a registry write once, and an unanswered one is reconciled by its effect.** A `create` or
151
+ * `compareAndSwap` that fails with {@link TransientError} (a throttle, a lost response, a timeout) may or may not
152
+ * have landed, and may still be in flight. The publish reads the row once more and decides from what it holds:
153
+ *
154
+ * - the pointer names `key.generation`, on the incarnation the write was made against (the row's `createdAt`), and the
155
+ * write is the caller's own: the write is taken to have landed, and the publish returns `true`. A registry that
156
+ * reports each write's token ({@link IRegistryDriver.reportsWriteTokens}) settles it by the row's `pointerId`, which
157
+ * names the writer of the write that last set the pointer: the write is the call's own when that is the token of a
158
+ * write this call sent. One that does not is settled by `holdsOwnObject` finding the caller's bytes under the number:
159
+ * only the writer of a write-once object publishes its number, but a number is taken again once its object is
160
+ * deleted, and a load of the same cleartext ids writes the same bytes, so another writer's identical object under
161
+ * the number passes for the caller's;
162
+ * - the row is still the one the write was made against (the same token, or still no row): the write may land yet. The
163
+ * publish then waits, on the injected clock, and sends a **new** write from the row it just read: a request of its
164
+ * own, not a replay of the driver's, carrying the same version, so under the registry's fence at most one of the two
165
+ * lands and the other meets a conflict, which the next read settles. At most {@link UNANSWERED_RESENDS} follow, after
166
+ * a wait under 500 ms, then 1 s, then 2 s (full jitter when an `rng` is given); each is settled the same way. Still
167
+ * unanswered, or with no clock to wait on, this throws the registry's own `TransientError` and writes nothing more.
168
+ * A caller must not delete its object on it;
169
+ * - anything else: the row has moved past the state the write was conditioned on, so that write can never land, and
170
+ * the publish goes on as after a lost race, from the row it just read. A pointer at `key.generation` that is not over
171
+ * the caller's own object, or is on another incarnation, is a lost race too and answers `false`, never an already
172
+ * current re-publish.
173
+ *
174
+ * A failed read throws `TransientError` too. The same check of the pointer runs after a {@link WriteConflictError},
175
+ * so a write that landed and then met itself (a registry whose client re-sent it) is not taken for a lost race.
176
+ * The check runs before the next attempt's `expectAbsent`, `expectToken` and `expectFrom` fences, which a write of the
177
+ * caller's own that landed would fail. With a registry that reports tokens it names the writer of the write that last
178
+ * set the pointer, so another writer's identical object under the number does not pass it; with one that does not,
179
+ * that object can.
180
+ *
181
+ * **A `summary` goes in the same write that moves the pointer**, so a reader that sees this generation as current
182
+ * sees the count and metadata that describe it, and every attempt sends the same one. It must be the shape the row's
183
+ * keys call for: sealed for an object written with a key, clear for one written without.
184
+ */
185
+ export declare function publishGenerationKept(registry: IRegistryDriver, key: GenKey, options: PublishOptions): Promise<PublishResult>;
186
+ /** What a publish did: whether the pointer is at the generation, and what the write recorded in the row. */
187
+ export interface PublishResult {
188
+ readonly published: boolean;
189
+ /**
190
+ * What the write that moved the pointer recorded: the list, the generations the window dropped, and the new row's
191
+ * token. Absent when no write of this call is known to have landed (the pointer was already there, or the outcome
192
+ * was settled without knowing which write it was), and a collection then lists.
193
+ */
194
+ readonly kept?: PublishedKept;
195
+ /**
196
+ * A token of the row whose pointer is at the generation, when it is published: the one the write was given, or the
197
+ * row read that showed the pointer there. Every write of a row keeps its incarnation, so this names the incarnation
198
+ * the publish landed in, for the audit event.
199
+ */
200
+ readonly token?: Token;
201
+ }
202
+ /**
203
+ * {@link publishGenerationKept}'s answer to "did the pointer move", for a caller with no use for what it recorded:
204
+ * a publish with no `keep` leaves the row recording no list.
205
+ */
206
+ export declare function publishGeneration(registry: IRegistryDriver, key: GenKey, options: PublishOptions): Promise<boolean>;
207
+ export {};
@@ -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;