@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/PRIVACY.md CHANGED
@@ -44,7 +44,7 @@ from constructing a cross-region topology. The points where personal data moves
44
44
  |---|---|---|
45
45
  | **Storage** (object store) | immutable `.crbm` generations — every generation a segment has had, until a superseded one is collected. Azure Blob objects, and GCS objects above the simple-upload threshold, also carry a random write id in their metadata (`cbwid`): 128 random bits, no data from the bitmap or the source | the region of the bucket you wire |
46
46
  | **Registry** (S3 / GCS / Azure Blob / local) | one row per segment: the current-generation pointer, wrapped keys, retention metadata, and the current generation's id count and your metadata (sealed when the segment is encrypted) — no IDs, unless you put one in the metadata, which you must not. An Azure Blob row also carries the random write id in its metadata | the region of the bucket you wire |
47
- | **cache** (process RAM) | decoded chunks, bounded LRU | **wherever your process/Lambda runs** — an EU segment queried from a US function is processed in the US |
47
+ | **cache** (process RAM) | decoded chunks, and the stored chunk bytes (ciphertext on an encrypted segment) a reader keeps of a small generation it read whole; bounded LRU | **wherever your process/Lambda runs** — an EU segment queried from a US function is processed in the US |
48
48
  | **Loads and rewrites** (`store.load()`, the `*Into` verbs, `eraseSubject`) | read your source (or existing generations), write a new generation | run wherever you run them — a loader in one region writing to a bucket in another is a transfer |
49
49
  | **Intersection** | pulls chunks from N segments into one process | co-locates those segments in one region |
50
50
 
@@ -126,7 +126,7 @@ after it returns, and re-run any erasure a rollback interrupted.
126
126
  Erasure and crypto-shred are **immediate in storage and immediate in the store whose verb performed them**, for
127
127
  every read that starts after the verb returns. A read already in progress in that store can still yield an erased id
128
128
  from a chunk it had requested before the verb ran: up to 32 chunks for `iterate` and `count`, and up to `concurrency`
129
- keys (32 by default) for a combine. They are not immediate in any *other* store, in the same process or another, and this library ships nothing that could
129
+ keys (32 by default) for a combine. A combine or `iterate` reads ranges of the object, and resolves the segment again before it serves each chunk, as a read of that chunk alone does, so the ranges it had already requested are dropped, not served. They are not immediate in any *other* store, in the same process or another, and this library ships nothing that could
130
130
  make them so — there is no daemon, no bus, and no connection between two stores that happen to point at the same
131
131
  bucket.
132
132
 
@@ -136,7 +136,7 @@ bucket.
136
136
  | the store whose verb made the call (`eraseSubject`, `dropSegment`, `retireExpired`) | on return, for every read that starts after it — it invalidates what it cached, and its pins then fail; a read already in progress there can still yield it from a chunk it had requested before |
137
137
  | another store, with a registry and a `cache.genTtlMs` above 0 | within `cache.genTtlMs` (default 2 s), when its snapshot re-resolves, **while the registry can be read** (see below) |
138
138
  | another store with **no registry** (a bare `IStorageDriver`), with `cache: { genTtlMs: 0 }`, or on a storage source built with **no clock** | **no bound** — only when its caches happen to let the segment go, or something tells it |
