@cloudbitmaps/core 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/{chunk-TNWTIAON.js → chunk-YSC7NJWV.js} +314 -216
  2. package/dist/chunk-YSC7NJWV.js.map +7 -0
  3. package/dist/core/audit.d.ts +56 -3
  4. package/dist/core/combine-many.d.ts +13 -4
  5. package/dist/core/consistency.d.ts +10 -4
  6. package/dist/core/crbm/fingerprint.d.ts +18 -0
  7. package/dist/core/crbm/format.d.ts +2 -4
  8. package/dist/core/crbm/frame.d.ts +10 -0
  9. package/dist/core/crbm/reader.d.ts +1 -3
  10. package/dist/core/crbm-storage-source.d.ts +249 -96
  11. package/dist/core/engine.d.ts +22 -14
  12. package/dist/core/erase-id.d.ts +62 -26
  13. package/dist/core/generation-gc.d.ts +3 -2
  14. package/dist/core/keys.d.ts +28 -0
  15. package/dist/core/leases.d.ts +7 -0
  16. package/dist/core/load.d.ts +11 -7
  17. package/dist/core/metrics.d.ts +5 -5
  18. package/dist/core/pinned-storage-source.d.ts +3 -3
  19. package/dist/core/pointer-id.d.ts +19 -0
  20. package/dist/core/ports.d.ts +131 -35
  21. package/dist/core/reader-defaults.d.ts +14 -3
  22. package/dist/core/rollback.d.ts +2 -1
  23. package/dist/core/summary.d.ts +38 -21
  24. package/dist/core/token.d.ts +13 -8
  25. package/dist/driver-kit.d.ts +2 -1
  26. package/dist/driver-kit.js +11 -4
  27. package/dist/driver-kit.js.map +2 -2
  28. package/dist/drivers/_shared/object-registry.d.ts +1 -1
  29. package/dist/drivers/_shared/registry.d.ts +26 -17
  30. package/dist/drivers/localfs/storage.d.ts +7 -6
  31. package/dist/drivers/memory.d.ts +8 -6
  32. package/dist/drivers/retry/retrying-chunk-source.d.ts +1 -5
  33. package/dist/export/index.d.ts +16 -0
  34. package/dist/index.d.ts +2 -5
  35. package/dist/index.js +1288 -1080
  36. package/dist/index.js.map +4 -4
  37. package/package.json +1 -1
  38. package/dist/chunk-TNWTIAON.js.map +0 -7
  39. package/dist/core/cost.d.ts +0 -351
