@cloudbitmaps/core 0.11.1 → 0.12.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 (42) 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/codec.d.ts +33 -1
  5. package/dist/core/consistency.d.ts +21 -2
  6. package/dist/core/cost.d.ts +91 -10
  7. package/dist/core/crbm/format.d.ts +31 -1
  8. package/dist/core/crbm/reader.d.ts +77 -18
  9. package/dist/core/crbm/writer.d.ts +32 -3
  10. package/dist/core/crbm-storage-source.d.ts +204 -23
  11. package/dist/core/crypto.d.ts +11 -7
  12. package/dist/core/determinism.d.ts +7 -0
  13. package/dist/core/engine.d.ts +22 -7
  14. package/dist/core/erase-id.d.ts +20 -0
  15. package/dist/core/erasure.d.ts +8 -4
  16. package/dist/core/generation-gc.d.ts +102 -4
  17. package/dist/core/load-input.d.ts +55 -0
  18. package/dist/core/load.d.ts +65 -10
  19. package/dist/core/metadata.d.ts +42 -0
  20. package/dist/core/pinned-storage-source.d.ts +2 -1
  21. package/dist/core/ports.d.ts +119 -13
  22. package/dist/core/registry-scan.d.ts +6 -0
  23. package/dist/core/retention-sweep.d.ts +47 -2
  24. package/dist/core/retry.d.ts +14 -0
  25. package/dist/core/rollback.d.ts +34 -4
  26. package/dist/core/summary.d.ts +61 -0
  27. package/dist/core/token.d.ts +36 -0
  28. package/dist/core/validate.d.ts +2 -0
  29. package/dist/driver-kit.js +39 -20
  30. package/dist/driver-kit.js.map +2 -2
  31. package/dist/drivers/_shared/entropy.d.ts +5 -0
  32. package/dist/drivers/_shared/object-registry.d.ts +36 -6
  33. package/dist/drivers/_shared/registry.d.ts +47 -19
  34. package/dist/drivers/localfs/registry.d.ts +10 -0
  35. package/dist/drivers/memory.d.ts +9 -3
  36. package/dist/drivers/retry/retrying-chunk-source.d.ts +2 -1
  37. package/dist/index.d.ts +4 -3
  38. package/dist/index.js +1922 -512
  39. package/dist/index.js.map +4 -4
  40. package/dist/testing/fuzz-core.d.ts +6 -5
  41. package/package.json +1 -1
  42. package/dist/chunk-5OMC5U7U.js.map +0 -7
@@ -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,7 +3,7 @@ import type { Budget, BudgetOption } from './budget.js';
3
3
  import type { Clock } from './determinism.js';
4
4
  import type { BoundedLru } from './lru.js';
5
5
  import type { IMetricsSink } from './metrics.js';
