@cloudbitmaps/core 0.20.0 → 0.21.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 (75) hide show
  1. package/README.md +85 -0
  2. package/dist/{chunk-YSC7NJWV.js → chunk-IGLIZBR7.js} +740 -137
  3. package/dist/chunk-IGLIZBR7.js.map +7 -0
  4. package/dist/core/absent-operands.d.ts +8 -0
  5. package/dist/core/audit.d.ts +12 -2
  6. package/dist/core/bit-route.d.ts +2 -1
  7. package/dist/core/blob.d.ts +12 -1
  8. package/dist/core/bulk-load.d.ts +148 -0
  9. package/dist/core/cas-retry.d.ts +27 -0
  10. package/dist/core/check.d.ts +22 -0
  11. package/dist/core/chunk-checks.d.ts +6 -1
  12. package/dist/core/chunk-window.d.ts +11 -4
  13. package/dist/core/combine-expr.d.ts +1 -3
  14. package/dist/core/combine-feed.d.ts +2 -16
  15. package/dist/core/combine-many.d.ts +12 -9
  16. package/dist/core/concurrency.d.ts +8 -0
  17. package/dist/core/consistency.d.ts +69 -17
  18. package/dist/core/crbm/fingerprint.d.ts +5 -0
  19. package/dist/core/crbm/format.d.ts +0 -1
  20. package/dist/core/crbm/plan-reads.d.ts +9 -3
  21. package/dist/core/crbm/reader.d.ts +52 -6
  22. package/dist/core/crbm/varint.d.ts +2 -1
  23. package/dist/core/crbm/writer.d.ts +5 -1
  24. package/dist/core/crbm-open.d.ts +154 -0
  25. package/dist/core/crbm-storage-source.d.ts +65 -452
  26. package/dist/core/crbm-write.d.ts +75 -0
  27. package/dist/core/crypto.d.ts +6 -1
  28. package/dist/core/engine.d.ts +222 -33
  29. package/dist/core/erase-id.d.ts +42 -7
  30. package/dist/core/erasure.d.ts +4 -3
  31. package/dist/core/errors.d.ts +8 -2
  32. package/dist/core/generation-gc.d.ts +129 -15
  33. package/dist/core/generation-list.d.ts +11 -0
  34. package/dist/core/id-window.d.ts +46 -0
  35. package/dist/core/invalidation-scope.d.ts +77 -0
  36. package/dist/core/known-generations-shape.d.ts +13 -0
  37. package/dist/core/known-generations.d.ts +147 -0
  38. package/dist/core/leases.d.ts +4 -3
  39. package/dist/core/ledger-seed.d.ts +17 -0
  40. package/dist/core/load.d.ts +17 -38
  41. package/dist/core/lru.d.ts +2 -15
  42. package/dist/core/metrics.d.ts +4 -2
  43. package/dist/core/order.d.ts +2 -0
  44. package/dist/core/pinned-storage-source.d.ts +4 -4
  45. package/dist/core/ports.d.ts +89 -16
  46. package/dist/core/publish.d.ts +207 -0
  47. package/dist/core/registry-scan.d.ts +0 -2
  48. package/dist/core/retention-sweep.d.ts +69 -65
  49. package/dist/core/retention.d.ts +1 -1
  50. package/dist/core/retry.d.ts +12 -19
  51. package/dist/core/rollback.d.ts +60 -31
  52. package/dist/core/summary.d.ts +50 -18
  53. package/dist/core/token.d.ts +2 -2
  54. package/dist/core/validate.d.ts +2 -0
  55. package/dist/driver-kit.d.ts +4 -0
  56. package/dist/driver-kit.js +80 -7
  57. package/dist/driver-kit.js.map +3 -3
  58. package/dist/drivers/_shared/bytes.d.ts +6 -0
  59. package/dist/drivers/_shared/faults.d.ts +15 -0
  60. package/dist/drivers/_shared/object-keys.d.ts +18 -0
  61. package/dist/drivers/_shared/object-registry-keys.d.ts +0 -2
  62. package/dist/drivers/_shared/object-registry.d.ts +2 -0
  63. package/dist/drivers/_shared/options.d.ts +19 -0
  64. package/dist/drivers/_shared/registry.d.ts +13 -10
  65. package/dist/drivers/localfs/exact-case.d.ts +0 -3
  66. package/dist/drivers/localfs/fs-util.d.ts +22 -0
  67. package/dist/drivers/localfs/registry.d.ts +10 -4
  68. package/dist/drivers/localfs/storage.d.ts +9 -2
  69. package/dist/drivers/memory.d.ts +7 -4
  70. package/dist/index.d.ts +1 -1
  71. package/dist/index.js +3647 -2410
  72. package/dist/index.js.map +4 -4
  73. package/package.json +1 -1
  74. package/dist/chunk-YSC7NJWV.js.map +0 -7
  75. package/dist/core/kept-generations.d.ts +0 -40
