@cloudbitmaps/core 0.13.0 → 0.15.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/dist/{chunk-5W3XS2IU.js → chunk-B7IGS3W6.js} +8 -1
- package/dist/chunk-B7IGS3W6.js.map +7 -0
- package/dist/core/chunk-stream.d.ts +22 -0
- package/dist/core/chunk-window.d.ts +8 -2
- package/dist/core/codec.d.ts +11 -0
- package/dist/core/cost.d.ts +12 -7
- package/dist/core/crbm/plan-reads.d.ts +44 -0
- package/dist/core/crbm/reader.d.ts +84 -3
- package/dist/core/crbm-storage-source.d.ts +49 -5
- package/dist/core/engine.d.ts +96 -14
- package/dist/core/item-pull.d.ts +29 -0
- package/dist/core/metrics.d.ts +13 -6
- package/dist/core/pinned-storage-source.d.ts +7 -1
- package/dist/core/ports.d.ts +69 -0
- package/dist/core/validate.d.ts +5 -0
- package/dist/driver-kit.js +2 -2
- package/dist/driver-kit.js.map +1 -1
- package/dist/drivers/retry/retrying-chunk-source.d.ts +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +951 -145
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/chunk-5W3XS2IU.js.map +0 -7
|
@@ -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
|
|
104
|
-
* how the metadata is weighed may
|
|
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,8 +1,8 @@
|
|
|
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';
|
|
@@ -55,12 +55,16 @@ export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
|
|
|
55
55
|
*/
|
|
56
56
|
readonly maxOpenSegments?: number;
|
|
57
57
|
/**
|
|
58
|
-
* Aggregate byte ceiling on
|
|
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,
|
|
63
|
-
* of the count/byte bounds binds first.
|
|
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>;
|
|
@@ -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: Iterable<number> | AsyncIterable<number> | DecodedLoadInput, 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
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* `concurrency`
|
|
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). */
|
|
@@ -83,6 +94,11 @@ export declare class SegmentEngine {
|
|
|
83
94
|
* promise per distinct key in flight.
|
|
84
95
|
*/
|
|
85
96
|
private readonly openReads;
|
|
97
|
+
/**
|
|
98
|
+
* The chunk streams open right now. An {@link invalidate} of a segment marks its streams, which then cache no chunk
|
|
99
|
+
* they deliver, as a read whose entry an invalidation dropped does not (streams hold no `openReads` entry).
|
|
100
|
+
*/
|
|
101
|
+
private readonly openStreams;
|
|
86
102
|
constructor(deps: EngineDeps);
|
|
87
103
|
/** Membership: one chunk lookup — the cache, else one Storage fetch of that chunk. */
|
|
88
104
|
has(seg: SegmentRef, id: number): Promise<boolean>;
|
|
@@ -117,9 +133,17 @@ export declare class SegmentEngine {
|
|
|
117
133
|
/** The chunks of `seg` at `chunkKeys`, read through a window of {@link DEFAULT_INTERSECT_CONCURRENCY}. */
|
|
118
134
|
private chunkWindow;
|
|
119
135
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
136
|
+
* The chunks of `seg` at `chunkKeys`, taken in ascending key order by a read that may stop at any point: one stream of
|
|
137
|
+
* coalesced ranges when the source has `getChunks`, opening 1, 2, 4 … ranges wide up to {@link DEFAULT_INTERSECT_CONCURRENCY},
|
|
138
|
+
* else the chunk-by-chunk window of that many reads. `close` stops what is left of it.
|
|
139
|
+
*/
|
|
140
|
+
private chunkSequence;
|
|
141
|
+
/**
|
|
142
|
+
* Every id, ascending, reading the chunks as one stream of coalesced ranges (chunks that sit near each other are one
|
|
143
|
+
* request) that opens 1, 2, 4 … 32 ranges wide, so a read that stops early has asked for only a few ranges past the
|
|
144
|
+
* one it stopped in (each at most 1 MiB); with `range`, only the ids in `(after, through]`, fetching only the
|
|
145
|
+
* chunks the range overlaps (see {@link IdRange}). A source that reads chunk by chunk is read through a window of up to
|
|
146
|
+
* 32 chunk fetches that opens 1, 2, 4 … 32 wide.
|
|
123
147
|
*
|
|
124
148
|
* Two generators, not one with a branch: a second `yield` site in the full read's generator grows its frame, and
|
|
125
149
|
* every id of a read that asked for no range paid for it (measured at about 5% per id). The full read keeps the
|
|
@@ -128,6 +152,13 @@ export declare class SegmentEngine {
|
|
|
128
152
|
iterate(seg: SegmentRef, range?: IdRange): AsyncGenerator<number>;
|
|
129
153
|
private iterateAll;
|
|
130
154
|
private iterateRange;
|
|
155
|
+
/**
|
|
156
|
+
* {@link iterate}, one `Uint32Array` per chunk instead of one id at a time: the same chunks, in the same order, with
|
|
157
|
+
* the same ids (a range cut at its edges the same way), and no array for a chunk that holds none of them. The
|
|
158
|
+
* 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
|
|
159
|
+
* the caller's to keep.
|
|
160
|
+
*/
|
|
161
|
+
iterateBatches(seg: SegmentRef, range?: IdRange): AsyncGenerator<Uint32Array>;
|
|
131
162
|
/**
|
|
132
163
|
* `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
|
|
133
164
|
*
|
|
@@ -142,11 +173,11 @@ export declare class SegmentEngine {
|
|
|
142
173
|
*
|
|
143
174
|
* Chunk-skipping intersection. Aligns each segment's chunk-key set (read from the `.crbm` index, no payload),
|
|
144
175
|
* keeps only keys present in **all** operands (a key missing from any operand can't contribute → its Storage
|
|
145
|
-
* chunks are never fetched — the core saving), then
|
|
146
|
-
*
|
|
176
|
+
* chunks are never fetched — the core saving), then reads each operand's surviving chunks as one stream of coalesced
|
|
177
|
+
* ranges and hands them, key by key, to the codec for the AND, streaming results through a bounded window.
|
|
147
178
|
*
|
|
148
|
-
* **Memory:** the Storage payload footprint is bounded by the window (`concurrency × operands × chunk
|
|
149
|
-
* segment size — that's the Lambda-friendly property.
|
|
179
|
+
* **Memory:** the Storage payload footprint is bounded by the window (`concurrency × operands × chunk`, a range being
|
|
180
|
+
* at most a chunk's cap of 1 MiB), not by segment size — that's the Lambda-friendly property.
|
|
150
181
|
*
|
|
151
182
|
* Generation-consistent within the call (normal case): each operand's current generation is resolved **once**
|
|
152
183
|
* up front (before the fan-out) and threaded into every chunk read, so a concurrent load can't corrupt or tear
|
|
@@ -157,7 +188,9 @@ export declare class SegmentEngine {
|
|
|
157
188
|
* waiting for the TTL, so a shorter call can meet them too: **the reader cache evicting an operand mid-call**
|
|
158
189
|
* (`maxOpenSegments`), whose re-read re-resolves fresh; a sweep deleting the generation it was reading, which
|
|
159
190
|
* heals the read forward; and an invalidation, which this store's own `load`, `rollback` and `eraseSubject` make
|
|
160
|
-
* and `invalidate()` makes on request. Still whole/immutable per read, never torn.
|
|
191
|
+
* and `invalidate()` makes on request. Still whole/immutable per read, never torn. An operand's stream resolves its
|
|
192
|
+
* segment again before it serves each chunk, so each of those moves a running read as it would a read of one chunk, and
|
|
193
|
+
* the ranges it had requested of the earlier generation are dropped, not served.
|
|
161
194
|
*/
|
|
162
195
|
intersect(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<number>;
|
|
163
196
|
/**
|
|
@@ -176,7 +209,20 @@ export declare class SegmentEngine {
|
|
|
176
209
|
* the cost scales with the segment being filtered rather than with the size of the suppression list.
|
|
177
210
|
*/
|
|
178
211
|
andNot(seg: SegmentRef, excludes: readonly SegmentRef[], options?: Omit<CombineOptions, 'exclude'>): AsyncGenerator<number>;
|
|
212
|
+
/** {@link intersect}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
|
|
213
|
+
intersectBatches(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<Uint32Array>;
|
|
214
|
+
/** {@link union}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
|
|
215
|
+
unionBatches(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<Uint32Array>;
|
|
216
|
+
/** {@link andNot}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
|
|
217
|
+
andNotBatches(seg: SegmentRef, excludes: readonly SegmentRef[], options?: Omit<CombineOptions, 'exclude'>): AsyncGenerator<Uint32Array>;
|
|
218
|
+
/**
|
|
219
|
+
* Everything a combine decides before it reads a chunk: validation, each operand's keys and generation, the
|
|
220
|
+
* absent-operand refusal, key alignment, the budget and the metric. `null` when the range is empty. The per-id
|
|
221
|
+
* read and the batch read share it, so the two cannot disagree about what is read or charged.
|
|
222
|
+
*/
|
|
223
|
+
private combinePlan;
|
|
179
224
|
private combine;
|
|
225
|
+
private combineBatches;
|
|
180
226
|
/**
|
|
181
227
|
* The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
|
|
182
228
|
* Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
|
|
@@ -187,6 +233,32 @@ export declare class SegmentEngine {
|
|
|
187
233
|
* used as-is — one clone per key, not one per operand.
|
|
188
234
|
*/
|
|
189
235
|
private combineChunk;
|
|
236
|
+
/**
|
|
237
|
+
* One operand's chunk at `key`: from its stream when it has one, else a read of that chunk on its own. Not `async`,
|
|
238
|
+
* so a stream's take is made before anything is awaited: concurrent takes of one stream must be made in key order.
|
|
239
|
+
*/
|
|
240
|
+
private operandChunk;
|
|
241
|
+
/**
|
|
242
|
+
* Open `keys` of `seg` as a stream of coalesced ranges, or `undefined` when the read is per key: a source with no
|
|
243
|
+
* `getChunks`, a segment with no generation, or nothing to read. The chunks already decoded in the cache are taken out
|
|
244
|
+
* of the key list first and served from there, so a repeat read asks the source for nothing it holds. Nothing is
|
|
245
|
+
* requested until the first chunk is asked for.
|
|
246
|
+
*/
|
|
247
|
+
private openStreamed;
|
|
248
|
+
private closeStreamed;
|
|
249
|
+
/**
|
|
250
|
+
* The decoded chunk at `chunkKey` of a streamed read: the next chunk of its stream, or, for a chunk the cache held
|
|
251
|
+
* when the read opened, the cached instance while it is still there, and a read of that chunk alone once it is not.
|
|
252
|
+
*/
|
|
253
|
+
private streamedChunk;
|
|
254
|
+
/**
|
|
255
|
+
* The version a streamed chunk is cached under, or `null` for one not to cache: the version its read reports when the
|
|
256
|
+
* source names versions as its lookups do (`currentVersion`), and otherwise the one the read planned under, since a
|
|
257
|
+
* lookup by generation number would never find a chunk cached under a stream's version.
|
|
258
|
+
*/
|
|
259
|
+
private streamedVersion;
|
|
260
|
+
/** The cache key of a chunk: by the version it was read under, or by segment and key alone for a source with none. */
|
|
261
|
+
private chunkCacheKey;
|
|
190
262
|
/** The segment's chunk keys, ascending — a shape read off the index, no payload. Keys are untrusted (invariant 5). */
|
|
191
263
|
private chunkKeys;
|
|
192
264
|
/**
|
|
@@ -277,6 +349,16 @@ export declare class SegmentEngine {
|
|
|
277
349
|
* one): callers read it or clone it, never mutate it.
|
|
278
350
|
*/
|
|
279
351
|
private storageChunk;
|
|
280
|
-
/**
|
|
352
|
+
/**
|
|
353
|
+
* A chunk's stored bytes decoded under the size cap (invariant 5) and range-checked: the one place a chunk becomes a
|
|
354
|
+
* bitmap, whether its bytes came from a request of its own or from a stream of coalesced ranges.
|
|
355
|
+
*/
|
|
356
|
+
private decodeChunk;
|
|
357
|
+
/**
|
|
358
|
+
* One chunk, decoded, range-checked and cached. A source with `getChunks` is read through it, as a one-key stream, so
|
|
359
|
+
* a `storage.get` is reported only for a request that was sent (a chunk the source holds in memory sends none);
|
|
360
|
+
* the chunk is cached under the version it came from, as a stream's chunk is. A source without it is read by
|
|
361
|
+
* `getChunk`, one event a call.
|
|
362
|
+
*/
|
|
281
363
|
private fetchChunk;
|
|
282
364
|
}
|
|
@@ -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
|
+
}
|
package/dist/core/metrics.d.ts
CHANGED
|
@@ -26,19 +26,26 @@ export type MetricEvent = {
|
|
|
26
26
|
readonly kind: 'storage.get';
|
|
27
27
|
readonly namespace?: string;
|
|
28
28
|
readonly segment: string;
|
|
29
|
-
/**
|
|
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
|
|
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
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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>;
|
package/dist/core/ports.d.ts
CHANGED
|
@@ -40,11 +40,80 @@ export interface GenKey extends SegmentRef {
|
|
|
40
40
|
export interface SegmentSize {
|
|
41
41
|
readonly sizeBytes: number;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* One chunk of a {@link StorageChunkSource.getChunks} stream: the key asked for, its bytes (`null` where the generation
|
|
45
|
+
* holds none), and the version of the generation the bytes were read from, or `null` when the segment has no
|
|
46
|
+
* generation. The version is what {@link StorageChunkSource.currentVersion} reports for that generation, so a caller
|
|
47
|
+
* can key what it keeps by the generation the bytes came from rather than the one it resolved earlier.
|
|
48
|
+
*/
|
|
49
|
+
export interface ChunkRead {
|
|
50
|
+
readonly key: number;
|
|
51
|
+
readonly bytes: Uint8Array | null;
|
|
52
|
+
readonly version: string | null;
|
|
53
|
+
}
|
|
54
|
+
/** Options of {@link StorageChunkSource.getChunks}. */
|
|
55
|
+
export interface ReadChunksOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Runs one storage request, and may run it again if it fails. A source passes each request it makes through
|
|
58
|
+
* this, so a caller that retries transient faults repeats the one request that failed, not the ones that landed.
|
|
59
|
+
* Absent, each request runs once.
|
|
60
|
+
*/
|
|
61
|
+
readonly retry?: <T>(request: () => Promise<T>) => Promise<T>;
|
|
62
|
+
/**
|
|
63
|
+
* How many range requests the stream holds ahead of its consumer, in flight or landed and not yet taken: a positive
|
|
64
|
+
* integer, 32 by default. It bounds the requests in flight, and the memory the stream holds, at this many times the
|
|
65
|
+
* largest range (1 MiB, 1 MiB + 28 B for one encrypted chunk).
|
|
66
|
+
*/
|
|
67
|
+
readonly concurrency?: number;
|
|
68
|
+
/**
|
|
69
|
+
* Open the window 1, 2, 4 … ranges wide up to `concurrency` instead of at full width, so a reader that stops early
|
|
70
|
+
* has asked for little; a number starts it that wide instead of at 1 (a positive integer), and it doubles from there.
|
|
71
|
+
*/
|
|
72
|
+
readonly ramp?: boolean | number;
|
|
73
|
+
/**
|
|
74
|
+
* Called once for every range request the stream sends, when it settles: a request the consumer took, one that was
|
|
75
|
+
* still in flight when the consumer stopped, and one that failed. `bytes` is what the request moved, the gaps between
|
|
76
|
+
* the chunks it carried included (0 when it failed), and `ms` how long it took, retries included (0 when the source
|
|
77
|
+
* has no clock). It is how a caller counts what it is billed for, since several chunks can share a request and
|
|
78
|
+
* a stopped stream leaves some in flight. A throw from it is ignored.
|
|
79
|
+
*/
|
|
80
|
+
readonly onRequest?: (request: {
|
|
81
|
+
readonly bytes: number;
|
|
82
|
+
readonly ms: number;
|
|
83
|
+
}) => void;
|
|
84
|
+
}
|
|
43
85
|
/** Per-chunk read view of the immutable Storage tier (implemented by the `.crbm` reader). */
|
|
44
86
|
export interface StorageChunkSource {
|
|
45
87
|
/** Read-only bytes for the chunk, or `null` if absent. Callers must not mutate the buffer. */
|
|
46
88
|
getChunk(ref: ChunkRef): Promise<Uint8Array | null>;
|
|
47
89
|
listChunkKeys(ref: SegmentRef): Promise<number[]>;
|
|
90
|
+
/**
|
|
91
|
+
* Optional: several chunks of one segment as a stream, read **from one generation** with fewer storage requests than
|
|
92
|
+
* one per chunk where the chunks sit near each other in the object. `keys` are chunk keys in ascending order (a key
|
|
93
|
+
* may repeat; any other order is a `ValidationError`). The stream yields one {@link ChunkRead} per position of
|
|
94
|
+
* `keys`, in that order, as the requests that carry them land; a key the generation does not hold yields `null`
|
|
95
|
+
* bytes, as {@link StorageChunkSource.getChunk} answers. Each chunk is checked as a read of it alone is.
|
|
96
|
+
*
|
|
97
|
+
* **Nothing is resolved or read until the first chunk is asked for**, and the source holds at most
|
|
98
|
+
* {@link ReadChunksOptions.concurrency} ranges at a time (in flight, or landed and not yet taken), however many keys
|
|
99
|
+
* there are: a consumer that is slow holds the stream back, and one that stops early (`break`, or `return()`) stops
|
|
100
|
+
* the reads, so the requests it had already started (at most that many) finish and are dropped, and are not retried.
|
|
101
|
+
* A stream that fails raises at once and sends nothing further, and the requests it left in flight finish in the
|
|
102
|
+
* background and never raise. A plain chunk may be a view into a buffer of up to 1 MiB that it shares with its
|
|
103
|
+
* neighbours (and the same view at two positions for a key asked twice), so do not write to it, and copy a chunk to
|
|
104
|
+
* keep it.
|
|
105
|
+
*
|
|
106
|
+
* One stream reads one generation. If that generation is swept, or its object replaced, while the stream runs, a
|
|
107
|
+
* source may re-resolve the segment and continue with the keys not yet yielded from the generation that is current
|
|
108
|
+
* then; each chunk says which version it came from, and each is whole and verified either way. Before it does, it
|
|
109
|
+
* waits for the requests the failed stream left in flight, so a heal never opens a second window beside them: a
|
|
110
|
+
* request that never answers delays a heal, as it would hang a read of one chunk, until the driver's read timeout (if
|
|
111
|
+
* it has one) ends it, and never delays an error. A source that reads a pinned generation never heals.
|
|
112
|
+
*
|
|
113
|
+
* A source that cannot read a range of an object omits this, and a caller reads chunk by chunk; the `.crbm`
|
|
114
|
+
* source, which is what the stores the library ships read through, implements it.
|
|
115
|
+
*/
|
|
116
|
+
getChunks?(ref: SegmentRef, keys: readonly number[], options?: ReadChunksOptions): AsyncIterable<ChunkRead>;
|
|
48
117
|
/**
|
|
49
118
|
* Optional: the current generation's grounded size, cheaply (from the already-parsed `.crbm` index — no
|
|
50
119
|
* payload reads), or `null` if the segment has no Storage generation. Powers the grounded `costReport()`.
|
package/dist/core/validate.d.ts
CHANGED
|
@@ -42,5 +42,10 @@ export declare function isReservedNamespace(namespace: string | undefined): bool
|
|
|
42
42
|
export declare function validateUserNamespace(namespace: string): void;
|
|
43
43
|
/** Validate a segment ref an application hands in: the name rules, and a namespace outside the reserved prefix. */
|
|
44
44
|
export declare function validateUserRef(ref: SegmentRef): void;
|
|
45
|
+
/**
|
|
46
|
+
* Validate the keys of a `getChunks` stream: in ascending order, a key may repeat. Checked before anything is
|
|
47
|
+
* resolved or read, so a caller's mistake costs no request.
|
|
48
|
+
*/
|
|
49
|
+
export declare function validateChunkKeyOrder(keys: readonly number[]): void;
|
|
45
50
|
/** Validate a chunk ref: the segment/namespace rules plus `chunkKey` ∈ `[0, 65535]` (a u16). A driver-boundary check. */
|
|
46
51
|
export declare function validateChunkRef(ref: ChunkRef): void;
|
package/dist/driver-kit.js
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
validateRegistryPatch,
|
|
29
29
|
validateSegmentRef,
|
|
30
30
|
webCryptoEntropy
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-B7IGS3W6.js";
|
|
32
32
|
|
|
33
33
|
// src/drivers/_shared/object-registry-keys.ts
|
|
34
34
|
var REGISTRY_SUFFIX = ".reg";
|
|
@@ -104,7 +104,7 @@ function parseRegistryKey(prefix, objectKey) {
|
|
|
104
104
|
var MAX_ROW_BYTES = 1 * 1024 * 1024;
|
|
105
105
|
var MAX_DELETE_ATTEMPTS = 8;
|
|
106
106
|
var MAX_READ_ATTEMPTS = 8;
|
|
107
|
-
var LIST_READ_CONCURRENCY =
|
|
107
|
+
var LIST_READ_CONCURRENCY = 48;
|
|
108
108
|
var ObjectVersionRaced = class extends Error {
|
|
109
109
|
constructor(objectKey) {
|
|
110
110
|
super(`registry object was overwritten mid-read: ${objectKey}`);
|