@cloudbitmaps/core 0.14.0 → 0.16.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.
@@ -1,6 +1,6 @@
1
1
  import type { BlobReader } from '../blob.js';
2
2
  import type { CrbmCrypto } from '../crypto.js';
3
- import type { GenerationMetadata } from '../ports.js';
3
+ import type { ChunkRead, GenerationMetadata, ReadChunksOptions } from '../ports.js';
4
4
  /**
5
5
  * A parsed index as parallel typed arrays, one slot per entry, ascending by key. Typed arrays hold each field at
6
6
  * its own width with no per-entry object, so the retained heap is the arrays' byte length and nothing else.
@@ -58,6 +58,11 @@ export declare const fingerprintFor: (size: number, footerCrc: number) => string
58
58
  * checking that its target is what the row says.
59
59
  */
60
60
  export declare function footerSaysEncrypted(blob: BlobReader): Promise<boolean>;
61
+ /**
62
+ * {@link CrbmReader.open} for a reader that keeps a copy of the chunk region when the tail read returned the whole
63
+ * object and that region is at most `keepChunkBytesUpTo` bytes. Not exported from a package entry.
64
+ */
65
+ export declare function openCrbmReaderKeeping(blob: BlobReader, options: CrbmReaderOptions, keepChunkBytesUpTo: number | undefined): Promise<CrbmReader>;
61
66
  export declare class CrbmReader {
62
67
  private readonly blob;
63
68
  private readonly objectSize;
@@ -82,6 +87,14 @@ export declare class CrbmReader {
82
87
  readonly metadata: GenerationMetadata | undefined;
83
88
  /** What that metadata adds to {@link retainedBytes}; 0 when there is none. */
84
89
  private readonly metadataWeight;
90
+ /** Where the chunk payloads end: the extension block's start when there is one, else the index's. */
91
+ private readonly payloadEnd;
92
+ /**
93
+ * A copy of the chunk region, `[PAYLOAD_START, payloadEnd)`, when `open()` kept one (see
94
+ * {@link openCrbmReaderKeeping}); `undefined` otherwise. Never handed out: a read of it is a copy or is
95
+ * consumed before it returns.
96
+ */
97
+ private readonly kept;
85
98
  private constructor();
86
99
  /**
87
100
  * Which object this reader opened, as its size and its footer's CRC. The footer covers the index's CRC, so two
@@ -100,8 +113,9 @@ export declare class CrbmReader {
100
113
  /**
101
114
  * What this reader holds, the weight the storage reader cache bounds on: its parsed index
102
115
  * ({@link retainedIndexBytes}), plus a weight for the metadata it decoded, from its canonical length and its number
103
- * of keys. Equal to {@link retainedIndexBytes} for a generation with no metadata. A cache heuristic, not a contract:
104
- * how the metadata is weighed may change in any release.
116
+ * of keys, plus the chunk bytes kept from the open, when it kept any. Equal to {@link retainedIndexBytes} for a
117
+ * generation with no metadata and nothing kept. A cache heuristic, not a contract: how the metadata is weighed may
118
+ * change in any release.
105
119
  */
106
120
  get retainedBytes(): number;
107
121
  /** Per-chunk cardinality (`chunkKey → count`) from the parsed index — no payload reads. */
@@ -130,7 +144,74 @@ export declare class CrbmReader {
130
144
  * not mutate it.
131
145
  */
132
146
  getChunk(chunkKey: number): Promise<Uint8Array | null>;
147
+ /**
148
+ * `[offset, offset + length)` of the object from the kept chunk region, or `undefined` when it is not wholly in it
149
+ * (or nothing was kept). The bounds checks are defence on untrusted bytes: the index's validation at open keeps every
150
+ * extent inside the region, so they do not fire for a reader that opened.
151
+ *
152
+ * The range is returned as a copy, so nothing a caller or an AEAD does with it changes the next read.
153
+ */
154
+ private keptRange;
155
+ /**
156
+ * Check one chunk's stored bytes against the index, and open them: the CRC32C first, then, on an encrypted object,
157
+ * the AEAD with the chunk's own associated data. The one check every read of a chunk goes through, whether the
158
+ * bytes came from a range of their own or from a slice of a larger one.
159
+ */
160
+ private openChunk;
161
+ /**
162
+ * The next needed chunk of the range in `cur`, checked and opened, as a stream hands it out; the range is let go of
163
+ * when this is its last chunk.
164
+ */
165
+ private nextChunk;
166
+ /**
167
+ * Read several chunks as a stream, one storage request per merged range ({@link planChunkReads}) rather than one per
168
+ * chunk. `chunkKeys` are in ascending order, a key may repeat, and anything else is a {@link ValidationError}. One
169
+ * item comes out per position of `chunkKeys`, in order: the chunk's bytes, or `null` where this generation has none,
170
+ * as {@link getChunk} answers for an absent key. A key given twice is read once and answered at both positions with
171
+ * the same view.
172
+ *
173
+ * The whole key list is planned once, up front, against this one generation's index. The ranges then run through a
174
+ * window of `options.concurrency` reads ({@link MAX_RANGES_IN_FLIGHT} by default) that counts a range until the
175
+ * consumer has taken it, so a stream never holds more than that many ranges, in flight or landed, however many keys
176
+ * it was given or how slowly it is read; with `options.ramp` the window opens 1, 2, 4 … wide. A range is at most 1 MiB
177
+ * unless it is one chunk, which is at most the payload cap, so the bound is that many times the per-chunk cap
178
+ * (1 MiB, and 28 bytes more for an encrypted chunk). Nothing is read until the first item is asked for, and a
179
+ * consumer that stops launches no more: ranges already in flight finish and are dropped, and a failure of one of
180
+ * those is never raised.
181
+ *
182
+ * Every chunk is held to the check a read of it alone gets: its slice of the range must match its index entry's
183
+ * CRC32C, and on an encrypted object it opens under its own associated data, so a chunk moved to another place in
184
+ * the object, or another key's bytes, is refused. The bytes in a gap between needed chunks are never parsed. A
185
+ * range that comes back shorter than asked is an {@link IntegrityError}, and a chunk that fails its check ends the
186
+ * stream with that error when its turn comes, as a read of it alone would fail.
187
+ *
188
+ * `options.readRange`, when given, runs each range read, so a caller can retry one request without repeating the
189
+ * others; `options.now` times each request for `options.onRequest`, which hears of every range request that was sent, once, when
190
+ * it settles: taken, abandoned after the consumer stopped, or failed (a reader has no clock of its own, so `ms` is 0
191
+ * without one).
192
+ * A plain chunk is a writable view into the range it was read in: copy a chunk to keep it.
193
+ *
194
+ * A stream that fails (a range that errors, a chunk that fails its check) raises at once and sends nothing further:
195
+ * the ranges it still had in flight finish in the background, are not retried and never raise. Its `settled()`
196
+ * resolves when those have finished, for a caller that is about to read again and does not want them beside its next
197
+ * window (the source awaits it before a heal, and only then). It waits as long as the slowest of them takes to
198
+ * answer, which only the store's own read timeout bounds, so a read that never answers delays a heal and never an
199
+ * error.
200
+ */
201
+ readChunks(chunkKeys: readonly number[], options?: {
202
+ readonly concurrency?: number;
203
+ readonly ramp?: boolean | number;
204
+ readonly readRange?: <T>(read: () => Promise<T>) => Promise<T>;
205
+ readonly now?: () => number;
206
+ readonly onRequest?: ReadChunksOptions['onRequest'];
207
+ }): ChunkStream;
208
+ private streamChunks;
133
209
  }
210
+ /** What {@link CrbmReader.readChunks} returns: the stream, and a way to wait for the reads it left in flight. */
211
+ export type ChunkStream = AsyncGenerator<Omit<ChunkRead, 'version'>> & {
212
+ /** Resolves, never rejects, once every range the stream launched and did not hand out has finished. */
213
+ settled(): Promise<void>;
214
+ };
134
215
  /**
135
216
  * Read the sections of an extension block, its bytes before the trailer, once their CRC has passed. Each section is
136
217
  * `u8 type ‖ u32 length ‖ bytes`, in strictly ascending type order, and together they fill the region exactly. Type 1
@@ -1,12 +1,12 @@
1
1
  import { type IAuditSink } from './audit.js';
2
2
  import type { Yielder } from './cooperative.js';
3
3
  import type { Clock, Rng } from './determinism.js';
4
- import type { CrbmCrypto, IKeystore, WrappedDek } from './crypto.js';
5
- import type { ChunkRef, StorageChunkSource, GenerationMetadata, GenerationSummary, GenKey, IStorageDriver, IRegistryDriver, RegistryRecord, RegistrySummary, SegmentRef, SegmentSize, Token } from './ports.js';
4
+ import type { Aead, CrbmCrypto, IKeystore, WrappedDek } from './crypto.js';
5
+ import type { ChunkRef, ChunkRead, ReadChunksOptions, StorageChunkSource, GenerationMetadata, GenerationSummary, GenKey, IStorageDriver, IRegistryDriver, RegistryRecord, RegistrySummary, SegmentRef, SegmentSize, Token } from './ports.js';
6
6
  import { CrbmReader } from './crbm/reader.js';
7
7
  import type { CrbmReaderOptions } from './crbm/reader.js';
8
8
  import type { CodecBitmap, CodecInterface } from './codec.js';
9
- import { DecodedLoadInput } from './load-input.js';
9
+ import { ChunkLoadInput, DecodedLoadInput } from './load-input.js';
10
10
  export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
11
11
  /**
12
12
  * Optional {@link IRegistryDriver}. When provided, the current generation is resolved via the registry's
@@ -55,12 +55,16 @@ export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
55
55
  */
56
56
  readonly maxOpenSegments?: number;
57
57
  /**
58
- * Aggregate byte ceiling on the parsed `.crbm` indices resident in the reader cache (default 64 MiB) — the
58
+ * Aggregate byte ceiling on what the reader cache holds (default 64 MiB): the parsed `.crbm` indices, and the chunk bytes a
59
+ * reader keeps when its first read returned a whole small object (see below) — the
59
60
  * **second half of that memory bound**. `maxOpenSegments` alone bounds by *count*, but a wide/dense segment's
60
61
  * parsed index can reach about 1.3 MB, so 1024 wide indices could pin over a GB and blow a small heap (e.g. a 128 MB
61
62
  * Lambda) while the count is nominally "in bounds". This caps the summed {@link CrbmReader.retainedBytes} (the
62
- * parsed index, and any metadata a generation carries) across cached readers; the least-recently-used reader is evicted once the total would exceed it — whichever
63
- * of the count/byte bounds binds first. Lower it for memory-tight deployments with wide segments; a single
63
+ * parsed index, any metadata a generation carries, and any kept chunk bytes) across cached readers; the least-recently-used reader is evicted once the total would exceed it — whichever
64
+ * of the count/byte bounds binds first. A reader whose open read the whole object keeps a copy of its chunk region when
65
+ * that region is at most this ceiling divided by {@link maxOpenSegments} (64 KiB by default) — its fair share — so a small generation's
66
+ * chunks are served from memory with no further request, and a bigger one is read by range. Only a source with a timed
67
+ * pointer refresh ({@link currentGenTtlMs} above 0, a registry and a clock) keeps chunk bytes. Lower it for memory-tight deployments with wide segments; a single
64
68
  * segment whose index alone exceeds the budget is still cached (it can't be shrunk) but nothing else alongside.
65
69
  */
66
70
  readonly maxOpenIndexBytes?: number;
@@ -122,6 +126,8 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
122
126
  private readonly keystore;
123
127
  private readonly requireEncryption;
124
128
  private readonly readerOptions;
129
+ /** The most chunk bytes a reader keeps from its open: the reader cache's byte bound over its count bound, per reader; 0 without a timed pointer refresh. */
130
+ private readonly keepChunkBytesUpTo;
125
131
  private readonly clock;
126
132
  private readonly currentGenTtlMs;
127
133
  constructor(driver: IStorageDriver, options?: CrbmStorageChunkSourceOptions);
@@ -317,6 +323,22 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
317
323
  /** Build the per-generation decryption context for an encrypted segment (undefined for cleartext). */
318
324
  private cryptoForRead;
319
325
  getChunk(ref: ChunkRef): Promise<Uint8Array | null>;
326
+ /**
327
+ * Several chunks as a stream, in as few storage requests as {@link CrbmReader.readChunks} can make them (see
328
+ * {@link StorageChunkSource.getChunks}). The stream is served from one resolved generation, and every chunk says
329
+ * which version it came from. A generation swept, or an object replaced under its number, while the stream runs
330
+ * re-resolves the segment and carries on with the keys not yet yielded from whatever is current then, so each chunk
331
+ * is whole and from one generation and the stream may describe two; a second failure with no chunk yielded in between
332
+ * propagates. `options.retry` runs the resolution, each range request and the check that a failed read's object was
333
+ * replaced, so a transient fault repeats that step alone. Nothing is resolved until the first chunk is asked for.
334
+ */
335
+ getChunks(ref: SegmentRef, keys: readonly number[], options?: ReadChunksOptions): AsyncGenerator<ChunkRead>;
336
+ /**
337
+ * {@link getChunks} of a specific generation, the pinned shape read. It yields the chunks without a version: the
338
+ * caller that pinned the generation knows it. It never moves to another generation; `held` as for
339
+ * {@link getChunkAt}.
340
+ */
341
+ getChunksAt(ref: SegmentRef, generation: number, keys: readonly number[], held?: PinnedObject, options?: ReadChunksOptions): AsyncGenerator<Omit<ChunkRead, 'version'>>;
320
342
  listChunkKeys(ref: SegmentRef): Promise<number[]>;
321
343
  sizeOf(ref: SegmentRef): Promise<SegmentSize | null>;
322
344
  cardinalities(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
@@ -633,7 +655,7 @@ export interface BulkLoadResult {
633
655
  * {@link WriteConflictError} (write-once). Without a registry a `StorageChunkSource` serves the **highest**
634
656
  * generation present, so a too-high number silently shadows real data; with one, `publishGeneration` decides.
635
657
  */
636
- export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenKey, ids: Iterable<number> | AsyncIterable<number> | DecodedLoadInput, options?: {
658
+ export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenKey, ids: Iterable<number> | AsyncIterable<number> | DecodedLoadInput | ChunkLoadInput, options?: {
637
659
  registry?: IRegistryDriver;
638
660
  keystore?: IKeystore;
639
661
  requireEncryption?: boolean;
@@ -681,6 +703,28 @@ export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenK
681
703
  */
682
704
  row?: RegistryRecord | null;
683
705
  }): Promise<BulkLoadResult>;
706
+ /**
707
+ * What a load has already started when it calls {@link bulkLoadAhead}: its number, and the segment's data key, each
708
+ * still in flight so their round trips overlap the encoding. Both are joined where the write needs them, and neither
709
+ * may be left to reject unobserved.
710
+ */
711
+ export interface LoadAhead {
712
+ /** The number the generation takes (see {@link nextLoadGeneration}); joined just before the object is written. */
713
+ readonly generation: PromiseLike<number>;
714
+ /**
715
+ * The key a caller already asked the keystore to unwrap, with the wrappings it asked about. Used only when the
716
+ * row the write reads after the ids carries exactly these wrappings; otherwise the write unwraps for itself.
717
+ */
718
+ readonly unwrapped?: {
719
+ readonly wrapped: readonly WrappedDek[];
720
+ readonly aead: PromiseLike<Aead>;
721
+ };
722
+ }
723
+ /**
724
+ * {@link bulkLoadCrbmGeneration} for a load whose number, and perhaps its key, are still being fetched: the same
725
+ * options, with the same meaning, and the same writes. The number is joined only where the object is written.
726
+ */
727
+ export declare function bulkLoadAhead(driver: IStorageDriver, ref: SegmentRef, ahead: LoadAhead, ids: Parameters<typeof bulkLoadCrbmGeneration>[2], options?: NonNullable<Parameters<typeof bulkLoadCrbmGeneration>[3]>): Promise<BulkLoadResult>;
684
728
  /**
685
729
  * Open a {@link CrbmReader} on one generation over the storage driver's range/tail reads (decrypting if `crypto`).
686
730
  * The reader the write paths use to re-read what they wrote, and the erasure rewrite uses to stream the old
@@ -4,6 +4,13 @@ import type { Clock } from './determinism.js';
4
4
  import type { BoundedLru } from './lru.js';
5
5
  import type { IMetricsSink } from './metrics.js';
6
6
  import type { GenerationMetadata, StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
7
+ /**
8
+ * Range requests a stream holds ahead of a combine or `iterate` by default (chunk keys, on a source that reads chunk by
9
+ * chunk, and for `count`'s fallback) — bounds memory and concurrent reads (invariant 6). A read that does not fit
10
+ * one range of a source's stream takes about one sequential round trip per this many ranges, so this, not the network,
11
+ * is what sets how long a long read takes.
12
+ */
13
+ export declare const DEFAULT_INTERSECT_CONCURRENCY = 32;
7
14
  export interface EngineDeps {
8
15
  readonly storage: StorageChunkSource;
9
16
  /** Optional cache of decoded (immutable) Storage chunks. */
@@ -49,11 +56,15 @@ export interface IdRange {
49
56
  /** Options common to the chunk-aligned combines. */
50
57
  export interface CombineOptions extends IdRange {
51
58
  /**
52
- * Max chunk keys resolved concurrently — bounds the Storage footprint (about `concurrency × operands` chunks in
53
- * flight). A positive integer; default 32. The window opens 8 keys wide (or `concurrency`, if lower) and widens
54
- * as keys are taken, so a read that stops early fetches no more than a few keys ahead; one that runs to the end
55
- * spends nearly all of its round trips at the full width. A read that stops early has requested up to
56
- * `concurrency` keys per operand past the last one it used.
59
+ * How far ahead each operand is read: a positive integer, default 32. The library's own stores read each operand as
60
+ * one stream of coalesced ranges (chunks that sit near each other are one request), and `concurrency` is how many
61
+ * range requests a stream holds ahead of the read, in flight or landed and not yet used. So about
62
+ * `concurrency × operands` requests are in flight at most, and, since a range is at most 1 MiB (one chunk, when a chunk
63
+ * is larger), about `concurrency × operands` MiB are held, whatever the segment's size. The stream opens 4 ranges wide
64
+ * (or `concurrency`, if lower) and widens as ranges are taken, so a read that stops early has asked for little past
65
+ * 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
67
+ * stream holds its operand's reader outside the reader cache's bounds until it ends or the segment moves on.
57
68
  */
58
69
  readonly concurrency?: number;
59
70
  /** Override the store's per-op budget for this call (`false` lifts it). */
@@ -67,6 +78,11 @@ export interface CombineOptions extends IdRange {
67
78
  */
68
79
  readonly allowAbsentOperands?: boolean;
69
80
  }
81
+ /** One chunk of a combine's result: the bitmap of its remainders, owned by whoever reads it. */
82
+ export interface CombinedChunk {
83
+ readonly chunkKey: number;
84
+ readonly bitmap: CodecBitmap;
85
+ }
70
86
  export declare class SegmentEngine {
71
87
  private readonly storage;
72
88
  private readonly cache;
@@ -83,6 +99,13 @@ export declare class SegmentEngine {
83
99
  * promise per distinct key in flight.
84
100
  */
85
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).
105
+ */
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;
86
109
  constructor(deps: EngineDeps);
87
110
  /** Membership: one chunk lookup — the cache, else one Storage fetch of that chunk. */
88
111
  has(seg: SegmentRef, id: number): Promise<boolean>;
@@ -117,9 +140,17 @@ export declare class SegmentEngine {
117
140
  /** The chunks of `seg` at `chunkKeys`, read through a window of {@link DEFAULT_INTERSECT_CONCURRENCY}. */
118
141
  private chunkWindow;
119
142
  /**
120
- * Every id, ascending, reading ahead through a window of up to 32 chunk fetches that opens 1, 2, 4 … 32 wide, so a
121
- * read that stops early has fetched only a handful of chunks past the one it stopped in; with `range`, only the ids in `(after, through]`, fetching only the
122
- * chunks the range overlaps (see {@link IdRange}).
143
+ * The chunks of `seg` at `chunkKeys`, taken in ascending key order by a read that may stop at any point: one stream of
144
+ * coalesced ranges when the source has `getChunks`, opening 1, 2, 4 … ranges wide up to {@link DEFAULT_INTERSECT_CONCURRENCY},
145
+ * else the chunk-by-chunk window of that many reads. `close` stops what is left of it.
146
+ */
147
+ private chunkSequence;
148
+ /**
149
+ * Every id, ascending, reading the chunks as one stream of coalesced ranges (chunks that sit near each other are one
150
+ * request) that opens 1, 2, 4 … 32 ranges wide, so a read that stops early has asked for only a few ranges past the
151
+ * one it stopped in (each at most 1 MiB); with `range`, only the ids in `(after, through]`, fetching only the
152
+ * chunks the range overlaps (see {@link IdRange}). A source that reads chunk by chunk is read through a window of up to
153
+ * 32 chunk fetches that opens 1, 2, 4 … 32 wide.
123
154
  *
124
155
  * Two generators, not one with a branch: a second `yield` site in the full read's generator grows its frame, and
125
156
  * every id of a read that asked for no range paid for it (measured at about 5% per id). The full read keeps the
@@ -149,11 +180,11 @@ export declare class SegmentEngine {
149
180
  *
150
181
  * Chunk-skipping intersection. Aligns each segment's chunk-key set (read from the `.crbm` index, no payload),
151
182
  * keeps only keys present in **all** operands (a key missing from any operand can't contribute → its Storage
152
- * chunks are never fetched — the core saving), then for each surviving key fetches the operands' chunks in
153
- * parallel and hands them to the codec for the AND, streaming results through a bounded in-flight window.
183
+ * chunks are never fetched — the core saving), then reads each operand's surviving chunks as one stream of coalesced
184
+ * ranges and hands them, key by key, to the codec for the AND, streaming results through a bounded window.
154
185
  *
155
- * **Memory:** the Storage payload footprint is bounded by the window (`concurrency × operands × chunk`), not by
156
- * segment size — that's the Lambda-friendly property.
186
+ * **Memory:** the Storage payload footprint is bounded by the window (`concurrency × operands × chunk`, a range being
187
+ * at most a chunk's cap of 1 MiB), not by segment size — that's the Lambda-friendly property.
157
188
  *
158
189
  * Generation-consistent within the call (normal case): each operand's current generation is resolved **once**
159
190
  * up front (before the fan-out) and threaded into every chunk read, so a concurrent load can't corrupt or tear
@@ -164,7 +195,9 @@ export declare class SegmentEngine {
164
195
  * waiting for the TTL, so a shorter call can meet them too: **the reader cache evicting an operand mid-call**
165
196
  * (`maxOpenSegments`), whose re-read re-resolves fresh; a sweep deleting the generation it was reading, which
166
197
  * heals the read forward; and an invalidation, which this store's own `load`, `rollback` and `eraseSubject` make
167
- * and `invalidate()` makes on request. Still whole/immutable per read, never torn.
198
+ * and `invalidate()` makes on request. Still whole/immutable per read, never torn. An operand's stream resolves its
199
+ * segment again before it serves each chunk, so each of those moves a running read as it would a read of one chunk, and
200
+ * the ranges it had requested of the earlier generation are dropped, not served.
168
201
  */
169
202
  intersect(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<number>;
170
203
  /**
@@ -189,6 +222,20 @@ export declare class SegmentEngine {
189
222
  unionBatches(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<Uint32Array>;
190
223
  /** {@link andNot}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
191
224
  andNotBatches(seg: SegmentRef, excludes: readonly SegmentRef[], options?: Omit<CombineOptions, 'exclude'>): AsyncGenerator<Uint32Array>;
225
+ /**
226
+ * {@link intersect}, as the chunks the result is made of: `{ chunkKey, bitmap }` in ascending key order, each a
227
+ * non-empty bitmap of the chunk's 16-bit remainders, cut to the read's range. What a materialisation writes
228
+ * straight into its new generation, with no id built on the way.
229
+ *
230
+ * **Each bitmap belongs to the caller, who may change it:** it is the combine's own accumulator or a bitmap built
231
+ * for the range's edge, never a chunk the cache holds or another operand's. The reads, the budget, the metric and the
232
+ * errors are the id reads' own.
233
+ */
234
+ intersectChunks(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<CombinedChunk>;
235
+ /** {@link union}, as chunks: see {@link intersectChunks}. */
236
+ unionChunks(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<CombinedChunk>;
237
+ /** {@link andNot}, as chunks: see {@link intersectChunks}. */
238
+ andNotChunks(seg: SegmentRef, excludes: readonly SegmentRef[], options?: Omit<CombineOptions, 'exclude'>): AsyncGenerator<CombinedChunk>;
192
239
  /**
193
240
  * Everything a combine decides before it reads a chunk: validation, each operand's keys and generation, the
194
241
  * absent-operand refusal, key alignment, the budget and the metric. `null` when the range is empty. The per-id
@@ -197,6 +244,10 @@ export declare class SegmentEngine {
197
244
  private combinePlan;
198
245
  private combine;
199
246
  private combineBatches;
247
+ /** The chunk read: the plan every combine shares, then each key's result, cut to the range at its edges. */
248
+ private combineChunks;
249
+ /** An edge chunk's bitmap cut to the window, as a new bitmap: the remainders `edgeIds` would yield. */
250
+ private cutToWindow;
200
251
  /**
201
252
  * The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
202
253
  * Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
@@ -207,6 +258,37 @@ export declare class SegmentEngine {
207
258
  * used as-is — one clone per key, not one per operand.
208
259
  */
209
260
  private combineChunk;
261
+ /**
262
+ * One operand's chunk at `key`: from its stream when it has one, else a read of that chunk on its own. Not `async`,
263
+ * so a stream's take is made before anything is awaited: concurrent takes of one stream must be made in key order.
264
+ */
265
+ private operandChunk;
266
+ /**
267
+ * `keys` of `seg` as a stream of coalesced ranges, or `undefined` when the read is per key: a source with no
268
+ * `getChunks`, a segment with no generation, or nothing to read. Nothing is requested, and no cache lookup made, until
269
+ * the first chunk is asked for (see {@link StreamedChunks}).
270
+ */
271
+ private openStreamed;
272
+ /**
273
+ * Open the stream of `streamed`, at its first uncached chunk `from`: over that chunk and the later keys the cache does
274
+ * 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.
275
+ */
276
+ private startStream;
277
+ private closeStreamed;
278
+ /**
279
+ * The decoded chunk at `chunkKey` of a streamed read: from the cache while the stream is unopened and the cache holds
280
+ * it; else the next chunk of its stream, or, for a chunk the cache held when the stream opened, the cached instance
281
+ * while it is still there, and a read of that chunk alone once it is not.
282
+ */
283
+ private streamedChunk;
284
+ /**
285
+ * The version a streamed chunk is cached under, or `null` for one not to cache: the version its read reports when the
286
+ * source names versions as its lookups do (`currentVersion`), and otherwise the one the read planned under, since a
287
+ * lookup by generation number would never find a chunk cached under a stream's version.
288
+ */
289
+ private streamedVersion;
290
+ /** The cache key of a chunk: by the version it was read under, or by segment and key alone for a source with none. */
291
+ private chunkCacheKey;
210
292
  /** The segment's chunk keys, ascending — a shape read off the index, no payload. Keys are untrusted (invariant 5). */
211
293
  private chunkKeys;
212
294
  /**
@@ -297,6 +379,16 @@ export declare class SegmentEngine {
297
379
  * one): callers read it or clone it, never mutate it.
298
380
  */
299
381
  private storageChunk;
300
- /** One request for one chunk, decoded, range-checked and cached. */
382
+ /**
383
+ * A chunk's stored bytes decoded under the size cap (invariant 5) and range-checked: the one place a chunk becomes a
384
+ * bitmap, whether its bytes came from a request of its own or from a stream of coalesced ranges.
385
+ */
386
+ private decodeChunk;
387
+ /**
388
+ * One chunk, decoded, range-checked and cached. A source with `getChunks` is read through it, as a one-key stream, so
389
+ * a `storage.get` is reported only for a request that was sent (a chunk the source holds in memory sends none);
390
+ * the chunk is cached under the version it came from, as a stream's chunk is. A source without it is read by
391
+ * `getChunk`, one event a call.
392
+ */
301
393
  private fetchChunk;
302
394
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Pulls the items of an async iterator one at a time so that the code relaying them never holds the one it just
3
+ * handed on. A binding of a generator's body (`for await (const item of …)`, `yield*`) is kept in the generator's
4
+ * frame across its next `yield`, so the relay would hold the previous item, and whatever buffer it is a view into,
5
+ * for as long as it waits for the next. Here the item sits in the pull between {@link advance} and {@link take}, and
6
+ * `take` clears it before it returns.
7
+ *
8
+ * ```ts
9
+ * const pull = new ItemPull(source);
10
+ * try {
11
+ * while (await pull.advance()) yield pull.take((item) => wrap(item));
12
+ * } finally {
13
+ * await pull.close();
14
+ * }
15
+ * ```
16
+ *
17
+ * Internal to core: not exported from any entry point.
18
+ */
19
+ export declare class ItemPull<A> {
20
+ private readonly iterator;
21
+ private item;
22
+ constructor(source: AsyncIterable<A>);
23
+ /** Waits for the next item and keeps it inside; `false` once the source has ended. A failure of the source throws. */
24
+ advance(): Promise<boolean>;
25
+ /** Hands over the item {@link advance} fetched, through `map`, and forgets it. */
26
+ take<B>(map: (item: A) => B): B;
27
+ /** Ends the source early (a consumer that stopped), as leaving a `for await` does. */
28
+ close(): Promise<void>;
29
+ }
@@ -10,6 +10,7 @@
10
10
  * All of it runs before the load's first request, so a malformed input costs no round trip and writes nothing.
11
11
  */
12
12
  import type { CodecBitmap, CodecInterface } from './codec.js';
13
+ import type { CombinedChunk } from './engine.js';
13
14
  /**
14
15
  * Anything that serializes itself to portable Roaring: `roaring`'s `RoaringBitmap32` is one. Typed by shape, so
15
16
  * no codec's own class appears in a signature. A bitmap that can also say how large its serialization is
@@ -44,6 +45,14 @@ export declare class DecodedLoadInput {
44
45
  readonly bitmap: CodecBitmap;
45
46
  constructor(bitmap: CodecBitmap);
46
47
  }
48
+ /**
49
+ * A combine's result as the bitmaps of its chunks, ascending by key: written as they are, never id by id. Made only by
50
+ * {@link loadSegmentChunks}, never by {@link prepareLoadInput}, so no input a load is handed can be one.
51
+ */
52
+ export declare class ChunkLoadInput {
53
+ readonly chunks: AsyncIterable<CombinedChunk>;
54
+ constructor(chunks: AsyncIterable<CombinedChunk>);
55
+ }
47
56
  /**
48
57
  * Check a load's input and decode a bitmap input, before the load makes any request.
49
58
  *
@@ -20,7 +20,7 @@
20
20
  * object, so it stays, and collection takes it like any other generation once one above it is current.
21
21
  */
22
22
  import { type IAuditSink } from './audit.js';
23
- import { type CodecInterface } from './codec.js';
23
+ import { type CodecBitmap, type CodecInterface } from './codec.js';
24
24
  import type { Clock, Rng } from './determinism.js';
25
25
  import type { IKeystore } from './crypto.js';
26
26
  import { type LoadInput } from './load-input.js';
@@ -174,3 +174,22 @@ export interface LoadResult {
174
174
  * interesting cases (a guard tripped, a racing writer won) are operational facts rather than faults.
175
175
  */
176
176
  export declare function loadSegment(ref: SegmentRef, input: LoadInput, deps: LoadDeps, options?: LoadOptions): Promise<LoadResult>;
177
+ /**
178
+ * {@link loadSegment} for the result of a combine, as the chunks it is made of: each `{ chunkKey, bitmap }` is the
179
+ * bitmap of one 16-bit chunk's remainders, and they ascend by key. The chunks are written as they are, so no id is built
180
+ * for a value. What the `*Into` verbs call; the unwired form of them, as {@link loadSegment} is of `store.load`.
181
+ *
182
+ * Every chunk is checked before anything is written. Its bitmap must be one the `codec` made (`codec.owns`, so a codec
183
+ * without it refuses every chunk), its key an integer in `[0, 65535]` above the one before, and its values 16-bit;
184
+ * anything else is a {@link ValidationError}. An empty bitmap is left out, as no empty chunk is stored.
185
+ *
186
+ * **The load consumes the bitmaps it is given.** Writing a chunk may re-encode its bitmap in place for size, which
187
+ * changes its representation and never its members, so pass bitmaps you made for this call and do not reuse them. A
188
+ * bitmap the chunk cache holds, or one a caller still reads, is not one to pass.
189
+ *
190
+ * Everything else is `loadSegment`'s, the options, the result and the refusals included.
191
+ */
192
+ export declare function loadSegmentChunks(ref: SegmentRef, chunks: AsyncIterable<{
193
+ readonly chunkKey: number;
194
+ readonly bitmap: CodecBitmap;
195
+ }>, deps: LoadDeps, options?: LoadOptions): Promise<LoadResult>;
@@ -26,19 +26,26 @@ export type MetricEvent = {
26
26
  readonly kind: 'storage.get';
27
27
  readonly namespace?: string;
28
28
  readonly segment: string;
29
- /** One event per request, however many callers were waiting on it. Bytes returned (0 if the chunk was absent — a GET still happened). */
29
+ /**
30
+ * One event per request for chunks: a range request of a combine or `iterate`, which carries every chunk the read
31
+ * needs from a stretch of the object, or the one chunk of a read made on its own, however many callers were waiting
32
+ * on it. Bytes returned: the range's, the gaps between its chunks included (0 if a single chunk was absent — a GET
33
+ * still happened).
34
+ */
30
35
  readonly bytes: number;
31
36
  /**
32
- * Elapsed wall time of the read — includes any transient-retry backoff on the storage call. From the
33
- * injected clock, clamped to ≥ 0 (0 under the no-wait test clock).
37
+ * Elapsed wall time of the request — includes any transient-retry backoff on the storage call. From the
38
+ * injected clock, clamped to ≥ 0 (0 under the no-wait test clock). For a range of a stream, the time from the
39
+ * range being asked for to its answer, which the source measures.
34
40
  */
35
41
  readonly ms: number;
36
42
  } | {
37
43
  /**
38
44
  * One decoded-chunk cache lookup (emitted only when a cache is configured). `hit: false` means the lookup found
39
- * no cached chunk. A caller that missed while another caller's read of the same chunk was open waits on that
40
- * read and adds no `storage.get`, so the misses can outnumber the `storage.get` events; the `storage.get`
41
- * count is the number of requests.
45
+ * no cached chunk. A combine or `iterate` looks up every chunk it needs when it opens its streams. A caller that
46
+ * missed while another caller's read of the same chunk was open waits on that read and adds no `storage.get`, and a
47
+ * range carries several chunks, so the misses can outnumber the `storage.get` events; the `storage.get` count is
48
+ * the number of requests for chunks.
42
49
  */
43
50
  readonly kind: 'cache';
44
51
  readonly hit: boolean;
@@ -21,7 +21,7 @@
21
21
  * on some other handle. That is why the routing is by segment identity here rather than by which object the
22
22
  * caller happened to start from.
23
23
  */
24
- import type { ChunkRef, GenerationSummary, StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
24
+ import type { ChunkRef, ChunkRead, ReadChunksOptions, GenerationSummary, StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
25
25
  import type { CrbmStorageChunkSource } from './crbm-storage-source.js';
26
26
  /**
27
27
  * What a pin holds for one segment: the generation, the version identifying those exact bytes, and the pinned
@@ -64,6 +64,12 @@ export declare class PinnedStorageChunkSource implements StorageChunkSource {
64
64
  */
65
65
  private pinFor;
66
66
  getChunk(ref: ChunkRef): Promise<Uint8Array | null>;
67
+ /**
68
+ * A pinned segment's chunks come from the pinned generation alone, under the version a pinned read keys by
69
+ * ({@link currentVersion}); another segment's go to the live source, which answers with its own.
70
+ */
71
+ getChunks(ref: SegmentRef, keys: readonly number[], options?: ReadChunksOptions): AsyncIterable<ChunkRead>;
72
+ private pinnedChunks;
67
73
  listChunkKeys(ref: SegmentRef): Promise<number[]>;
68
74
  sizeOf(ref: SegmentRef): Promise<SegmentSize | null>;
69
75
  cardinalities(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;