@@ -0,0 +1,75 @@
1
+ import type { Yielder } from './cooperative.js';
2
+ import type { CrbmCrypto } from './crypto.js';
3
+ import type { GenerationMetadata, GenKey, IStorageDriver } from './ports.js';
4
+ import type { CodecBitmap, EncodedChunk } from './codec.js';
5
+ /**
6
+ * Write one immutable generation from in-memory bitmaps. Chunks are
7
+ * sorted ascending and empty bitmaps skipped (empty chunks are never stored). Returns the driver's
8
+ * `{ size, sha256 }` for the written object. Pass `options.crypto` to AES-256-GCM-encrypt the generation
9
+ * (built from the segment's DEK, with associated data bound to `(segment, generation)`).
10
+ */
11
+ export declare function writeCrbmGeneration(driver: IStorageDriver, key: GenKey, chunks: Iterable<{
12
+ chunkKey: number;
13
+ bitmap: CodecBitmap;
14
+ }>, options?: {
15
+ crypto?: CrbmCrypto;
16
+ clock?: Yielder;
17
+ metadata?: GenerationMetadata;
18
+ }): Promise<{
19
+ size: number;
20
+ sha256: string;
21
+ fingerprint: string;
22
+ tailBytes: number;
23
+ }>;
24
+ /** Each non-empty bitmap as the chunk it stores, in the order given. */
25
+ export declare function encodeEach(chunks: Iterable<{
26
+ chunkKey: number;
27
+ bitmap: CodecBitmap;
28
+ }>): Generator<EncodedChunk>;
29
+ /**
30
+ * Write one immutable generation from chunks already encoded, which must ascend by key (the writer refuses one
31
+ * that does not). Returns the driver's `{ size, sha256 }` and a tally of what was written.
32
+ */
33
+ export declare function writeEncodedChunks(driver: IStorageDriver, key: GenKey, chunks: Iterable<EncodedChunk>, options: {
34
+ crypto?: CrbmCrypto;
35
+ clock?: Yielder;
36
+ metadata?: GenerationMetadata;
37
+ }): Promise<{
38
+ size: number;
39
+ sha256: string;
40
+ chunkCount: number;
41
+ cardinality: number;
42
+ fingerprint: string;
43
+ /** The bytes after the chunk payloads ({@link CrbmReader.tailBytes}). */
44
+ tailBytes: number;
45
+ }>;
46
+ /** What {@link writeCrbmGenerationStream} wrote: the driver's `{ size, sha256 }` + a tally of the generation. */
47
+ interface StreamWriteResult {
48
+ readonly size: number;
49
+ readonly sha256: string;
50
+ /** Non-empty chunk keys written, in ascending order (for the post-write verify, since the stream is consumed). */
51
+ readonly chunkKeys: number[];
52
+ /** Total ids written. */
53
+ readonly cardinality: number;
54
+ /** The fingerprint of what this call wrote ({@link CrbmReader.fingerprint}), which tells it from another object under its key. */
55
+ readonly fingerprint: string;
56
+ /** The bytes after the chunk payloads ({@link CrbmReader.tailBytes}). */
57
+ readonly tailBytes: number;
58
+ }
59
+ /**
60
+ * Streaming variant of {@link writeCrbmGeneration} for a **constant-memory** rewrite (the erasure rewrite, a
61
+ * sorted load): consumes an **already-ascending** async stream of `{ chunkKey, bitmap }`, feeding each to the
62
+ * codec and freeing it, instead of materializing the whole generation. Paired with a streaming storage sink (S3
63
+ * multipart / LocalFs temp file), the writer itself holds ~one chunk + one part. Returns a tally so the caller can verify
64
+ * the re-opened object without re-iterating the (now-consumed) stream. Input **must** be ascending by `chunkKey`
65
+ * — the codec rejects an out-of-order chunk; empty bitmaps are skipped.
66
+ */
67
+ export declare function writeCrbmGenerationStream(driver: IStorageDriver, key: GenKey, chunks: AsyncIterable<{
68
+ chunkKey: number;
69
+ bitmap: CodecBitmap;
70
+ }>, options?: {
71
+ crypto?: CrbmCrypto;
72
+ clock?: Yielder;
73
+ metadata?: GenerationMetadata;
74
+ }): Promise<StreamWriteResult>;
75
+ export {};
@@ -56,7 +56,10 @@ export interface IKeystore {
56
56
  /**
57
57
  * Reconstruct the {@link Aead} from a segment's stored wrappings by unwrapping with any KEK currently held.
58
58
  * Throws {@link KeyUnavailableError} if it holds none of the referenced KEKs (lost key, or a shredded
59
- * segment whose wrappings were deleted).
59
+ * segment whose wrappings were deleted). Throw {@link TransientError} for a fault worth riding out (a throttle, a 5xx,
60
+ * a dropped connection, a request timeout): a rollback below a segment's pointer goes ahead without the key on that
61
+ * error, as on `KeyUnavailableError`, an `IntegrityError` or an `Error` named `TimeoutError`, and is stopped by any
62
+ * other error the keystore throws.
60
63
  */
61
64
  openDek(wrapped: readonly WrappedDek[]): Promise<Aead>;
62
65
  }