6
- import type { 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. */
@@ -74,23 +74,38 @@ export declare class SegmentEngine {
74
74
  /** Membership: one chunk lookup — the cache, else one Storage fetch of that chunk. */
75
75
  has(seg: SegmentRef, id: number): Promise<boolean>;
76
76
  /**
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.
77
+ * Cardinality. When the Storage source can describe the current generation (the `.crbm` source can), the count is
78
+ * that description's: the registry row's summary when the row has one it can use, which is one request when cold
79
+ * and no read of the object, and otherwise the sum of the object's index. Either way, **zero payload reads**. A
80
+ * source that can only serve per-chunk cardinality sums those, and one with neither (the in-memory source) falls back
81
+ * to fetching every chunk.
80
82
  *
81
- * The index sum is trusted, not confirmed: the reader checked the index for internal consistency when it opened
83
+ * The row's summary and the index sum are trusted, not confirmed: the source holds the summary against the object
84
+ * whenever it opens it, and the reader checked the index for internal consistency when it opened
82
85
  * the object, and a corrupt index that is still internally consistent yields a wrong count. `iterate()` and the
83
86
  * combines decode the payloads.
84
87
  */
85
88
  count(seg: SegmentRef): Promise<number>;
89
+ /**
90
+ * What the segment's current generation is: its number, its id count and its metadata, from one resolution, so the
91
+ * three cannot straddle a publish. A source with no summary answers from `count()` and the generation it resolves.
92
+ */
93
+ stat(seg: SegmentRef): Promise<{
94
+ generation: number | null;
95
+ cardinality: number;
96
+ metadata?: GenerationMetadata;
97
+ }>;
86
98
  /** Whether the Storage source can measure segment size (for grounded cost); false ⇒ storage isn't grounded. */
87
99
  get supportsStorageSize(): boolean;
88
100
  /** How often the Storage source re-reads a pointer while reading (cost reporting); undefined if it does not say. */
89
101
  get pointerRefreshMs(): number | undefined;
90
102
  /** Grounded Storage size of a segment's current generation (cost reporting), or null if it has no generation. */
91
103
  segmentSize(seg: SegmentRef): Promise<SegmentSize | null>;
104
+ /** The chunks of `seg` at `chunkKeys`, read through a window of {@link DEFAULT_INTERSECT_CONCURRENCY}. */
105
+ private chunkWindow;
92
106
  /**
93
- * Every id, ascending, one chunk at a time; with `range`, only the ids in `(after, through]`, fetching only the
107
+ * Every id, ascending, reading ahead through a window of up to 8 chunk fetches that opens 1, 2, 4, 8 wide, so a
108
+ * 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
94
109
  * chunks the range overlaps (see {@link IdRange}).
95
110
  *
96
111
  * Two generators, not one with a branch: a second `yield` site in the full read's generator grows its frame, and
@@ -124,7 +139,7 @@ export declare class SegmentEngine {
124
139
  * up front (before the fan-out) and threaded into every chunk read, so a concurrent load can't corrupt or tear
125
140
  * the result — every chunk read is a whole, checksum-verified, immutable generation. The edge a *long* call can
126
141
  * hit is invariant 3's: if it straddles a mid-call `cache.genTtlMs` boundary and a load has published, an
127
- * operand's not-yet-read chunks may re-resolve forward to the newer generation (a generation hop within one long
142
+ * operand's not-yet-requested chunks may re-resolve forward to the newer generation (a generation hop within one long
128
143
  * call) — the call never crashes or returns a torn object, but may mix generations. Three things hop it without
129
144
  * waiting for the TTL, so a shorter call can meet them too: **the reader cache evicting an operand mid-call**
130
145
  * (`maxOpenSegments`), whose re-read re-resolves fresh; a sweep deleting the generation it was reading, which
@@ -9,6 +9,12 @@
9
9
  * (`keep: 0`), so the bit is **physically gone from the bucket when this returns**. Constant memory: one chunk in
10
10
  * flight, never the whole segment.
11
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.
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
14
20
  * above the pointer as well as below it, and there can be several. Before it reports `erased: true` the call
@@ -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;
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;
93
106
  /** Per-chunk decode ceiling (invariant 5); defaults to 1 MiB. */
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
@@ -1,4 +1,4 @@
1
- import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
1
+ import type { GenKey, IStorageDriver, IRegistryDriver, RegistryRecord, SegmentRef } from './ports.js';
2
2
  /** The two ports generation bookkeeping needs: the objects, and the pointer that says which one is current. */
3
3
  export interface GenerationDeps {
4
4
  readonly storage: IStorageDriver;
@@ -6,7 +6,9 @@ export interface GenerationDeps {
6
6
  }
7
7
  /**
8
8
  * The generation number a writer should use for the segment's **next** object: one above the highest generation
9
- * the registry points at *or* that is present in Storage — whichever is higher.
9
+ * the registry points at *or* that is present in Storage — whichever is higher. The erasure rewrite numbers this
10
+ * way; a load numbers with {@link nextLoadGeneration}, which lists as this does when its existence check finds the
11
+ * number taken or cannot answer.
10
12
  *
11
13
  * Both are consulted because they can disagree. A load that wrote its object and crashed before publishing leaves
12
14
  * an object *above* `currentGen`; a writer that consulted only the pointer would pick that same number and hit the
@@ -17,6 +19,46 @@ export interface GenerationDeps {
17
19
  * A segment with no row and no objects starts at 0. An admin/write-path helper, never on the read path.
18
20
  */
19
21
  export declare function nextGeneration(ref: SegmentRef, deps: GenerationDeps): Promise<number>;
22
+ /** The number a load takes, and whether one existence check proved it free (so nothing is above the pointer). */
23
+ export interface LoadNumber {
24
+ readonly generation: number;
25
+ /**
26
+ * `true` when the number is `currentGen + 1` and the check found no object holding it. Collection by name rests
27
+ * on it: the row named `generation - 1` as current, so every number the window evicts is below it, and nothing
28
+ * the listing would have kept is a name the pass takes. `false` when the listing numbered the load.
29
+ */
30
+ readonly checked: boolean;
31
+ }
32
+ /**
33
+ * The generation number a load takes, from the row it has already read: `currentGen + 1` (0 with no row or no
34
+ * pointer yet) when no object holds that number, which one existence check proves (a zero-byte `getTail`, one
35
+ * metadata request on every shipped driver); otherwise one above the pointer and above everything listed, as
36
+ * {@link nextGeneration} numbers. The check finding the number taken (a crashed load's orphan, the object of a
37
+ * load still in flight, or the generations a rollback left above the pointer) and the check failing in any way
38
+ * other than "not found" both take the listing, so the listing stays the authority whenever the check cannot
39
+ * prove the number free. A failed check falls back silently: nothing records it, and what it costs is the listing,
40
+ * one more PUT-class request on S3, so a fault that makes every check fail shows only in the bill.
41
+ *
42
+ * A load can therefore take a number **below** an object already in the bucket: an orphan at `currentGen + 2`
43
+ * with `currentGen + 1` free is numbered under, not past. That is as safe as a listing's number. A load never
44
+ * takes a number an object holds, and a put that races onto one fails write-once, so the load reports
45
+ * `superseded`. The orphan above stays unpublished; the load whose check meets it numbers past it by the listing,
46
+ * and collection takes it once a generation above it is current. Like a listing's, the number can be one whose
47
+ * object was deleted (an erasure removes the generations above a rolled-back pointer that held the id), which is
48
+ * why nothing identifies a generation by its number alone: the reader cache, the chunk cache and a pin all key on
49
+ * the number and the row's token, and the token has moved on.
50
+ */
51
+ export declare function nextLoadGeneration(ref: SegmentRef, deps: GenerationDeps, row: RegistryRecord | null): Promise<LoadNumber>;
52
+ /**
53
+ * How often a load that could collect by name lists instead: every generation divisible by this. A name-only pass
54
+ * takes one generation a load and never looks at the rest, so what it misses (a refused load's object that stayed
55
+ * because the row had changed, extras left by a `keep` that shrank, an object a rollback or an erasure left
56
+ * stranded below the pointer) waits for a listing. The number is a generation's, not a clock's, so the choice needs
57
+ * no state and no time, and a segment that loads cleanly lists once in this many loads at most. The bound is in
58
+ * generation numbers: a rollback moves the pointer down, and the loads that then take the numbers above it count
59
+ * from there.
60
+ */
61
+ export declare const LIST_COLLECTION_CADENCE = 16;
20
62
  /**
21
63
  * Garbage-collect superseded Storage generations for a segment: everything strictly below `currentGen`, keeping
22
64
  * the most recent `keep` of them as a grace window, so a read still fetching from a just-superseded
@@ -42,8 +84,8 @@ export declare function nextGeneration(ref: SegmentRef, deps: GenerationDeps): P
42
84
  * an empty array is not: `eraseIdFromSegment` reads the returned list as the physical half of its erasure
43
85
  * receipt, and would otherwise report `erased: true` over bytes still in the bucket. Re-run it.
44
86
  *
45
- * That state is reachable in practice: a `dropSegment` whose Storage sweep threw part-way, or a load that was
46
- * already writing its object when the tombstone landed and finished the write afterwards. `dropSegment` re-sweeps
87
+ * That state is reachable in practice: a `dropSegment` whose Storage sweep threw part-way, or a load that had read
88
+ * the row before the tombstone landed, wrote its object afterwards, and stopped before its refusal deleted it. `dropSegment` re-sweeps
47
89
  * and reports whatever it could not reclaim in `generationsRemaining`, but a drop that was never re-run leaves a
48
90
  * residual, and this is what eventually collects it from the retention sweep.
49
91
  *
@@ -54,3 +96,59 @@ export declare function nextGeneration(ref: SegmentRef, deps: GenerationDeps): P
54
96
  export declare function gcOrphanGenerations(ref: SegmentRef, deps: GenerationDeps, options?: {
55
97
  keep?: number;
56
98
  }): Promise<number[]>;
99
+ /**
100
+ * Collect after a load's publish: by name when `byName` (the load numbered its generation with one existence check,
101
+ * the object its row named was there when the guard looked, and its caller does not ask for a listing) and `keep` is
102
+ * at most one, and by listing otherwise ({@link gcOrphanGenerations}). A load lists in three cases: `keep` of 2 or
103
+ * more, which a name cannot serve because the window then counts generations that may be absent (a refused
104
+ * neighbour's number is a gap, and by name would take one the window promised to keep); a number the listing chose,
105
+ * so something sits above the pointer or the check could not answer, or a current object the load's check found gone; and
106
+ * every {@link LIST_COLLECTION_CADENCE}th generation, which collects what a name-only pass leaves. It makes no
107
+ * request when `keep` is at least the generation published, since no more generations than that exist below it. A
108
+ * load onto a `destroyed` row is refused at its publish and never reaches here; a drop that lands after the publish
109
+ * leaves this pass collecting the tombstone's generations, one by name or all of them by listing, and the drop's own
110
+ * sweep takes the rest.
111
+ */
112
+ export declare function collectAfterLoad(ref: SegmentRef, deps: GenerationDeps, options: {
113
+ generation: number;
114
+ keep: number;
115
+ byName: boolean;
116
+ /**
117
+ * The object the load's row named as current, which the caller did not open. Before it collects by name a generation
118
+ * the window keeps (a `keep` of 1), the pass looks for this object with one zero-byte read, and lists instead
119
+ * unless that finds it: a name-only pass is safe only while the object the row named is in the bucket.
120
+ */
121
+ proveCurrent?: GenKey;
122
+ }): Promise<number[]>;
123
+ /**
124
+ * Collect, with no listing, the one generation a publish of `generation` pushed out of a grace window of `keep`
125
+ * (0 or 1): `generation - keep - 1`, when that is a number at all. Returns that name, whether or not an object was
126
+ * there: a delete of an absent object succeeds on every backend and says nothing, so the name returned is the one
127
+ * asked for, not one found, and a generation a concurrent pass, an erasure or a lifecycle rule had already taken is
128
+ * listed all the same. A list is not a receipt (invariant 4), and neither is this.
129
+ *
130
+ * The caller has published `generation` having numbered it with one existence check, so the row named
131
+ * `generation - 1` and every generation the window newly evicts is below it. With `keep` of 0 or 1 that name is
132
+ * the one a listing pass would also take, or one it left to a later pass: never a generation the listing keeps.
133
+ * That rests on the object the row named being in the bucket, and a fault can break it (a lifecycle rule or a
134
+ * partial restore removing it, or an erasure deleting the object of a load whose publish then landed;
135
+ * `checkConsistency` reports the state). The caller, which learns that from its guard's read of the current
136
+ * generation or, when the guard took the size from the row's summary and opened nothing, from one zero-byte read of
137
+ * it (see {@link collectAfterLoad}), lists instead; a load that made neither (`allowEmpty` without `minRetained`)
138
+ * cannot tell, and deletes by name the older generation a listing would have kept as the window. `keep` of 2 or more is refused,
139
+ * because there the window counts the generations that exist, which a name cannot know.
140
+ *
141
+ * Collection never touches the current generation, and the row is re-proved before the delete as every pass does.
142
+ * A row that is gone, or a pointer that has fallen below `generation` (a rollback, or a name purged and re-created
143
+ * with fewer loads), deletes nothing and returns `[]`: the publish already landed, nothing here protects it or
144
+ * needs the result, and a throw would hand the caller a failure for a load that took effect. A pointer that has
145
+ * moved above `generation` does not stop it: a publish landing in the meantime leaves routine collection working.
146
+ * A fault, a registry read or a delete that throws, still propagates, with the pointer at `generation`. The bound
147
+ * is `generation`, the pointer this load's own publish set, so the lower-of-two-pointers rule of a listing pass
148
+ * holds with one observation in place of two. Nothing is deleted when `generation` has nothing below its window,
149
+ * and no request is made.
150
+ */
151
+ export declare function collectByName(ref: SegmentRef, deps: GenerationDeps, options: {
152
+ generation: number;
153
+ keep: number;
154
+ }): Promise<number[]>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * What a load accepts: ids, or a whole bitmap as `{ serialized }` bytes or as a `{ bitmap }` that serializes to
3
+ * them.
4
+ *
5
+ * Every bitmap input takes one path. The bytes are size-capped, checked structurally and decoded by the codec's
6
+ * safe deserializer into a bitmap of the codec's own, which then writes its chunks straight from its containers:
7
+ * no id passes through JavaScript. `{ bitmap }` is only a spelling of `{ serialized }`, serialized once, here, so
8
+ * a caller's bitmap is never trusted as a structure and a change made to it after the call is not loaded.
9
+ *
10
+ * All of it runs before the load's first request, so a malformed input costs no round trip and writes nothing.
11
+ */
12
+ import type { CodecBitmap, CodecInterface } from './codec.js';
13
+ /**
14
+ * Anything that serializes itself to portable Roaring: `roaring`'s `RoaringBitmap32` is one. Typed by shape, so
15
+ * no codec's own class appears in a signature. A bitmap that can also say how large its serialization is
16
+ * (`getSerializationSizeInBytes`) is asked first, so one over the cap is refused before it is serialized.
17
+ */
18
+ export interface PortableBitmap {
19
+ serialize(format: 'portable'): Uint8Array;
20
+ getSerializationSizeInBytes?(format: 'portable'): number;
21
+ }
22
+ /**
23
+ * What a load writes as the segment's new contents.
24
+ *
25
+ * - **ids**: an iterable or async iterable of integers in `[0, 2^32)`, consumed lazily. Not a `Uint8Array`,
26
+ * `Uint8ClampedArray` or `Buffer`, which a load refuses, since each byte would load as an id.
27
+ * - **`{ serialized }`**: one bitmap in the portable Roaring format, checked before anything is written.
28
+ * - **`{ bitmap }`**: an object with `serialize('portable')`, loaded as `{ serialized: bitmap.serialize('portable') }`.
29
+ */
30
+ export type LoadInput = Iterable<number> | AsyncIterable<number> | {
31
+ readonly serialized: Uint8Array;
32
+ } | {
33
+ readonly bitmap: PortableBitmap;
34
+ };
35
+ /**
36
+ * No canonical 32-bit Roaring bitmap serializes to more than this. Under the run cookie, the larger of the two
37
+ * headers, it is a 4-byte cookie, 8,192 bytes of run flags and 8 bytes of header for each of the 65,536
38
+ * containers, and no container's body is over 8,192 bytes: a bitset is 8,192, and a run container is kept only
39
+ * where it is smaller. 537,403,396 bytes, about 512.5 MiB.
40
+ */
41
+ export declare const MAX_SERIALIZED_LOAD_BYTES: number;
42
+ /** A bitmap input, decoded into the codec's own bitmap: written from its chunks, never id by id. */
43
+ export declare class DecodedLoadInput {
44
+ readonly bitmap: CodecBitmap;
45
+ constructor(bitmap: CodecBitmap);
46
+ }
47
+ /**
48
+ * Check a load's input and decode a bitmap input, before the load makes any request.
49
+ *
50
+ * @returns ids unchanged, to be consumed lazily by the write, or the decoded bitmap.
51
+ * @throws {ValidationError} for a byte array passed as ids; bytes that are malformed, over the cap, or followed by
52
+ * more bytes; a `{ bitmap }` with no `serialize` method, or whose `serialize('portable')` returns no `Uint8Array`;
53
+ * and anything that is none of the three inputs. An error the caller's own `serialize` throws propagates as it is.
54
+ */
55
+ export declare function prepareLoadInput(input: LoadInput, codec: CodecInterface): Iterable<number> | AsyncIterable<number> | DecodedLoadInput;
@@ -15,15 +15,17 @@
15
15
  *
16
16
  * Refusing therefore has to clean up after itself. The object is already durable at that point, and it sits
17
17
  * ABOVE `currentGen`, where generation collection deliberately never looks (it deletes strictly below the
18
- * pointer), so a refused load deletes its own object before returning, while the segment's row is unchanged or
19
- * gone. Once another write has changed the row, the object's number may name a re-created segment's live object,
20
- * so it stays, and collection takes it like any other generation once one above it is current.
18
+ * pointer), so a refused load deletes its own object before returning, while the segment's row is unchanged, gone
19
+ * or `destroyed`. Once another write has changed the row, the object's number may name a re-created segment's live
20
+ * object, so it stays, and collection takes it like any other generation once one above it is current.
21
21
  */
22
22
  import { type IAuditSink } from './audit.js';
23
23
  import { type CodecInterface } from './codec.js';
24
- import type { Clock } from './determinism.js';
24
+ import type { Clock, Rng } from './determinism.js';
25
25
  import type { IKeystore } from './crypto.js';
26
- import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
26
+ import { type LoadInput } from './load-input.js';
27
+ import { type ReadRetry } from './retry.js';
28
+ import type { GenerationMetadata, IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
27
29
  /** What {@link loadSegment} needs: the objects, the pointer, the codec, and key material if encrypted. */
28
30
  export interface LoadDeps {
29
31
  readonly storage: IStorageDriver;
@@ -32,6 +34,27 @@ export interface LoadDeps {
32
34
  readonly keystore?: IKeystore;
33
35
  readonly requireEncryption?: boolean;
34
36
  readonly clock?: Clock;
37
+ /**
38
+ * The random source that spreads the waits between a publish's fresh compare-and-swaps (a random time under each
39
+ * bound). Absent, the read retry's source is used if it has one, and otherwise each wait is its bound.
40
+ */
41
+ readonly rng?: Rng;
42
+ /**
43
+ * The store's read retry, for the read the guard makes of the current generation: a transient fault there is run
44
+ * again under it rather than failing the load. Absent, the read is made once. It does not govern the write: a
45
+ * registry write that gets no answer is settled by reading the row, and sent again only as a bounded fresh
46
+ * compare-and-swap.
47
+ */
48
+ readonly readRetry?: ReadRetry;
49
+ /**
50
+ * Collect by listing the segment's objects after the publish, whatever `keep` is. Absent, a load that keeps at
51
+ * most one generation and found nothing above its pointer deletes by name the one generation its publish pushed
52
+ * out of the window, and lists every sixteenth generation to take what that leaves; the generations older than
53
+ * the window wait for that listing. Set it for a caller whose `keep` promises every generation below the new
54
+ * one beyond the window is gone when the call returns: the `*Into` verbs, whose `keep` is how an operator clears
55
+ * a destination that earlier materialisations kept in full.
56
+ */
57
+ readonly collectByListing?: boolean;
35
58
  }
36
59
  /**
37
60
  * What a plausible result looks like, so an implausible one is refused instead of published.
@@ -71,8 +94,27 @@ export interface LoadOptions {
71
94
  * collected. Keep at least one generation for every one that can be written above the pinned one while your
72
95
  * longest pinned job runs, on every writer that loads the segment — see "Generations and `keep`" in the loading
73
96
  * guide.
97
+ *
98
+ * With `keep` of 0 or 1, a load that found nothing above the pointer collects without listing: it deletes the one
99
+ * generation its publish pushed out of the window, and lists the segment's objects on every sixteenth generation to
100
+ * take whatever that pass leaves, such as the generations an earlier, wider `keep` held. With `keep` of 2 or more
101
+ * it lists on every load, and so does one whose check found the current generation's object gone: the guard's read of
102
+ * it, or, when the guard took the size from the row's summary, one zero-byte read made before a `keep` of 1 takes a
103
+ * name. A `keep` at least the generation published collects nothing and asks for nothing.
74
104
  */
75
105
  readonly keep?: number;
106
+ /**
107
+ * Small, immutable metadata of your own for this generation: what it was computed from, such as a definition's
108
+ * version, the time its data landed, or a run id. A flat object of string keys and string or finite-number values,
109
+ * at most 1,024 bytes as canonical JSON (keys sorted by UTF-16 code unit, no whitespace), and no key longer than 128
110
+ * bytes. It is stored in the generation's object, and written to the segment's row by the write that makes the
111
+ * generation current, so whoever sees this generation as current sees its metadata. It never changes once written:
112
+ * a new generation is how it changes. `undefined` and the empty object store nothing. Anything else that breaks a
113
+ * rule (a boolean, `null`, an array, nesting, `NaN`, a key that is not well-formed text) throws `ValidationError`
114
+ * before a request is made. **Never put a subject's id in it**: an erasure rewrites the ids and carries the metadata
115
+ * over as it is. Sealed under the segment's key when the segment is encrypted.
116
+ */
117
+ readonly metadata?: GenerationMetadata;
76
118
  readonly audit?: IAuditSink;
77
119
  }
78
120
  /** Why a load did not become current. */
@@ -82,13 +124,14 @@ export type LoadRefusal = 'empty' | 'min-cardinality' | 'min-retained'
82
124
  * put refused this one and it wrote nothing (`size: 0`); or the segment's registry row changed while the load was
83
125
  * writing: another load published, a retention change, a rollback or an erasure wrote the row, or the row was
84
126
  * deleted. Once another write has changed the row, the object stays in the bucket, because its generation number
85
- * may by then name another incarnation's live object; once the row is gone, the object is deleted.
127
+ * may by then name another incarnation's live object; once the row is `destroyed` (dropped or crypto-shredded),
128
+ * the object is deleted, and once the row is gone, so is an object its footer proves this load's own.
86
129
  */
87
130
  | 'superseded';
88
131
  export interface LoadResult {
89
132
  /**
90
133
  * The generation written. Present even when refused: a refusal deletes that object while the segment's row is
91
- * unchanged or gone, and leaves it in the bucket once another write has changed the row. A load that lost its
134
+ * unchanged or `destroyed`, or gone once its footer proves it this load's own, and leaves it in the bucket once another write has changed the row. A load that lost its
92
135
  * generation number to another wrote nothing, and reports `size: 0`.
93
136
  */
94
137
  readonly generation: number;
@@ -108,14 +151,26 @@ export interface LoadResult {
108
151
  * whatever happens next.
109
152
  */
110
153
  readonly cardinalityBefore: number | null;
111
- /** The superseded generations collected after publishing. Empty when nothing was published. */
154
+ /**
155
+ * The superseded generations collected after publishing. Empty when nothing was published.
156
+ *
157
+ * When collection deleted by name (see {@link LoadOptions.keep}) this is the name it deleted, and that generation
158
+ * may have been gone already: a delete of an absent object succeeds on every backend and does not say so, so the
159
+ * list names what the pass asked the bucket to delete, not what it found there. A listing pass names the
160
+ * generations it found and deleted. Neither is a receipt, since a concurrent collector may have taken a generation
161
+ * first.
162
+ */
112
163
  readonly collected: readonly number[];
113
164
  }
114
165
  /**
115
- * Replace a segment's contents with `ids`, as one immutable generation, and make it current.
166
+ * Replace a segment's contents with `input`, as one immutable generation, and make it current.
167
+ *
168
+ * `input` is ids, or a whole bitmap as `{ serialized }` portable Roaring bytes or a `{ bitmap }` with
169
+ * `serialize('portable')` ({@link LoadInput}). A bitmap input is checked, decoded and serialized before the first
170
+ * request, and is written from its own chunks; the generation is byte for byte the one its ids would write.
116
171
  *
117
172
  * Returns what was written and whether it became current. A refusal is **not** an exception: `published: false`
118
173
  * with a `reason` is a normal outcome a caller branches on, in the same shape as a successful load, because the
119
174
  * interesting cases (a guard tripped, a racing writer won) are operational facts rather than faults.
120
175
  */
121
- export declare function loadSegment(ref: SegmentRef, ids: Iterable<number> | AsyncIterable<number>, deps: LoadDeps, options?: LoadOptions): Promise<LoadResult>;
176
+ export declare function loadSegment(ref: SegmentRef, input: LoadInput, deps: LoadDeps, options?: LoadOptions): Promise<LoadResult>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Generation metadata: a small, flat record of strings and finite numbers attached to one generation.
3
+ *
4
+ * The rules live here once, for every boundary that meets a value: a caller's input, refused with
5
+ * `ValidationError`, and bytes read back from a tier, refused with `IntegrityError`. The caller passes the error
6
+ * to raise. Pure: no I/O.
7
+ */
8
+ import type { GenerationMetadata } from './ports.js';
9
+ /** Cap on the canonical JSON of one generation's metadata, in UTF-8 bytes (braces and quotes included). */
10
+ export declare const MAX_METADATA_BYTES = 1024;
11
+ /** Cap on one metadata key, in UTF-8 bytes. */
12
+ export declare const MAX_METADATA_KEY_BYTES = 128;
13
+ /**
14
+ * Validate `value` as generation metadata (`GenerationMetadata` in the ports) and return its canonical JSON: RFC 8785
15
+ * (the JSON Canonicalization Scheme) for a flat object of strings and finite numbers. Keys are sorted by UTF-16 code
16
+ * unit, strings are escaped as `JSON.stringify` escapes them, numbers are written as ECMAScript's
17
+ * `Number.prototype.toString` writes them (so `-0` is `0`, and `1e21` is `1e+21`), and there is no whitespace. Key
18
+ * order therefore never changes the bytes, and `tests/golden/metadata-canonical.json` holds vectors a port can check
19
+ * itself against. Both caps are in UTF-8 bytes. The container is an object whose prototype is
20
+ * `Object.prototype` or `null`, holding only enumerable data properties with string keys. `fail` raises the error the
21
+ * boundary calls for. The empty object passes and returns `{}`: whether it may be stored is the caller's rule.
22
+ */
23
+ export declare function canonicalMetadataJson(value: unknown, fail: (message: string) => never): string;
24
+ /**
25
+ * The bytes a generation stores for `value`: its canonical JSON in UTF-8, or `undefined` when it has none (`undefined`
26
+ * or the empty object, which store nothing). Synchronous, so what is stored is what the caller passed at the call,
27
+ * whatever it does with its object afterwards. `fail` raises the boundary's error.
28
+ */
29
+ export declare function metadataBytes(value: unknown, fail: (message: string) => never): Uint8Array | undefined;
30
+ /**
31
+ * A caller's metadata, checked against the rules and copied, so what is stored is what the caller passed at the call,
32
+ * whatever it does with its object afterwards: the frozen record in canonical key order, or `undefined` when there is
33
+ * none (`undefined`, or the empty object, which store nothing). Synchronous. `fail` raises the boundary's error.
34
+ */
35
+ export declare function copiedMetadata(value: unknown, fail: (message: string) => never): GenerationMetadata | undefined;
36
+ /**
37
+ * Read metadata back from bytes a tier holds, which are untrusted. They must be at most {@link MAX_METADATA_BYTES},
38
+ * valid UTF-8, and exactly the canonical JSON of a non-empty record, so a stored copy has one spelling: whitespace, a
39
+ * key out of order or listed twice, and a number or an escape `JSON.stringify` would write another way are all
40
+ * refused. Returns a frozen record of its own. `fail` raises the boundary's error.
41
+ */
42
+ export declare function metadataFromBytes(bytes: Uint8Array, fail: (message: string) => never): GenerationMetadata;
@@ -21,7 +21,7 @@
21
21
  * on some other handle. That is why the routing is by segment identity here rather than by which object the
22
22
  * caller happened to start from.
23
23
  */
24
- import type { ChunkRef, StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
24
+ import type { ChunkRef, GenerationSummary, StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
25
25
  import type { CrbmStorageChunkSource } from './crbm-storage-source.js';
26
26
  /**
27
27
  * What a pin holds for one segment: the generation, the version identifying those exact bytes, and the pinned
@@ -67,6 +67,7 @@ export declare class PinnedStorageChunkSource implements StorageChunkSource {
67
67
  listChunkKeys(ref: SegmentRef): Promise<number[]>;
68
68
  sizeOf(ref: SegmentRef): Promise<SegmentSize | null>;
69
69
  cardinalities(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
70
+ summary(ref: SegmentRef): Promise<GenerationSummary | null>;
70
71
  currentGeneration(ref: SegmentRef): Promise<number | null>;
71
72
  /**
72
73
  * A pinned segment reports the version captured when it was pinned, marked as a pin's, so its decoded chunks