@cloudbitmaps/core 0.11.2 → 0.13.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 (44) hide show
  1. package/dist/{chunk-5OMC5U7U.js → chunk-5W3XS2IU.js} +292 -22
  2. package/dist/chunk-5W3XS2IU.js.map +7 -0
  3. package/dist/core/audit.d.ts +6 -0
  4. package/dist/core/chunk-window.d.ts +24 -0
  5. package/dist/core/codec.d.ts +33 -1
  6. package/dist/core/consistency.d.ts +21 -2
  7. package/dist/core/cost.d.ts +91 -10
  8. package/dist/core/crbm/format.d.ts +42 -3
  9. package/dist/core/crbm/reader.d.ts +45 -3
  10. package/dist/core/crbm/writer.d.ts +32 -3
  11. package/dist/core/crbm-storage-source.d.ts +204 -23
  12. package/dist/core/crypto.d.ts +11 -7
  13. package/dist/core/determinism.d.ts +7 -0
  14. package/dist/core/engine.d.ts +41 -8
  15. package/dist/core/erase-id.d.ts +23 -3
  16. package/dist/core/erasure.d.ts +8 -4
  17. package/dist/core/generation-gc.d.ts +102 -4
  18. package/dist/core/load-input.d.ts +55 -0
  19. package/dist/core/load.d.ts +65 -10
  20. package/dist/core/metadata.d.ts +42 -0
  21. package/dist/core/metrics.d.ts +7 -1
  22. package/dist/core/pinned-storage-source.d.ts +2 -1
  23. package/dist/core/ports.d.ts +119 -13
  24. package/dist/core/registry-scan.d.ts +6 -0
  25. package/dist/core/retention-sweep.d.ts +47 -2
  26. package/dist/core/retry.d.ts +14 -0
  27. package/dist/core/rollback.d.ts +34 -4
  28. package/dist/core/summary.d.ts +61 -0
  29. package/dist/core/token.d.ts +36 -0
  30. package/dist/core/validate.d.ts +2 -0
  31. package/dist/driver-kit.js +39 -20
  32. package/dist/driver-kit.js.map +2 -2
  33. package/dist/drivers/_shared/entropy.d.ts +5 -0
  34. package/dist/drivers/_shared/object-registry.d.ts +36 -6
  35. package/dist/drivers/_shared/registry.d.ts +47 -19
  36. package/dist/drivers/localfs/registry.d.ts +10 -0
  37. package/dist/drivers/memory.d.ts +9 -3
  38. package/dist/drivers/retry/retrying-chunk-source.d.ts +2 -1
  39. package/dist/index.d.ts +4 -3
  40. package/dist/index.js +1878 -501
  41. package/dist/index.js.map +4 -4
  42. package/dist/testing/fuzz-core.d.ts +7 -6
  43. package/package.json +1 -1
  44. package/dist/chunk-5OMC5U7U.js.map +0 -7
@@ -1,11 +1,12 @@
1
1
  import { type IAuditSink } from './audit.js';
2
2
  import type { Yielder } from './cooperative.js';
3
- import type { Clock } from './determinism.js';
3
+ import type { Clock, Rng } from './determinism.js';
4
4
  import type { CrbmCrypto, IKeystore, WrappedDek } from './crypto.js';
5
- import type { ChunkRef, StorageChunkSource, GenKey, IStorageDriver, IRegistryDriver, SegmentRef, SegmentSize, Token } from './ports.js';
5
+ import type { ChunkRef, StorageChunkSource, GenerationMetadata, GenerationSummary, GenKey, IStorageDriver, IRegistryDriver, RegistryRecord, RegistrySummary, SegmentRef, SegmentSize, Token } from './ports.js';
6
6
  import { CrbmReader } from './crbm/reader.js';
7
7
  import type { CrbmReaderOptions } from './crbm/reader.js';
8
8
  import type { CodecBitmap, CodecInterface } from './codec.js';
9
+ import { DecodedLoadInput } from './load-input.js';
9
10
  export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
10
11
  /**
11
12
  * Optional {@link IRegistryDriver}. When provided, the current generation is resolved via the registry's
@@ -57,8 +58,8 @@ export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
57
58
  * Aggregate byte ceiling on the parsed `.crbm` indices resident in the reader cache (default 64 MiB) — the
58
59
  * **second half of that memory bound**. `maxOpenSegments` alone bounds by *count*, but a wide/dense segment's
59
60
  * parsed index can reach about 1.3 MB, so 1024 wide indices could pin over a GB and blow a small heap (e.g. a 128 MB
60
- * Lambda) while the count is nominally "in bounds". This caps the summed {@link CrbmReader.retainedIndexBytes}
61
- * across cached readers; the least-recently-used reader is evicted once the total would exceed it — whichever
61
+ * Lambda) while the count is nominally "in bounds". This caps the summed {@link CrbmReader.retainedBytes} (the
62
+ * parsed index, and any metadata a generation carries) across cached readers; the least-recently-used reader is evicted once the total would exceed it — whichever
62
63
  * of the count/byte bounds binds first. Lower it for memory-tight deployments with wide segments; a single
63
64
  * segment whose index alone exceeds the budget is still cached (it can't be shrunk) but nothing else alongside.
64
65
  */
@@ -110,6 +111,13 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
110
111
  * check again.
111
112
  */
112
113
  private invalidations;