139
- | a pinned handle (`seg.pin()`) in another store | **no bound** — until that store's reader cache evicts the pin's reader and its chunk cache evicts the chunks the pin decoded, or something tells it |
139
+ | a pinned handle (`seg.pin()`) in another store | **no bound** — until that store's reader cache evicts the pin's reader (in a store with a timed pointer refresh, a small generation's reader holds all of its chunks, decoded or not) and its chunk cache evicts the chunks the pin decoded, or something tells it |
140
140
 
141
141
  **An outage of the registry extends that bound.** A refresh that cannot read the row because of a transient fault
142
142
  (throttling, a 5xx, a dropped connection) keeps serving the generation the reader already holds, and the key it
package/README.md CHANGED
@@ -60,7 +60,7 @@ name.
60
60
  | Option | What it holds |
61
61
  |---|---|
62
62
  | `storage` (required) | a backend (see above) |
63
- | `cache` | `maxChunks`, `genTtlMs` (how soon a reader sees a new load, default 2000 ms), `readerMax`, `readerMaxBytes` |
63
+ | `cache` | `maxChunks`, `ttlMs` (an optional age limit on a cached chunk; omitted, a chunk stays until evicted), `genTtlMs` (how soon a reader sees a new load, default 2000 ms), `readerMax`, `readerMaxBytes` |
64
64
  | `encryption` | `{ keystore, required }`: encrypt segments at rest with your own key |
65
65
  | `retry` | tune the retry of reads, or `false` to turn it off |
66
66
  | `metrics` | a metrics sink; off by default |
@@ -69,10 +69,12 @@ name.
69
69
 
70
70
  ## What it costs
71
71
 
72
- Measured against real S3 in `us-east-1`, from inside the region, with the pointer in the same bucket as the data: the median cold `intersect`
73
- of two 500,000-id segments sharing 100 of their 1,999 chunks made 204 GETs, **$81.60 per million**, requesting only
74
- the shared chunks, and took 492.69 ms from inside the region, by an engine whose combine window held 8 chunk keys (this release opens it 8 wide and widens it to 32, so fewer rounds are expected; not yet measured). A segment's first `store.load()`
75
- is **$11.60 per million**, pointer included, from its measured requests at list prices. A cold `count()` is one pointer read: it reads no payload and no object.
72
+ A cold `intersect` of two 500,000-id segments sharing 100 of their 1,999 chunks is expected to make 6 GETs, **$2.40 per
73
+ million**: counted by running the engine and priced at list, not yet measured on a cloud. On a spread layout it reads more
74
+ bytes than it needs, which cross-region can cost more than the requests it saves. A segment's first `store.load()` is
75
+ **$11.60 per million**, pointer included, from its measured requests at list prices (run of 2026-10-04, in-region, whose
76
+ client had 50 sockets; the client this release builds allows 128, and its effect is not measured). A cold `count()` is one
77
+ pointer read: it reads no payload and no object.
76
78
 
77
79
  The trade is stated plainly. A membership check that misses the cache costs a ranged GET against object storage,
78
80
  where an in-process store costs a memory read. If you need sub-millisecond answers on a working set that fits a
@@ -449,6 +449,10 @@ var SafeBitmap = class _SafeBitmap {
449
449
  toArray() {
450
450
  return this.bitmap.toArray();
451
451
  }
452
+ /** The values as one fresh ascending `Uint32Array`, exported natively. */
453
+ toUint32Array() {
454
+ return this.bitmap.toUint32Array();
455
+ }
452
456
  };
453
457
  function bitmapAsLoadInput(input) {
454
458
  return input instanceof RoaringBitmap32 ? { bitmap: input } : input;
@@ -1668,9 +1672,14 @@ var CloudRoaring = class _CloudRoaring {
1668
1672
  return estimateCost(input);
1669
1673
  }
1670
1674
  };
1671
- var failing = (err) => ({
1672
- [Symbol.asyncIterator]: () => ({ next: () => Promise.reject(err) })
1673
- });
1675
+ var withBatches = (gen, batches) => Object.assign(gen, { batches });
1676
+ var failing = (err) => {
1677
+ const rejecting = () => ({ next: () => Promise.reject(err) });
1678
+ return {
1679
+ [Symbol.asyncIterator]: rejecting,
1680
+ batches: () => ({ [Symbol.asyncIterator]: rejecting })
1681
+ };
1682
+ };
1674
1683
  var samePin = (a, b) => a === void 0 || b === void 0 ? a === b : a.generation === b.generation && a.fingerprint === b.fingerprint;
1675
1684
  var twoPins = (a, b) => {
1676
1685
  const one = (p) => p === void 0 ? "live" : `pinned at generation ${p.generation ?? "none"}`;
@@ -1689,6 +1698,8 @@ var readOptions = (options) => ({
1689
1698
  var NO_SEGMENTS = [];
1690
1699
  var EMPTY_IDS = {
1691
1700
  async *[Symbol.asyncIterator]() {
1701
+ },
1702
+ async *batches() {
1692
1703
  }
1693
1704
  };
1694
1705
  var KEEP_EVERY_GENERATION = Number.MAX_SAFE_INTEGER;
@@ -1944,7 +1955,9 @@ var Segment = class _Segment {
1944
1955
  if (this.expired()) return EMPTY_IDS;
1945
1956
  const range = options == null ? void 0 : rangeOf(options);
1946
1957
  const none = range === void 0 || range.after === void 0 && range.through === void 0;
1947
- return this.engine.iterate(this.ref, none ? void 0 : range);
1958
+ const { engine, ref } = this;
1959
+ const bounds = none ? void 0 : range;
1960
+ return withBatches(engine.iterate(ref, bounds), () => engine.iterateBatches(ref, bounds));
1948
1961
  }
1949
1962
  /**
1950
1963
  * The options a combine hands the engine, read once, when it is called ({@link readOptions}), with the `exclude`
@@ -1980,8 +1993,9 @@ var Segment = class _Segment {
1980
1993
  /**
1981
1994
  * Chunk-skipping intersection: stream the ids in **this** segment AND every segment in `others`, ascending.
1982
1995
  * Fetches only the Storage chunks present in *all* operands (a key absent from any operand contributes nothing
1983
- * and is never downloaded), streaming under a bounded in-flight window — so the Storage footprint stays small
1984
- * (Lambda-friendly) regardless of segment size. Pass `concurrency` to tune that window (a positive integer, 32 by default).
1996
+ * and is never downloaded), reading each operand's as coalesced ranges under a bounded in-flight window — so the Storage
1997
+ * footprint stays small (Lambda-friendly) regardless of segment size. Pass `concurrency` to tune that window (the range
1998
+ * requests held ahead per operand: a positive integer, 32 by default).
1985
1999
  * AND is commutative, so `a.intersect([b])` and `b.intersect([a])` yield the same ids. Pass `budget` to
1986
2000
  * override the store's per-op denial-of-wallet budget for this call (or `false` to lift it).
1987
2001
  */
@@ -1994,7 +2008,9 @@ var Segment = class _Segment {
1994
2008
  } catch (err) {
1995
2009
  return failing(err);
1996
2010
  }
1997
- return engine.intersect([this.ref, ...others.map((o) => o.ref)], this.refsIn(options, exclude));
2011
+ const refs = [this.ref, ...others.map((o) => o.ref)];
2012
+ const opts = this.refsIn(options, exclude);
2013
+ return withBatches(engine.intersect(refs, opts), () => engine.intersectBatches(refs, opts));
1998
2014
  }
1999
2015
  /**
2000
2016
  * Materialize `this ∩ others…` (minus `exclude`) as a **new generation of `dest`** — `dest`'s previous contents
@@ -2052,7 +2068,9 @@ var Segment = class _Segment {
2052
2068
  } catch (err) {
2053
2069
  return failing(err);
2054
2070
  }
2055
- return engine.union([this.ref, ...others.map((o) => o.ref)], this.refsIn(options, exclude));
2071
+ const refs = [this.ref, ...others.map((o) => o.ref)];
2072
+ const opts = this.refsIn(options, exclude);
2073
+ return withBatches(engine.union(refs, opts), () => engine.unionBatches(refs, opts));
2056
2074
  }
2057
2075
  /** Materialize `this ∪ others…` (minus `exclude`) as a **new generation of `dest`** — see {@link intersectInto}. */
2058
2076
  async unionInto(dest, others, options) {
@@ -2091,10 +2109,12 @@ var Segment = class _Segment {
2091
2109
  } catch (err) {
2092
2110
  return failing(err);
2093
2111
  }
2094
- return engine.andNot(
2095
- this.ref,
2096
- excludes.map((o) => o.ref),
2097
- options == null ? void 0 : readOptions(options)
2112
+ const base = this.ref;
2113
+ const refs = excludes.map((o) => o.ref);
2114
+ const opts = options == null ? void 0 : readOptions(options);
2115
+ return withBatches(
2116
+ engine.andNot(base, refs, opts),
2117
+ () => engine.andNotBatches(base, refs, opts)
2098
2118
  );
2099
2119
  }
2100
2120
  /** Materialize `this \ (excludes…)` as a **new generation of `dest`** — see {@link intersectInto}. */