@cloudbitmaps/core 0.12.0 → 0.13.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
+ }
@@ -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
  *
@@ -255,7 +268,15 @@ export declare class SegmentEngine {
255
268
  * `gen === undefined` ⇒ the source can't report a generation (it only ever serves one) ⇒ the key stays
256
269
  * generation-free. Superseded-generation entries age out under the LRU ceiling — no active purge.
257
270
  *
258
- * The returned instance is **shared** (it may be the cached one): callers read it or clone it, never mutate it.
271
+ * Concurrent misses of one key share **one** request, with or without a cache: the first caller starts the read,
272
+ * later ones await it, and the entry goes when the read settles, so a failure is every waiting caller's and the
273
+ * next call reads again. The key carries the version exactly as the cache's does, so two versions of one chunk
274
+ * are two reads, never one.
275
+ *
276
+ * The returned instance is **shared** (it may be the cached one, and every caller that joined a read gets the same
277
+ * one): callers read it or clone it, never mutate it.
259
278
  */
260
279
  private storageChunk;
280
+ /** One request for one chunk, decoded, range-checked and cached. */
281
+ private fetchChunk;
261
282
  }
@@ -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
  } | {
package/dist/index.js CHANGED
@@ -102,6 +102,110 @@ async function collectWithinBudget(source, budget, op) {
102
102
  return out;
103
103
  }
104
104
 
105
+ // src/core/chunk-window.ts
106
+ var ChunkWindow = class {
107
+ constructor(keys, fetch, max, ramp) {
108
+ this.keys = keys;
109
+ this.fetch = fetch;
110
+ this.max = max;
111
+ this.ramp = ramp;
112
+ }
113
+ keys;
114
+ fetch;
115
+ max;
116
+ ramp;
117
+ open = [];
118
+ launched = 0;
119
+ taken = 0;
120
+ /** The next read's result in key order, or the error that read raised. */
121
+ async take() {
122
+ const width = this.ramp ? Math.min(this.max, 2 ** Math.min(this.taken, 30)) : this.max;
123
+ while (this.launched < this.keys.length && this.launched - this.taken < width) {
124
+ this.open.push(
125
+ this.fetch(this.keys[this.launched++]).then(
126
+ (value) => ({ value }),
127
+ (cause) => ({ value: void 0, error: { cause } })
128
+ )
129
+ );
130
+ }
131
+ const slot = await this.open.shift();
132
+ this.taken += 1;
133
+ if (slot.error) throw slot.error.cause;
134
+ return slot.value;
135
+ }
136
+ };
137
+
138
+ // src/core/crbm/format.ts
139
+ var MAGIC = Uint8Array.of(67, 82, 66, 77);
140
+ var VERSION_MAJOR = 1;
141
+ var VERSION_MINOR = 0;
142
+ var PREAMBLE_BYTES = 8;
143
+ var PAYLOAD_START = PREAMBLE_BYTES;
144
+ var FOOTER_BYTES = 104;
145
+ var FOOTER = {
146
+ indexOffset: 0,
147
+ // u64
148
+ indexLength: 8,
149
+ // u64
150
+ indexCrc32c: 16,
151
+ // u32
152
+ flags: 20,
153
+ // u32
154
+ payloadCodecId: 24,
155
+ // u16 — which codec wrote the chunk payloads
156
+ elementWidth: 26,
157
+ // u8
158
+ containerCodec: 27,
159
+ // u8
160
+ versionMajor: 28,
161
+ // u8
162
+ versionMinor: 29,
163
+ // u8
164
+ reserved2: 30,
165
+ // u16
166
+ generation: 32,
167
+ // u64
168
+ indexNonce: 40,
169
+ // 12 B
170
+ indexTag: 52,
171
+ // 16 B
172
+ keyId: 68,
173
+ // 16 B
174
+ chunkCount: 84,
175
+ // u32
176
+ totalCardinality: 88,
177
+ // u64
178
+ footerCrc32c: 96,
179
+ // u32 — covers footer bytes [0, 96)
180
+ endMagic: 100
181
+ // char[4]
182
+ };
183
+ var FOOTER_CRC_COVERAGE = FOOTER.footerCrc32c;
184
+ var FLAG_ENCRYPTED = 1 << 0;
185
+ var FLAG_INDEX_COMPRESSED = 1 << 1;
186
+ var FLAG_LITTLE_ENDIAN = 1 << 2;
187
+ var FLAG_EXTENSION = 1 << 3;
188
+ var KNOWN_FLAGS = FLAG_ENCRYPTED | FLAG_LITTLE_ENDIAN | FLAG_EXTENSION;
189
+ var ELEMENT_WIDTH_32 = 32;
190
+ var PAYLOAD_CODEC_ROARING_PORTABLE = 1;
191
+ var KNOWN_PAYLOAD_CODEC_IDS = /* @__PURE__ */ new Set([
192
+ PAYLOAD_CODEC_ROARING_PORTABLE
193
+ ]);
194
+ var CONTAINER_CODEC_NONE = 0;
195
+ var DEFAULT_TAIL_BYTES = 256 * 1024;
196
+ var DEFAULT_MAX_INDEX_BYTES = 8 * 1024 * 1024;
197
+ var DEFAULT_MAX_BITMAP_BYTES = 1 << 20;
198
+ var MAX_CHUNK_CARDINALITY = 65536;
199
+ var EXT_MAGIC = Uint8Array.of(67, 82, 66, 88);
200
+ var EXT_TRAILER_BYTES = 12;
201
+ var EXT_SECTION_HEADER_BYTES = 5;
202
+ var MAX_EXT_BYTES = 4 * 1024;
203
+ var EXT_SECTION_METADATA = 1;
204
+ var CRC32C_BYTES = 4;
205
+ var AEAD_NONCE_BYTES = 12;
206
+ var AEAD_TAG_BYTES = 16;
207
+ var DEFAULT_MAX_PAYLOAD_BYTES = DEFAULT_MAX_BITMAP_BYTES + AEAD_NONCE_BYTES + AEAD_TAG_BYTES;
208
+
105
209
  // src/core/keys.ts
106
210
  var FIELD = " ";
107
211
  var NO_NAMESPACE = "/";
@@ -214,8 +318,8 @@ var CountingMetricsSink = class _CountingMetricsSink {
214
318
  };
215
319
 
216
320
  // src/core/engine.ts
217
- var DEFAULT_MAX_BITMAP_BYTES = 1 << 20;
218
- var DEFAULT_INTERSECT_CONCURRENCY = 8;
321
+ var DEFAULT_INTERSECT_CONCURRENCY = 32;
322
+ var COMBINE_WINDOW_START = 8;
219
323
  var ZERO_CLOCK = { now: () => 0 };
220
324
  function windowOf(range) {
221
325
  const after = range?.after;
@@ -268,37 +372,6 @@ function keysWithin(keys, w) {
268
372
  };
269
373
  return keys.slice(firstAtLeast(w.loKey), firstAtLeast(w.hiKey + 1));
270
374
  }
271
- var ChunkWindow = class {
272
- constructor(keys, fetch, max, ramp) {
273
- this.keys = keys;
274
- this.fetch = fetch;
275
- this.max = max;
276
- this.ramp = ramp;
277
- }
278
- keys;
279
- fetch;
280
- max;
281
- ramp;
282
- open = [];
283
- launched = 0;
284
- taken = 0;
285
- /** The next chunk in key order (null if the source holds none), or the error its read raised. */
286
- async take() {
287
- const width = this.ramp ? Math.min(this.max, 2 ** Math.min(this.taken, 30)) : this.max;
288
- while (this.launched < this.keys.length && this.launched - this.taken < width) {
289
- this.open.push(
290
- this.fetch(this.keys[this.launched++]).then(
291
- (chunk) => ({ chunk }),
292
- (cause) => ({ chunk: null, error: { cause } })
293
- )
294
- );
295
- }
296
- const slot = await this.open.shift();
297
- this.taken += 1;
298
- if (slot.error) throw slot.error.cause;
299
- return slot.chunk;
300
- }
301
- };
302
375
  var SegmentEngine = class {
303
376
  storage;
304
377
  cache;
@@ -309,6 +382,12 @@ var SegmentEngine = class {
309
382
  metricsOn;
310
383
  /** Resolved per-op budget (null = disabled); undefined deps ⇒ the generous default. */
311
384
  budget;
385
+ /**
386
+ * The chunk reads open right now, by cache key: a caller that misses the cache and finds its key here awaits that
387
+ * read instead of making its own. An entry lives from the request until it settles, so the map holds at most one
388
+ * promise per distinct key in flight.
389
+ */
390
+ openReads = /* @__PURE__ */ new Map();
312
391
  constructor(deps) {
313
392
  this.storage = deps.storage;
314
393
  this.cache = deps.cache;
@@ -389,7 +468,7 @@ var SegmentEngine = class {
389
468
  );
390
469
  }
391
470
  /**
392
- * Every id, ascending, reading ahead through a window of up to 8 chunk fetches that opens 1, 2, 4, 8 wide, so a
471
+ * Every id, ascending, reading ahead through a window of up to 32 chunk fetches that opens 1, 2, 4 … 32 wide, so a
393
472
  * 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
394
473
  * chunks the range overlaps (see {@link IdRange}).
395
474
  *
@@ -548,10 +627,17 @@ var SegmentEngine = class {
548
627
  );
549
628
  const inFlight = [];
550
629
  let next = 0;
551
- while (next < common.length && inFlight.length < limit) inFlight.push(startAt(common[next++]));
630
+ let taken = 0;
631
+ const fill = () => {
632
+ const width = Math.min(limit, Math.max(COMBINE_WINDOW_START, 2 ** Math.min(taken, 30)));
633
+ while (next < common.length && inFlight.length < width)
634
+ inFlight.push(startAt(common[next++]));
635
+ };
636
+ fill();
552
637
  while (inFlight.length > 0) {
553
638
  const slot = await inFlight.shift();
554
- if (next < common.length) inFlight.push(startAt(common[next++]));
639
+ taken += 1;
640
+ fill();
555
641
  if (slot.error !== void 0) throw slot.error;
556
642
  if (slot.result && !slot.result.isEmpty) {
557
643
  if (w !== null && isEdge(slot.key, w)) {
@@ -574,9 +660,13 @@ var SegmentEngine = class {
574
660
  async combineChunk(operands, excludes, mode, chunkKey) {
575
661
  const present = mode === "all" ? operands : operands.filter((o) => o.keys.has(chunkKey));
576
662
  if (present.length === 0) return null;
577
- const chunks = await Promise.all(
578
- present.map((op) => this.storageChunk({ ...op.seg, chunkKey }, op.gen))
663
+ const relevant = excludes.filter((e) => e.keys.has(chunkKey));
664
+ const together = relevant.length > 0 && (mode === "any" || present.length === 1);
665
+ const reads = together ? [...present, ...relevant] : present;
666
+ const fetched = await Promise.all(
667
+ reads.map((op) => this.storageChunk({ ...op.seg, chunkKey }, op.gen))
579
668
  );
669
+ const chunks = together ? fetched.slice(0, present.length) : fetched;
580
670
  if (mode === "all" && chunks.some((c) => c === null)) return null;
581
671
  const first = chunks[0];
582
672
  const acc = first ? first.clone() : this.codec.empty();
@@ -591,11 +681,8 @@ var SegmentEngine = class {
591
681
  }
592
682
  }
593
683
  if (acc.isEmpty) return null;
594
- const relevant = excludes.filter((e) => e.keys.has(chunkKey));
595
684
  if (relevant.length > 0) {
596
- const cuts = await Promise.all(
597
- relevant.map((e) => this.storageChunk({ ...e.seg, chunkKey }, e.gen))
598
- );
685
+ const cuts = together ? fetched.slice(present.length) : await Promise.all(relevant.map((e) => this.storageChunk({ ...e.seg, chunkKey }, e.gen)));
599
686
  for (const cut of cuts) {
600
687
  if (cut === null) continue;
601
688
  acc.andNotInPlace(cut);
@@ -699,6 +786,7 @@ var SegmentEngine = class {
699
786
  invalidate(ref) {
700
787
  const prefix = segmentPrefix(ref);
701
788
  this.cache?.deleteWhere((key) => key.startsWith(prefix));
789
+ for (const key of this.openReads.keys()) if (key.startsWith(prefix)) this.openReads.delete(key);
702
790
  this.storage.invalidate?.(ref);
703
791
  }
704
792
  /** The segment's current generation, resolved once per op (`undefined` ⇒ source can't report it). */
@@ -728,7 +816,13 @@ var SegmentEngine = class {
728
816
  * `gen === undefined` ⇒ the source can't report a generation (it only ever serves one) ⇒ the key stays
729
817
  * generation-free. Superseded-generation entries age out under the LRU ceiling — no active purge.
730
818
  *
731
- * The returned instance is **shared** (it may be the cached one): callers read it or clone it, never mutate it.
819
+ * Concurrent misses of one key share **one** request, with or without a cache: the first caller starts the read,
820
+ * later ones await it, and the entry goes when the read settles, so a failure is every waiting caller's and the
821
+ * next call reads again. The key carries the version exactly as the cache's does, so two versions of one chunk
822
+ * are two reads, never one.
823
+ *
824
+ * The returned instance is **shared** (it may be the cached one, and every caller that joined a read gets the same
825
+ * one): callers read it or clone it, never mutate it.
732
826
  */
733
827
  async storageChunk(ref, gen) {
734
828
  if (gen === null) return null;
@@ -741,6 +835,19 @@ var SegmentEngine = class {
741
835
  }
742
836
  if (this.metricsOn) this.metrics.onEvent({ kind: "cache", hit: false });
743
837
  }
838
+ const open4 = this.openReads.get(cacheKey);
839
+ if (open4) return open4.read;
840
+ const token = {};
841
+ const entry = { token, read: this.fetchChunk(ref, cacheKey, token) };
842
+ this.openReads.set(cacheKey, entry);
843
+ const settled = () => {
844
+ if (this.openReads.get(cacheKey) === entry) this.openReads.delete(cacheKey);
845
+ };
846
+ entry.read.then(settled, settled);
847
+ return entry.read;
848
+ }
849
+ /** One request for one chunk, decoded, range-checked and cached. */
850
+ async fetchChunk(ref, cacheKey, token) {
744
851
  const startedAt = this.metricsOn ? this.clock.now() : 0;
745
852
  const bytes = await this.storage.getChunk(ref);
746
853
  if (this.metricsOn) {
@@ -755,7 +862,7 @@ var SegmentEngine = class {
755
862
  if (!bytes) return null;
756
863
  const bitmap = this.codec.safeDeserialize(bytes, this.maxBitmapBytes);
757
864
  this.assertChunkPayloadInRange(bitmap, ref.chunkKey);
758
- this.cache?.set(cacheKey, bitmap);
865
+ if (this.openReads.get(cacheKey)?.token === token) this.cache?.set(cacheKey, bitmap);
759
866
  return bitmap;
760
867
  }
761
868
  };
@@ -2269,76 +2376,6 @@ function aadFor(ref, generation, scope) {
2269
2376
  return out;
2270
2377
  }
2271
2378
 
2272
- // src/core/crbm/format.ts
2273
- var MAGIC = Uint8Array.of(67, 82, 66, 77);
2274
- var VERSION_MAJOR = 1;
2275
- var VERSION_MINOR = 0;
2276
- var PREAMBLE_BYTES = 8;
2277
- var PAYLOAD_START = PREAMBLE_BYTES;
2278
- var FOOTER_BYTES = 104;
2279
- var FOOTER = {
2280
- indexOffset: 0,
2281
- // u64
2282
- indexLength: 8,
2283
- // u64
2284
- indexCrc32c: 16,
2285
- // u32
2286
- flags: 20,
2287
- // u32
2288
- payloadCodecId: 24,
2289
- // u16 — which codec wrote the chunk payloads
2290
- elementWidth: 26,
2291
- // u8
2292
- containerCodec: 27,
2293
- // u8
2294
- versionMajor: 28,
2295
- // u8
2296
- versionMinor: 29,
2297
- // u8
2298
- reserved2: 30,
2299
- // u16
2300
- generation: 32,
2301
- // u64
2302
- indexNonce: 40,
2303
- // 12 B
2304
- indexTag: 52,
2305
- // 16 B
2306
- keyId: 68,
2307
- // 16 B
2308
- chunkCount: 84,
2309
- // u32
2310
- totalCardinality: 88,
2311
- // u64
2312
- footerCrc32c: 96,
2313
- // u32 — covers footer bytes [0, 96)
2314
- endMagic: 100
2315
- // char[4]
2316
- };
2317
- var FOOTER_CRC_COVERAGE = FOOTER.footerCrc32c;
2318
- var FLAG_ENCRYPTED = 1 << 0;
2319
- var FLAG_INDEX_COMPRESSED = 1 << 1;
2320
- var FLAG_LITTLE_ENDIAN = 1 << 2;
2321
- var FLAG_EXTENSION = 1 << 3;
2322
- var KNOWN_FLAGS = FLAG_ENCRYPTED | FLAG_LITTLE_ENDIAN | FLAG_EXTENSION;
2323
- var ELEMENT_WIDTH_32 = 32;
2324
- var PAYLOAD_CODEC_ROARING_PORTABLE = 1;
2325
- var KNOWN_PAYLOAD_CODEC_IDS = /* @__PURE__ */ new Set([
2326
- PAYLOAD_CODEC_ROARING_PORTABLE
2327
- ]);
2328
- var CONTAINER_CODEC_NONE = 0;
2329
- var DEFAULT_TAIL_BYTES = 256 * 1024;
2330
- var DEFAULT_MAX_INDEX_BYTES = 8 * 1024 * 1024;
2331
- var DEFAULT_MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
2332
- var MAX_CHUNK_CARDINALITY = 65536;
2333
- var EXT_MAGIC = Uint8Array.of(67, 82, 66, 88);
2334
- var EXT_TRAILER_BYTES = 12;
2335
- var EXT_SECTION_HEADER_BYTES = 5;
2336
- var MAX_EXT_BYTES = 4 * 1024;
2337
- var EXT_SECTION_METADATA = 1;
2338
- var CRC32C_BYTES = 4;
2339
- var AEAD_NONCE_BYTES = 12;
2340
- var AEAD_TAG_BYTES = 16;
2341
-
2342
2379
  // src/core/crbm/crc32c.ts
2343
2380
  var TABLES = (() => {
2344
2381
  const POLY = 2197175160;
@@ -2540,7 +2577,6 @@ var CrbmReader = class _CrbmReader {
2540
2577
  }
2541
2578
  static async open(blob, options = {}) {
2542
2579
  const tailBytes = Math.max(options.tailBytes ?? DEFAULT_TAIL_BYTES, FOOTER_BYTES);
2543
- const maxPayloadBytes = options.maxPayloadBytes ?? DEFAULT_MAX_PAYLOAD_BYTES;
2544
2580
  const maxIndexBytes = options.maxIndexBytes ?? DEFAULT_MAX_INDEX_BYTES;
2545
2581
  const { bytes: tail, size } = await blob.getTail(tailBytes);
2546
2582
  const { footer, fview, storedFooterCrc } = checkedFooter(tail, size);
@@ -2682,7 +2718,7 @@ var CrbmReader = class _CrbmReader {
2682
2718
  const index = parseIndex(
2683
2719
  indexForParse,
2684
2720
  payloadEnd,
2685
- maxPayloadBytes,
2721
+ options.maxPayloadBytes ?? (encrypted ? DEFAULT_MAX_PAYLOAD_BYTES : DEFAULT_MAX_BITMAP_BYTES),
2686
2722
  encrypted ? AEAD_NONCE_BYTES + AEAD_TAG_BYTES : 1
2687
2723
  );
2688
2724
  if (encrypted) {
@@ -2869,7 +2905,9 @@ function parseIndex(indexBytes, payloadEnd, maxPayloadBytes, minPayloadBytes = 1
2869
2905
  }
2870
2906
  if (chunkKey > 65535) throw new IntegrityError(`.crbm chunkKey ${chunkKey} out of range`);
2871
2907
  if (len < minPayloadBytes || len > maxPayloadBytes) {
2872
- throw new IntegrityError(`.crbm chunk ${chunkKey} length ${len} invalid`);
2908
+ throw new IntegrityError(
2909
+ `.crbm chunk ${chunkKey} length ${len} invalid (most is ${maxPayloadBytes})`
2910
+ );
2873
2911
  }
2874
2912
  if (card < 1 || card > MAX_CHUNK_CARDINALITY) {
2875
2913
  throw new IntegrityError(`.crbm chunk ${chunkKey} cardinality ${card} invalid`);
@@ -5094,12 +5132,11 @@ function describingSummary(record) {
5094
5132
  }
5095
5133
 
5096
5134
  // src/core/erase-id.ts
5097
- var DEFAULT_MAX_BITMAP_BYTES2 = 1 << 20;
5098
5135
  async function eraseIdFromSegment(ref, id, deps, options = {}) {
5099
5136
  validateUserRef(ref);
5100
5137
  const { chunkKey, remainder } = splitId(id);
5101
5138
  const codec = requireCodec(deps.codec, "eraseIdFromSegment");
5102
- const maxBytes = deps.maxBitmapBytes ?? DEFAULT_MAX_BITMAP_BYTES2;
5139
+ const maxBytes = deps.maxBitmapBytes ?? DEFAULT_MAX_BITMAP_BYTES;
5103
5140
  const base = { segment: ref.segment, namespace: ref.namespace };
5104
5141
  const read = (op) => retryRead(op, deps.readRetry);
5105
5142
  const record = await deps.registry.get(ref);
@@ -5311,14 +5348,21 @@ async function eraseIdFromSegment(ref, id, deps, options = {}) {
5311
5348
  if (left !== void 0) throw cannotRemove(left);
5312
5349
  return { ...base, erased: true, fromGeneration: from, generation, collected };
5313
5350
  }
5351
+ var REWRITE_READ_AHEAD = 32;
5314
5352
  async function* rewrite(reader, chunkKey, replacement, codec, maxBytes, read) {
5315
5353
  const keys = [...reader.chunkKeys()].sort((a, b) => a - b);
5354
+ const window = new ChunkWindow(
5355
+ keys.filter((k) => k !== chunkKey),
5356
+ (k) => read(() => reader.getChunk(k)),
5357
+ REWRITE_READ_AHEAD,
5358
+ false
5359
+ );
5316
5360
  for (const k of keys) {
5317
5361
  if (k === chunkKey) {
5318
5362
  yield { chunkKey: k, bitmap: replacement };
5319
5363
  continue;
5320
5364
  }
5321
- const bytes = await read(() => reader.getChunk(k));
5365
+ const bytes = await window.take();
5322
5366
  if (bytes === null) continue;
5323
5367
  const bitmap = codec.safeDeserialize(bytes, maxBytes);
5324
5368
  assertRemaindersInRange(bitmap, k);