114
+ /**
115
+ * Generations whose row summary this process found to disagree with their object, by segment and generation. A
116
+ * summary here is not used again: a count of that generation reads the object. Bounded as the readers are, so a
117
+ * long-lived process cannot grow it without limit; one that falls out is trusted again until its next open finds
118
+ * the disagreement again.
119
+ */
120
+ private readonly distrusted;
113
121
  private readonly registry;
114
122
  private readonly keystore;
115
123
  private readonly requireEncryption;
@@ -117,7 +125,12 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
117
125
  private readonly clock;
118
126
  private readonly currentGenTtlMs;
119
127
  constructor(driver: IStorageDriver, options?: CrbmStorageChunkSourceOptions);
120
- /** The current resolved reader for a segment, refreshing on the TTL. Cheap within the TTL window. */
128
+ /**
129
+ * The segment's live snapshot, refreshing on the TTL. Cheap within the TTL window. The snapshot holds the resolved
130
+ * target, and its reader once a read has asked for one.
131
+ */
132
+ private liveSnapshot;
133
+ /** The current resolved reader for a segment, refreshing on the TTL. */
121
134
  private resolvedReader;
122
135
  /**
123
136
  * A refresh failed transiently and the prior reader keeps serving: make this snapshot lapse after
@@ -125,12 +138,16 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
125
138
  * that asks the registry again. It costs no read of its own; it only changes when the next refresh happens.
126
139
  */
127
140
  private retrySoon;
128
- /** TTL elapsed: cheaply re-resolve `currentGen`; reopen only if it actually changed, else reuse the prior reader. */
129
- private computeRefreshedReader;
130
141
  /**
131
- * Memoize a reader-promise as the segment's snapshot at `now()`, forgetting it later if it resolves to
132
- * null / throws (identity-guarded, so a stale cleanup never clobbers a fresher snapshot). Installing the
133
- * (pending) promise **synchronously** is what lets concurrent callers coalesce onto one in-flight resolve.
142
+ * TTL elapsed: cheaply re-resolve the pointer. The row read is always fresh, so what the new snapshot says of the
143
+ * generation (its summary included) is what the row says now, never the prior snapshot's. The reader is reopened
144
+ * only if the generation or the row changed, and only when a read asks for it.
145
+ */
146
+ private refreshedTarget;
147
+ /**
148
+ * Memoize a snapshot as the segment's at `now()`, forgetting it later if it resolves to null / throws
149
+ * (identity-guarded, so a stale cleanup never clobbers a fresher snapshot). Installing the (pending) snapshot
150
+ * **synchronously** is what lets concurrent callers coalesce onto one in-flight resolve.
134
151
  */
135
152
  private install;
136
153
  private now;
@@ -273,26 +290,51 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
273
290
  */
274
291
  exists(ref: SegmentRef): Promise<boolean>;
275
292
  /**
276
- * Evict a snapshot we just failed to read from — matched by its reader *promise*, so a concurrent call's
293
+ * Evict a snapshot we just failed to read from — matched by the snapshot itself, so a concurrent call's
277
294
  * fresher snapshot is never clobbered. The identity guard is the whole point: `install` replaces the entry
278
295
  * wholesale, so comparing anything else would drop a good snapshot on the floor.
279
296
  */
280
297
  private dropStale;
281
- private resolveLatest;
282
298
  /** Open a {@link CrbmReader} for an already-resolved generation target (decrypting if the segment is encrypted). */
283
299
  private openForTarget;
300
+ private openGeneration;
301
+ /**
302
+ * Open the generation a resolution found, and hold the row's summary against the object while it is open anyway:
303
+ * the same count and metadata, or the summary is not believed again in this process ({@link distrusted}). This
304
+ * costs no request; a mismatch is not an error, since the object is the truth and this read has it.
305
+ */
306
+ private openLive;
307
+ private distrustKey;
308
+ /** What the row says of the generation, unless it is a summary this process has found to disagree with its object. */
309
+ private trustedSummary;
284
310
  /**
285
311
  * The current generation + its DEK wrappings: the registry's authoritative record (one strong read), or a
286
312
  * `list` scan for the max generation when there's no registry (a registry-less source can only read cleartext
287
- * — there's nowhere a wrapped DEK could live).
313
+ * — there's nowhere a wrapped DEK could live). The row's summary rides along, to be used or not by whoever
314
+ * asks.
288
315
  */
289
- private resolveTarget;
316
+ private resolveLive;
290
317
  /** Build the per-generation decryption context for an encrypted segment (undefined for cleartext). */
291
318
  private cryptoForRead;
292
319
  getChunk(ref: ChunkRef): Promise<Uint8Array | null>;
293
320
  listChunkKeys(ref: SegmentRef): Promise<number[]>;
294
321
  sizeOf(ref: SegmentRef): Promise<SegmentSize | null>;