@@ -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 { GenerationMetadata, StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
6
+ import type { GenerationMetadata, StorageChunkSource, SegmentRef } from './ports.js';
7
7
  /**
8
8
  * Range requests a stream holds ahead of a combine or `iterate` by default (chunk keys, on a source that reads chunk by
9
9
  * chunk, and for `count`'s fallback) — bounds memory and concurrent reads (invariant 6). A read that does not fit
@@ -106,6 +106,11 @@ export declare class SegmentEngine {
106
106
  private readonly openStreams;
107
107
  /** How many times {@link invalidate} has been called: a read compares it with the count it began under. */
108
108
  private invalidations;
109
+ /**
110
+ * The cache-key prefixes ({@link segmentPrefix}) of the segments this engine reads, kept rather than encoded for each
111
+ * chunk. A kept prefix is the one {@link invalidate} finds the segment's chunks by.
112
+ */
113
+ private readonly prefixes;
109
114
  constructor(deps: EngineDeps);
110
115
  /** Membership: one chunk lookup — the cache, else one Storage fetch of that chunk. */
111
116
  has(seg: SegmentRef, id: number): Promise<boolean>;
@@ -123,20 +128,21 @@ export declare class SegmentEngine {
123
128
  */
124
129
  count(seg: SegmentRef): Promise<number>;
125
130
  /**
126
- * What the segment's current generation is: its number, its id count and its metadata, from one resolution, so the
127
- * three cannot straddle a publish. A source with no summary answers from `count()` and the generation it resolves.
131
+ * What the segment's current generation is: its number, its id count, its metadata and the size of its object in
132
+ * bytes. A source with a `stat` (the `.crbm` source) answers all four from one resolution, so they cannot straddle
133
+ * a publish: from the registry row's summary when it can use it, else from the opened generation's footer and
134
+ * index, with no payload read. A source without one answers from its other methods, each its own resolution: its
135
+ * summary when it has one, else `count()` and the generation it resolves, and its `sizeOf`, or `sizeBytes: null`
136
+ * when it has none. A segment with no generation answers `{ generation: null, cardinality: 0, sizeBytes: null }`.
128
137
  */
129
138
  stat(seg: SegmentRef): Promise<{
130
139
  generation: number | null;
131
140
  cardinality: number;
132
141
  metadata?: GenerationMetadata;
142
+ sizeBytes: number | null;
133
143
  }>;
134
- /** Whether the Storage source can measure segment size (for grounded cost); false ⇒ storage isn't grounded. */
135
- get supportsStorageSize(): boolean;
136
- /** How often the Storage source re-reads a pointer while reading (cost reporting); undefined if it does not say. */
137
- get pointerRefreshMs(): number | undefined;
138
- /** Grounded Storage size of a segment's current generation (cost reporting), or null if it has no generation. */
139
- segmentSize(seg: SegmentRef): Promise<SegmentSize | null>;
144
+ /** The size of the segment's current generation from the source's `sizeOf`, or `null` when it cannot say. */
145
+ private sizeOf;
140
146
  /** The chunks of `seg` at `chunkKeys`, read through a window of {@link DEFAULT_INTERSECT_CONCURRENCY}. */
141
147
  private chunkWindow;
142
148
  /**
@@ -214,11 +220,13 @@ export declare class SegmentEngine {
214
220
  * the result — every chunk read is a whole, checksum-verified, immutable generation. The edge a *long* call can
215
221
  * hit is invariant 3's: if it straddles a mid-call `cache.genTtlMs` boundary and a load has published, an
216
222
  * operand's not-yet-requested chunks may re-resolve forward to the newer generation (a generation hop within one long
217
- * call) — the call never crashes or returns a torn object, but may mix generations. Three things hop it without
218
- * waiting for the TTL, so a shorter call can meet them too: **the reader cache evicting an operand mid-call**
219
- * (`maxOpenSegments`), whose re-read re-resolves fresh; a sweep deleting the generation it was reading, which
220
- * heals the read forward; and an invalidation, which this store's own `load`, `rollback` and `eraseSubject` make
221
- * and `invalidate()` makes on request. Still whole/immutable per read, never torn. An operand's stream resolves its
223
+ * call) — the call never crashes or returns a torn object, but may mix generations. Some things hop it without
224
+ * waiting for the TTL, so a shorter call can meet them too: **the reader cache evicting an operand mid-call** on a
225
+ * store with no timed refresh (`maxOpenSegments`), whose re-read re-resolves fresh, and on a store with one the
226
+ * resolution cache letting the operand's resolution go; a sweep deleting the generation it was reading, or another
227
+ * object under its number, met by this read or another, which heals the read forward, as another read's failed open
228
+ * of the operand does; and an invalidation, which this store's own `load`, `rollback` and `eraseSubject` make and
229
+ * `invalidate()` makes on request. Still whole/immutable per read, never torn. An operand's stream resolves its
222
230
  * segment again before it serves each chunk, so each of those moves a running read as it would a read of one chunk, and
223
231
  * the ranges it had requested of the earlier generation are dropped, not served.
224
232
  */
@@ -11,9 +11,11 @@
11
11
  *
12
12
  * The rewrite is the same generation without one id, so it keeps everything else: the new object carries the source's
13
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.
14
+ * metadata. Metadata is not scanned for the id. A source object whose metadata block was stripped is another object
15
+ * than the row's summary names: its size is not the one the summary records, so the fingerprint check refuses it
16
+ * before the erasure reads any of its ids. Only one forged to match that fingerprint gets past, and on an encrypted
17
+ * segment it is rewritten with the metadata of the row's sealed summary: the block's presence is not authenticated, so
18
+ * the authenticated copy decides.
17
19
  *
18
20
  * **`erased: true` is a claim about every generation of the segment, not only the one it replaced.** A rollback
19
21
  * leaves generations above the pointer that were once current and can be made current again, so a holder can sit
@@ -48,16 +50,17 @@
48
50
  * Which reason comes from **re-reading the row**, not from assuming a supersession: a moved pointer is
49
51
  * `'superseded'`, a tombstoned row `'destroyed'`, a purged row `'absent'`, a row with no pointer
50
52
  * `'no-generation'`. A pointer still on `from` means the object it names is genuinely gone — the forbidden
51
- * `missing-storage-generation` state — and that throws, because no re-run fixes it. That `NotFoundError` is the only
52
- * signal of that state, so a faulting re-read rethrows it rather than replacing it with a transient-looking
53
- * registry error.
53
+ * `missing-storage-generation` state — or is another object than the row's summary names by its fingerprint, and that
54
+ * throws, because no re-run fixes either. That `NotFoundError` is the only signal of those states, so a faulting
55
+ * re-read rethrows it rather than replacing it with a transient-looking registry error.
54
56
  *
55
57
  * **A refused rewrite deletes its own object when it would outlive the winner above the pointer.** `putImmutable`
56
58
  * commits atomically, so a rewrite whose stream throws leaves no object at all. One that completed is either
57
59
  * below the winner's pointer, where the winner's collection takes it, or above it: the case where this call took
58
60
  * its number after the winner's object was already in the bucket. Up there nothing collects it, and it was derived
59
61
  * from a generation the winner has replaced, so when the winner was another erasure it still holds the id that
60
- * erasure has just reported gone. That object is deleted before this call returns — see `discardRefused`.
62
+ * erasure has just reported gone. That object is deleted before this call returns, once its footer proves it the
63
+ * rewrite's own, under the version that read reported — see `discardRefused`.
61
64
  *
62
65
  * Without that fence the failure was silent and severe, and both halves were reproduced: `nextGeneration` picks a
63
66
  * number above everything in the bucket, so a forward-only publish here always won — discarding a concurrent
@@ -68,10 +71,21 @@
68
71
  * A live read still fetching from the collected generation heals forward to the rewrite, and a pin of it fails with
69
72
  * `NotFoundError` for any chunk it has yet to read: that is the documented cost of physical deletion on return. **Do not re-load the id while erasing it**: a load that lands after this rewrite
70
73
  * carries whatever its source held, and the library cannot know that source was meant to exclude the id.
71
- * A load already in flight writes its object above the pointer before it publishes. An erasure that finds the id in that
72
- * object writes the row before deleting it, so the load's publish, fenced on the row it read, is refused
73
- * (`published: false`) and the pointer never names a missing object (see `fenceInFlight`). A first load onto a row with
74
- * no pointer has nothing to be fenced on, so an erasure that finds the id in its object refuses instead.
74
+ * A load already in flight writes its object above the pointer before it publishes, or, as a first load onto a row with
75
+ * no pointer, before there is a pointer at all. An erasure that finds the id in that object renews the row's `pointerId`
76
+ * before deleting it, so the load's publish, fenced on the row it read, is refused (`published: false`) and the pointer
77
+ * never names a missing object (see `fenceInFlight`). An erasure that finds no holder writes nothing.
78
+ *
79
+ * An object sealed under a key its row does not hold cannot be searched, and no read of the segment can open it: a
80
+ * first load's on a row with no pointer, whose key that load has not published; on a segment with a key, a first
81
+ * load's that lost the race to the one that published, or crashed, under a key it made and never stored; and on a
82
+ * cleartext segment with a pointer, any encrypted object, since a key is made only for a segment's first generation
83
+ * and a publish never adds one to a segment that has generations: a first load's that minted a key and crashed, or
84
+ * lost the race to a cleartext first load. It counts as
85
+ * a holder whatever the id: an erasure of any id that finds one deletes it, under the same renewal of the row and read
86
+ * before the delete, and lists it in `collected`, and a first load still in flight that wrote it is refused. Only a
87
+ * searched object that held the id makes the answer `erased: true`. The current generation is never one of them: one
88
+ * the row's key does not open throws, as every read of the segment does.
75
89
  */
76
90
  import { type IAuditSink } from './audit.js';
77
91
  import type { CodecInterface } from './codec.js';
@@ -97,11 +111,15 @@ export interface EraseIdDeps {
97
111
  readonly keystore?: IKeystore;
98
112
  /** When true, refuse to rewrite a **cleartext** segment (the same guard the read path and a load offer). */
99
113
  readonly requireEncryption?: boolean;
100
- /** Supplying a clock that can yield makes the rewrite cooperative, as it makes a load. */
114
+ /**
115
+ * Supplying a clock that can yield makes the rewrite cooperative, as it makes a load. Its `sleep` is what a registry
116
+ * write that got no answer waits on before a fresh one is sent; with no `sleep`, such a write is not sent again and
117
+ * its `TransientError` is thrown.
118
+ */
101
119
  readonly clock?: Yielder;
102
120
  /**
103
- * The random source that spreads the waits between the rewrite's fresh compare-and-swaps. Absent, the read retry's
104
- * source is used if it has one, and otherwise each wait is its bound.
121
+ * The random source that spreads the waits between the fresh compare-and-swaps of the rewrite and of the row's
122
+ * renewal. Absent, the read retry's source is used if it has one, and otherwise each wait is its bound.
105
123
  */
106
124
  readonly rng?: Rng;
107
125
  /** 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. */
@@ -110,7 +128,7 @@ export interface EraseIdDeps {
110
128
  * The store's read retry, for the reads the rewrite makes along the way: the generation it rewrites and each of its
111
129
  * chunks, the read-back that verifies the generation it wrote, and any other generation it checks for the id. A
112
130
  * transient fault on one is run again under it rather than failing the erasure. Absent, each read is made once.
113
- * The deletes are not retried, and the rewrite's registry write is settled as a load's is.
131
+ * The deletes are not retried, and the rewrite's registry write and the row's renewal are settled as a load's is.
114
132
  */
115
133
  readonly readRetry?: ReadRetry;
116
134
  }
@@ -126,13 +144,20 @@ export interface EraseIdResult {
126
144
  readonly erased: boolean;
127
145
  /**
128
146
  * Why the id was not erased, when `erased` is false. `'absent'` (no registry row), `'destroyed'` (a crypto-shred
129
- * tombstone — already unreadable), `'no-generation'` (a row with no Storage data yet), `'not-member'` (no
130
- * generation in the bucket holds the id — the common case across a fleet scan), or `'superseded'`.
147
+ * tombstone — already unreadable), `'no-generation'` (a row with no generation yet: its bucket can hold first loads'
148
+ * objects, none of them found to hold the id, and `collected` names any this call deleted unsearched, see below),
149
+ * `'not-member'` (no
150
+ * generation in the bucket holds the id — the common case across a fleet scan — with the same note on `collected`),
151
+ * or `'superseded'`.
131
152
  *
132
153
  * **`'superseded'` means this call did not erase the id, not that the id is still there.** Another writer — a
133
- * load, another erasure, or a rollback — moved the pointer off the generation this call read, so what it was
134
- * doing no longer follows from what is current: a rewrite derived from that generation is not a valid successor
135
- * to the new one, and a generation it meant to delete above the pointer may be the one the pointer now names.
154
+ * load, another erasure, or a rollback — moved the pointer off the generation this call read (on a row with no
155
+ * pointer, any other write of the row before this call renewed it, a `setRetention` included), or replaced an
156
+ * object it meant to delete (another erasure deleted a holder above the pointer, or on a row with no pointer, and a
157
+ * load stored another object under its number, so the storage driver's conditional delete refused this call's,
158
+ * whether or not the pointer moved). What it was doing no longer follows from what is current: a rewrite derived
159
+ * from that generation is not a valid successor to the new one, and a generation it meant to delete may be the one
160
+ * the pointer now names, or another object than the one it read.
136
161
  * Re-run against the new generation: if the id is still present it is erased then; if the racing writer was
137
162
  * another erasure of the *same* id, the re-run reports `'not-member'` because it is already gone. Either way the
138
163
  * re-run settles it, which is why it is the documented action for this reason and for no other.
@@ -150,14 +175,20 @@ export interface EraseIdResult {
150
175
  readonly fromGeneration?: number;
151
176
  /**
152
177
  * The generation written without the id (present whenever one was written, even if `superseded`). A refused
153
- * rewrite whose object sits above the winner's pointer deletes that object again before it returns.
178
+ * rewrite whose object sits above the winner's pointer deletes that object again before it returns, when its footer
179
+ * proves it the rewrite's own.
154
180
  */
155
181
  readonly generation?: number;
156
182
  /**
157
183
  * Generations this call deleted. On a rewrite that is normally `[fromGeneration]`, plus any older orphans. When
158
- * the current generation did not hold the id, it is every generation below the pointer (`keep: 0` takes them
159
- * together) and each one above the pointer that held the id; the ones above it that did not hold the id stay,
160
- * because they are an operator's rollback targets. Empty when nothing was deleted.
184
+ * the current generation did not hold the id and a generation this call searched did, it is every generation below
185
+ * the pointer (`keep: 0` takes them together) and each one above the pointer that held the id; the ones above it
186
+ * that did not hold the id stay, because they are an operator's rollback targets. On a row with no pointer it is
187
+ * each first load's object that held the id. Wherever it found them, it is also each object sealed under a key the
188
+ * row does not hold, which this call cannot search and deletes whatever the id. When those are the only holders, it
189
+ * is only those, deleted one by one, and every generation this call searched stays: `reason` is then
190
+ * `'no-generation'` on a row with no pointer and `'not-member'` on one with a pointer. Empty when nothing was
191
+ * deleted.
161
192
  *
162
193
  * **It is what THIS call deleted, not the proof that the id is gone** — those differ: a concurrent collector can
163
194
  * take a generation holding the id first, and then `erased: true` is returned without it in `collected`, because
@@ -169,7 +200,10 @@ export interface EraseIdResult {
169
200
  * collection pass that could not prove the segment was still the same one (which an ordinary retirement landing
170
201
  * mid-call is enough to cause); the same refusal on the path where the current generation did not hold the id
171
202
  * and nothing was published at all; and a generation still holding the id when the bucket is listed at the end
172
- * — one an operator rolled the pointer onto while a rewrite was collecting, say.
203
+ * — one an operator rolled the pointer onto while a rewrite was collecting, say, or, on a row with no pointer, the
204
+ * object of a first load that read the row before this call renewed it and wrote it after this call listed the
205
+ * bucket. The row write an erasure makes before deleting a holder a load may still publish, when it gets no answer
206
+ * that reading the row can settle, throws the registry's `TransientError`, and that holder is not deleted.
173
207
  *
174
208
  * **Re-run it**, and read what the re-run says rather than assuming it finished the job. The re-run looks for
175
209
  * the id in every generation in the bucket, not only the current one, and there are three outcomes:
@@ -191,7 +225,9 @@ export interface EraseIdResult {
191
225
  * it. See the module note for the contract.
192
226
  *
193
227
  * Emits one `segment.rewrite` audit event at the publish (before the superseded generation is collected), so the
194
- * compliance record exists the moment the generation without the id is authoritative.
228
+ * compliance record exists the moment the generation without the id is authoritative. An erasure that rewrites nothing,
229
+ * because only other generations held the id, emits one `segment.collect` once no generation holds it, and so does one
230
+ * that deleted only objects no read of the segment can open, which names no generation the id was found in.
195
231
  */
196
232
  export declare function eraseIdFromSegment(ref: SegmentRef, id: number, deps: EraseIdDeps, options?: {
197
233
  audit?: IAuditSink;
@@ -45,8 +45,9 @@ export interface LoadNumber {
45
45
  * `superseded`. The orphan above stays unpublished; the load whose check meets it numbers past it by the listing,
46
46
  * and collection takes it once a generation above it is current. Like a listing's, the number can be one whose
47
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.
48
+ * why nothing identifies a generation by its number alone: the reader cache and the chunk cache key on the number and
49
+ * the row's `pointerId`, which every write that takes a number again renews, and an open of a row with a usable
50
+ * summary holds the object to the fingerprint the summary records; a pin keys on its object's fingerprint.
50
51
  */
51
52
  export declare function nextLoadGeneration(ref: SegmentRef, deps: GenerationDeps, row: RegistryRecord | null): Promise<LoadNumber>;
52
53
  /**
@@ -1,7 +1,35 @@
1
1
  import type { ChunkRef, SegmentRef } from './ports.js';
2
2
  export declare function segmentKey(ref: SegmentRef): string;
3
3
  export declare function segmentPrefix(ref: SegmentRef): string;
4
+ /**
5
+ * How many segments' encoded keys a {@link KeptSegmentKeys} holds before it empties: the reader cache's default
6
+ * segment count, a working set a long-lived reader keeps open. With 30-character names that is about 150 KB.
7
+ */
8
+ export declare const KEPT_SEGMENT_KEYS = 1024;
9
+ /**
10
+ * Segments' encoded keys ({@link segmentKey}, or {@link segmentPrefix}: whichever `encode` gives), kept so a read that
11
+ * looks up chunk after chunk compares names rather than encoding them again, which costs about a third of a warm
12
+ * `has()`. Held by namespace and then by segment, so segments of one name in several namespaces sit side by side, and
13
+ * no number of segments read in turn makes every lookup encode again. Bounded: at `max` entries it empties and fills
14
+ * again. An encoding is the names' and nothing else, so a kept one is the one an encoding would give now.
15
+ */
16
+ export declare class KeptSegmentKeys {
17
+ private readonly encode;
18
+ private readonly max;
19
+ private readonly byNamespace;
20
+ private kept;
21
+ constructor(encode: (ref: SegmentRef) => string, max?: number);
22
+ /** The segment's encoded key. */
23
+ of(ref: SegmentRef): string;
24
+ /** How many encoded keys are kept. */
25
+ get size(): number;
26
+ }
4
27
  export declare function chunkRefKey(ref: ChunkRef): string;
28
+ /**
29
+ * {@link chunkRefKey}, or {@link chunkGenKey} with a `version`, from the segment's {@link segmentPrefix} already
30
+ * encoded: for a caller that looks up many chunks of a few segments and keeps their prefixes.
31
+ */
32
+ export declare function chunkKeyUnder(prefix: string, chunkKey: number, version?: string | number): string;
5
33
  /**
6
34
  * Cache key for a chunk scoped to a specific **version** of the segment — {@link chunkRefKey} plus that
7
35
  * version (space-delimited, injection-proof exactly as above — the name parts are already encoded). A publish bumps the version, so it naturally
@@ -17,6 +17,13 @@ export declare const MAX_STORED_LEASES = 256;
17
17
  * plus a few, so that the most a segment can hold, all arriving at once, can all land.
18
18
  */
19
19
  export declare const LEASE_CAS_ATTEMPTS: number;
20
+ /**
21
+ * Fresh writes sent after one that got no answer and left the row as it was, by the lease writers and by an erasure's
22
+ * renewal of the row.
23
+ */
24
+ export declare const UNANSWERED_RESENDS = 3;
25
+ /** The upper bound of the first of those waits, in ms; each next one doubles it. */
26
+ export declare const UNANSWERED_BASE_MS = 500;
20
27
  /**
21
28
  * Whether an entry holds its generation against a collector reading `now`: it has not ended (`until` plus the margin
22
29
  * is ahead), and it does not run further past `now` than any honest holder with a clock inside the margin could have
@@ -15,9 +15,11 @@
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, 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.
18
+ * pointer), so a refused load deletes its own object before returning, while the segment's row is unchanged (or
19
+ * differs only in its leases), gone or `destroyed`, or has key material when the load wrote cleartext. Under a row
20
+ * that is gone or keyed, a read of the object's footer must prove it the load's own first. Once another write has
21
+ * otherwise changed the row, the object's number may name a re-created segment's live object, so it stays, and
22
+ * collection takes it like any other generation once one above it is current.
21
23
  */
22
24
  import { type IAuditSink } from './audit.js';
23
25
  import { type CodecBitmap, type CodecInterface } from './codec.js';
@@ -169,10 +171,12 @@ export type LoadRefusal = 'empty' | 'min-cardinality' | 'min-retained' | 'max-gr
169
171
  /**
170
172
  * Another writer got there first. Either another load wrote the same generation number first, so the write-once
171
173
  * put refused this one and it wrote nothing (`size: 0`); or the segment's registry row changed while the load was
172
- * writing: another load published, a retention change, a rollback or an erasure wrote the row, or the row was
173
- * deleted. Once another write has changed the row, the object stays in the bucket, because its generation number
174
- * may by then name another incarnation's live object; once the row is `destroyed` (dropped or crypto-shredded),
175
- * the object is deleted, and once the row is gone, so is an object its footer proves this load's own.
174
+ * writing: another load published, a retention change, a rollback or an erasure wrote the row, the row was
175
+ * deleted, or a row appeared where the load found none. Once another write has changed the row, the object stays in
176
+ * the bucket, still billed, until a later load's collection deletes it (at the latest the listing a load runs
177
+ * every sixteenth generation) or the segment is dropped, because its generation number may by then name another
178
+ * incarnation's live object; once the row is `destroyed` (dropped or crypto-shredded), the object is deleted, and
179
+ * once the row is gone, so is an object its footer proves this load's own.
176
180
  */
177
181
  | 'superseded';
178
182
  export interface LoadResult {
@@ -5,8 +5,8 @@
5
5
  * Design: the library emits **neutral domain events** and stays vendor-agnostic — you (or a ~12-line
6
6
  * adapter) map them to OpenTelemetry / Datadog / a log line. The default is a **no-op**, and emission is
7
7
  * skipped entirely when no sink is wired (near-zero overhead, no telemetry dependency). Events carry **raw
8
- * observations** (bytes, counts, ms); turning those into billable units / dollars is the cost model's job,
9
- * which keeps rates pluggable.
8
+ * observations** (bytes, counts, ms); turning those into billable units / dollars is left to whoever reads them,
9
+ * which keeps rates out of the library (`@cloudbitmaps/tools` prices a workload you give it).
10
10
  *
11
11
  * Two caveats for sink authors: `onEvent` runs **synchronously on the I/O path**, so keep it cheap and
12
12
  * non-blocking (offload batching/network to your own async queue); and `segment`/`namespace` are
@@ -168,9 +168,9 @@ export interface MetricsSnapshot {
168
168
  }>>;
169
169
  }
170
170
  /**
171
- * A ready-made sink that tallies events into a {@link MetricsSnapshot} — handy for tests, quick scripts,
172
- * and as the grounded-request-cost source behind `costReport()`. `snapshot()` returns an independent copy;
173
- * `reset()` zeroes the counters.
171
+ * A ready-made sink that tallies events into a {@link MetricsSnapshot} — handy for tests, quick scripts, and for
172
+ * reading the request rates a cost model is given. `snapshot()` returns an independent copy; `reset()` zeroes the
173
+ * counters.
174
174
  */
175
175
  export declare class CountingMetricsSink implements IMetricsSink {
176
176
  private storage;
@@ -53,8 +53,6 @@ export declare class PinnedStorageChunkSource implements StorageChunkSource {
53
53
  * wrong answer this exists to prevent.
54
54
  */
55
55
  pins: ReadonlyMap<string, PinnedAt>);
56
- /** The unpinned segments refresh as the store's own source does; a pinned one never does. */
57
- get pointerRefreshMs(): number;
58
56
  /**
59
57
  * The pin for this segment, or `undefined` if it is not pinned here.
60
58
  *
@@ -74,12 +72,14 @@ export declare class PinnedStorageChunkSource implements StorageChunkSource {
74
72
  sizeOf(ref: SegmentRef): Promise<SegmentSize | null>;
75
73
  cardinalities(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
76
74
  summary(ref: SegmentRef): Promise<GenerationSummary | null>;
75
+ stat(ref: SegmentRef): Promise<(GenerationSummary & SegmentSize) | null>;
77
76
  currentGeneration(ref: SegmentRef): Promise<number | null>;
78
77
  /**
79
78
  * A pinned segment reports the version captured when it was pinned, marked as a pin's, so its decoded chunks
80
79
  * are cached under keys that no live read writes. A live read keys its fetches by the version it resolved when
81
80
  * it began, but is served whatever generation the live source holds when each fetch lands — after a publish
82
- * and a lapsed `cache.genTtlMs`, a reader-cache eviction, a sweep that heals the read forward, or an
81
+ * and a lapsed `cache.genTtlMs`, a reader-cache eviction on a store with no timed refresh, the resolution
82
+ * cache letting the segment's resolution go on a store with one, a sweep that heals the read forward, or an
83
83
  * invalidation, a different one — so an entry under a live version can hold another generation's chunk.
84
84
  * Invariant 3 lets that live call see it, and later live reads resolve the current version and never look the
85
85
  * entry up; a pin sharing the key would be handed it, and return a read that mixed two generations. A pinned
@@ -0,0 +1,19 @@
1
+ import type { RegistryPatch, RegistryRecord, Token } from './ports.js';
2
+ /** The fields a read resolves through: a patch that names any of them renews the row's `pointerId`. */
3
+ export declare const RESOLVED_FIELDS: readonly ["currentGen", "status", "wrappedDeks", "keyId", "summary"];
4
+ /**
5
+ * Whether a compare-and-swap of `patch` renews the row's `pointerId`: whether it names a resolved field, by presence
6
+ * (`'field' in patch`), whatever the value, as every patch rule reads a patch.
7
+ */
8
+ export declare function renewsPointer(patch: RegistryPatch): boolean;
9
+ /**
10
+ * The patch that renews a row's `pointerId` and changes nothing a read resolves: the pointer, named at the value it
11
+ * has. A same-value pointer keeps the summary and the kept window, which follow the pointer only when it changes value.
12
+ */
13
+ export declare function renewPointer(row: Pick<RegistryRecord, 'currentGen'>): RegistryPatch;
14
+ /**
15
+ * The row's `pointerId`, refusing a row that has none with {@link UnsupportedError}: a registry written against an
16
+ * earlier port contract returns rows without it, and a reader that cannot tell which write set the row's resolution
17
+ * cannot key a cache on it. A TypeScript registry fails to compile instead; this catches one in JavaScript.
18
+ */
19
+ export declare function pointerIdOf(row: RegistryRecord): Token;