@@ -82,3 +85,5 @@ export declare function aadFor(ref: {
82
85
  readonly namespace?: string;
83
86
  readonly segment: string;
84
87
  }, generation: number, scope: number | 'index' | 'metadata' | 'summary'): Uint8Array;
88
+ /** Whether two rows carry the same wrapped keys. */
89
+ export declare const sameKeys: (a: readonly WrappedDek[] | undefined, b: readonly WrappedDek[]) => boolean;
@@ -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
- /** Optional cache of decoded (immutable) Storage chunks. */
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 once, before the first fetch, for every chunk in the range, so with
48
- * `after` alone it is charged to the end of the segment however early the caller stops; pass `through` to bound it.
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 as before: `concurrency` chunk keys at once, opening 8 wide. A running
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 chunk reads open right now, by cache key: a caller that misses the cache and finds its key here awaits that
98
- * read instead of making its own. An entry lives from the request until it settles, so the map holds at most one
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 openStreams;
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
- /** The chunks of `seg` at `chunkKeys`, read through a window of {@link DEFAULT_INTERSECT_CONCURRENCY}. */
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 the
163
- * loop it always had.
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 and the window are the per-id read's. Each array is at most 65,536 ids (256 KiB) and is
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
- * One operand's chunk at `key`: from its stream when it has one, else a read of that chunk on its own. Not `async`,
294
- * so a stream's take is made before anything is awaited: concurrent takes of one stream must be made in key order.
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. `streamed`: the read
328
- * this chunk belongs to, which a move carries to the version now current, so the rest of it looks there: a read whose
329
- * stream is not yet open reads the rest as one stream, not a chunk at a time.
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
- * Drop every piece of state this engine derived from `ref`, and tell the Storage source to do the same.
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}). A point read resolves `gen` just before it asks, and passes none.
401
- * `report`: emit the `cache` event for this lookup; a key already counted for this read does not.
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
  /**
@@ -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 proves it the
63
- * rewrite's own, under the version that read reported — see `discardRefused`.
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 `fenceInFlight`). An erasure that finds no holder writes nothing.
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
@@ -94,6 +106,20 @@ import type { 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;
@@ -176,7 +202,7 @@ export interface EraseIdResult {
176
202
  /**
177
203
  * The generation written without the id (present whenever one was written, even if `superseded`). A refused
178
204
  * 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.
205
+ * matches the rewrite's and a renewal of the row refuses every publish fenced on a row read before it.
180
206
  */
