@cloudbitmaps/roaring 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/index.d.ts CHANGED
@@ -136,14 +136,17 @@ export interface CacheOptions {
136
136
  /**
137
137
  * Ceiling on how many segments' `.crbm` readers (each holding a parsed index) the store keeps open at once
138
138
  * (default 1024) — the steady-state memory bound for a long-running server that reads across many segments.
139
- * Past it the least-recently-used segment's reader is evicted; re-opening it later is one cheap tail GET.
139
+ * Past it the least-recently-used segment's reader is evicted; re-opening it later is one cheap tail GET. A reader of a small generation, one whose whole object came with its tail read,
140
+ * also holds its chunk bytes, so a read of it makes no chunk request until the reader is evicted or the pointer refresh
141
+ * moves it on; a store with no timed refresh (`genTtlMs: 0`, or no registry) keeps none.
140
142
  * Applies whenever the store builds its own read path — a backend or a bare `IStorageDriver`. A pre-built
141
143
  * `StorageChunkSource` manages its own reader cache.
142
144
  */
143
145
  readonly readerMax?: number;
144
146
  /**
145
- * Aggregate byte ceiling on what the open readers hold, their parsed `.crbm` indices and the metadata a
146
- * generation carries (default 64 MiB) — the byte half
147
+ * Aggregate byte ceiling on what the open readers hold, their parsed `.crbm` indices, the metadata a
148
+ * generation carries, and the chunk bytes a reader keeps when its tail read returned a whole object whose chunks
149
+ * total at most this ceiling divided by {@link CacheOptions.readerMax} (default 64 MiB) — the byte half
147
150
  * of the memory bound, complementing the {@link CacheOptions.readerMax} *count* bound. A wide/dense segment's
148
151
  * parsed index can reach about 1.3 MB, so a count-only bound could let the open readers pin over a GB and blow a small
149
152
  * heap (e.g. a 128 MB Lambda); this evicts the least-recently-used reader once the summed index footprint
@@ -959,11 +962,15 @@ export declare class CloudRoaring {
959
962
  */
960
963
  export interface BaseCombineOptions extends IdRange {
961
964
  /**
962
- * Max chunk keys resolved concurrently — bounds the Storage footprint (about `concurrency × operands` chunks in
963
- * flight). A positive integer; default 32. The window opens 8 keys wide (or `concurrency`, if lower) and widens
964
- * as keys are taken, so a read that stops early fetches no more than a few keys ahead; one that runs to the end
965
- * spends nearly all of its round trips at the full width. A read that stops early has requested up to
966
- * `concurrency` keys per operand past the last one it used.
965
+ * How far ahead each operand is read: a positive integer, default 32. The library's own stores read each operand as
966
+ * one stream of coalesced ranges (chunks that sit near each other are one request), and `concurrency` is how many
967
+ * range requests a stream holds ahead of the read, in flight or landed and not yet used. So about
968
+ * `concurrency × operands` requests are in flight at most, and, since a range is at most 1 MiB (one chunk, when a chunk
969
+ * is larger), about `concurrency × operands` MiB are held, whatever the segment's size. The stream opens 4 ranges wide
970
+ * (or `concurrency`, if lower) and widens as ranges are taken, so a read that stops early has asked for little past
971
+ * where it stopped, and a read that fits one range makes one request however many chunks it needs. A source that
972
+ * reads chunk by chunk (a custom one) is read as before: `concurrency` chunk keys at once, opening 8 wide. A running
973
+ * stream holds its operand's reader outside the reader cache's bounds until it ends or the segment moves on.
967
974
  */
968
975
  readonly concurrency?: number;
969
976
  /** Override the store's per-op denial-of-wallet budget for this call (`false` lifts it). */
@@ -1051,6 +1058,36 @@ export interface MaterializeOptions extends CombineOptions {
1051
1058
  */
1052
1059
  readonly keep?: number;
1053
1060
  }
1061
+ /**
1062
+ * What a streaming read returns: the ids, ascending, one at a time under `for await`, or one chunk at a time from
1063
+ * {@link IdStream.batches}.
1064
+ *
1065
+ * It is an `AsyncIterable<number>`, so every consumer of an id stream takes it as it always did. A live read is the
1066
+ * engine's async generator itself, with `batches` attached: `for await` it, or drive it with `next()`, `return()` and
1067
+ * `throw()`, exactly as before, and it is single-use (a second `for await` over it yields nothing). `batches()` is a
1068
+ * separate, new read: it starts when called, fetches its chunks afresh and charges the per-op budget again, whether or
1069
+ * not the per-id stream was read, and reading both is two reads (invariant 3 applies to each separately). Nothing is
1070
+ * fetched until a read is first pulled.
1071
+ */
1072
+ export interface IdStream extends AsyncIterable<number> {
1073
+ /**
1074
+ * The same ids as the per-id stream, in the same order, one `Uint32Array` per chunk: each array ascending, holding
1075
+ * every id of one chunk the read yields (a range trimmed at its edges exactly as the per-id stream trims it), and
1076
+ * no array is empty. Chunks arrive in ascending order, so the concatenation is the per-id stream.
1077
+ *
1078
+ * One `await` per chunk instead of one per id: measured locally, an in-memory read goes several times faster, and
1079
+ * a large scan stops being bound by the event loop. The reads, the budget and the read-ahead window are the per-id
1080
+ * stream's, and so is stopping early: leaving a `for await` over it (`break`, `return`, a throw) ends the read.
1081
+ *
1082
+ * **Memory:** one array per chunk, at most 65,536 ids (256 KiB), allocated for that chunk alone and yours to
1083
+ * keep, change or hand on; the read holds no reference to it after yielding it.
1084
+ *
1085
+ * ```ts
1086
+ * for await (const ids of seg.andNot([optOut]).batches()) await send(ids);
1087
+ * ```
1088
+ */
1089
+ batches(): AsyncIterable<Uint32Array>;
1090
+ }
1054
1091
  /**
1055
1092
  * What `andNotInto` takes: the write options without `exclude`, because its `excludes` argument IS the
1056
1093
  * subtraction — a second one in the options would be two spellings of one thing.
@@ -1263,7 +1300,7 @@ export declare class Segment {
1263
1300
  * stream is first read. `after >= through` is an empty range, which reads nothing. An expired handle reads empty
1264
1301
  * without checking its options, as every read of one does.
1265
1302
  */
1266
- iterate(options?: IdRange): AsyncIterable<number>;
1303
+ iterate(options?: IdRange): IdStream;
1267
1304
  /**
1268
1305
  * The options a combine hands the engine, read once, when it is called ({@link readOptions}), with the `exclude`
1269
1306
  * it was given, already reduced to its live handles ({@link liveExcludes}) and mapped down to the plain refs
@@ -1288,12 +1325,13 @@ export declare class Segment {
1288
1325
  /**
1289
1326
  * Chunk-skipping intersection: stream the ids in **this** segment AND every segment in `others`, ascending.
1290
1327
  * Fetches only the Storage chunks present in *all* operands (a key absent from any operand contributes nothing
1291
- * and is never downloaded), streaming under a bounded in-flight window — so the Storage footprint stays small
1292
- * (Lambda-friendly) regardless of segment size. Pass `concurrency` to tune that window (a positive integer, 32 by default).
1328
+ * and is never downloaded), reading each operand's as coalesced ranges under a bounded in-flight window — so the Storage
1329
+ * footprint stays small (Lambda-friendly) regardless of segment size. Pass `concurrency` to tune that window (the range
1330
+ * requests held ahead per operand: a positive integer, 32 by default).
1293
1331
  * AND is commutative, so `a.intersect([b])` and `b.intersect([a])` yield the same ids. Pass `budget` to
1294
1332
  * override the store's per-op denial-of-wallet budget for this call (or `false` to lift it).
1295
1333
  */
1296
- intersect(others: Segment[], options?: CombineOptions): AsyncIterable<number>;
1334
+ intersect(others: Segment[], options?: CombineOptions): IdStream;
1297
1335
  /**
1298
1336
  * Materialize `this ∩ others…` (minus `exclude`) as a **new generation of `dest`** — `dest`'s previous contents
1299
1337
  * are superseded, not added to. Streaming + bounded-memory; the result is one immutable object published
@@ -1325,7 +1363,7 @@ export declare class Segment {
1325
1363
  * deliberately. If you find yourself unioning the same segments on every read, materializing the combined
1326
1364
  * segment once (`unionInto`, or a load) is the cheaper shape.
1327
1365
  */
1328
- union(others: Segment[], options?: CombineOptions): AsyncIterable<number>;
1366
+ union(others: Segment[], options?: CombineOptions): IdStream;
1329
1367
  /** Materialize `this ∪ others…` (minus `exclude`) as a **new generation of `dest`** — see {@link intersectInto}. */
1330
1368
  unionInto(dest: Segment, others: Segment[], options?: MaterializeOptions): Promise<MaterializeResult>;
1331
1369
  /**
@@ -1343,7 +1381,7 @@ export declare class Segment {
1343
1381
  * To filter the *result of an intersection*, do not chain — pass `exclude` to {@link intersect} instead, so
1344
1382
  * the suppression folds into the same pass rather than materializing an intermediate segment first.
1345
1383
  */
1346
- andNot(excludes: Segment[], options?: BaseCombineOptions): AsyncIterable<number>;
1384
+ andNot(excludes: Segment[], options?: BaseCombineOptions): IdStream;
1347
1385
  /** Materialize `this \ (excludes…)` as a **new generation of `dest`** — see {@link intersectInto}. */
1348
1386
  andNotInto(dest: Segment, excludes: Segment[], options?: AndNotIntoOptions): Promise<MaterializeResult>;
1349
1387
  /**
package/dist/index.js CHANGED
@@ -439,6 +439,10 @@ var SafeBitmap = class _SafeBitmap {
439
439
  toArray() {
440
440
  return this.bitmap.toArray();
441
441
  }
442
+ /** The values as one fresh ascending `Uint32Array`, exported natively. */
443
+ toUint32Array() {
444
+ return this.bitmap.toUint32Array();
445
+ }
442
446
  };
443
447
  function bitmapAsLoadInput(input) {
444
448
  return input instanceof RoaringBitmap32 ? { bitmap: input } : input;
@@ -1658,9 +1662,14 @@ var CloudRoaring = class _CloudRoaring {
1658
1662
  return estimateCost(input);
1659
1663
  }
1660
1664
  };
1661
- var failing = (err) => ({
1662
- [Symbol.asyncIterator]: () => ({ next: () => Promise.reject(err) })
1663
- });
1665
+ var withBatches = (gen, batches) => Object.assign(gen, { batches });
1666
+ var failing = (err) => {
1667
+ const rejecting = () => ({ next: () => Promise.reject(err) });
1668
+ return {
1669
+ [Symbol.asyncIterator]: rejecting,
1670
+ batches: () => ({ [Symbol.asyncIterator]: rejecting })
1671
+ };
1672
+ };
1664
1673
  var samePin = (a, b) => a === void 0 || b === void 0 ? a === b : a.generation === b.generation && a.fingerprint === b.fingerprint;
1665
1674
  var twoPins = (a, b) => {
1666
1675
  const one = (p) => p === void 0 ? "live" : `pinned at generation ${p.generation ?? "none"}`;
@@ -1679,6 +1688,8 @@ var readOptions = (options) => ({
1679
1688
  var NO_SEGMENTS = [];
1680
1689
  var EMPTY_IDS = {
1681
1690
  async *[Symbol.asyncIterator]() {
1691
+ },
1692
+ async *batches() {
1682
1693
  }
1683
1694
  };
1684
1695
  var KEEP_EVERY_GENERATION = Number.MAX_SAFE_INTEGER;
@@ -1934,7 +1945,9 @@ var Segment = class _Segment {
1934
1945
  if (this.expired()) return EMPTY_IDS;
1935
1946
  const range = options == null ? void 0 : rangeOf(options);
1936
1947
  const none = range === void 0 || range.after === void 0 && range.through === void 0;
1937
- return this.engine.iterate(this.ref, none ? void 0 : range);
1948
+ const { engine, ref } = this;
1949
+ const bounds = none ? void 0 : range;
1950
+ return withBatches(engine.iterate(ref, bounds), () => engine.iterateBatches(ref, bounds));
1938
1951
  }
1939
1952
  /**
1940
1953
  * The options a combine hands the engine, read once, when it is called ({@link readOptions}), with the `exclude`
@@ -1970,8 +1983,9 @@ var Segment = class _Segment {
1970
1983
  /**
1971
1984
  * Chunk-skipping intersection: stream the ids in **this** segment AND every segment in `others`, ascending.
1972
1985
  * Fetches only the Storage chunks present in *all* operands (a key absent from any operand contributes nothing
1973
- * and is never downloaded), streaming under a bounded in-flight window — so the Storage footprint stays small
1974
- * (Lambda-friendly) regardless of segment size. Pass `concurrency` to tune that window (a positive integer, 32 by default).
1986
+ * and is never downloaded), reading each operand's as coalesced ranges under a bounded in-flight window — so the Storage
1987
+ * footprint stays small (Lambda-friendly) regardless of segment size. Pass `concurrency` to tune that window (the range
1988
+ * requests held ahead per operand: a positive integer, 32 by default).
1975
1989
  * AND is commutative, so `a.intersect([b])` and `b.intersect([a])` yield the same ids. Pass `budget` to
1976
1990
  * override the store's per-op denial-of-wallet budget for this call (or `false` to lift it).
1977
1991
  */
@@ -1984,7 +1998,9 @@ var Segment = class _Segment {
1984
1998
  } catch (err) {
1985
1999
  return failing(err);
1986
2000
  }
1987
- return engine.intersect([this.ref, ...others.map((o) => o.ref)], this.refsIn(options, exclude));
2001
+ const refs = [this.ref, ...others.map((o) => o.ref)];
2002
+ const opts = this.refsIn(options, exclude);
2003
+ return withBatches(engine.intersect(refs, opts), () => engine.intersectBatches(refs, opts));
1988
2004
  }
1989
2005
  /**
1990
2006
  * Materialize `this ∩ others…` (minus `exclude`) as a **new generation of `dest`** — `dest`'s previous contents
@@ -2042,7 +2058,9 @@ var Segment = class _Segment {
2042
2058
  } catch (err) {
2043
2059
  return failing(err);
2044
2060
  }
2045
- return engine.union([this.ref, ...others.map((o) => o.ref)], this.refsIn(options, exclude));
2061
+ const refs = [this.ref, ...others.map((o) => o.ref)];
2062
+ const opts = this.refsIn(options, exclude);
2063
+ return withBatches(engine.union(refs, opts), () => engine.unionBatches(refs, opts));
2046
2064
  }
2047
2065
  /** Materialize `this ∪ others…` (minus `exclude`) as a **new generation of `dest`** — see {@link intersectInto}. */
2048
2066
  async unionInto(dest, others, options) {
@@ -2081,10 +2099,12 @@ var Segment = class _Segment {
2081
2099
  } catch (err) {
2082
2100
  return failing(err);
2083
2101
  }
2084
- return engine.andNot(
2085
- this.ref,
2086
- excludes.map((o) => o.ref),
2087
- options == null ? void 0 : readOptions(options)
2102
+ const base = this.ref;
2103
+ const refs = excludes.map((o) => o.ref);
2104
+ const opts = options == null ? void 0 : readOptions(options);
2105
+ return withBatches(
2106
+ engine.andNot(base, refs, opts),
2107
+ () => engine.andNotBatches(base, refs, opts)
2088
2108
  );
2089
2109
  }
2090
2110
  /** Materialize `this \ (excludes…)` as a **new generation of `dest`** — see {@link intersectInto}. */