295
322
  cardinalities(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
323
+ /**
324
+ * The current generation's number, id count and metadata. The answer comes from the row the snapshot resolved when
325
+ * it can: the row's summary of the generation it names, on an active row, in the shape the row's keys call for
326
+ * (a sealed one is opened with the segment's key), and one this process has not found to disagree with its
327
+ * object. That is one registry read when the snapshot is cold, none when it is warm, and no read of the object.
328
+ * With no summary it can use (an older row, one for another generation, one that does not open), it opens the
329
+ * generation and answers from the object. A row that names an object that is gone still answers, while a read
330
+ * of the object throws: the number is true of the generation the row names.
331
+ *
332
+ * What the row says is not confirmed here: a summary edited to agree with itself by whoever can write the row is
333
+ * believed until the next time the object is opened, which holds it against the summary.
334
+ */
335
+ summary(ref: SegmentRef): Promise<GenerationSummary | null>;
336
+ /** {@link summary} of a specific generation, from the object a pin holds. `held` as for {@link getChunkAt}. */
337
+ summaryAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<GenerationSummary | null>;
296
338
  /**
297
339
  * Run `read` against the pinned snapshot, healing the torn-read window generation GC can open: if the
298
340
  * generation we resolved was superseded *and* swept (the grace window elapsed), the Storage driver throws
@@ -320,6 +362,17 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
320
362
  * `intersect` would do it N times. Gated in `tests/core/storage-source-heal-open.test.ts` by counting calls.
321
363
  */
322
364
  private withFreshSnapshot;
365
+ /**
366
+ * Whether `err`, raised by a read through the snapshot's reader, came from an object that is no longer the one the
367
+ * reader opened. A generation number can be taken again once its object is deleted (an erasure removes the
368
+ * generations above a rolled-back pointer that held the id, a collection pass can stop part-way, and a load then
369
+ * numbers `currentGen + 1` again), so a reader that still holds the old object's index reads the new object's
370
+ * bytes at the old offsets: a chunk checksum fails, or a range runs past the end. The object's footer says which it
371
+ * was, as it does for a pin ({@link CrbmReader.sameObject}): replaced, or gone, re-resolves; the same object means
372
+ * the error is real, and it stands. Only these two errors pay for the footer read, never a read that succeeded. A
373
+ * transient fault in the check is rethrown; a footer that cannot be read says nothing, and the error stands.
374
+ */
375
+ private replacedUnder;
323
376
  }
324
377
  /**
325
378
  * Write one immutable generation from in-memory bitmaps. Chunks are
@@ -333,9 +386,30 @@ export declare function writeCrbmGeneration(driver: IStorageDriver, key: GenKey,
333
386
  }>, options?: {
334
387
  crypto?: CrbmCrypto;
335
388
  clock?: Yielder;
389
+ metadata?: GenerationMetadata;
336
390
  }): Promise<{
337
391
  size: number;
338
392
  sha256: string;
393
+ fingerprint: string;
394
+ }>;
395
+ /**
396
+ * Whether the object under `key` is provably the one `fingerprint` names ({@link CrbmReader.fingerprint}), from one
397
+ * read of its footer. False when it is another object, when it is gone, and when the read fails in any way: a caller
398
+ * that deletes on `true` deletes nothing it cannot prove is its own.
399
+ */
400
+ export declare function holdsObject(storage: IStorageDriver, key: GenKey, fingerprint: string): Promise<boolean>;
401
+ /** Whether the object under `key` says it is encrypted, from one read of its footer, with no key. */
402
+ export declare function objectIsEncrypted(storage: IStorageDriver, key: GenKey): Promise<boolean>;
403
+ /**
404
+ * What one tail read of the object under `key` tells a rollback about it: whether the footer says it is encrypted, and,
405
+ * when the object and the key agree (an encrypted object opened with `crypto`, a cleartext one with none), the reader
406
+ * the open gives, which holds its count and metadata. No reader when they disagree, which the caller refuses as a
407
+ * target the row cannot use, and none for an encrypted object when the caller has no key. A footer that fails its own
408
+ * checks, an object another generation's, and an index or metadata that do not open are an {@link IntegrityError}.
409
+ */
410
+ export declare function openRollbackTarget(storage: IStorageDriver, key: GenKey, crypto: CrbmCrypto | undefined): Promise<{
411
+ encrypted: boolean;
412
+ reader: CrbmReader | undefined;
339
413
  }>;
340
414
  /** What {@link writeCrbmGenerationStream} wrote: the driver's `{ size, sha256 }` + a tally of the generation. */
341
415
  export interface StreamWriteResult {
@@ -345,12 +419,14 @@ export interface StreamWriteResult {
345
419
  readonly chunkKeys: number[];
346
420
  /** Total ids written. */
347
421
  readonly cardinality: number;
422
+ /** The fingerprint of what this call wrote ({@link CrbmReader.fingerprint}), which tells it from another object under its key. */
423
+ readonly fingerprint: string;
348
424
  }
349
425
  /**
350
426
  * Streaming variant of {@link writeCrbmGeneration} for a **constant-memory** rewrite (the erasure rewrite, a
351
427
  * sorted load): consumes an **already-ascending** async stream of `{ chunkKey, bitmap }`, feeding each to the
352
428
  * codec and freeing it, instead of materializing the whole generation. Paired with a streaming storage sink (S3
353
- * multipart / LocalFs temp file), peak memory is ~one chunk + one part. Returns a tally so the caller can verify
429
+ * multipart / LocalFs temp file), the writer itself holds ~one chunk + one part. Returns a tally so the caller can verify
354
430
  * the re-opened object without re-iterating the (now-consumed) stream. Input **must** be ascending by `chunkKey`
355
431
  * — the codec rejects an out-of-order chunk; empty bitmaps are skipped.
356
432
  */
@@ -360,6 +436,7 @@ export declare function writeCrbmGenerationStream(driver: IStorageDriver, key: G
360
436
  }>, options?: {
361
437
  crypto?: CrbmCrypto;
362
438
  clock?: Yielder;
439
+ metadata?: GenerationMetadata;
363
440
  }): Promise<StreamWriteResult>;