181
207
  readonly generation?: number;
182
208
  /**
@@ -202,8 +228,16 @@ export interface EraseIdResult {
202
228
  * and nothing was published at all; and a generation still holding the id when the bucket is listed at the end
203
229
  * — one an operator rolled the pointer onto while a rewrite was collecting, say, or, on a row with no pointer, the
204
230
  * 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.
231
+ * bucket. The row write an erasure makes before deleting a holder a load may still publish, or before a refused
232
+ * rewrite discards its own object, when it gets no answer that reading the row can settle, throws the registry's
233
+ * `TransientError`, and that object is not deleted.
234
+ *
235
+ * A rollback that lands between the call's read of the row and one of its deletes, onto the generation being deleted,
236
+ * 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
237
+ * once more after its last delete, and **throws `NotFoundError`** rather than report anything: every read of the
238
+ * segment fails until a rollback moves the pointer to a generation still in the bucket, and nothing restores the
239
+ * deleted object. When nothing left in the bucket holds the id, the call has emitted its `segment.collect` event
240
+ * first, so the deletion is on the record. Re-run it after that rollback.
207
241
  *
208
242
  * **Re-run it**, and read what the re-run says rather than assuming it finished the job. The re-run looks for
209
243
  * the id in every generation in the bucket, not only the current one, and there are three outcomes:
@@ -227,7 +261,8 @@ export interface EraseIdResult {
227
261
  * Emits one `segment.rewrite` audit event at the publish (before the superseded generation is collected), so the
228
262
  * compliance record exists the moment the generation without the id is authoritative. An erasure that rewrites nothing,
229
263
  * 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.
264
+ * that deleted only objects no read of the segment can open, which names no generation the id was found in. Either
265
+ * event comes before the `NotFoundError` of a rollback that moved the pointer onto a generation the call deleted.
231
266
  */
232
267
  export declare function eraseIdFromSegment(ref: SegmentRef, id: number, deps: EraseIdDeps, options?: {
233
268
  audit?: IAuditSink;
@@ -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 { type ChurnDeps } from './leases.js';
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;
@@ -60,7 +60,7 @@ export declare function destroySegment(ref: SegmentRef, deps: EraseDeps, options
60
60
  * listing is capped at `maxScanSegments` (default {@link DEFAULT_MAX_SCAN_SEGMENTS}, 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 finite number >= 1 throws `ValidationError`, again before anything is destroyed.
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
- export interface SweepDrop {
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 {};
@@ -26,7 +26,11 @@ export declare class CloudRoaringError extends Error {
26
26
  readonly [ERROR_BRAND]: true;
27
27
  constructor(message: string);
28
28
  }
29
- /** Invalid caller input (bad id, segment name, options). Raised before any storage call. */
29
+ /**
30
+ * Invalid input, which the same call fails on again. Most is refused before any storage call (a bad id, segment name
31
+ * or option); some is found by the call itself: a read past the end of an object, or a bucket or container a cloud
32
+ * driver finds does not exist.
33
+ */
30
34
  export declare class ValidationError extends CloudRoaringError {
31
35
  }
32
36
  /** An OCC conditional write/delete lost the race — the row changed since it was read. */
@@ -148,7 +152,9 @@ export declare function isLeaseLimitError(err: unknown): err is LeaseLimitError;
148
152
  export declare function isNotFoundError(err: unknown): err is NotFoundError;
149
153
  /** Corrupt/oversized/failed-checksum bytes from a tier. */
150
154
  export declare function isIntegrityError(err: unknown): err is IntegrityError;
151
- /** Invalid caller input. */
155
+ /**
156
+ * Invalid caller input; also the error a cloud driver raises for a bucket or container it finds does not exist.
157
+ */
152
158
  export declare function isValidationError(err: unknown): err is ValidationError;
153
159
  /** This build or this store's wiring cannot do what was asked, though nothing is malformed. */
154
160
  export declare function isUnsupportedError(err: unknown): err is UnsupportedError;