@cloudbitmaps/core 0.10.0 → 0.11.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.
- package/README.md +32 -36
- package/dist/{chunk-BUPDGZRR.js → chunk-5OMC5U7U.js} +87 -40
- package/dist/chunk-5OMC5U7U.js.map +7 -0
- package/dist/core/audit.d.ts +15 -14
- package/dist/core/budget.d.ts +5 -3
- package/dist/core/codec.d.ts +18 -10
- package/dist/core/consistency.d.ts +3 -3
- package/dist/core/cooperative.d.ts +9 -9
- package/dist/core/cost.d.ts +167 -15
- package/dist/core/crbm/format.d.ts +7 -9
- package/dist/core/crbm/reader.d.ts +18 -1
- package/dist/core/crbm/writer.d.ts +2 -2
- package/dist/core/crbm-storage-source.d.ts +150 -60
- package/dist/core/determinism.d.ts +1 -1
- package/dist/core/discover.d.ts +7 -5
- package/dist/core/due-index.d.ts +3 -5
- package/dist/core/engine.d.ts +55 -13
- package/dist/core/erase-id.d.ts +76 -41
- package/dist/core/erasure.d.ts +22 -16
- package/dist/core/errors.d.ts +13 -20
- package/dist/core/generation-gc.d.ts +9 -5
- package/dist/core/keys.d.ts +2 -2
- package/dist/core/load.d.ts +28 -9
- package/dist/core/metrics.d.ts +8 -4
- package/dist/core/name-codec.d.ts +21 -31
- package/dist/core/pinned-storage-source.d.ts +29 -8
- package/dist/core/ports.d.ts +142 -59
- package/dist/core/reader-defaults.d.ts +18 -0
- package/dist/core/registry-scan.d.ts +15 -6
- package/dist/core/retention-sweep.d.ts +26 -18
- package/dist/core/retry.d.ts +8 -4
- package/dist/core/rollback.d.ts +11 -7
- package/dist/core/validate.d.ts +27 -3
- package/dist/driver-kit.d.ts +2 -3
- package/dist/driver-kit.js +13 -52
- package/dist/driver-kit.js.map +3 -3
- package/dist/drivers/_shared/keys.d.ts +4 -5
- package/dist/drivers/_shared/object-registry.d.ts +10 -5
- package/dist/drivers/_shared/registry.d.ts +9 -5
- package/dist/drivers/backends.d.ts +10 -16
- package/dist/drivers/localfs/fs-util.d.ts +3 -2
- package/dist/drivers/localfs/registry.d.ts +4 -4
- package/dist/drivers/memory.d.ts +2 -10
- package/dist/drivers/retry/retrying-chunk-source.d.ts +59 -0
- package/dist/export/index.d.ts +7 -6
- package/dist/index.d.ts +15 -24
- package/dist/index.js +1014 -373
- package/dist/index.js.map +4 -4
- package/dist/testing/fuzz-core.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-BUPDGZRR.js.map +0 -7
- package/dist/drivers/_shared/aws-errors.d.ts +0 -18
- package/dist/drivers/retry/retrying-drivers.d.ts +0 -90
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bridges the `.crbm` archive format to the engine's Storage seam.
|
|
3
|
-
*
|
|
4
|
-
* `CrbmStorageChunkSource` implements the {@link StorageChunkSource} the engine reads through, over an
|
|
5
|
-
* {@link IStorageDriver}: it resolves a segment's current generation, opens its {@link CrbmReader} once, and serves
|
|
6
|
-
* per-chunk payloads. `writeCrbmGeneration` / `writeCrbmGenerationStream` are the write primitives (a generation
|
|
7
|
-
* built from bitmaps, in memory or streamed), `bulkLoadCrbmGeneration` is the load path over them, and
|
|
8
|
-
* `publishGeneration` is the forward-only pointer advance every write ends with.
|
|
9
|
-
*/
|
|
10
1
|
import { type IAuditSink } from './audit.js';
|
|
11
2
|
import type { Yielder } from './cooperative.js';
|
|
12
3
|
import type { Clock } from './determinism.js';
|
|
@@ -22,7 +13,7 @@ export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
|
|
|
22
13
|
* retirement of that scan. When absent, the source falls back to the `list`-scan (so the
|
|
23
14
|
* in-memory / simple setups keep working with no registry). The resolved generation is cached and
|
|
24
15
|
* **re-resolved on a short TTL** ({@link currentGenTtlMs}, needs a {@link clock}) so a long-lived source
|
|
25
|
-
* observes
|
|
16
|
+
* observes another process's load within the TTL, not only when something else happens to re-resolve it.
|
|
26
17
|
*/
|
|
27
18
|
readonly registry?: IRegistryDriver;
|
|
28
19
|
/**
|
|
@@ -40,16 +31,19 @@ export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
|
|
|
40
31
|
readonly requireEncryption?: boolean;
|
|
41
32
|
/**
|
|
42
33
|
* Time source for the current-generation TTL refresh — the determinism seam; `core/` never reads
|
|
43
|
-
* ambient time. Refresh needs **both** a clock and a `registry
|
|
44
|
-
*
|
|
45
|
-
*
|
|
34
|
+
* ambient time. Refresh needs **both** a clock and a `registry`. Without either there is no timed refresh, and
|
|
35
|
+
* that is the only difference: a segment is still re-resolved when its reader is evicted, when a fetch finds its
|
|
36
|
+
* generation swept, and on {@link CrbmStorageChunkSource.invalidate}. What goes is the bound on how long another
|
|
37
|
+
* process's publish takes to arrive. The `CloudRoaring` facade passes its clock automatically, so wiring a
|
|
38
|
+
* `registry` is enough to get the bounded refresh.
|
|
46
39
|
*/
|
|
47
40
|
readonly clock?: Pick<Clock, 'now'>;
|
|
48
41
|
/**
|
|
49
42
|
* How long (ms) a resolved `currentGen` is trusted before the next read re-resolves it (default 2000) — the
|
|
50
43
|
* bound on read staleness after a load publishes: a reader may serve the prior generation for up to this long,
|
|
51
44
|
* then converges. Lazy (checked on read — no timer); ≤ one cheap registry read per segment per window, and a
|
|
52
|
-
* new {@link CrbmReader} is opened only when the generation actually changed. `0`
|
|
45
|
+
* new {@link CrbmReader} is opened only when the generation actually changed. `0` turns the refresh off, as a
|
|
46
|
+
* missing clock or registry does: {@link clock} says what that changes, and what it does not.
|
|
53
47
|
*/
|
|
54
48
|
readonly currentGenTtlMs?: number;
|
|
55
49
|
/**
|
|
@@ -70,21 +64,52 @@ export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
|
|
|
70
64
|
*/
|
|
71
65
|
readonly maxOpenIndexBytes?: number;
|
|
72
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* What a pin holds of the object it pinned: the version its cache entries are keyed by, and the object's
|
|
69
|
+
* fingerprint, which a pinned read checks each time it opens the generation. `pinGeneration` always records one.
|
|
70
|
+
* A pin built by hand without one is not checked: it reads whatever object is under its generation's key.
|
|
71
|
+
*/
|
|
72
|
+
export interface PinnedObject {
|
|
73
|
+
readonly version: string;
|
|
74
|
+
readonly fingerprint?: string;
|
|
75
|
+
}
|
|
73
76
|
export declare class CrbmStorageChunkSource implements StorageChunkSource {
|
|
74
77
|
private readonly driver;
|
|
75
78
|
/**
|
|
76
79
|
* One resolved reader per segment, re-resolved on a short TTL ({@link CrbmStorageChunkSourceOptions.currentGenTtlMs},
|
|
77
80
|
* needs a clock). Within the TTL a segment's Storage bytes are treated as an immutable snapshot; when the TTL
|
|
78
81
|
* elapses the next read cheaply re-resolves `currentGen` and, only if it advanced (a load published),
|
|
79
|
-
* opens the new generation — so a long-lived source observes new generations within the TTL
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* one exists. **Bounded** by a {@link BoundedLru} ({@link CrbmStorageChunkSourceOptions.maxOpenSegments}, default
|
|
82
|
+
* opens the new generation — so a long-lived source observes new generations within the TTL. The engine pairs
|
|
83
|
+
* this with a **generation-keyed** cache so a bump never serves a stale decoded chunk. Without a clock or a
|
|
84
|
+
* registry there is no timed refresh, and a segment's reader is replaced only when it is evicted, when a read
|
|
85
|
+
* has to fetch from a generation a sweep deleted, or when {@link invalidate} drops it. A segment with no generation yet is not memoized, so
|
|
86
|
+
* it's re-checked until one exists. **Bounded** by a {@link BoundedLru} ({@link CrbmStorageChunkSourceOptions.maxOpenSegments}, default
|
|
84
87
|
* 1024): past the ceiling the least-recently-used segment's reader (and its parsed index) is evicted — the
|
|
85
88
|
* steady-state memory bound; re-opening an evicted segment is one cheap tail GET.
|
|
86
89
|
*/
|
|
87
90
|
private readonly snapshots;
|
|
91
|
+
/**
|
|
92
|
+
* Pins ({@link heldKey}) whose object has been found replaced: another object is under its generation's key. Kept,
|
|
93
|
+
* and bounded as the readers are, so that a pin's later chunk reads and any reopen fail at once rather than pay for
|
|
94
|
+
* the check again. A replacement can be undone, by a restore that puts the pinned object back, so
|
|
95
|
+
* {@link invalidate} forgets the segment's, and a pin of the same version that opens the object forgets the one
|
|
96
|
+
* against it. An object found gone is not kept: that is the open's own `NotFoundError`, and a 404 can pass. A
|
|
97
|
+
* reader still memoised keeps answering from the index it holds, so the pin's `count()` does, until a later pin of
|
|
98
|
+
* that version opens the object now under the key, which takes its place.
|
|
99
|
+
*/
|
|
100
|
+
private readonly replacedPins;
|
|
101
|
+
/**
|
|
102
|
+
* A check of a pinned object still under way, by {@link heldKey}, so the reads that ask at once share one footer
|
|
103
|
+
* read. One forgotten before it ends, by {@link invalidate} or a pin, keeps nothing it finds.
|
|
104
|
+
*/
|
|
105
|
+
private readonly checking;
|
|
106
|
+
/**
|
|
107
|
+
* How many times {@link invalidate} has run, so a read that waited can tell whether the store was told something
|
|
108
|
+
* meanwhile. An eviction from the reader cache is not an invalidation, and what a read found stands through one. It
|
|
109
|
+
* counts every segment's invalidations: a read that waited through another segment's only costs a later read the
|
|
110
|
+
* check again.
|
|
111
|
+
*/
|
|
112
|
+
private invalidations;
|
|
88
113
|
private readonly registry;
|
|
89
114
|
private readonly keystore;
|
|
90
115
|
private readonly requireEncryption;
|
|
@@ -94,6 +119,12 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
|
|
|
94
119
|
constructor(driver: IStorageDriver, options?: CrbmStorageChunkSourceOptions);
|
|
95
120
|
/** The current resolved reader for a segment, refreshing on the TTL. Cheap within the TTL window. */
|
|
96
121
|
private resolvedReader;
|
|
122
|
+
/**
|
|
123
|
+
* A refresh failed transiently and the prior reader keeps serving: make this snapshot lapse after
|
|
124
|
+
* {@link REFRESH_RETRY_MS} (never longer than the TTL) rather than after a whole TTL, so the next read after
|
|
125
|
+
* that asks the registry again. It costs no read of its own; it only changes when the next refresh happens.
|
|
126
|
+
*/
|
|
127
|
+
private retrySoon;
|
|
97
128
|
/** TTL elapsed: cheaply re-resolve `currentGen`; reopen only if it actually changed, else reuse the prior reader. */
|
|
98
129
|
private computeRefreshedReader;
|
|
99
130
|
/**
|
|
@@ -103,85 +134,143 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
|
|
|
103
134
|
*/
|
|
104
135
|
private install;
|
|
105
136
|
private now;
|
|
137
|
+
/**
|
|
138
|
+
* How often this source re-reads a segment's pointer while the segment is being read, in ms: its TTL, or 0 when
|
|
139
|
+
* it has no timed refresh (no clock, no registry, or a TTL of 0). The grounded cost report prices the refresh at
|
|
140
|
+
* this.
|
|
141
|
+
*/
|
|
142
|
+
get pointerRefreshMs(): number;
|
|
106
143
|
private expired;
|
|
107
144
|
/**
|
|
108
145
|
* The segment's current generation number — the engine keys its chunk cache by this so a generation bump
|
|
109
146
|
* is observed instead of serving a stale decoded chunk. Served from the (TTL-refreshed)
|
|
110
147
|
* snapshot, so no extra backend read within the TTL window. `null` if the segment has no committed generation.
|
|
111
148
|
*
|
|
112
|
-
* Heals a swept generation exactly like {@link withFreshSnapshot}
|
|
113
|
-
* before any chunk fetch,
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
149
|
+
* Heals a swept generation exactly like {@link withFreshSnapshot}: a caller that resolves the generation once
|
|
150
|
+
* per op, before any chunk fetch, would otherwise fail the whole operation rather than one chunk. The engine
|
|
151
|
+
* keys by {@link currentVersion} instead, which heals the same way, and falls back to this for a source that
|
|
152
|
+
* cannot report a version. It is spelled out rather than delegated because a lookup like this runs once per
|
|
153
|
+
* operand of every `has`/`count`/`iterate`/`intersect`, almost always served from the cached snapshot with
|
|
154
|
+
* no backend call at all. Routing it through the generic helper cost ~115 ns/op on that path (a second async
|
|
117
155
|
* frame, a per-call closure, and an `await` on a plain number) for a race that fires only during a
|
|
118
156
|
* concurrent sweep. One retry, then propagate — same contract, same eviction rule.
|
|
119
157
|
*/
|
|
120
158
|
currentGeneration(ref: SegmentRef): Promise<number | null>;
|
|
121
159
|
/**
|
|
122
160
|
* Forget everything derived from `ref`: the resolved snapshot and the open reader behind it (and with the
|
|
123
|
-
* reader, the DEK it unwrapped at open time)
|
|
161
|
+
* reader, the DEK it unwrapped at open time), and what its pins' objects were found to be. The next read
|
|
162
|
+
* resolves the segment again.
|
|
124
163
|
*
|
|
125
164
|
* Needed because the TTL refresh only ever answers "has the pointer moved?", and every *destructive* verb —
|
|
126
165
|
* an erasure that deletes the generation holding the bit, a `dropSegment`, a crypto-shred, a retirement —
|
|
127
166
|
* leaves the pointer's answer unchanged from this source's point of view while making the snapshot wrong.
|
|
128
167
|
* Until this is called the source keeps serving that snapshot with no backend read at all, so nothing on the
|
|
129
|
-
* storage side can close the window; with `cache.genTtlMs: 0
|
|
168
|
+
* storage side can close the window; with no registry, no clock or `cache.genTtlMs: 0`, nothing bounds it at all.
|
|
130
169
|
*/
|
|
131
170
|
invalidate(ref: SegmentRef): void;
|
|
132
171
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
172
|
+
* `<generation>` for a registry-less source, or `<generation>:<row token>` with one. The token moves on every row
|
|
173
|
+
* write, so an unrelated write (a `setRetention`) costs the segment's decoded chunks once — bounded, and on an
|
|
174
|
+
* admin path. Exactness in the direction that matters: with a registry, two incarnations can never share a version
|
|
175
|
+
* string. Without one they can: a name purged and loaded again out of band restarts at the same bare number, and
|
|
176
|
+
* a live read that reopens it after an eviction reads the new object's chunks beside the old one's cached ones. A
|
|
177
|
+
* store with no registry cannot write, so that takes a change made from outside it; a pin is kept apart from it by
|
|
178
|
+
* the fingerprint it records.
|
|
137
179
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
*
|
|
143
|
-
* `<generation>:<row token>` with one. The token moves on every row write, so an unrelated write (a
|
|
144
|
-
* `setRetention`) costs the segment's decoded chunks once — bounded, and on an admin path. Exactness in the
|
|
145
|
-
* direction that matters: two incarnations can never share a version string.
|
|
180
|
+
* This is the call the engine makes **once per operand of every read**, before any chunk fetch, to key its
|
|
181
|
+
* chunk cache, so it heals a swept generation exactly as {@link currentGeneration} does: unhealed, a cold read
|
|
182
|
+
* racing a publish and a `keep: 0` sweep failed the whole operation with `NotFoundError`, where the same race
|
|
183
|
+
* on a chunk fetch heals. Spelled out rather than delegated for the reason `currentGeneration` gives: it is
|
|
184
|
+
* almost always served from the cached snapshot with no backend call at all. One retry, then propagate.
|
|
146
185
|
*/
|
|
147
186
|
currentVersion(ref: SegmentRef): Promise<string | null>;
|
|
148
187
|
/**
|
|
149
188
|
* Resolve the segment **once** and report what a pin should hold: the generation, and the version that
|
|
150
189
|
* identifies those exact bytes. `null` when the segment resolves to no generation — there is nothing to pin,
|
|
151
190
|
* and a caller must treat that as "this handle reads empty", not "pinning is unsupported here".
|
|
191
|
+
*
|
|
192
|
+
* Heals a generation swept between the registry read and the open, as {@link currentVersion} does: a publish
|
|
193
|
+
* and a `keep: 0` sweep can land in that gap, and nothing has been handed out yet, so the generation current
|
|
194
|
+
* once the swept one is gone is exactly what a pin taken now should hold. One retry, then propagate.
|
|
152
195
|
*/
|
|
153
|
-
pinGeneration(ref: SegmentRef): Promise<{
|
|
196
|
+
pinGeneration(ref: SegmentRef): Promise<({
|
|
154
197
|
generation: number;
|
|
155
|
-
|
|
156
|
-
|
|
198
|
+
} & Required<PinnedObject>) | null>;
|
|
199
|
+
private pinOnce;
|
|
200
|
+
/**
|
|
201
|
+
* Where a pinned reader of one object is memoised. With a registry that is its version, which names the object:
|
|
202
|
+
* a name purged and loaded again gets a new row, and with it a new token. Without one the version is the bare
|
|
203
|
+
* generation number, which two incarnations share, so the object's fingerprint is added to tell them apart.
|
|
204
|
+
*/
|
|
205
|
+
private pinnedKey;
|
|
206
|
+
/**
|
|
207
|
+
* One pin's hold on one segment: its version, and the fingerprint of the object it pinned. A check of that object
|
|
208
|
+
* is shared under it, and what the check finds is kept under it, with a registry or without one: pins of one
|
|
209
|
+
* version can hold different objects, since a pin built by hand can name any, and an object replaced from outside
|
|
210
|
+
* the store leaves the row, and so the version, as it was.
|
|
211
|
+
*/
|
|
212
|
+
private heldKey;
|
|
157
213
|
/**
|
|
158
214
|
* A reader for one **specific** generation, memoized in the same bounded LRU as the live snapshots under a
|
|
159
215
|
* generation-qualified key.
|
|
160
216
|
*
|
|
161
217
|
* Sharing the LRU is the point, not an implementation detail. A pinned reader held in a private field is
|
|
162
218
|
* outside the memory ceiling the library advertises — measured at 10.34 MiB per live pin, 32 pins holding
|
|
163
|
-
* 111.7 MiB against an 8 MiB configured bound. A pinned *generation number* has no such problem:
|
|
164
|
-
*
|
|
165
|
-
*
|
|
219
|
+
* 111.7 MiB against an 8 MiB configured bound. A pinned *generation number* has no such problem: an object is
|
|
220
|
+
* immutable, so eviction is harmless, and re-opening at the same number reopens the same bytes unless the name
|
|
221
|
+
* was purged and loaded again, which the pin's fingerprint then says. It also removes a memoized-rejection bug by
|
|
222
|
+
* construction — `install` forgets a promise that rejects,
|
|
166
223
|
* where a hand-rolled `this.reader ??= open()` cached the rejection for the life of the handle and made a pin
|
|
167
224
|
* the one read path with no resilience.
|
|
168
225
|
*
|
|
169
226
|
* **`status` is re-checked here**, on every open rather than once at pin time. A pin taken before a
|
|
170
227
|
* crypto-shred must not keep unwrapping a DEK the shred destroyed; a destroyed segment resolves no generation
|
|
171
|
-
* for anyone,
|
|
228
|
+
* for anyone, and a pin's read of one fails. That holds from the pin's next open: a reader already open keeps
|
|
229
|
+
* the key it unwrapped until it is evicted or invalidated.
|
|
230
|
+
*
|
|
231
|
+
* **And the object is checked**, when the caller says which one it pinned. A generation number is not an
|
|
232
|
+
* identity (invariant 1): once a name is purged and loaded again, its new segment starts again at generation 0,
|
|
233
|
+
* so a pin of the old one would open the new one's object as its own, and read it beside the chunks it had
|
|
234
|
+
* already cached from the old one. The row's token cannot tell them apart, since it moves on every write, a
|
|
235
|
+
* publish included; the object's fingerprint can, and a pin whose object has been replaced fails, as one whose
|
|
236
|
+
* generation has been swept does. The memo is keyed so that two pins of one generation number in two
|
|
237
|
+
* incarnations never share a reader, with a registry or without one ({@link pinnedKey}).
|
|
172
238
|
*/
|
|
173
239
|
private readerAt;
|
|
174
240
|
private openAt;
|
|
175
|
-
/**
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Read one chunk of a specific generation — the pinned read path. `held`, when given, is what a pin holds of the
|
|
243
|
+
* object, and the read fails if the generation is now another object.
|
|
244
|
+
*/
|
|
245
|
+
getChunkAt(ref: ChunkRef, generation: number, held?: PinnedObject): Promise<Uint8Array | null>;
|
|
246
|
+
/**
|
|
247
|
+
* Throws the pin's NotFoundError if the object under `generation`'s key is no longer the one the pin holds. Its
|
|
248
|
+
* footer says, read with no key and no row ({@link CrbmReader.sameObject}), so an object written under a key this
|
|
249
|
+
* store lacks answers the same way, and so does one gone from its key. A replacement is kept
|
|
250
|
+
* ({@link replacedPins}), and a check under way is shared by the reads that ask at once; neither "gone" nor "the
|
|
251
|
+
* same" is kept, since either can change. A transient fault is rethrown, for the store's retries to see for what it
|
|
252
|
+
* is. A footer that cannot be read otherwise says nothing about which object is there, so this returns, and the
|
|
253
|
+
* caller's own error stands.
|
|
254
|
+
*/
|
|
255
|
+
private throwIfReplaced;
|
|
256
|
+
/** Chunk keys of a specific generation — the pinned shape read. `held` as for {@link getChunkAt}. */
|
|
257
|
+
listChunkKeysAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<number[]>;
|
|
258
|
+
/**
|
|
259
|
+
* Per-chunk cardinalities of a specific generation — powers a pinned `count()` with no payload reads. `held` as
|
|
260
|
+
* for {@link getChunkAt}.
|
|
261
|
+
*/
|
|
262
|
+
cardinalitiesAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<ReadonlyMap<number, number> | null>;
|
|
263
|
+
/** Grounded size of a specific generation. `held` as for {@link getChunkAt}. */
|
|
264
|
+
sizeOfAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<SegmentSize | null>;
|
|
265
|
+
/**
|
|
266
|
+
* Whether the segment is a registered name at all. With a registry that is exactly "a row exists" — including
|
|
267
|
+
* a row with no generation yet (minted by `setRetention` before the first load) and a `destroyed` tombstone,
|
|
268
|
+
* both of which are names somebody deliberately created. Without a registry there is no registration to
|
|
269
|
+
* consult, so the honest answer is whether the bucket holds any generation of it.
|
|
270
|
+
*
|
|
271
|
+
* Deliberately NOT served from the snapshot memo: the memo resolves to `null` for both states this exists to
|
|
272
|
+
* tell apart.
|
|
273
|
+
*/
|
|
185
274
|
exists(ref: SegmentRef): Promise<boolean>;
|
|
186
275
|
/**
|
|
187
276
|
* Evict a snapshot we just failed to read from — matched by its reader *promise*, so a concurrent call's
|
|
@@ -233,7 +322,7 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
|
|
|
233
322
|
private withFreshSnapshot;
|
|
234
323
|
}
|
|
235
324
|
/**
|
|
236
|
-
* Write one immutable generation from in-memory bitmaps
|
|
325
|
+
* Write one immutable generation from in-memory bitmaps. Chunks are
|
|
237
326
|
* sorted ascending and empty bitmaps skipped (empty chunks are never stored). Returns the driver's
|
|
238
327
|
* `{ size, sha256 }` for the written object. Pass `options.crypto` to AES-256-GCM-encrypt the generation
|
|
239
328
|
* (built from the segment's DEK, with associated data bound to `(segment, generation)`).
|
|
@@ -356,9 +445,9 @@ export interface BulkLoadResult {
|
|
|
356
445
|
* `false` means the object is durable but a **concurrent writer published a higher generation first**, so this
|
|
357
446
|
* one is an orphan that no reader will ever resolve. The write succeeded and the *load* did not: whatever this
|
|
358
447
|
* call was asked to make the segment contain, the segment does not contain. That is not a detail a caller can
|
|
359
|
-
* be left to infer — a `*Into` materialisation
|
|
360
|
-
*
|
|
361
|
-
*
|
|
448
|
+
* be left to infer — a `*Into` materialisation that reported the generation it wrote as "the destination's new
|
|
449
|
+
* current generation" on this path would state a plain untruth — so the flag is on the result and the verbs that
|
|
450
|
+
* promise a published generation check it.
|
|
362
451
|
*/
|
|
363
452
|
readonly becameCurrent?: boolean;
|
|
364
453
|
}
|
|
@@ -409,7 +498,8 @@ export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenK
|
|
|
409
498
|
* DEK comes back on the result so the deferred publish can store it.
|
|
410
499
|
*
|
|
411
500
|
* A caller that defers the publish owns what it wrote: an unpublished object sits ABOVE `currentGen`, where
|
|
412
|
-
* generation collection deliberately never looks, so nothing
|
|
501
|
+
* generation collection deliberately never looks, so nothing reclaims it until a later generation above it is
|
|
502
|
+
* current.
|
|
413
503
|
*/
|
|
414
504
|
publish?: boolean;
|
|
415
505
|
}): Promise<BulkLoadResult>;
|
|
@@ -27,7 +27,7 @@ export interface Clock {
|
|
|
27
27
|
* yields to nothing at all, and a co-resident server stays blocked for the loop's full duration. A real yield
|
|
28
28
|
* needs a macrotask. `sleep(1)` is one, but it buys the relief at ~1 ms of dead wall-clock per yield.
|
|
29
29
|
*
|
|
30
|
-
* **Optional**, so
|
|
30
|
+
* **Optional**, so a minimal `Clock` — `now` and `sleep` alone — still satisfies the interface. Callers must
|
|
31
31
|
* therefore degrade rather than assume: `clock.yieldNow?.() ?? clock.sleep(1)` — correct on any clock, cheap
|
|
32
32
|
* on one that implements this. `core/` cannot supply it itself (it is timer-free, lint-enforced); production
|
|
33
33
|
* wiring backs it with `setImmediate`, and a test can make it a no-op so virtual time is not perturbed
|
package/dist/core/discover.d.ts
CHANGED
|
@@ -57,11 +57,13 @@ export declare function segmentExists(ref: SegmentRef, registry: IRegistryDriver
|
|
|
57
57
|
* **Scoping to a namespace narrows the LIST prefix**, so it really is the difference between reading one
|
|
58
58
|
* tenant and reading all of them.
|
|
59
59
|
*
|
|
60
|
-
* Streams, so a large fleet need not be held at once, and stopping the iteration stops the scan —
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
60
|
+
* Streams, so a large fleet need not be held at once, and stopping the iteration stops the scan — as long as the
|
|
61
|
+
* registry driver's own `list` streams. One that collects its whole enumeration before yielding defeats both
|
|
62
|
+
* properties, since the scan is then paid for and resident before the first row reaches you, so a custom driver's
|
|
63
|
+
* `list` should yield each row as it reads it. The registry drivers this library ships all stream; on S3, GCS and
|
|
64
|
+
* Azure Blob the granularity is a page, so one LIST page and its in-flight row reads complete regardless. Nothing
|
|
65
|
+
* here retries the scan: a transient fault part-way through ends the iteration with that error, and calling
|
|
66
|
+
* `listSegments` again scans from the start, since the iterable one call returns is used up.
|
|
65
67
|
*
|
|
66
68
|
* Yields `destroyed` tombstones, and rows whose `currentGen` is `null`, because hiding either would make this
|
|
67
69
|
* disagree with the registry it reports on — a filtered enumeration that looks complete is how a retention
|
package/dist/core/due-index.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { MAX_NAME_LENGTH } from './validate.js';
|
|
2
2
|
import type { RegistryRecord, SegmentRef } from './ports.js';
|
|
3
|
-
/** Namespace prefix for due-index rows. */
|
|
4
|
-
export declare const DUE_NAMESPACE_PREFIX = "cbm.due.";
|
|
5
3
|
/** Bucket width. One day: small enough that a cycle reads little, coarse enough that the index stays tiny. */
|
|
6
4
|
export declare const DUE_BUCKET_MS = 86400000;
|
|
7
5
|
/**
|
|
@@ -24,13 +22,13 @@ export declare function isDueIndexRow(record: Pick<RegistryRecord, 'namespace'>)
|
|
|
24
22
|
* Encode a ref into one index-row name, unambiguously.
|
|
25
23
|
*
|
|
26
24
|
* `${namespaceLength}.${namespace}${segment}` — a decimal length, a dot, then the two parts concatenated. The
|
|
27
|
-
* length prefix is what makes it reversible:
|
|
28
|
-
*
|
|
25
|
+
* length prefix is what makes it reversible: a name may contain any character, the dot included, so no
|
|
26
|
+
* separator character could ever be unambiguous on its own. Reading the digits
|
|
29
27
|
* up to the first dot tells the parser exactly where the namespace ends.
|
|
30
28
|
*/
|
|
31
29
|
export declare function encodeDueName(ref: SegmentRef): string;
|
|
32
30
|
/**
|
|
33
|
-
* Can this ref be indexed at all? False when the encoded name would exceed the
|
|
31
|
+
* Can this ref be indexed at all? False when the encoded name would exceed the 256-character name cap —
|
|
34
32
|
* possible only for a ref whose namespace and segment are together near the limit.
|
|
35
33
|
*
|
|
36
34
|
* **Not indexable is not "not retired".** The repair scan still sees the segment's own row, so the consequence
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface EngineDeps {
|
|
|
12
12
|
/**
|
|
13
13
|
* The bitmap codec — **required**. `core/` is codec-agnostic: it can have no default, because the concrete codec
|
|
14
14
|
* lives in a *flavor* package that depends on core (a default here would invert that arrow). A flavor's facade
|
|
15
|
-
* injects it — `@cloudbitmaps/roaring` passes
|
|
15
|
+
* injects it — `@cloudbitmaps/roaring` passes the roaring codec — so applications never see this.
|
|
16
16
|
*/
|
|
17
17
|
readonly codec: CodecInterface;
|
|
18
18
|
/** Time source for the `storage.get` latency metric only; defaults to a clock that reads 0. */
|
|
@@ -26,8 +26,27 @@ export interface EngineDeps {
|
|
|
26
26
|
*/
|
|
27
27
|
readonly budget?: Budget | null;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* A range of ids for a read to yield: those greater than `after` and no greater than `through`, `(after, through]`.
|
|
31
|
+
*
|
|
32
|
+
* Built for keyset paging: `after` is the last id the caller already has, `through` the end of its window. Each is
|
|
33
|
+
* optional: left out, the read starts at the first id or runs to the last. Each is an integer in `0..4294967295`, or
|
|
34
|
+
* the read throws {@link ValidationError} when it is first read. `after >= through` is an empty range, not an error,
|
|
35
|
+
* because a cursor that has reached the end of its window is normal; an empty range reads nothing, not even whether
|
|
36
|
+
* a combine's operands exist.
|
|
37
|
+
*
|
|
38
|
+
* A read with a range fetches only the chunks the range overlaps, on a combine for every operand and every
|
|
39
|
+
* `exclude`. The per-op budget is charged once, before the first fetch, for every chunk in the range, so with
|
|
40
|
+
* `after` alone it is charged to the end of the segment however early the caller stops; pass `through` to bound it.
|
|
41
|
+
*/
|
|
42
|
+
export interface IdRange {
|
|
43
|
+
/** Exclusive lower bound: the read yields only ids greater than this. */
|
|
44
|
+
readonly after?: number | undefined;
|
|
45
|
+
/** Inclusive upper bound: the read yields only ids up to and including this. */
|
|
46
|
+
readonly through?: number | undefined;
|
|
47
|
+
}
|
|
29
48
|
/** Options common to the chunk-aligned combines. */
|
|
30
|
-
export interface CombineOptions {
|
|
49
|
+
export interface CombineOptions extends IdRange {
|
|
31
50
|
/** Max chunk keys resolved concurrently — bounds the Storage footprint. A positive integer. */
|
|
32
51
|
readonly concurrency?: number;
|
|
33
52
|
/** Override the store's per-op budget for this call (`false` lifts it). */
|
|
@@ -58,14 +77,29 @@ export declare class SegmentEngine {
|
|
|
58
77
|
* Cardinality. When the Storage source can serve per-chunk cardinality from its `.crbm` index (the `.crbm`
|
|
59
78
|
* source can), the count is summed from the index with **zero payload reads**. A source without that
|
|
60
79
|
* capability (the in-memory source) falls back to fetching every chunk.
|
|
80
|
+
*
|
|
81
|
+
* The index sum is trusted, not confirmed: the reader checked the index for internal consistency when it opened
|
|
82
|
+
* the object, and a corrupt index that is still internally consistent yields a wrong count. `iterate()` and the
|
|
83
|
+
* combines decode the payloads.
|
|
61
84
|
*/
|
|
62
85
|
count(seg: SegmentRef): Promise<number>;
|
|
63
86
|
/** Whether the Storage source can measure segment size (for grounded cost); false ⇒ storage isn't grounded. */
|
|
64
87
|
get supportsStorageSize(): boolean;
|
|
88
|
+
/** How often the Storage source re-reads a pointer while reading (cost reporting); undefined if it does not say. */
|
|
89
|
+
get pointerRefreshMs(): number | undefined;
|
|
65
90
|
/** Grounded Storage size of a segment's current generation (cost reporting), or null if it has no generation. */
|
|
66
91
|
segmentSize(seg: SegmentRef): Promise<SegmentSize | null>;
|
|
67
|
-
/**
|
|
68
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Every id, ascending, one chunk at a time; with `range`, only the ids in `(after, through]`, fetching only the
|
|
94
|
+
* chunks the range overlaps (see {@link IdRange}).
|
|
95
|
+
*
|
|
96
|
+
* Two generators, not one with a branch: a second `yield` site in the full read's generator grows its frame, and
|
|
97
|
+
* every id of a read that asked for no range paid for it (measured at about 5% per id). The full read keeps the
|
|
98
|
+
* loop it always had.
|
|
99
|
+
*/
|
|
100
|
+
iterate(seg: SegmentRef, range?: IdRange): AsyncGenerator<number>;
|
|
101
|
+
private iterateAll;
|
|
102
|
+
private iterateRange;
|
|
69
103
|
/**
|
|
70
104
|
* `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
|
|
71
105
|
*
|
|
@@ -89,11 +123,13 @@ export declare class SegmentEngine {
|
|
|
89
123
|
* Generation-consistent within the call (normal case): each operand's current generation is resolved **once**
|
|
90
124
|
* up front (before the fan-out) and threaded into every chunk read, so a concurrent load can't corrupt or tear
|
|
91
125
|
* the result — every chunk read is a whole, checksum-verified, immutable generation. The edge a *long* call can
|
|
92
|
-
* hit: if it straddles a mid-call `cache.genTtlMs` boundary and a load has published, an
|
|
93
|
-
* chunks may re-resolve forward to the newer generation (a generation hop within one long
|
|
94
|
-
* never crashes or returns a torn object, but may mix generations.
|
|
95
|
-
*
|
|
96
|
-
*
|
|
126
|
+
* 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
|
|
128
|
+
* call) — the call never crashes or returns a torn object, but may mix generations. Three things hop it without
|
|
129
|
+
* waiting for the TTL, so a shorter call can meet them too: **the reader cache evicting an operand mid-call**
|
|
130
|
+
* (`maxOpenSegments`), whose re-read re-resolves fresh; a sweep deleting the generation it was reading, which
|
|
131
|
+
* heals the read forward; and an invalidation, which this store's own `load`, `rollback` and `eraseSubject` make
|
|
132
|
+
* and `invalidate()` makes on request. Still whole/immutable per read, never torn.
|
|
97
133
|
*/
|
|
98
134
|
intersect(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<number>;
|
|
99
135
|
/**
|
|
@@ -142,6 +178,11 @@ export declare class SegmentEngine {
|
|
|
142
178
|
*
|
|
143
179
|
* Costs one `maximum()` per chunk, not per id — `maximum` is optional on the codec seam precisely so a codec
|
|
144
180
|
* that cannot answer in O(1) opts out instead of making the read path walk every value.
|
|
181
|
+
*
|
|
182
|
+
* One value stands for all of them only because the decode is structurally checked. Roaring answers `maximum()`
|
|
183
|
+
* from its last container, so a payload listing its containers out of order would report the wrong one's
|
|
184
|
+
* largest value and pass here while holding values above 65,535. The codec's `safeDeserialize` refuses that
|
|
185
|
+
* payload, and every other shape that would make this answer wrong, before it reaches this check.
|
|
145
186
|
*/
|
|
146
187
|
private assertChunkPayloadInRange;
|
|
147
188
|
/**
|
|
@@ -161,7 +202,8 @@ export declare class SegmentEngine {
|
|
|
161
202
|
* **Costs nothing on a normal combine.** `exists` is consulted only for an operand that resolved to zero
|
|
162
203
|
* chunks — a segment with data is self-evidently registered — so the check is a registry read exactly when
|
|
163
204
|
* a combine was about to do something suspicious, and no reads at all otherwise. A source that cannot answer
|
|
164
|
-
* `exists` (it has no notion of registration) skips the check rather than guessing.
|
|
205
|
+
* `exists` (it has no notion of registration) skips the check rather than guessing. An operand is judged by
|
|
206
|
+
* all its chunks, not those inside a range: one with nothing in range but data elsewhere is not suspicious.
|
|
165
207
|
*/
|
|
166
208
|
private refuseAbsentOperands;
|
|
167
209
|
/**
|
|
@@ -182,8 +224,8 @@ export declare class SegmentEngine {
|
|
|
182
224
|
* The cache-key component identifying **which bytes** this op will read: the source's `currentVersion` when
|
|
183
225
|
* it has one, else the generation alone.
|
|
184
226
|
*
|
|
185
|
-
* The generation alone is not an identity.
|
|
186
|
-
* bucket emptied, so a retired-and-re-created name serves different data at the same `currentGen` — and a
|
|
227
|
+
* The generation alone is not an identity. A load's generation number restarts at 0 once a row is purged
|
|
228
|
+
* and the bucket emptied, so a retired-and-re-created name serves different data at the same `currentGen` — and a
|
|
187
229
|
* decoded chunk cached under `(segment, chunk, 0)` is handed straight back to the new incarnation. That is an
|
|
188
230
|
* erased id reappearing with no read to intercept it, which is why this is keyed on the version rather than
|
|
189
231
|
* the number.
|
|
@@ -195,7 +237,7 @@ export declare class SegmentEngine {
|
|
|
195
237
|
* keyed by it, so a load that advances the generation misses the cache and re-reads the new bytes instead of
|
|
196
238
|
* serving a stale decoded chunk (an erased id can't resurrect from a cached superseded chunk). `gen === null`
|
|
197
239
|
* ⇒ the source reports no current generation ⇒ no storage bytes for any chunk, so skip the fetch entirely.
|
|
198
|
-
* `gen === undefined` ⇒ the source can't report a generation (
|
|
240
|
+
* `gen === undefined` ⇒ the source can't report a generation (it only ever serves one) ⇒ the key stays
|
|
199
241
|
* generation-free. Superseded-generation entries age out under the LRU ceiling — no active purge.
|
|
200
242
|
*
|
|
201
243
|
* The returned instance is **shared** (it may be the cached one): callers read it or clone it, never mutate it.
|