@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,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;
@@ -26,7 +26,7 @@ export type MetricEvent = {
26
26
  readonly kind: 'storage.get';
27
27
  readonly namespace?: string;
28
28
  readonly segment: string;
29
- /** Bytes returned (0 if the chunk was absent — a GET still happened). */
29
+ /** One event per request, however many callers were waiting on it. Bytes returned (0 if the chunk was absent — a GET still happened). */
30
30
  readonly bytes: number;
31
31
  /**
32
32
  * Elapsed wall time of the read — includes any transient-retry backoff on the storage call. From the
@@ -34,6 +34,12 @@ export type MetricEvent = {
34
34
  */
35
35
  readonly ms: number;
36
36
  } | {
37
+ /**
38
+ * One decoded-chunk cache lookup (emitted only when a cache is configured). `hit: false` means the lookup found
39
+ * no cached chunk. A caller that missed while another caller's read of the same chunk was open waits on that
40
+ * read and adds no `storage.get`, so the misses can outnumber the `storage.get` events; the `storage.get`
41
+ * count is the number of requests.
42
+ */
37
43
  readonly kind: 'cache';
38
44
  readonly hit: boolean;
39
45
  } | {
@@ -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
@@ -59,10 +59,18 @@ export interface StorageChunkSource {
59
59
  /**
60
60
  * Optional: the current generation's **per-chunk cardinality** (`chunkKey → count`), read from the
61
61
  * already-parsed `.crbm` index with **no payload reads**, or `null` if the segment has no Storage generation.
62
- * Powers the free `count()` — the engine sums the index instead of fetching a single chunk. A source with no
62
+ * Powers the free `count()` of a source with no {@link StorageChunkSource.summary} — the engine sums the index
63
+ * instead of fetching a single chunk. A source with no
63
64
  * index (e.g. the in-memory source) omits this, and `count()` falls back to fetching every chunk.
64
65
  */
65
66
  cardinalities?(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
67
+ /**
68
+ * Optional: the current generation's number, id count and metadata, or `null` if the segment has no Storage
69
+ * generation. The `.crbm` source answers from the registry row's summary when it can use it, which is one
70
+ * registry read and no read of the object, and from the opened generation when it cannot. A source with no
71
+ * summary omits this, and `count()` keeps its other paths.
72
+ */
73
+ summary?(ref: SegmentRef): Promise<GenerationSummary | null>;
66
74
  /**
67
75
  * Optional: the segment's **current generation number** as this source resolves it right now (registry
68
76
  * `currentGen`, or the highest storage generation), or `null` if the segment has no Storage generation. The engine
@@ -105,10 +113,10 @@ export interface StorageChunkSource {
105
113
  * the generation *and* the incarnation of the name it belongs to.
106
114
  *
107
115
  * {@link StorageChunkSource.currentGeneration} is not enough to key a decoded-chunk cache, and the gap is not
108
- * theoretical: a load numbers its generation `max(currentGen, highest object) + 1`, so the number **restarts at 0** once
109
- * a registry row is purged and the bucket emptied. A retired, re-created name therefore serves different
110
- * data at the same `currentGen`, and a cache keyed on `(segment, chunk, generation)` hands back the previous
111
- * incarnation's ids — an erased id reappearing, with no read to intercept.
116
+ * theoretical: a load numbers its generation from the row's pointer and what is in the bucket, so the number
117
+ * **restarts at 0** once a registry row is purged and the bucket emptied. A retired, re-created name therefore
118
+ * serves different data at the same `currentGen`, and a cache keyed on `(segment, chunk, generation)` hands back
119
+ * the previous incarnation's ids — an erased id reappearing, with no read to intercept.
112
120
  *
113
121
  * Note that putting the incarnation in the *object key* would not fix this: the new incarnation still starts
114
122
  * at generation 0, so the cache key collides either way. The identity has to reach the cache.
@@ -160,8 +168,9 @@ export interface StorageCaps {
160
168
  * loses its response, sent again, finds its own object and would report a collision. Either send each write
161
169
  * once, with the backend client's retry off for that request, or recognise your own write when the
162
170
  * precondition fails by reading an id you stored with it back, and treat a match as success. The first is
163
- * what the S3 driver and the single-request GCS upload do; the second is what the Azure Blob driver and the
164
- * resumable GCS upload do.
171
+ * what the registry writes of every shipped driver do; the second is what the Azure Blob driver and the
172
+ * resumable GCS upload do, and what the S3 driver and the single-request GCS upload do when they send an object
173
+ * again after a throttle, which is the one replay a driver may make on purpose.
165
174
  * - **Raise a transient fault as {@link TransientError}** (throttling, a 5xx, a dropped connection), so the read
166
175
  * retry can ride it out and a write's caller can tell it from a deterministic failure.
167
176
  */
@@ -212,6 +221,50 @@ export type RegistryStatus = 'active' | 'destroyed';
212
221
  * shape-later: stored and round-tripped, no semantics attached.
213
222
  */
214
223
  export type GovernanceMeta = Record<string, unknown>;
224
+ /**
225
+ * A cached description of one generation, carried on the registry row: how many ids it holds, and the metadata it
226
+ * was loaded with. The generation's `.crbm` object stays the truth; this is a copy, written by the same write that
227
+ * moves the pointer, and it names the generation it describes so a copy left behind by a writer that did not
228
+ * carry it can never be taken for another generation's.
229
+ *
230
+ * Two shapes. On a cleartext segment the values are in the clear. On an encrypted segment (a row with
231
+ * `wrappedDeks`) they are sealed under the segment's data key, as the generation's index is, so the row reveals
232
+ * neither the count nor the metadata. A shipped registry refuses a write whose summary disagrees with the row's
233
+ * keys. It still reads a stored row that disagrees, so that one such row cannot stop every listing: a reader must not
234
+ * use that row's summary.
235
+ */
236
+ export type RegistrySummary = ClearRegistrySummary | SealedRegistrySummary;
237
+ /**
238
+ * A generation's metadata: string keys, string or finite-number values, no nesting, at most 1 KiB as canonical JSON.
239
+ * The rules are in `core/metadata.ts`.
240
+ */
241
+ export type GenerationMetadata = Readonly<Record<string, string | number>>;
242
+ /** What a generation is, as one read answers it: its number, its id count and the metadata it was written with. */
243
+ export interface GenerationSummary {
244
+ readonly generation: number;
245
+ readonly cardinality: number;
246
+ /** Absent when the generation has none. */
247
+ readonly metadata?: GenerationMetadata;
248
+ }
249
+ /** {@link RegistrySummary} on a cleartext segment. */
250
+ export interface ClearRegistrySummary {
251
+ /** The generation this describes. A non-negative safe integer. */
252
+ readonly generation: number;
253
+ /** How many ids the generation holds: an integer from 0 to 2^32. */
254
+ readonly cardinality: number;
255
+ /** The generation's metadata, when it has any. Never the empty object. */
256
+ readonly metadata?: GenerationMetadata;
257
+ }
258
+ /** {@link RegistrySummary} on an encrypted segment. */
259
+ export interface SealedRegistrySummary {
260
+ /** The generation this describes. A non-negative safe integer. */
261
+ readonly generation: number;
262
+ /**
263
+ * Base64 of `nonce(12) ‖ ciphertext ‖ tag(16)`, sealing the cardinality as a little-endian u64 followed by the
264
+ * metadata's canonical JSON, if any. The count is fixed-width, so the length reveals only the metadata's size.
265
+ */
266
+ readonly sealed: string;
267
+ }
215
268
  /**
216
269
  * One registry row — the authoritative per-segment record. Exactly one per segment.
217
270
  */
@@ -249,10 +302,23 @@ export interface RegistryRecord extends SegmentRef {
249
302
  */
250
303
  readonly retention?: GovernanceMeta;
251
304
  readonly residency?: GovernanceMeta;
305
+ /**
306
+ * A cached description of the current generation (see {@link RegistrySummary}). Optional: a row without one is
307
+ * correct, and a reader then opens the generation instead. Trusted only for the generation it names.
308
+ */
309
+ readonly summary?: RegistrySummary;
252
310
  /** Epoch-ms of creation / last mutation (from the driver's injected clock). */
253
311
  readonly createdAt: number;
254
312
  readonly updatedAt: number;
255
- /** Opaque OCC token — compare-by-equality, never reused (ABA-safe). */
313
+ /**
314
+ * Opaque OCC token — compare-by-equality, ABA-safe. A shipped registry gives no two writes under one name the same
315
+ * token: not two writes of this row, not a write of an earlier row under the name, and not a write after this row is
316
+ * restored from a backup. It holds with overwhelming probability rather than by construction, since the token carries random parts: a
317
+ * 128-bit incarnation id drawn when the row is created, and a 64-bit part drawn for each write. Two incarnations of one
318
+ * name meet with probability 2^-128 for any pair (about n^2 / 2^129 among n of them), and two writes at one counter,
319
+ * after a restore, with probability 2^-64. A bare decimal token, on a row no 0.12 or later registry has written,
320
+ * carries neither part.
321
+ */
256
322
  readonly token: Token;
257
323
  }
258
324
  /** The caller-settable fields at {@link IRegistryDriver.create} (audit + token are driver-managed). */
@@ -265,14 +331,47 @@ export interface NewRegistryRecord {
265
331
  readonly status?: RegistryStatus;
266
332
  readonly retention?: GovernanceMeta;
267
333
  readonly residency?: GovernanceMeta;
334
+ /** Must name `currentGen` when given. */
335
+ readonly summary?: RegistrySummary;
268
336
  }
269
- /** Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity + audit + token are off-limits). */
270
- export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency'>>;
337
+ /**
338
+ * Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity + audit + token are off-limits).
339
+ *
340
+ * Presence-based: a field the patch does not mention is left as it was. `summary` is the one exception, because it
341
+ * describes the current generation: a patch that moves `currentGen` and does not mention `summary` drops the old
342
+ * one rather than keep a description of another generation. A `summary` the patch gives must name the
343
+ * `currentGen` the row will have.
344
+ */
345
+ export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency' | 'summary'>>;
271
346
  /** Capabilities a registry driver advertises; validated fail-fast at wiring time. */
272
347
  export interface RegCaps {
273
348
  /** REQUIRED — `currentGen` feeds read correctness + the publish CAS, so reads must be strongly consistent. */
274
349
  readonly strongRead: true;
350
+ /**
351
+ * `false` when this registry cannot write a row in this runtime, though it reads: a shipped registry draws random
352
+ * bytes for every token it issues, and on a runtime with no Web Crypto it has none to draw. A write path that
353
+ * writes an object before its row (a load, an erasure rewrite) checks it first and refuses with `UnsupportedError`
354
+ * before it writes anything, rather than leave an object no row names. Absent means the registry can write.
355
+ */
356
+ readonly canWrite?: false;
357
+ /**
358
+ * `true` when this registry's `delete` removes a row from its backend for good, so that no later `list` reads it, and
359
+ * removes it only while it is still the exact version the delete read: by a delete the backend applies under a
360
+ * precondition (an S3 or Azure Blob ETag, a GCS object generation), or under a lock no other writer of the backend
361
+ * can take. Only a row whose token carries an incarnation id is removed so. A row first written by a release before
362
+ * 0.12 has a bare decimal token, and a delete still tombstones it: a process on that release, re-creating the name
363
+ * over nothing, would issue those counters again from 0, so its row could not be told apart from the deleted one.
364
+ *
365
+ * `false` or absent: every `delete` leaves a tombstone, which every later full `list` still reads. A shipped registry
366
+ * says which it is.
367
+ */
368
+ readonly conditionalDelete?: boolean;
275
369
  }
370
+ /**
371
+ * Refuse, with `UnsupportedError`, a write that would write an object before its row when the registry reports it
372
+ * cannot write a row (see {@link RegCaps.canWrite}). Called before the write's first request.
373
+ */
374
+ export declare function assertRegistryCanWrite(registry: IRegistryDriver, what: string): void;
276
375
  /**
277
376
  * Brand for {@link StorageBackend}.
278
377
  *
@@ -365,8 +464,13 @@ export declare function isStorageBackend(value: unknown): value is StorageBacken
365
464
  * - **`create` and `compareAndSwap` are atomic conditional writes.** A `create` over a live row, and a
366
465
  * `compareAndSwap` whose token is not the stored one, throw {@link WriteConflictError} and change nothing.
367
466
  * Two racing writers have exactly one winner.
368
- * - **Tokens are never reused**, not even across `delete` then `create`: a delete leaves a tombstone (or a
369
- * global counter) so a recreated row always carries a fresh token and a stale holder cannot swap into it.
467
+ * - **A token is not reused, not even across `delete` then `create`**, short of a collision of probability 2^-128 for
468
+ * any two incarnations of one name: a recreated row carries a token no earlier incarnation held, so a stale holder
469
+ * cannot swap into it. The shipped drivers draw a random 128-bit incarnation id into the token of every row they
470
+ * create and a random 64-bit part into the token of every write, beside a counter a tombstone (or a global counter)
471
+ * keeps going; where nothing of the earlier row is left, or the row is restored from a backup to an older counter, the
472
+ * random parts alone keep the tokens apart: with probability 1 - 2^-128 per pair of incarnations, and 1 - 2^-64 per
473
+ * pair of writes at one counter.
370
474
  * - **`delete` is idempotent** without an `expected` token: deleting an absent row is a no-op, not an error.
371
475
  * **With `expected` it is fenced**: it lands only while the row still carries that token, and otherwise throws
372
476
  * {@link WriteConflictError} and leaves the row.
@@ -408,7 +512,9 @@ export interface IRegistryDriver {
408
512
  */
409
513
  list(namespace?: string): AsyncIterable<RegistryRecord>;
410
514
  /**
411
- * Remove the row (tombstoned for ABA-safety — a later `create` still gets a fresh, greater token).
515
+ * Remove the row. A registry whose {@link RegCaps.conditionalDelete} is `true` removes a row whose token carries an
516
+ * incarnation id from its backend; every other row is tombstoned. Either way a later `create` gets a token no
517
+ * earlier incarnation of the name held, but for a collision of probability 2^-128 per pair of incarnations.
412
518
  *
413
519
  * Without `expected`, idempotent: deleting an absent row is a no-op.
414
520
  *
@@ -27,6 +27,12 @@ export declare function drainRegistry(registry: IRegistryDriver, options: {
27
27
  * one namespace, so it passes false and the refusal does not tell it to narrow what it cannot.
28
28
  */
29
29
  narrowable?: boolean;
30
+ /**
31
+ * Told of each bookkeeping row an unscoped scan skips, which the listing has already paid to read. Only the
32
+ * retention sweep uses it, to find the due-index pointers whose segment has no row. It is not counted against
33
+ * `maxScanSegments`, and a caller that holds the rows it is given bounds them itself.
34
+ */
35
+ onReserved?: (record: RegistryRecord) => void;
30
36
  }): Promise<RegistryRecord[]>;
31
37
  /**
32
38
  * Is this record **bookkeeping** rather than a segment? Covers every reserved family — today the due-index