364
441
  /**
365
442
  * Point a segment's registry `currentGen` at `key.generation` — the publish step that makes a freshly-written
@@ -385,17 +462,43 @@ export declare function writeCrbmGenerationStream(driver: IStorageDriver, key: G
385
462
  * so the check and the write see the same row.
386
463
  *
387
464
  * **`expectToken` fences the LINEAGE, and a derived writer needs both.** A generation number identifies a
388
- * generation only within one incarnation of a name: `nextGeneration` restarts at `0` once the row is purged and
465
+ * generation only within one incarnation of a name: the numbering restarts at `0` once the row is purged and
389
466
  * the bucket empty, so a name that is retired and re-created has a *different* segment wearing the *same*
390
467
  * `currentGen`. `expectFrom` alone matched it — and an erasure rewrite then published one incarnation's content
391
468
  * over another's, deleted the live objects with its `keep: 0` collection, and returned `erased: true`. The row's
392
- * OCC token is the identity that survives this: {@link IRegistryDriver.delete} tombstones rather than unlinks,
393
- * so "a later `create` still gets a fresh, greater token" and no token is ever reused across incarnations.
469
+ * OCC token is the identity that survives this: a shipped registry's later `create` gets a token never issued before
470
+ * under that name, but for a collision of probability 2^-128 per pair of incarnations, so no token is reused across them.
394
471
  *
395
472
  * Pass the token read alongside `expectFrom` and the publish lands only on the same row it was derived from.
396
473
  * The check is deliberately **conservative**: the token also changes on writes that are not supersessions at
397
474
  * all (a `setRetention`, a due-index reindex), so such a write makes a derived publish report `superseded` and
398
475
  * the caller re-derive. That costs a re-run on a rare, unrelated write; the alternative costs a segment.
476
+ *
477
+ * **The driver sends a registry write once, and an unanswered one is reconciled by its effect.** A `create` or
478
+ * `compareAndSwap` that fails with {@link TransientError} (a throttle, a lost response, a timeout) may or may not
479
+ * have landed, and may still be in flight. The publish reads the row once more and decides from what it holds:
480
+ *
481
+ * - the pointer names `key.generation`, on the incarnation the write was made against (the row's `createdAt`), and
482
+ * `holdsOwnObject` proves the object under it the caller's: the write landed, and the publish returns `true`. Only
483
+ * the writer of a write-once object publishes its number, and a number is never taken while an object holds it;
484
+ * - the row is still the one the write was made against (the same token, or still no row): the write may land yet. The
485
+ * publish then waits, on the injected clock, and sends a **new** write from the row it just read: a request of its
486
+ * own, not a replay of the driver's, carrying the same version, so under the registry's fence at most one of the two
487
+ * lands and the other meets a conflict, which the next read settles. At most {@link UNANSWERED_RESENDS} follow, after
488
+ * 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
489
+ * unanswered, or with no clock to wait on, this throws the registry's own `TransientError` and writes nothing more.
490
+ * A caller must not delete its object on it;
491
+ * - anything else: the row has moved past the state the write was conditioned on, so that write can never land, and
492
+ * the publish goes on as after a lost race, from the row it just read. A pointer at `key.generation` that is not over
493
+ * the caller's own object, or is on another incarnation, is a lost race too and answers `false`, never an already
494
+ * current re-publish.
495
+ *
496
+ * A failed read throws `TransientError` too. The same check of the pointer runs after a {@link WriteConflictError},
497
+ * so a write that landed and then met itself (a registry whose client re-sent it) is not taken for a lost race.
498
+ *
499
+ * **A `summary` goes in the same write that moves the pointer**, so a reader that sees this generation as current
500
+ * sees the count and metadata that describe it, and every attempt sends the same one. It must be the shape the row's
501
+ * keys call for: sealed for an object written with a key, clear for one written without.
399
502
  */
