@cloudbitmaps/core 0.12.0 → 0.14.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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * An ordered window of reads: up to `max` reads open ahead of the one being consumed, handed back in key order.
3
+ * With `ramp`, the window opens 1, 2, 4 … wide up to `max` instead of `max` at once, so a consumer that stops
4
+ * after a few chunks has fetched a handful, not a full window.
5
+ *
6
+ * `T` is whatever one read yields (a decoded chunk, or the raw bytes of one). Each read is wrapped to resolve and
7
+ * never reject, so a read nobody consumes (the consumer stopped, or an earlier chunk failed) cannot raise an
8
+ * unhandled rejection; its error surfaces from {@link take} only if the consumer reaches that chunk. Memory is
9
+ * bounded by the window: at most `max` results are held ahead.
10
+ *
11
+ * Internal to core: not exported from any entry point.
12
+ */
13
+ export declare class ChunkWindow<T> {
14
+ private readonly keys;
15
+ private readonly fetch;
16
+ private readonly max;
17
+ private readonly ramp;
18
+ private readonly open;
19
+ private launched;
20
+ private taken;
21
+ constructor(keys: readonly number[], fetch: (chunkKey: number) => Promise<T>, max: number, ramp: boolean);
22
+ /** The next read's result in key order, or the error that read raised. */
23
+ take(): Promise<T>;
24
+ }
@@ -52,6 +52,17 @@ export interface CodecBitmap {
52
52
  /** Ascending iterator over the set values. */
53
53
  [Symbol.iterator](): IterableIterator<number>;
54
54
  toArray(): number[];
55
+ /**
56
+ * The set's values as one ascending `Uint32Array` the caller owns: what a batch read converts a chunk with, in one
57
+ * native step instead of one iterator step per value.
58
+ *
59
+ * **Optional.** A codec without it is read through its iterator, which is correct and slower.
60
+ *
61
+ * **It must return a NEW array on every call, which the caller owns:** the engine adds the chunk's base to each
62
+ * value in place and hands the array on, so an array that is shared, cached or a view of the bitmap's own storage
63
+ * would be corrupted, and would corrupt the bitmap. It holds exactly what the iterator yields, ascending.
64
+ */
65
+ toUint32Array?(): Uint32Array;
55
66
  /**
56
67
  * The largest value in the set, or `undefined` when empty.
57
68
  *
@@ -104,8 +104,11 @@ export declare const DEFAULT_TAIL_BYTES: number;
104
104
  * above the ~650 KB full-span worst case, well below a denial-of-wallet allocation.
105
105
  */
106
106
  export declare const DEFAULT_MAX_INDEX_BYTES: number;
107
- /** Hard cap on a single chunk payload, defending the native deserializer against oversized input. */
108
- export declare const DEFAULT_MAX_PAYLOAD_BYTES: number;
107
+ /**
108
+ * Largest serialized bitmap the engine decodes (1 MiB, per chunk). A well-formed 16-bit chunk serializes to at most
109
+ * about 8.2 KiB, so no legitimate chunk is near it; both the read engine and the erase path decode under this one cap.
110
+ */
111
+ export declare const DEFAULT_MAX_BITMAP_BYTES: number;
109
112
  /** A chunk's cardinality is in `[1, 65536]` (empty chunks are never written). */
110
113
  export declare const MAX_CHUNK_CARDINALITY = 65536;
111
114
  /**
@@ -142,3 +145,9 @@ export declare const CRC32C_BYTES = 4;
142
145
  */
143
146
  export declare const AEAD_NONCE_BYTES = 12;
144
147
  export declare const AEAD_TAG_BYTES = 16;
148
+ /**
149
+ * Hard cap on one encrypted chunk's stored payload (a cleartext one is capped at the decode cap): the decode cap plus the AEAD framing, so an
150
+ * entry the engine would refuse to decode is refused when the object is opened, before any payload is read. No writer
151
+ * exceeds it: a stored payload is a serialized 16-bit chunk (at most about 8.2 KiB) plus, when encrypted, that framing.
152
+ */
153
+ export declare const DEFAULT_MAX_PAYLOAD_BYTES: number;
@@ -31,7 +31,7 @@ export declare function indexCapacity(indexLength: number): number;
31
31
  export interface CrbmReaderOptions {
32
32
  /** Speculative tail size in bytes (default 256 KB; clamped up to at least the footer size). */
33
33
  readonly tailBytes?: number;
34
- /** Hard cap on a single chunk payload length (default 16 MB). */
34
+ /** Hard cap on a single chunk payload length (default: the 1 MiB decode cap, plus the 28-byte nonce and tag on an encrypted object). The engine decodes under `maxBitmapBytes`; raise this with it. */
35
35
  readonly maxPayloadBytes?: number;
36
36
  /** Hard cap on the whole index region fetched/parsed from one object (default 8 MB). */
37
37
  readonly maxIndexBytes?: number;
@@ -426,7 +426,7 @@ export interface StreamWriteResult {
426
426
  * Streaming variant of {@link writeCrbmGeneration} for a **constant-memory** rewrite (the erasure rewrite, a
427
427
  * sorted load): consumes an **already-ascending** async stream of `{ chunkKey, bitmap }`, feeding each to the
428
428
  * codec and freeing it, instead of materializing the whole generation. Paired with a streaming storage sink (S3
429
- * multipart / LocalFs temp file), peak memory is ~one chunk + one part. Returns a tally so the caller can verify
429
+ * multipart / LocalFs temp file), the writer itself holds ~one chunk + one part. Returns a tally so the caller can verify
430
430
  * the re-opened object without re-iterating the (now-consumed) stream. Input **must** be ascending by `chunkKey`
431
431
  * — the codec rejects an out-of-order chunk; empty bitmaps are skipped.
432
432
  */
@@ -8,6 +8,7 @@ export interface EngineDeps {
8
8
  readonly storage: StorageChunkSource;
9
9
  /** Optional cache of decoded (immutable) Storage chunks. */
10
10
  readonly cache?: BoundedLru<string, CodecBitmap>;
11
+ /** 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. */
11
12
  readonly maxBitmapBytes?: number;
12
13
  /**
13
14
  * The bitmap codec — **required**. `core/` is codec-agnostic: it can have no default, because the concrete codec
@@ -47,7 +48,13 @@ export interface IdRange {
47
48
  }
48
49
  /** Options common to the chunk-aligned combines. */
49
50
  export interface CombineOptions extends IdRange {
50
- /** Max chunk keys resolved concurrently — bounds the Storage footprint. A positive integer. */
51
+ /**
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.
57
+ */
51
58
  readonly concurrency?: number;
52
59
  /** Override the store's per-op budget for this call (`false` lifts it). */
53
60
  readonly budget?: BudgetOption;
@@ -70,6 +77,12 @@ export declare class SegmentEngine {
70
77
  private readonly metricsOn;
71
78
  /** Resolved per-op budget (null = disabled); undefined deps ⇒ the generous default. */
72
79
  private readonly budget;
80
+ /**
81
+ * The chunk reads open right now, by cache key: a caller that misses the cache and finds its key here awaits that
82
+ * read instead of making its own. An entry lives from the request until it settles, so the map holds at most one
83
+ * promise per distinct key in flight.
84
+ */
85
+ private readonly openReads;
73
86
  constructor(deps: EngineDeps);
74
87
  /** Membership: one chunk lookup — the cache, else one Storage fetch of that chunk. */
75
88
  has(seg: SegmentRef, id: number): Promise<boolean>;
@@ -104,7 +117,7 @@ export declare class SegmentEngine {
104
117
  /** The chunks of `seg` at `chunkKeys`, read through a window of {@link DEFAULT_INTERSECT_CONCURRENCY}. */
105
118
  private chunkWindow;
106
119
  /**
107
- * Every id, ascending, reading ahead through a window of up to 8 chunk fetches that opens 1, 2, 4, 8 wide, so a
120
+ * Every id, ascending, reading ahead through a window of up to 32 chunk fetches that opens 1, 2, 4 … 32 wide, so a
108
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
109
122
  * chunks the range overlaps (see {@link IdRange}).
110
123
  *
@@ -115,6 +128,13 @@ export declare class SegmentEngine {
115
128
  iterate(seg: SegmentRef, range?: IdRange): AsyncGenerator<number>;
116
129
  private iterateAll;
117
130
  private iterateRange;
131
+ /**
132
+ * {@link iterate}, one `Uint32Array` per chunk instead of one id at a time: the same chunks, in the same order, with
133
+ * the same ids (a range cut at its edges the same way), and no array for a chunk that holds none of them. The
134
+ * 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
135
+ * the caller's to keep.
136
+ */
137
+ iterateBatches(seg: SegmentRef, range?: IdRange): AsyncGenerator<Uint32Array>;
118
138
  /**
119
139
  * `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
120
140
  *
@@ -163,7 +183,20 @@ export declare class SegmentEngine {
163
183
  * the cost scales with the segment being filtered rather than with the size of the suppression list.
164
184
  */
165
185
  andNot(seg: SegmentRef, excludes: readonly SegmentRef[], options?: Omit<CombineOptions, 'exclude'>): AsyncGenerator<number>;
186
+ /** {@link intersect}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
187
+ intersectBatches(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<Uint32Array>;
188
+ /** {@link union}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
189
+ unionBatches(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<Uint32Array>;
190
+ /** {@link andNot}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
191
+ andNotBatches(seg: SegmentRef, excludes: readonly SegmentRef[], options?: Omit<CombineOptions, 'exclude'>): AsyncGenerator<Uint32Array>;
192
+ /**
193
+ * Everything a combine decides before it reads a chunk: validation, each operand's keys and generation, the
194
+ * absent-operand refusal, key alignment, the budget and the metric. `null` when the range is empty. The per-id
195
+ * read and the batch read share it, so the two cannot disagree about what is read or charged.
196
+ */
197
+ private combinePlan;
166
198
  private combine;
199
+ private combineBatches;
167
200
  /**
168
201
  * The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
169
202
  * Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
@@ -255,7 +288,15 @@ export declare class SegmentEngine {
255
288
  * `gen === undefined` ⇒ the source can't report a generation (it only ever serves one) ⇒ the key stays
256
289
  * generation-free. Superseded-generation entries age out under the LRU ceiling — no active purge.
257
290
  *
258
- * The returned instance is **shared** (it may be the cached one): callers read it or clone it, never mutate it.
291
+ * Concurrent misses of one key share **one** request, with or without a cache: the first caller starts the read,
292
+ * later ones await it, and the entry goes when the read settles, so a failure is every waiting caller's and the
293
+ * next call reads again. The key carries the version exactly as the cache's does, so two versions of one chunk
294
+ * are two reads, never one.
295
+ *
296
+ * The returned instance is **shared** (it may be the cached one, and every caller that joined a read gets the same
297
+ * one): callers read it or clone it, never mutate it.
259
298
  */
260
299
  private storageChunk;
300
+ /** One request for one chunk, decoded, range-checked and cached. */
301
+ private fetchChunk;
261
302
  }
@@ -6,8 +6,8 @@
6
6
  * is what every other write in this library is: a new generation. The current generation is streamed chunk by
7
7
  * chunk through the ascending writer — every chunk decoded, range-checked and re-encoded, the one holding the id
8
8
  * with that bit cleared — then published fenced on the generation it streamed, and that generation is collected immediately
9
- * (`keep: 0`), so the bit is **physically gone from the bucket when this returns**. Constant memory: one chunk in
10
- * flight, never the whole segment.
9
+ * (`keep: 0`), so the bit is **physically gone from the bucket when this returns**. Bounded memory: a window of 32
10
+ * chunk reads ahead of the writer, never the whole segment.
11
11
  *
12
12
  * The rewrite is the same generation without one id, so it keeps everything else: the new object carries the source's
13
13
  * metadata as it is, and the row's summary of it, built from what was written, counts one id fewer and holds the same
@@ -103,7 +103,7 @@ export interface EraseIdDeps {
103
103
  * source is used if it has one, and otherwise each wait is its bound.
104
104
  */
105
105
  readonly rng?: Rng;
106
- /** Per-chunk decode ceiling (invariant 5); defaults to 1 MiB. */
106
+ /** 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. */
107
107
  readonly maxBitmapBytes?: number;
108
108
  /**
109
109
  * The store's read retry, for the reads the rewrite makes along the way: the generation it rewrites and each of its
@@ -26,7 +26,7 @@ export type MetricEvent = {
26
26
  readonly kind: 'storage.get';
27
27
  readonly namespace?: string;
28
28
  readonly segment: string;
29
- /** Bytes returned (0 if the chunk was absent — a GET still happened). */
29
+ /** One event per request, however many callers were waiting on it. Bytes returned (0 if the chunk was absent — a GET still happened). */
30
30
  readonly bytes: number;
31
31
  /**
32
32
  * Elapsed wall time of the read — includes any transient-retry backoff on the storage call. From the
@@ -34,6 +34,12 @@ export type MetricEvent = {
34
34
  */
35
35
  readonly ms: number;
36
36
  } | {
37
+ /**
38
+ * 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.
42
+ */
37
43
  readonly kind: 'cache';
38
44
  readonly hit: boolean;
39
45
  } | {