@cloudbitmaps/core 0.20.0 → 0.22.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 +101 -5
- package/dist/{chunk-YSC7NJWV.js → chunk-OSU7W2Y3.js} +811 -164
- package/dist/chunk-OSU7W2Y3.js.map +7 -0
- package/dist/core/audit.d.ts +12 -2
- package/dist/core/bit-route.d.ts +9 -1
- package/dist/core/blob.d.ts +12 -1
- package/dist/core/codec.d.ts +1 -1
- package/dist/core/combine-expr.d.ts +10 -8
- package/dist/core/combine-feed.d.ts +12 -18
- package/dist/core/combine-many.d.ts +29 -22
- package/dist/core/concurrency.d.ts +8 -0
- package/dist/core/consistency.d.ts +69 -17
- package/dist/core/crbm/reader.d.ts +52 -6
- package/dist/core/crbm-storage-source.d.ts +65 -452
- package/dist/core/crypto.d.ts +6 -1
- package/dist/core/determinism.d.ts +2 -1
- package/dist/core/engine.d.ts +222 -33
- package/dist/core/erase-id.d.ts +60 -15
- package/dist/core/erasure.d.ts +5 -4
- package/dist/core/errors.d.ts +61 -11
- package/dist/core/leases.d.ts +4 -3
- package/dist/core/load.d.ts +58 -45
- package/dist/core/lru.d.ts +2 -15
- package/dist/core/metrics.d.ts +5 -3
- package/dist/core/pinned-storage-source.d.ts +4 -4
- package/dist/core/ports.d.ts +96 -22
- package/dist/core/registry-scan.d.ts +0 -7
- package/dist/core/retention-sweep.d.ts +70 -66
- package/dist/core/retention.d.ts +2 -2
- package/dist/core/retry.d.ts +15 -18
- package/dist/core/rollback.d.ts +61 -31
- package/dist/core/validate.d.ts +2 -0
- package/dist/driver-kit.d.ts +11 -2
- package/dist/driver-kit.js +84 -16
- package/dist/driver-kit.js.map +3 -3
- package/dist/drivers/_shared/bytes.d.ts +6 -0
- package/dist/drivers/_shared/faults.d.ts +15 -0
- package/dist/drivers/_shared/object-keys.d.ts +18 -0
- package/dist/drivers/_shared/object-registry-keys.d.ts +0 -2
- package/dist/drivers/_shared/object-registry.d.ts +2 -0
- package/dist/drivers/_shared/options.d.ts +19 -0
- package/dist/drivers/backends.d.ts +2 -0
- package/dist/drivers/crypto.d.ts +1 -1
- package/dist/export/index.d.ts +9 -8
- package/dist/index.d.ts +11 -10
- package/dist/index.js +4202 -2833
- package/dist/index.js.map +4 -4
- package/package.json +7 -2
- package/dist/chunk-YSC7NJWV.js.map +0 -7
- package/dist/core/chunk-checks.d.ts +0 -34
- package/dist/core/chunk-stream.d.ts +0 -22
- package/dist/core/chunk-window.d.ts +0 -30
- package/dist/core/combine-ledger.d.ts +0 -43
- package/dist/core/crbm/crc32c.d.ts +0 -10
- package/dist/core/crbm/fingerprint.d.ts +0 -18
- package/dist/core/crbm/format.d.ts +0 -151
- package/dist/core/crbm/frame.d.ts +0 -10
- package/dist/core/crbm/plan-reads.d.ts +0 -44
- package/dist/core/crbm/varint.d.ts +0 -13
- package/dist/core/crbm/writer.d.ts +0 -68
- package/dist/core/due-index.d.ts +0 -53
- package/dist/core/generation-gc.d.ts +0 -179
- package/dist/core/item-pull.d.ts +0 -29
- package/dist/core/kept-generations.d.ts +0 -40
- package/dist/core/metadata.d.ts +0 -42
- package/dist/core/reader-defaults.d.ts +0 -29
- package/dist/core/summary.d.ts +0 -78
- package/dist/core/token.d.ts +0 -41
- package/dist/drivers/_shared/entropy.d.ts +0 -5
- package/dist/drivers/_shared/registry.d.ts +0 -104
- package/dist/drivers/localfs/exact-case.d.ts +0 -37
- package/dist/drivers/localfs/fs-util.d.ts +0 -33
- package/dist/drivers/localfs/paths.d.ts +0 -40
- package/dist/drivers/localfs/registry.d.ts +0 -44
- package/dist/drivers/localfs/storage.d.ts +0 -21
- package/dist/drivers/memory.d.ts +0 -61
- package/dist/testing/fuzz-core.d.ts +0 -15
package/dist/core/engine.d.ts
CHANGED
|
@@ -13,7 +13,11 @@ import type { GenerationMetadata, StorageChunkSource, SegmentRef } from './ports
|
|
|
13
13
|
export declare const DEFAULT_INTERSECT_CONCURRENCY = 32;
|
|
14
14
|
export interface EngineDeps {
|
|
15
15
|
readonly storage: StorageChunkSource;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Optional cache of decoded (immutable) Storage chunks. A cache whose `size` is 0 is taken to hold nothing: a read
|
|
18
|
+
* that opens a stream on it looks up the chunk the stream starts at and counts each later chunk a miss without
|
|
19
|
+
* looking it up.
|
|
20
|
+
*/
|
|
17
21
|
readonly cache?: BoundedLru<string, CodecBitmap>;
|
|
18
22
|
/** 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. */
|
|
19
23
|
readonly maxBitmapBytes?: number;
|
|
@@ -33,6 +37,19 @@ export interface EngineDeps {
|
|
|
33
37
|
* Read ops refuse before fan-out if they'd exceed it.
|
|
34
38
|
*/
|
|
35
39
|
readonly budget?: Budget | null;
|
|
40
|
+
/**
|
|
41
|
+
* An engine over the same `cache` whose invalidations this one shares: an {@link SegmentEngine.invalidate} through
|
|
42
|
+
* either reaches the reads running on both. It is opt-in, and engines that read through one chunk cache must be
|
|
43
|
+
* built with it: two built without it share nothing, so a read running on one when the other invalidates its
|
|
44
|
+
* segment writes the chunks it read before the invalidation back into the cache, and later reads through it are
|
|
45
|
+
* answered from them, an erased id included, until the cache evicts them. The `CloudRoaring` facade builds the
|
|
46
|
+
* engines of its pins and of its combines with a pinned operand this way.
|
|
47
|
+
*
|
|
48
|
+
* The engines share their requests in flight too: a cache miss on one joins a request the other opened for the same
|
|
49
|
+
* cache key, and gets what that engine's storage, codec and `maxBitmapBytes` made of it, as a cache hit hands over
|
|
50
|
+
* the chunk the other decoded. Throws {@link ValidationError} unless it is a `SegmentEngine` over the same `cache`.
|
|
51
|
+
*/
|
|
52
|
+
readonly sharesInvalidationsWith?: SegmentEngine;
|
|
36
53
|
}
|
|
37
54
|
/**
|
|
38
55
|
* A range of ids for a read to yield: those greater than `after` and no greater than `through`, `(after, through]`.
|
|
@@ -44,8 +61,9 @@ export interface EngineDeps {
|
|
|
44
61
|
* a combine's operands exist.
|
|
45
62
|
*
|
|
46
63
|
* A read with a range fetches only the chunks the range overlaps, on a combine for every operand and every
|
|
47
|
-
* `exclude`. The per-op budget is charged
|
|
48
|
-
*
|
|
64
|
+
* `exclude`. The per-op budget is charged before the first fetch, for every chunk in the range, so with `after` alone
|
|
65
|
+
* it is charged to the end of the segment however early the caller stops; pass `through` to bound it. A read that
|
|
66
|
+
* lists its keys again after a move is charged again, with the chunks it has already taken.
|
|
49
67
|
*/
|
|
50
68
|
export interface IdRange {
|
|
51
69
|
/** Exclusive lower bound: the read yields only ids greater than this. */
|
|
@@ -63,7 +81,7 @@ export interface CombineOptions extends IdRange {
|
|
|
63
81
|
* is larger), about `concurrency × operands` MiB are held, whatever the segment's size. The stream opens 4 ranges wide
|
|
64
82
|
* (or `concurrency`, if lower) and widens as ranges are taken, so a read that stops early has asked for little past
|
|
65
83
|
* where it stopped, and a read that fits one range makes one request however many chunks it needs. A source that
|
|
66
|
-
* reads chunk by chunk (a custom one) is read
|
|
84
|
+
* reads chunk by chunk (a custom one) is read `concurrency` chunk keys at once, opening 8 wide. A running
|
|
67
85
|
* stream holds its operand's reader outside the reader cache's bounds until it ends or the segment moves on.
|
|
68
86
|
*/
|
|
69
87
|
readonly concurrency?: number;
|
|
@@ -94,18 +112,10 @@ export declare class SegmentEngine {
|
|
|
94
112
|
/** Resolved per-op budget (null = disabled); undefined deps ⇒ the generous default. */
|
|
95
113
|
private readonly budget;
|
|
96
114
|
/**
|
|
97
|
-
* The
|
|
98
|
-
*
|
|
99
|
-
* promise per distinct key in flight.
|
|
100
|
-
*/
|
|
101
|
-
private readonly openReads;
|
|
102
|
-
/**
|
|
103
|
-
* The chunk streams open right now. An {@link invalidate} of a segment marks its streams, which then cache no chunk
|
|
104
|
-
* they deliver, as a read whose entry an invalidation dropped does not (streams hold no `openReads` entry).
|
|
115
|
+
* The invalidations this engine shares with every engine over its chunk cache that was built to share them
|
|
116
|
+
* ({@link EngineDeps.sharesInvalidationsWith}), and the chunk reads open on all of them.
|
|
105
117
|
*/
|
|
106
|
-
private readonly
|
|
107
|
-
/** How many times {@link invalidate} has been called: a read compares it with the count it began under. */
|
|
108
|
-
private invalidations;
|
|
118
|
+
private readonly scope;
|
|
109
119
|
/**
|
|
110
120
|
* The cache-key prefixes ({@link segmentPrefix}) of the segments this engine reads, kept rather than encoded for each
|
|
111
121
|
* chunk. A kept prefix is the one {@link invalidate} finds the segment's chunks by.
|
|
@@ -143,12 +153,17 @@ export declare class SegmentEngine {
|
|
|
143
153
|
}>;
|
|
144
154
|
/** The size of the segment's current generation from the source's `sizeOf`, or `null` when it cannot say. */
|
|
145
155
|
private sizeOf;
|
|
146
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* The chunks of `seg` at `chunkKeys`, read through a window of {@link DEFAULT_INTERSECT_CONCURRENCY}. A cached chunk
|
|
158
|
+
* that is no longer the segment's version marks `moves`, when given.
|
|
159
|
+
*/
|
|
147
160
|
private chunkWindow;
|
|
148
161
|
/**
|
|
149
162
|
* The chunks of `seg` at `chunkKeys`, taken in ascending key order by a read that may stop at any point: one stream of
|
|
150
163
|
* coalesced ranges when the source has `getChunks`, opening 1, 2, 4 … ranges wide up to {@link DEFAULT_INTERSECT_CONCURRENCY},
|
|
151
|
-
* else the chunk-by-chunk window of that many reads. `close` stops what is left of it.
|
|
164
|
+
* else the chunk-by-chunk window of that many reads. `close` stops what is left of it. A chunk that shows the segment
|
|
165
|
+
* moved marks `moves`: one of the stream named by another version, or one cached under `gen` that is no longer the
|
|
166
|
+
* segment's.
|
|
152
167
|
*/
|
|
153
168
|
private chunkSequence;
|
|
154
169
|
/**
|
|
@@ -158,9 +173,13 @@ export declare class SegmentEngine {
|
|
|
158
173
|
* chunks the range overlaps (see {@link IdRange}). A source that reads chunk by chunk is read through a window of up to
|
|
159
174
|
* 32 chunk fetches that opens 1, 2, 4 … 32 wide.
|
|
160
175
|
*
|
|
176
|
+
* The chunk keys are listed once, before the first fetch. A move of the segment the read meets while it runs (see
|
|
177
|
+
* {@link Moves}) lists them again, from the generation current then, and the read goes on with the keys above the one
|
|
178
|
+
* it last took: it reads a chunk the new generation added, and not one it dropped.
|
|
179
|
+
*
|
|
161
180
|
* Two generators, not one with a branch: a second `yield` site in the full read's generator grows its frame, and
|
|
162
|
-
* every id of a read that asked for no range paid for it (measured at about 5% per id). The full read keeps
|
|
163
|
-
* loop
|
|
181
|
+
* every id of a read that asked for no range paid for it (measured at about 5% per id). The full read keeps one
|
|
182
|
+
* `yield`, in a loop of its own.
|
|
164
183
|
*/
|
|
165
184
|
iterate(seg: SegmentRef, range?: IdRange): AsyncGenerator<number>;
|
|
166
185
|
private iterateAll;
|
|
@@ -168,10 +187,71 @@ export declare class SegmentEngine {
|
|
|
168
187
|
/**
|
|
169
188
|
* {@link iterate}, one `Uint32Array` per chunk instead of one id at a time: the same chunks, in the same order, with
|
|
170
189
|
* the same ids (a range cut at its edges the same way), and no array for a chunk that holds none of them. The
|
|
171
|
-
* reads, the budget charge
|
|
172
|
-
* the caller's to keep.
|
|
190
|
+
* reads, the budget charge, the window and a move met while it runs are the per-id read's. Each array is at most
|
|
191
|
+
* 65,536 ids (256 KiB) and is the caller's to keep.
|
|
173
192
|
*/
|
|
174
193
|
iterateBatches(seg: SegmentRef, range?: IdRange): AsyncGenerator<Uint32Array>;
|
|
194
|
+
/**
|
|
195
|
+
* Plan an `iterate` of the keys of `seg` above `after` and inside `w`: list them with their version
|
|
196
|
+
* ({@link listKeys}), charge the budget one storage fetch for each and for each of the `spent` keys earlier plans
|
|
197
|
+
* took, before any fetch, and open their chunks.
|
|
198
|
+
*/
|
|
199
|
+
private iteratePlan;
|
|
200
|
+
/** The first plan of an `iterate`: every key in `w`. */
|
|
201
|
+
private planIterate;
|
|
202
|
+
/**
|
|
203
|
+
* The chunk at the next key of `run`, and `run.key` that key, or `undefined` once its keys are used up. Not `async`:
|
|
204
|
+
* when nothing has moved and no look is due, the chunk's own promise is all a key costs.
|
|
205
|
+
*/
|
|
206
|
+
private nextChunk;
|
|
207
|
+
private takeNext;
|
|
208
|
+
/**
|
|
209
|
+
* {@link nextChunk} once the read has looked at its segment's version, and planned again if it is not the one the
|
|
210
|
+
* plan was made under. A chunk that suggested a move is confirmed so: a source whose chunks name their version
|
|
211
|
+
* otherwise than its `currentVersion` does costs a look, never a plan.
|
|
212
|
+
*/
|
|
213
|
+
private nextChunkAfterLook;
|
|
214
|
+
/**
|
|
215
|
+
* Plan the rest of `run` again, after a move: the keys above the one it last took, from the generation current now.
|
|
216
|
+
* What the plan's stream still has in flight is dropped, not served.
|
|
217
|
+
*/
|
|
218
|
+
private replanIterate;
|
|
219
|
+
/**
|
|
220
|
+
* A segment's chunk keys, ascending and checked, and the version they are read under, with the invalidation epoch
|
|
221
|
+
* they were listed from. The version is resolved AFTER the shape read (`listChunkKeys`): a non-empty shape read means
|
|
222
|
+
* the source resolved and cached a non-null snapshot, so the version cannot then come back null while storage data is
|
|
223
|
+
* present, and `storageChunk` cannot skip every chunk on a stale null.
|
|
224
|
+
*
|
|
225
|
+
* A write of this store that moves the segment while the two are read would leave the keys of one generation under
|
|
226
|
+
* the version of the next, and nothing later could tell: the read's chunks and its looks would all name that newer
|
|
227
|
+
* version. So when the segment was invalidated in this store's scope while they were read, they are read again. Only
|
|
228
|
+
* that segment's invalidations count, so a store busy invalidating other segments costs a listing nothing more; with
|
|
229
|
+
* none since the listing began the check is a number compare.
|
|
230
|
+
*
|
|
231
|
+
* Honest note on how much the ordering buys, so a future reader does not take an untested claim for a tested one.
|
|
232
|
+
* Against a `CrbmStorageChunkSource`, the only source with a generation to resolve, both reads go through the same
|
|
233
|
+
* resolved-reader memo, so the pathological state (keys non-empty, version null) does not arise and swapping the two
|
|
234
|
+
* is observably identical. The ordering is kept because it is the order that is correct for *any* source satisfying
|
|
235
|
+
* the port (a custom source that resolves its shape and its generation independently can still produce that state),
|
|
236
|
+
* not because a test can currently tell the difference. Within a call shorter than `cache.genTtlMs` the two share one
|
|
237
|
+
* snapshot, so the read is generation-consistent, absent cache-pressure eviction (see `intersect`).
|
|
238
|
+
*/
|
|
239
|
+
private listKeys;
|
|
240
|
+
/**
|
|
241
|
+
* A read's {@link Looks}, from the invalidation epoch `epoch` its plan began at: due at the first key after a later
|
|
242
|
+
* invalidation, or after `every` keys.
|
|
243
|
+
*/
|
|
244
|
+
private looks;
|
|
245
|
+
/**
|
|
246
|
+
* Whether a look is due before the next key: an invalidation since the read last looked, or its keys counted down. The
|
|
247
|
+
* caller counts the key, as it takes one, before it asks.
|
|
248
|
+
*/
|
|
249
|
+
private lookDue;
|
|
250
|
+
/**
|
|
251
|
+
* The read looks now, before the key it takes next: the next look is due `every` keys on, or at the next
|
|
252
|
+
* invalidation. Called before the look awaits, so an invalidation while it waits is met at the next key.
|
|
253
|
+
*/
|
|
254
|
+
private looked;
|
|
175
255
|
/**
|
|
176
256
|
* The ids at 1-based ranks `n`, `2n`, `3n` … counted over the ids in `range`, ascending. The rank of each boundary
|
|
177
257
|
* is placed from the index's per-chunk counts, so only the chunks that hold a boundary are read, each once however
|
|
@@ -228,7 +308,11 @@ export declare class SegmentEngine {
|
|
|
228
308
|
* of the operand does; and an invalidation, which this store's own `load`, `rollback` and `eraseSubject` make and
|
|
229
309
|
* `invalidate()` makes on request. Still whole/immutable per read, never torn. An operand's stream resolves its
|
|
230
310
|
* segment again before it serves each chunk, so each of those moves a running read as it would a read of one chunk, and
|
|
231
|
-
* the ranges it had requested of the earlier generation are dropped, not served.
|
|
311
|
+
* the ranges it had requested of the earlier generation are dropped, not served. The read then lists every operand's
|
|
312
|
+
* keys again above the key it last took, from the generation current then, so a key an operand gained is read and one
|
|
313
|
+
* it lost is not asked for. It meets a move through a chunk it is served that comes from another generation than it
|
|
314
|
+
* listed its keys under, and by looking at its operands' generations before the first key after an invalidation, and,
|
|
315
|
+
* with an `exclude` or as a union, every `concurrency` keys.
|
|
232
316
|
*/
|
|
233
317
|
intersect(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<number>;
|
|
234
318
|
/**
|
|
@@ -273,12 +357,46 @@ export declare class SegmentEngine {
|
|
|
273
357
|
* read and the batch read share it, so the two cannot disagree about what is read or charged.
|
|
274
358
|
*/
|
|
275
359
|
private combinePlan;
|
|
360
|
+
/**
|
|
361
|
+
* Each operand's chunk-key set inside the read's range and above the key `after`, and the version it was listed under
|
|
362
|
+
* ({@link listKeys}), include operands first, each in its order. At most {@link OPERANDS_AT_ONCE} are listed at once;
|
|
363
|
+
* the first listing to fail rejects the plan at once, and no other is begun after it.
|
|
364
|
+
*/
|
|
365
|
+
private combineOperands;
|
|
366
|
+
/**
|
|
367
|
+
* The operands' streams over `keys` and the window that reads them. Each include is read as a stream of coalesced
|
|
368
|
+
* ranges, and so is each exclude that is read with the includes (one include, or a union). An exclude that waits on
|
|
369
|
+
* an AND of two or more includes is read in batches of the keys whose AND is not empty ({@link ExcludeBatch}), since
|
|
370
|
+
* an emptied AND never needs its chunk. The window starts its first reads at once, so a plan is opened only once its
|
|
371
|
+
* budget has passed.
|
|
372
|
+
*/
|
|
373
|
+
private openCombine;
|
|
374
|
+
/**
|
|
375
|
+
* The next key's combined chunk of `run`, `undefined` once its keys are used up. Not `async`: when nothing has moved
|
|
376
|
+
* and no look is due, the window's own promise is all a key costs.
|
|
377
|
+
*/
|
|
378
|
+
private nextSlot;
|
|
379
|
+
/**
|
|
380
|
+
* {@link nextSlot} once the read has looked at every operand's version, and planned again if one is not the version
|
|
381
|
+
* the plan was made under. An operand the keys left do not read would otherwise never show the read its move: an
|
|
382
|
+
* exclude whose keys the survivors do not reach, or an include of a union past its last key. A chunk that suggested a
|
|
383
|
+
* move is confirmed so, as in {@link nextChunkAfterLook}. The versions are looked up at most {@link OPERANDS_AT_ONCE}
|
|
384
|
+
* at once, all of them: a look that finds no move has compared every one.
|
|
385
|
+
*/
|
|
386
|
+
private nextSlotAfterLook;
|
|
387
|
+
/**
|
|
388
|
+
* Plan the rest of `run` again, after a move: every operand's keys above the key it last took, from the generation
|
|
389
|
+
* current now, aligned, charged to the budget with the reads of the keys already taken, and read through new streams
|
|
390
|
+
* and a new window. What the plan it replaces still has in flight is dropped, not served. The absent-operand refusal
|
|
391
|
+
* and the metric are the first plan's alone: a segment dropped while the read runs reads empty from there on.
|
|
392
|
+
*/
|
|
393
|
+
private replanCombine;
|
|
394
|
+
/** Stop the streams of `plan`; the reads its window holds settle on their own and are dropped. */
|
|
395
|
+
private closeCombine;
|
|
276
396
|
private combine;
|
|
277
397
|
private combineBatches;
|
|
278
398
|
/** The chunk read: the plan every combine shares, then each key's result, cut to the range at its edges. */
|
|
279
399
|
private combineChunks;
|
|
280
|
-
/** An edge chunk's bitmap cut to the window, as a new bitmap: the remainders `edgeIds` would yield. */
|
|
281
|
-
private cutToWindow;
|
|
282
400
|
/**
|
|
283
401
|
* The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
|
|
284
402
|
* Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
|
|
@@ -290,8 +408,64 @@ export declare class SegmentEngine {
|
|
|
290
408
|
*/
|
|
291
409
|
private combineChunk;
|
|
292
410
|
/**
|
|
293
|
-
*
|
|
294
|
-
*
|
|
411
|
+
* Read `exclude` in batches, when the source can read ranges and the exclude has a generation; one that names the same
|
|
412
|
+
* segment and version as an exclude already opened shares its batch. Nothing is requested until a key asks.
|
|
413
|
+
*/
|
|
414
|
+
private openBatch;
|
|
415
|
+
/**
|
|
416
|
+
* The chunk `exclude` holds at `chunkKey`, for a key whose AND is not empty. One the cache holds, or another read has
|
|
417
|
+
* in flight, is read as a read of that chunk alone is (cache lookup and version check, or joining the read); only a
|
|
418
|
+
* chunk that would be requested waits for the batch.
|
|
419
|
+
*/
|
|
420
|
+
private excludeChunk;
|
|
421
|
+
/**
|
|
422
|
+
* Ask `batch` for the exclude chunk at `key`. A batch is read when it holds half the combine's concurrency in keys; when
|
|
423
|
+
* the asking has gone quiet for {@link BATCH_QUIET_HOPS} turns of the microtask queue (checked as each key asks and as
|
|
424
|
+
* the caller asks for each slot) and the caller is waiting on a slot, or has come within half a
|
|
425
|
+
* window of the oldest key held, so a held key is read at least that far ahead of it; and when every key of the combine
|
|
426
|
+
* has been started (none is left that could still ask).
|
|
427
|
+
* Refused once the combine has stopped or planned again, so a key it was still working on sends nothing more.
|
|
428
|
+
*/
|
|
429
|
+
private askBatch;
|
|
430
|
+
/** Start the wait of `batch` for its asking to go quiet, unless it is running. */
|
|
431
|
+
private settleSoon;
|
|
432
|
+
/**
|
|
433
|
+
* Wait until no key has asked for {@link BATCH_QUIET_HOPS} turns of the microtask queue. If the caller is waiting on a
|
|
434
|
+
* slot, or is within half a window of the oldest key held, or every key of the combine has been started (so no more
|
|
435
|
+
* can ask), the keys held are read; otherwise they are held for the keys the combine has yet to start, until half a
|
|
436
|
+
* window of them is held or the caller comes near. The wait is of microtask turns, so it always ends, and the loop is left rather than spun while
|
|
437
|
+
* keys are held.
|
|
438
|
+
*/
|
|
439
|
+
private settleBatch;
|
|
440
|
+
/** Read the keys `batch` holds now. */
|
|
441
|
+
private flushBatch;
|
|
442
|
+
/**
|
|
443
|
+
* Read the keys that asked: one whose chunk is already being read (by another read, or by an earlier key of this batch
|
|
444
|
+
* that names it again) joins that read, and the rest are one stream of coalesced ranges over those keys alone, so a
|
|
445
|
+
* request is sent for each range of them and never for a chunk nobody asked for. The stream is a
|
|
446
|
+
* {@link StreamedChunks} like an include's, so a chunk is cached under the version it was read from, counted as a cache
|
|
447
|
+
* hit or miss once, checked against the segment's version when it comes from the cache, and marks `moves` when it
|
|
448
|
+
* shows the segment moved. Each key it reads is registered as the open read of its chunk, so a read of that chunk by
|
|
449
|
+
* anything else joins it. Every ask is answered, with its own key's chunk or the error its read raised.
|
|
450
|
+
*/
|
|
451
|
+
private readBatch;
|
|
452
|
+
/**
|
|
453
|
+
* Register the read of the chunk `ref` that a batch's stream makes as its open read, so a read of it by anything else
|
|
454
|
+
* joins it, until it settles. A joiner does not share the stream's failure: a stream ends at its first failed range
|
|
455
|
+
* and fails every take after it, those of keys whose own ranges did not fail included, so when the read fails the
|
|
456
|
+
* entry goes and a joiner reads the chunk on its own, one request for each joined key that failed.
|
|
457
|
+
*/
|
|
458
|
+
private registerRead;
|
|
459
|
+
/**
|
|
460
|
+
* Stop `batch`: the keys still waiting are refused. The streams of the batches in flight are not closed: every range of
|
|
461
|
+
* one was requested when it was read, a read of one of its chunks by another call may be joined to it, and each closes
|
|
462
|
+
* itself once its last key is answered.
|
|
463
|
+
*/
|
|
464
|
+
private closeBatch;
|
|
465
|
+
/**
|
|
466
|
+
* One operand's chunk at `key`: from its stream when it has one, else a read of that chunk on its own, whose cached
|
|
467
|
+
* chunk marks `moves` when it is no longer the operand's version. Not `async`, so a stream's take is made before
|
|
468
|
+
* anything is awaited: concurrent takes of one stream must be made in key order.
|
|
295
469
|
*/
|
|
296
470
|
private operandChunk;
|
|
297
471
|
/**
|
|
@@ -305,6 +479,10 @@ export declare class SegmentEngine {
|
|
|
305
479
|
* not hold. Each later key is counted as a cache hit or miss here, so a key is counted once whichever way it is served.
|
|
306
480
|
*/
|
|
307
481
|
private startStream;
|
|
482
|
+
/**
|
|
483
|
+
* Stop the stream of `streamed`. Nothing else is kept for an open stream, so a read whose caller drops it without
|
|
484
|
+
* closing it leaves nothing behind in the engine.
|
|
485
|
+
*/
|
|
308
486
|
private closeStreamed;
|
|
309
487
|
/**
|
|
310
488
|
* The decoded chunk at `chunkKey` of a streamed read: from the cache while the stream is unopened and the cache holds
|
|
@@ -324,9 +502,10 @@ export declare class SegmentEngine {
|
|
|
324
502
|
* stream does for the chunks it delivers; a chunk served from the cache is checked here, or a read whose chunks are
|
|
325
503
|
* all cached would go on serving the generation it planned under, an erased id included, for as long as it is pulled.
|
|
326
504
|
* Within `cache.genTtlMs` the source answers from its snapshot, so the check is a lookup, not a request. `report`:
|
|
327
|
-
* emit the `cache` event for this lookup, which a key counted when its stream opened does not. `
|
|
328
|
-
* this chunk belongs to
|
|
329
|
-
*
|
|
505
|
+
* emit the `cache` event for this lookup, which a key counted when its stream opened does not. `moves`: where the read
|
|
506
|
+
* this chunk belongs to learns of a move, so it plans its keys again. `streamed`: the read's stream, which a move
|
|
507
|
+
* carries to the version now current, so the rest of it looks there: a read whose stream is not yet open reads the
|
|
508
|
+
* rest as one stream, not a chunk at a time.
|
|
330
509
|
*/
|
|
331
510
|
private cachedIfCurrent;
|
|
332
511
|
/** The cache key of a chunk: by the version it was read under, or by segment and key alone for a source with none. */
|
|
@@ -351,11 +530,20 @@ export declare class SegmentEngine {
|
|
|
351
530
|
* chunks — a segment with data is self-evidently registered — so the check is a registry read exactly when
|
|
352
531
|
* a combine was about to do something suspicious, and no reads at all otherwise. A source that cannot answer
|
|
353
532
|
* `exists` (it has no notion of registration) skips the check rather than guessing. An operand is judged by
|
|
354
|
-
* all its chunks, not those inside a range: one with nothing in range but data elsewhere is not suspicious.
|
|
533
|
+
* all its chunks, not those inside a range: one with nothing in range but data elsewhere is not suspicious. At most
|
|
534
|
+
* {@link OPERANDS_AT_ONCE} are asked after at once.
|
|
355
535
|
*/
|
|
356
536
|
private refuseAbsentOperands;
|
|
357
537
|
/**
|
|
358
|
-
*
|
|
538
|
+
* Forget what was derived from `ref`: drop its decoded chunks from the chunk cache, which every engine sharing this
|
|
539
|
+
* one's invalidations reads through, record the invalidation where they all see it, and tell this engine's Storage
|
|
540
|
+
* source to forget the segment too. An engine sharing the invalidations over another source instance does not tell
|
|
541
|
+
* its own source: engines that read one source, as the facade's do, need only one of them told. A read of the
|
|
542
|
+
* segment already running on any of them caches nothing more of what it began reading before the call, whether a
|
|
543
|
+
* request already in flight or a stream already open; a stream that opens after the call, or a request sent after
|
|
544
|
+
* it, reads afresh and, on a source that names versions (`currentVersion`), caches what it reads. On one that does
|
|
545
|
+
* not, a read that began before the call caches nothing from a stream it opens after it, unless the call landed while
|
|
546
|
+
* the read listed the segment's keys: the read then lists them again, and caches as a read begun after the call.
|
|
359
547
|
*
|
|
360
548
|
* The decoded-chunk cache is keyed by generation, which handles a *publish* (the new generation misses) but
|
|
361
549
|
* not a *destruction*: a segment that was erased from, dropped, shredded or retired has no newer generation
|
|
@@ -397,8 +585,9 @@ export declare class SegmentEngine {
|
|
|
397
585
|
* one): callers read it or clone it, never mutate it.
|
|
398
586
|
*
|
|
399
587
|
* `recheck`: `gen` was planned for a read of many chunks, so a cached chunk is served only while it is still the
|
|
400
|
-
* segment's version ({@link cachedIfCurrent})
|
|
401
|
-
* `report`: emit the `cache` event for this lookup; a key already counted for this
|
|
588
|
+
* segment's version ({@link cachedIfCurrent}), and one that is not marks `moves`. A point read resolves `gen` just
|
|
589
|
+
* before it asks, and passes none. `report`: emit the `cache` event for this lookup; a key already counted for this
|
|
590
|
+
* read does not.
|
|
402
591
|
*/
|
|
403
592
|
private storageChunk;
|
|
404
593
|
/**
|
package/dist/core/erase-id.d.ts
CHANGED
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
* where the truth was to re-run.
|
|
49
49
|
*
|
|
50
50
|
* Which reason comes from **re-reading the row**, not from assuming a supersession: a moved pointer is
|
|
51
|
-
* `'superseded'`, a tombstoned row `'destroyed'`, a purged row `'absent'`, a row
|
|
52
|
-
* `'no-
|
|
51
|
+
* `'superseded'`, a tombstoned row `'destroyed'`, a purged row `'absent'`, a row that lost its pointer
|
|
52
|
+
* `'superseded'` too (its bucket was not searched, so no claim about it is made; a re-run searches it). A pointer still on `from` means the object it names is genuinely gone — the forbidden
|
|
53
53
|
* `missing-storage-generation` state — or is another object than the row's summary names by its fingerprint, and that
|
|
54
54
|
* throws, because no re-run fixes either. That `NotFoundError` is the only signal of those states, so a faulting
|
|
55
55
|
* re-read rethrows it rather than replacing it with a transient-looking registry error.
|
|
@@ -59,8 +59,13 @@
|
|
|
59
59
|
* below the winner's pointer, where the winner's collection takes it, or above it: the case where this call took
|
|
60
60
|
* its number after the winner's object was already in the bucket. Up there nothing collects it, and it was derived
|
|
61
61
|
* from a generation the winner has replaced, so when the winner was another erasure it still holds the id that
|
|
62
|
-
* erasure has just reported gone. That object is deleted before this call returns, once its footer
|
|
63
|
-
* rewrite's
|
|
62
|
+
* erasure has just reported gone. That object is deleted before this call returns, once its footer matches the
|
|
63
|
+
* rewrite's and a renewal of the row has refused every publish fenced on a row read before it, under the version the
|
|
64
|
+
* footer read reported, unless {@link DISCARD_ATTEMPTS} renewals lose to other writes (a load, a rollback, a
|
|
65
|
+
* retention write): then it is kept, and can hold the winner's id, until a later call takes it: any erasure that
|
|
66
|
+
* rewrites the segment (this one's re-run among them, while its id is still in the current generation) numbers above it
|
|
67
|
+
* and its `keep: 0` collection takes it, an erasure of the winner's id deletes it as a holder, and once loads number
|
|
68
|
+
* past it their collection takes it as it takes any orphan — see `discardRefused`.
|
|
64
69
|
*
|
|
65
70
|
* Without that fence the failure was silent and severe, and both halves were reproduced: `nextGeneration` picks a
|
|
66
71
|
* number above everything in the bucket, so a forward-only publish here always won — discarding a concurrent
|
|
@@ -74,7 +79,14 @@
|
|
|
74
79
|
* A load already in flight writes its object above the pointer before it publishes, or, as a first load onto a row with
|
|
75
80
|
* no pointer, before there is a pointer at all. An erasure that finds the id in that object renews the row's `pointerId`
|
|
76
81
|
* 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 `
|
|
82
|
+
* never names a missing object (see `fence`). An erasure that finds no holder writes nothing.
|
|
83
|
+
*
|
|
84
|
+
* Before it deletes anything, a call that found a holder takes every generation it will delete out of the row's ledger
|
|
85
|
+
* of known generations, in one write made after its search (the same write as the renewal where there is one). A
|
|
86
|
+
* generation the ledger names is one a collection by a count spares, so the delete comes after the name goes: a
|
|
87
|
+
* rollback that raced the write loses its swap, and a collection that reads the row before a delete spares what the row
|
|
88
|
+
* names. That write is made only when the ledger names something to delete, and it is the
|
|
89
|
+
* one case in which an erasure of an id held only by an older generation writes the row.
|
|
78
90
|
*
|
|
79
91
|
* 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
92
|
* first load's on a row with no pointer, whose key that load has not published; on a segment with a key, a first
|
|
@@ -90,10 +102,24 @@
|
|
|
90
102
|
import { type IAuditSink } from './audit.js';
|
|
91
103
|
import type { CodecInterface } from './codec.js';
|
|
92
104
|
import type { Yielder } from './cooperative.js';
|
|
93
|
-
import type { Rng } from './determinism.js';
|
|
105
|
+
import type { Entropy, Rng } from './determinism.js';
|
|
94
106
|
import type { IKeystore } from './crypto.js';
|
|
95
107
|
import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
|
|
96
108
|
import { type ReadRetry } from './retry.js';
|
|
109
|
+
/**
|
|
110
|
+
* Renewals a refused rewrite's discard makes before it keeps its object: the first, and one more each time a write that
|
|
111
|
+
* is neither a write of leases alone nor another erasure's renewal of the row lands between its read of the row and its
|
|
112
|
+
* renewal. Those two are waited out, up to the limit on lease waits (`LEASE_ONLY_RETRIES`), and cost none, so the
|
|
113
|
+
* losers of one race of erasures all delete their rewrites; a write that gets no answer is sent again within the same
|
|
114
|
+
* renewal.
|
|
115
|
+
*/
|
|
116
|
+
export declare const DISCARD_ATTEMPTS = 8;
|
|
117
|
+
/**
|
|
118
|
+
* Writes a call's fence makes before it gives up on a row that keeps changing under it: each one lost to another writer
|
|
119
|
+
* (a load, a rollback, another erasure) that is not a write of the row's leases alone costs one. The call then reports
|
|
120
|
+
* `'superseded'` with nothing deleted.
|
|
121
|
+
*/
|
|
122
|
+
export declare const FENCE_ATTEMPTS = 5;
|
|
97
123
|
/** What {@link eraseIdFromSegment} needs: the objects, the pointer, the codec, and the key material if encrypted. */
|
|
98
124
|
export interface EraseIdDeps {
|
|
99
125
|
readonly storage: IStorageDriver;
|
|
@@ -122,6 +148,13 @@ export interface EraseIdDeps {
|
|
|
122
148
|
* renewal. Absent, the read retry's source is used if it has one, and otherwise each wait is its bound.
|
|
123
149
|
*/
|
|
124
150
|
readonly rng?: Rng;
|
|
151
|
+
/**
|
|
152
|
+
* The source of the rewrite's identity: 12 random bytes its footer carries when the segment is cleartext (an encrypted
|
|
153
|
+
* rewrite carries its index's AEAD nonce), so the rewrite is not the same object as a load of the same ids at its
|
|
154
|
+
* number. Required: an erasure without one (a JavaScript caller can leave it out) is refused with
|
|
155
|
+
* {@link ValidationError} before any read, keystore call, delete or write, even when it would find nothing to rewrite.
|
|
156
|
+
*/
|
|
157
|
+
readonly entropy: Entropy;
|
|
125
158
|
/** 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. */
|
|
126
159
|
readonly maxBitmapBytes?: number;
|
|
127
160
|
/**
|
|
@@ -144,8 +177,9 @@ export interface EraseIdResult {
|
|
|
144
177
|
readonly erased: boolean;
|
|
145
178
|
/**
|
|
146
179
|
* Why the id was not erased, when `erased` is false. `'absent'` (no registry row), `'destroyed'` (a crypto-shred
|
|
147
|
-
* tombstone — already unreadable), `'no-generation'` (a row with no generation yet
|
|
148
|
-
* objects, none of them found to hold the id, and `collected` names any this
|
|
180
|
+
* tombstone — already unreadable), `'no-generation'` (a row with no generation yet when the call read it, its first
|
|
181
|
+
* read: its bucket can hold first loads' objects, none of them found to hold the id, and `collected` names any this
|
|
182
|
+
* call deleted unsearched, see below),
|
|
149
183
|
* `'not-member'` (no
|
|
150
184
|
* generation in the bucket holds the id — the common case across a fleet scan — with the same note on `collected`),
|
|
151
185
|
* or `'superseded'`.
|
|
@@ -162,10 +196,12 @@ export interface EraseIdResult {
|
|
|
162
196
|
* another erasure of the *same* id, the re-run reports `'not-member'` because it is already gone. Either way the
|
|
163
197
|
* re-run settles it, which is why it is the documented action for this reason and for no other.
|
|
164
198
|
*
|
|
165
|
-
*
|
|
199
|
+
* `'absent'` and `'destroyed'` are also returned when the row changes *underneath* a call already in flight, not
|
|
166
200
|
* only when it is read up front — a concurrent `dropSegment` gives `'destroyed'`, once the tombstone's objects have
|
|
167
|
-
* been searched as a fresh call searches them, and a retention sweep that purges the row gives `'absent'`.
|
|
168
|
-
*
|
|
201
|
+
* been searched as a fresh call searches them, and a retention sweep that purges the row gives `'absent'`.
|
|
202
|
+
* `'no-generation'` is only the answer to a row read with no generation: a row that loses its pointer *after* the
|
|
203
|
+
* call found one (a purge and re-creation, or a hand-made write) is `'superseded'`, because the call found a holder
|
|
204
|
+
* in that bucket and stopped before deleting it, and a re-run searches it again.
|
|
169
205
|
*/
|
|
170
206
|
readonly reason?: 'absent' | 'destroyed' | 'no-generation' | 'not-member' | 'superseded';
|
|
171
207
|
/**
|
|
@@ -176,7 +212,7 @@ export interface EraseIdResult {
|
|
|
176
212
|
/**
|
|
177
213
|
* The generation written without the id (present whenever one was written, even if `superseded`). A refused
|
|
178
214
|
* rewrite whose object sits above the winner's pointer deletes that object again before it returns, when its footer
|
|
179
|
-
*
|
|
215
|
+
* matches the rewrite's and a renewal of the row refuses every publish fenced on a row read before it.
|
|
180
216
|
*/
|
|
181
217
|
readonly generation?: number;
|
|
182
218
|
/**
|
|
@@ -202,8 +238,16 @@ export interface EraseIdResult {
|
|
|
202
238
|
* and nothing was published at all; and a generation still holding the id when the bucket is listed at the end
|
|
203
239
|
* — one an operator rolled the pointer onto while a rewrite was collecting, say, or, on a row with no pointer, the
|
|
204
240
|
* 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,
|
|
206
|
-
* that reading the row can settle, throws the registry's
|
|
241
|
+
* bucket. The row write an erasure makes before deleting a holder a load may still publish, or before a refused
|
|
242
|
+
* rewrite discards its own object, when it gets no answer that reading the row can settle, throws the registry's
|
|
243
|
+
* `TransientError`, and that object is not deleted.
|
|
244
|
+
*
|
|
245
|
+
* A rollback that lands between the call's read of the row and one of its deletes, onto the generation being deleted,
|
|
246
|
+
* leaves the pointer on an object that is gone. The call finds it at its next read of the row, or by reading the row
|
|
247
|
+
* once more after its last delete, and **throws `NotFoundError`** rather than report anything: every read of the
|
|
248
|
+
* segment fails until a rollback moves the pointer to a generation still in the bucket, and nothing restores the
|
|
249
|
+
* deleted object. When nothing left in the bucket holds the id, the call has emitted its `segment.collect` event
|
|
250
|
+
* first, so the deletion is on the record. Re-run it after that rollback.
|
|
207
251
|
*
|
|
208
252
|
* **Re-run it**, and read what the re-run says rather than assuming it finished the job. The re-run looks for
|
|
209
253
|
* the id in every generation in the bucket, not only the current one, and there are three outcomes:
|
|
@@ -227,7 +271,8 @@ export interface EraseIdResult {
|
|
|
227
271
|
* Emits one `segment.rewrite` audit event at the publish (before the superseded generation is collected), so the
|
|
228
272
|
* compliance record exists the moment the generation without the id is authoritative. An erasure that rewrites nothing,
|
|
229
273
|
* 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.
|
|
274
|
+
* that deleted only objects no read of the segment can open, which names no generation the id was found in. Either
|
|
275
|
+
* event comes before the `NotFoundError` of a rollback that moved the pointer onto a generation the call deleted.
|
|
231
276
|
*/
|
|
232
277
|
export declare function eraseIdFromSegment(ref: SegmentRef, id: number, deps: EraseIdDeps, options?: {
|
|
233
278
|
audit?: IAuditSink;
|
package/dist/core/erasure.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* operation — `eraseIdFromSegment` rewrites the generation without it.
|
|
14
14
|
*/
|
|
15
15
|
import { type IAuditSink } from './audit.js';
|
|
16
|
-
import {
|
|
16
|
+
import type { ChurnDeps } from './leases.js';
|
|
17
17
|
import type { IStorageDriver, IRegistryDriver, RegistryRecord, SegmentRef } from './ports.js';
|
|
18
18
|
export interface EraseDeps extends ChurnDeps {
|
|
19
19
|
readonly registry: IRegistryDriver;
|
|
@@ -57,10 +57,10 @@ export declare function destroySegment(ref: SegmentRef, deps: EraseDeps, options
|
|
|
57
57
|
* Returns a per-segment result (skips cleartext segments unless `allowCleartext`).
|
|
58
58
|
*
|
|
59
59
|
* **Bounded.** It lists the whole namespace before it destroys anything, and holds the listing resident, so the
|
|
60
|
-
* listing is capped at `maxScanSegments` (default
|
|
60
|
+
* listing is capped at `maxScanSegments` (default 250,000), the ceiling every
|
|
61
61
|
* other fleet scan keeps. A namespace over it throws `BudgetExceededError` **before any segment is destroyed**,
|
|
62
62
|
* never part-way through; raise `maxScanSegments` when the namespace really is that large and the memory is there.
|
|
63
|
-
* A `maxScanSegments` that is not a
|
|
63
|
+
* A `maxScanSegments` that is not a safe whole number of at least 1 throws `ValidationError`, again before anything is destroyed.
|
|
64
64
|
*/
|
|
65
65
|
export declare function eraseNamespace(namespace: string, deps: EraseDeps, options: {
|
|
66
66
|
confirmNamespace: string;
|
|
@@ -195,7 +195,7 @@ export declare function dropSegment(ref: SegmentRef, deps: DropDeps, options: {
|
|
|
195
195
|
* in the shred's own write, so only a tombstone a sweep wrote ever carries a sweep's mark, which is what lets a later
|
|
196
196
|
* sweep purge it and never a crypto-shred's.
|
|
197
197
|
*/
|
|
198
|
-
|
|
198
|
+
interface SweepDrop {
|
|
199
199
|
readonly wanted: (row: RegistryRecord) => boolean;
|
|
200
200
|
readonly stamp: (retention: RegistryRecord['retention']) => RegistryRecord['retention'];
|
|
201
201
|
}
|
|
@@ -208,3 +208,4 @@ export declare function dropSegmentFor(ref: SegmentRef, deps: DropDeps, options:
|
|
|
208
208
|
dryRun?: boolean;
|
|
209
209
|
audit?: IAuditSink;
|
|
210
210
|
}, sweep: SweepDrop | undefined): Promise<DropResult | null>;
|
|
211
|
+
export {};
|