400
503
  export declare function publishGeneration(registry: IRegistryDriver, key: GenKey, options?: {
401
504
  wrappedDeks?: readonly WrappedDek[];
@@ -417,7 +520,55 @@ export declare function publishGeneration(registry: IRegistryDriver, key: GenKey
417
520
  * destination that does not exist yet is the ordinary first run of a pipeline.
418
521
  */
419
522
  expectAbsent?: boolean;
523
+ /**
524
+ * The generation's summary (see {@link RegistrySummary}), written by the write that moves the pointer to it. It
525
+ * names `key.generation`. Absent, the row is left with none for this generation.
526
+ */
527
+ summary?: RegistrySummary;
528
+ /**
529
+ * The segment's row as the caller already read it (`null`: it found none). The first attempt acts on it
530
+ * instead of reading the row again, which is sound because that attempt writes only under the row's own
531
+ * fence: its compare-and-swap carries this row's token, and with no row it creates create-only, so a row
532
+ * that changed or appeared since makes the write lose and the next attempt reads the row afresh. Only the
533
+ * idempotent "already current" answer writes nothing, so a reused row never gives it.
534
+ */
535
+ row?: RegistryRecord | null;
536
+ /**
537
+ * The object was written without encryption. Its publish is refused, with {@link KeyUnavailableError}, onto a row
538
+ * that carries key material: a reader of that segment opens every generation with its key, and a later
539
+ * `destroySegment` would attest that shredding it made a readable object unreadable. A caller decides its key
540
+ * from the row it read before the write, so this closes the window in which another writer creates the
541
+ * segment encrypted meanwhile.
542
+ */
543
+ cleartext?: boolean;
544
+ /**
545
+ * Whether the object under `key` is the one the caller wrote, and throws when it cannot tell. Asked only when a
546
+ * registry write failed and the row then names `key.generation`, to tell this publish's write from another
547
+ * writer's at the same number. Every caller that writes the object passes it. Without it, a write that ended
548
+ * without an answer over a row that names `key.generation` cannot be settled: the publish throws
549
+ * {@link TransientError} and the caller keeps its object.
550
+ */
551
+ holdsOwnObject?: () => Promise<boolean>;
552
+ /**
553
+ * What a publish waits on before it sends a fresh compare-and-swap, after a write that ended without an answer and
554
+ * left the row as it was. Without a `sleep`, such a write throws at once instead of being sent again.
555
+ */
556
+ clock?: Yielder;
557
+ /** Spreads that wait (full jitter) so concurrent publishers do not retry together. Without it the wait is the bound. */
558
+ rng?: Rng;
559
+ /**
560
+ * Called once for each registry write that ended without an answer, before the row is read back. A caller whose
561
+ * publish then answers `false` can say that it cannot tell whether its own write landed first: the row may have
562
+ * held its generation for a while before another writer moved on.
563
+ */
564
+ onUnanswered?: () => void;
420
565
  }): Promise<boolean>;
566
+ /**
567
+ * Whether the object under `key` is the one `fingerprint` names, from one read of its footer: false when it is another
568
+ * object or gone. Unlike {@link holdsObject}, a read that cannot tell throws, for a caller that must not take "could
569
+ * not look" for "not this object".
570
+ */
571
+ export declare function provesOwnObject(storage: IStorageDriver, key: GenKey, fingerprint: string): Promise<boolean>;
421
572
  /** What a bulk-load wrote — the driver's `{ size, sha256 }` plus a summary of the built generation. */
422
573
  export interface BulkLoadResult {
423
574
  /** Bytes written to the Storage object. */
@@ -436,8 +587,18 @@ export interface BulkLoadResult {
436
587
  * unreadable: the bytes are encrypted with a key nothing recorded.
437
588
  */
438
589
  readonly wrappedDeks?: readonly WrappedDek[];
590
+ /** Whether the object was written encrypted: under the segment's key, or under one this call minted. */
591
+ readonly encrypted: boolean;
592
+ /** The written object's fingerprint, which tells it from an object stored later under the same key. */
593
+ readonly fingerprint: string;
439
594
  /** Total distinct ids in the generation (post-dedup). */
440
595
  readonly cardinality: number;
596
+ /**
597
+ * The summary of what was written, for the write that makes it current: its id count and metadata, sealed under the
598
+ * segment's key when the object was written with one. A caller that defers the publish (`publish: false`) passes it
599
+ * to {@link publishGeneration}.
600
+ */
601
+ readonly summary: RegistrySummary;
441
602
  /**
442
603
  * Whether this generation is now the segment's **current** one — `undefined` when no `registry` was wired, so
443
604
  * there is no pointer and no publish step to report on.
@@ -467,11 +628,12 @@ export interface BulkLoadResult {
467
628
  * commits only after the callback resolves). An empty source writes a valid empty generation.
468
629
  *
469
630
  * Writes a fresh full snapshot of a segment. The caller picks the generation number in `key` — `nextGeneration`
470
- * computes the right one from the registry and the bucket — and re-using an existing generation throws
631
+ * computes one from the registry and the bucket, as `nextLoadGeneration` does for a load — and re-using an existing
632
+ * generation throws
471
633
  * {@link WriteConflictError} (write-once). Without a registry a `StorageChunkSource` serves the **highest**
472
634
  * generation present, so a too-high number silently shadows real data; with one, `publishGeneration` decides.
473
635
  */
474
- export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenKey, ids: Iterable<number> | AsyncIterable<number>, options?: {
636
+ export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenKey, ids: Iterable<number> | AsyncIterable<number> | DecodedLoadInput, options?: {
475
637
  registry?: IRegistryDriver;
476
638
  keystore?: IKeystore;
477
639
  requireEncryption?: boolean;
@@ -502,6 +664,22 @@ export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenK
502
664
  * current.
503
665
  */
504
666
  publish?: boolean;
667
+ /**
668
+ * The generation's metadata (see {@link GenerationMetadata}), written into the object and carried in the summary.
669
+ * The caller has checked it against the metadata rules; the writer checks it again, and refuses a record that breaks
670
+ * one before a byte is written.
671
+ */
672
+ metadata?: GenerationMetadata;
673
+ /**
674
+ * The segment's row as the caller already read it (`null`: it found none), for a caller that defers the
675
+ * publish (`publish: false`) and fences it on that same row. A present cleartext row is then used as read,
676
+ * rather than read again: a row that changes in between (a publish, a drop, a purge and re-create) makes the
677
+ * fenced publish lose, so the object is never published on the strength of the stale read. A row read as
678
+ * absent, or one carrying key material, is read again after the ids, as with no row passed: a first load must
679
+ * see a row another writer created meanwhile, and an encrypted segment's key is unwrapped only from a row read
680
+ * after the ids, so a segment shredded while they streamed is refused before its key is used.
681
+ */
682
+ row?: RegistryRecord | null;
505
683
  }): Promise<BulkLoadResult>;
506
684
  /**
507
685
  * Open a {@link CrbmReader} on one generation over the storage driver's range/tail reads (decrypting if `crypto`).
@@ -511,12 +689,15 @@ export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenK
511
689
  export declare function openGenerationReader(storage: IStorageDriver, key: GenKey, crypto: CrbmCrypto | undefined, options?: Omit<CrbmReaderOptions, 'crypto'>): Promise<CrbmReader>;
512
690
  /**
513
691
  * Re-open a freshly written generation and assert it round-trips exactly what was streamed into it: the same
514
- * per-chunk key set *and* the same total cardinality (on top of the codec's own per-chunk CRC + footer checks).
515
- * `expected` is the streaming writer's tally (the stream is consumed, so it can't be re-iterated) — the key-set
516
- * comparison catches a dropped/extra chunk that a cardinality-only check could miss when two errors cancel out.
692
+ * per-chunk key set *and* the same total cardinality (on top of the codec's own per-chunk CRC + footer checks), and the
693
+ * metadata it was given. `expected` is the streaming writer's tally (the stream is consumed, so it can't be
694
+ * re-iterated) — the key-set comparison catches a dropped/extra chunk that a cardinality-only check could miss when
695
+ * two errors cancel out. The count and the metadata are what the generation's row summary is about to claim, so the
696
+ * summary is held to the object it describes before it is published.
517
697
  * Throws {@link IntegrityError}: the object is on disk but must not be published.
518
698
  */
519
699
  export declare function verifyGeneration(storage: IStorageDriver, key: GenKey, expected: {
520
700
  readonly chunkKeys: readonly number[];
521
701
  readonly cardinality: number;
702
+ readonly metadata?: GenerationMetadata | undefined;
522
703
  }, crypto: CrbmCrypto | undefined): Promise<void>;
@@ -62,19 +62,23 @@ export interface IKeystore {
62
62
  }
63
63
  /**
64
64
  * What the encrypting codec needs: the {@link Aead} for this segment's DEK plus a way to build the AAD for a
65
- * given scope (`'index'` or a chunkKey). Built per (segment, generation) by the storage-source bridge so the
66
- * codec itself stays segment-agnostic.
65
+ * given scope (a chunkKey, `'index'`, or `'metadata'` for the generation's metadata section). Built per
66
+ * (segment, generation) by the storage-source bridge so the codec itself stays segment-agnostic.
67
67
  */
68
68
  export interface CrbmCrypto {
69
69
  readonly aead: Aead;
70
- aadFor(scope: number | 'index'): Uint8Array;
70
+ aadFor(scope: number | 'index' | 'metadata'): Uint8Array;
71
71
  }
72
72
  /**
73
- * Build the AEAD associated-data binding a chunk (or the index) to its exact location:
74
- * `v1 ‖ len(namespace) ‖ namespace ‖ len(segment) ‖ segment ‖ generation ‖ scope ‖ chunkKey`. Length-prefixed
75
- * so distinct `(namespace, segment)` pairs can never collide. Pure (no crypto) — just the authenticated label.
73
+ * Build the AEAD associated-data binding a chunk, the index, the metadata section or a registry row's sealed summary
74
+ * to its exact location:
75
+ * `v1 ‖ len(namespace) ‖ namespace ‖ len(segment) ‖ segment ‖ generation ‖ scope ‖ chunkKey`, where scope is
76
+ * `0` for a chunk, `1` for the index, `2` for the metadata and `3` for the row's summary, and chunkKey is `0` for
77
+ * the last three.
78
+ * Length-prefixed so distinct `(namespace, segment)` pairs can never collide. Pure (no crypto) — just the
79
+ * authenticated label.
76
80
  */
77
81
  export declare function aadFor(ref: {
78
82
  readonly namespace?: string;
79
83
  readonly segment: string;
80
- }, generation: number, scope: number | 'index'): Uint8Array;
84
+ }, generation: number, scope: number | 'index' | 'metadata' | 'summary'): Uint8Array;
@@ -40,3 +40,10 @@ export interface Rng {
40
40
  /** A float in the half-open interval [0, 1). */
41
41
  next(): number;
42
42
  }
43
+ /**
44
+ * Injected entropy: `length` cryptographically strong random bytes, for an identity that must never repeat (a
45
+ * registry row's incarnation id, and the random part every registry write adds to its token). Deliberately not an
46
+ * {@link Rng}: an `Rng` is seedable for simulation, and a seeded source hands every process the same "random"
47
+ * identity. Production wiring uses the platform's Web Crypto; a test injects a deterministic source to replay a run.
48
+ */
49
+ export type Entropy = (length: number) => Uint8Array;
@@ -3,11 +3,12 @@ import type { Budget, BudgetOption } from './budget.js';
3
3
  import type { Clock } from './determinism.js';
4
4
  import type { BoundedLru } from './lru.js';
5
5
  import type { IMetricsSink } from './metrics.js';
6
- import type { StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
6
+ import type { GenerationMetadata, StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
7
7
  export interface EngineDeps {
8
8
  readonly storage: StorageChunkSource;
9
9
  /** Optional cache of decoded (immutable) Storage chunks. */
10
10
  readonly cache?: BoundedLru<string, CodecBitmap>;
11
+ /** Per-chunk decode ceiling (invariant 5); defaults to 1 MiB. The `.crbm` reader refuses an entry above its own `maxPayloadBytes` (1 MiB, plus 28 bytes when encrypted) at open, so raise that on the chunk source too. */
11
12
  readonly maxBitmapBytes?: number;
12
13
  /**
13
14
  * The bitmap codec — **required**. `core/` is codec-agnostic: it can have no default, because the concrete codec
@@ -47,7 +48,13 @@ export interface IdRange {
47
48
  }
48
49
  /** Options common to the chunk-aligned combines. */
49
50
  export interface CombineOptions extends IdRange {
50
- /** Max chunk keys resolved concurrently — bounds the Storage footprint. A positive integer. */
51
+ /**
52
+ * Max chunk keys resolved concurrently — bounds the Storage footprint (about `concurrency × operands` chunks in
53
+ * flight). A positive integer; default 32. The window opens 8 keys wide (or `concurrency`, if lower) and widens
54
+ * as keys are taken, so a read that stops early fetches no more than a few keys ahead; one that runs to the end
55
+ * spends nearly all of its round trips at the full width. A read that stops early has requested up to
56
+ * `concurrency` keys per operand past the last one it used.
57
+ */
51
58
  readonly concurrency?: number;
52
59
  /** Override the store's per-op budget for this call (`false` lifts it). */
53
60
  readonly budget?: BudgetOption;
@@ -70,19 +77,37 @@ export declare class SegmentEngine {
70
77
  private readonly metricsOn;
71
78
  /** Resolved per-op budget (null = disabled); undefined deps ⇒ the generous default. */
72
79
  private readonly budget;
80
+ /**
81
+ * The chunk reads open right now, by cache key: a caller that misses the cache and finds its key here awaits that
82
+ * read instead of making its own. An entry lives from the request until it settles, so the map holds at most one
83
+ * promise per distinct key in flight.
84
+ */
85
+ private readonly openReads;
73
86
  constructor(deps: EngineDeps);
74
87
  /** Membership: one chunk lookup — the cache, else one Storage fetch of that chunk. */
75
88
  has(seg: SegmentRef, id: number): Promise<boolean>;
76
89
  /**
77
- * Cardinality. When the Storage source can serve per-chunk cardinality from its `.crbm` index (the `.crbm`
78
- * source can), the count is summed from the index with **zero payload reads**. A source without that
79
- * capability (the in-memory source) falls back to fetching every chunk.
90
+ * Cardinality. When the Storage source can describe the current generation (the `.crbm` source can), the count is
91
+ * that description's: the registry row's summary when the row has one it can use, which is one request when cold
92
+ * and no read of the object, and otherwise the sum of the object's index. Either way, **zero payload reads**. A
93
+ * source that can only serve per-chunk cardinality sums those, and one with neither (the in-memory source) falls back
94
+ * to fetching every chunk.
80
95
  *
81
- * The index sum is trusted, not confirmed: the reader checked the index for internal consistency when it opened
96
+ * The row's summary and the index sum are trusted, not confirmed: the source holds the summary against the object
97
+ * whenever it opens it, and the reader checked the index for internal consistency when it opened
82
98
  * the object, and a corrupt index that is still internally consistent yields a wrong count. `iterate()` and the
83
99
  * combines decode the payloads.
84
100
  */
85
101
  count(seg: SegmentRef): Promise<number>;
102
+ /**
103
+ * What the segment's current generation is: its number, its id count and its metadata, from one resolution, so the
104
+ * three cannot straddle a publish. A source with no summary answers from `count()` and the generation it resolves.
105
+ */
106
+ stat(seg: SegmentRef): Promise<{
107
+ generation: number | null;
108
+ cardinality: number;
109
+ metadata?: GenerationMetadata;
110
+ }>;
86
111
  /** Whether the Storage source can measure segment size (for grounded cost); false ⇒ storage isn't grounded. */
87
112
  get supportsStorageSize(): boolean;
88
113
  /** How often the Storage source re-reads a pointer while reading (cost reporting); undefined if it does not say. */
@@ -92,7 +117,7 @@ export declare class SegmentEngine {
92
117
  /** The chunks of `seg` at `chunkKeys`, read through a window of {@link DEFAULT_INTERSECT_CONCURRENCY}. */
93
118
  private chunkWindow;
94
119
  /**
95
- * Every id, ascending, reading ahead through a window of up to 8 chunk fetches that opens 1, 2, 4, 8 wide, so a
120
+ * Every id, ascending, reading ahead through a window of up to 32 chunk fetches that opens 1, 2, 4 … 32 wide, so a
96
121
  * read that stops early has fetched only a handful of chunks past the one it stopped in; with `range`, only the ids in `(after, through]`, fetching only the
97
122
  * chunks the range overlaps (see {@link IdRange}).
98
123
  *
@@ -243,7 +268,15 @@ export declare class SegmentEngine {
243
268
  * `gen === undefined` ⇒ the source can't report a generation (it only ever serves one) ⇒ the key stays
244
269
  * generation-free. Superseded-generation entries age out under the LRU ceiling — no active purge.
245
270
  *
246
- * The returned instance is **shared** (it may be the cached one): callers read it or clone it, never mutate it.
271
+ * Concurrent misses of one key share **one** request, with or without a cache: the first caller starts the read,
272
+ * later ones await it, and the entry goes when the read settles, so a failure is every waiting caller's and the
273
+ * next call reads again. The key carries the version exactly as the cache's does, so two versions of one chunk
274
+ * are two reads, never one.
275
+ *
276
+ * The returned instance is **shared** (it may be the cached one, and every caller that joined a read gets the same
277
+ * one): callers read it or clone it, never mutate it.
247
278
  */
248
279
  private storageChunk;
280
+ /** One request for one chunk, decoded, range-checked and cached. */
281
+ private fetchChunk;
249
282
  }
@@ -6,8 +6,14 @@
6
6
  * is what every other write in this library is: a new generation. The current generation is streamed chunk by
7
7
  * chunk through the ascending writer — every chunk decoded, range-checked and re-encoded, the one holding the id
8
8
  * with that bit cleared — then published fenced on the generation it streamed, and that generation is collected immediately
9
- * (`keep: 0`), so the bit is **physically gone from the bucket when this returns**. Constant memory: one chunk in
10
- * flight, never the whole segment.
9
+ * (`keep: 0`), so the bit is **physically gone from the bucket when this returns**. Bounded memory: a window of 32
10
+ * chunk reads ahead of the writer, never the whole segment.
11
+ *
12
+ * The rewrite is the same generation without one id, so it keeps everything else: the new object carries the source's
13
+ * metadata as it is, and the row's summary of it, built from what was written, counts one id fewer and holds the same
14
+ * metadata. Metadata is not scanned for the id. On an encrypted segment, a source object with no metadata block whose
15
+ * row's sealed summary of it has metadata is rewritten with the row's: the block's presence is not authenticated, so
16
+ * the authenticated copy decides, and the erasure still goes through.
11
17
  *
12
18
  * **`erased: true` is a claim about every generation of the segment, not only the one it replaced.** A rollback
13
19
  * leaves generations above the pointer that were once current and can be made current again, so a holder can sit
@@ -69,8 +75,10 @@
69
75
  import { type IAuditSink } from './audit.js';
70
76
  import type { CodecInterface } from './codec.js';
71
77
  import type { Yielder } from './cooperative.js';
78
+ import type { Rng } from './determinism.js';
72
79
  import type { IKeystore } from './crypto.js';
73
80
  import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
81
+ import { type ReadRetry } from './retry.js';
74
82
  /** What {@link eraseIdFromSegment} needs: the objects, the pointer, the codec, and the key material if encrypted. */
75
83
  export interface EraseIdDeps {
76
84
  readonly storage: IStorageDriver;
@@ -90,8 +98,20 @@ export interface EraseIdDeps {
90
98
  readonly requireEncryption?: boolean;
91
99
  /** Supplying a clock that can yield makes the rewrite cooperative, as it makes a load. */
92
100
  readonly clock?: Yielder;
93
- /** Per-chunk decode ceiling (invariant 5); defaults to 1 MiB. */
101
+ /**
102
+ * The random source that spreads the waits between the rewrite's fresh compare-and-swaps. Absent, the read retry's
103
+ * source is used if it has one, and otherwise each wait is its bound.
104
+ */
105
+ readonly rng?: Rng;
106
+ /** Per-chunk decode ceiling (invariant 5); defaults to 1 MiB. The `.crbm` reader refuses an entry above its own `maxPayloadBytes` (1 MiB, plus 28 bytes when encrypted) at open, so raise that on the chunk source too. */
94
107
  readonly maxBitmapBytes?: number;
108
+ /**
109
+ * The store's read retry, for the reads the rewrite makes along the way: the generation it rewrites and each of its
110
+ * chunks, the read-back that verifies the generation it wrote, and any other generation it checks for the id. A
111
+ * transient fault on one is run again under it rather than failing the erasure. Absent, each read is made once.
112
+ * The deletes are not retried, and the rewrite's registry write is settled as a load's is.
113
+ */
114
+ readonly readRetry?: ReadRetry;
95
115
  }
96
116
  export interface EraseIdResult {
97
117
  readonly segment: string;
@@ -159,10 +159,14 @@ export interface DropResult {
159
159
  * cleaned up by re-running; a torn pointer costs correctness and is not self-healing. Given the choice, leak
160
160
  * bytes — but say so: whatever survives the sweep is reported in {@link DropResult.generationsRemaining}.
161
161
  *
162
- * **Why step 2 sweeps more than once.** A load that was already writing its object when the tombstone landed
163
- * still finishes the write — its publish is then refused, but the object survives, and it holds the complete
164
- * set. A single list-then-delete misses it entirely. The re-sweep converges because the tombstone *is* a hard
165
- * fence on **publishing**, so only already-in-flight writes can appear and they are finite.
162
+ * **Why step 2 sweeps more than once.** A load that had read the row before the tombstone landed, whether it was
163
+ * already writing its object or still consuming its ids, still finishes the write — its publish is then refused,
164
+ * but the object exists, and it holds the complete set. A single list-then-delete misses it entirely. The re-sweep
165
+ * converges because the tombstone *is* a hard fence on **publishing**, so only loads already under way can write
166
+ * and they are finite. One that writes after the last pass deletes its own object when its publish is refused, as a
167
+ * refused load does under a `destroyed` row; only one whose process stops in between, or whose publish fails without
168
+ * a definite answer (a lost response, a timeout), leaves it behind, for a re-run of the drop, and
169
+ * `generationsRemaining` cannot report an object written after this call returned.
166
170
  *
167
171
  * **When "reads as empty" starts being true.** Not instantly, for a store that has already read this segment: a
168
172
  * resolved generation is cached and decoded chunks sit in the cache, so an in-flight reader can answer from