@cloudbitmaps/roaring 0.17.0 → 0.18.1

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
@@ -45,7 +45,7 @@ from constructing a cross-region topology. The points where personal data moves
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
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
- | **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 |
48
+ | **Loads and rewrites** (`store.load()`, the `*Into` verbs, `store.materializeMany()`, `eraseSubject`) | read your source (or existing generations), write a new generation; a fed or held operand of `materializeMany` is read from your own process's memory | 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
 
51
51
  **Guidance (not enforced by the library):** to keep EU data in EU infrastructure, wire region-local drivers
@@ -121,6 +121,14 @@ ledger is not by itself proof the id is gone.
121
121
  generation the erasure did not rewrite, so the id can be current again. Roll back before the erasure starts or
122
122
  after it returns, and re-run any erasure a rollback interrupted.
123
123
 
124
+ ### A batch of materializations
125
+
126
+ A `store.materializeMany` call computes many `*Into` outputs in passes that read each operand once per group, and its window is the call's length, not one read's. An output of a `materializeMany` call can carry an id that was erased while the call ran, for as long as the call ran: the call reads each operand at the generation it pinned, and an output's publish is a load, so an erasure that lands after the chunks were read does not reach what the call holds, unless the output subtracts a pinned operand that the erasure rewrote, which the call re-reads just before the publishes and refuses. An erasure that rewrites a destination before that output's publish starts does not stop the publish, which, subject to the output's own `guard` and the refusal of an empty result over a non-empty destination, writes on top of the erasure's generation; an erasure that lands inside the publish's own write, between its pointer read and its pointer write, makes the publish lose with `WriteConflictError`; after any call that overlapped an erasure, re-run `eraseSubject` and keep both ledgers. `eraseSubject` deletes the generation it rewrites even when a call holds it pinned, so a pin does not outlive an erasure, and an output that still needs a deleted operand generation fails with `NotFoundError`.
127
+
128
+ A call with a feed reads operands the library never stores, which no erasure can reach, so the guarantee above does not describe it, and it is refused instead. A `materializeMany` call with a feed is refused at its next record, and immediately before each fed output's publish (after the output has waited for its room in the memory budget), once `eraseSubject` has started in this store, one already running when the call began included: nothing is published from operands the call read before the erasure returned, apart from the window below, and the one record in hand when the erasure lands is processed and discarded. The counter it reads is moved at the start and at the end of each `eraseSubject` this store runs, so an erasure that began and finished between two of the call's checks is seen too; `rollback`, `retireExpired` and `dropSegment` do not move it. An erasure that starts after a fed output's last check and finishes before that publish's pointer write is not caught by the counter, and its window is the publish's own object write and pointer write, which lasts as long as the object takes to upload; the fences above still apply to what it touched. Only this store's `eraseSubject` moves the counter such a call reads, so an erasure by any other path (another store, another process, or the free function `eraseIdFromSegment` in this process) is not seen by it, and is bounded by nothing. An id erased from the source of a feed has to be erased there: the library holds none of it.
129
+
130
+ A held operand, made by `store.memory`, is the caller's memory too, so no erasure can reach it either, and it is refused instead. A `materializeMany` call that reads an in-memory operand from `store.memory` fails the outputs that read it, before any request and immediately before each of their publishes, once `eraseSubject` has started in this store since the operand began to be made, one already running when it began included; an erasure by any path but this store's `eraseSubject` is not seen by such an operand, and is bounded by nothing. An erasure that starts after an output's last check and finishes before its publish writes is not caught by the counter, as for a feed, and the fences above still apply to what it touched. Build a held operand from your source of truth at the start of each refresh and release it at its end: the library keeps none of it once released.
131
+
124
132
  ### One process, and the rest of your fleet
125
133
 
126
134
  Erasure and crypto-shred are **immediate in storage and immediate in the store whose verb performed them**, for
@@ -137,6 +145,8 @@ bucket.
137
145
  | 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
146
  | 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
147
  | `seg.pinAt` of a generation the erasure rewrote, in any store | on return it fails with `NotFoundError`, since the object is deleted (and, with a registry, the row has moved on); during the erasure, between the rewrite's publish and its delete of the old generation, it can still open that generation, and a handle opened then is a pin, covered by the row below |
148
+ | a `materializeMany` call with a feed, and an erasure by any path but this store's `eraseSubject` | **no bound**: the call is refused only by this store's `eraseSubject` |
149
+ | a held in-memory operand (`store.memory`), and an erasure by any path but this store's `eraseSubject` | **no bound**: the operand is refused only by this store's `eraseSubject`, started after the operand began to be made |
140
150
  | 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 |
141
151
  | a leased pinned handle (`seg.pin({ leaseUntil })`) in another store | until its lease ends, at most 14 days after it was taken: its reads then throw `LeaseExpiredError`, whether or not its reader still holds the chunks. Until then, as for any pinned handle in the row above |
142
152
 
@@ -193,7 +203,7 @@ single-subject erasure is a rewrite (`eraseSubject`), while crypto-shred (`destr
193
203
  handles segment/tenant-level erasure and is the only erasure that reaches immutable backups / WORM copies of the
194
204
  objects — once no copy of the segment's registry row still holds its wrapped key, or every KEK that wrapped it
195
205
  is destroyed (below). And a
196
- **materialized segment** (`intersectInto` / `unionInto` / `andNotInto`) is a point-in-time snapshot of its
206
+ **materialized segment** (`intersectInto` / `unionInto` / `andNotInto`, or an output of `materializeMany`) is a point-in-time snapshot of its
197
207
  inputs: erasing a subject from a source does not touch a destination computed earlier — which is exactly why
198
208
  `eraseSubject` scans *every* registered segment, destinations included, rather than erasing per source. What that
199
209
  costs is one unit of the per-op `budget` for each segment, and one more for each generation it opens in a segment
package/README.md CHANGED
@@ -7,7 +7,7 @@ file, then ask `has`, `count`, `iterate`, `intersect`, `union` and `andNot`, wit
7
7
  > **ESM-only, Node ≥ 22.12.** Use `import`; for `require()`, Jest and TypeScript, see
8
8
  > [CommonJS, Jest and TypeScript](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#commonjs-jest-and-typescript).
9
9
 
10
- > Pre-1.0: the API and the on-disk format can still change. These docs describe `main`, ahead of the npm release.
10
+ > Pre-1.0: the API and the on-disk format can still change. On npm these docs match the version they ship in; on GitHub they describe `main`, and the changelog says what each release changed.
11
11
 
12
12
  ## Install
13
13
 
@@ -52,6 +52,11 @@ request handler. `store.exists(ref)` says whether a segment has been loaded. A l
52
52
  hold: `store.load(ref, { bitmap })` for a `RoaringBitmap32`, or `{ serialized }` for portable Roaring bytes, checked
53
53
  before anything is written and loaded with no per-id work, into the same bytes its ids would write.
54
54
 
55
+ To write many results over the same stored operands, `store.materializeMany` reads each operand once for a whole batch of
56
+ `and` / `or` / `andNot` outputs instead of once per `*Into` call, and `store.memory(ids)` holds a set you already have in
57
+ memory as an operand of that batch. [Many outputs from one pass](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/loading.md#many-outputs-from-one-pass-materializemany) and
58
+ [held operands](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/loading.md#operands-held-in-memory-storememory) cover both.
59
+
55
60
  ## Options
56
61
 
57
62
  `new CloudRoaring({ storage, ...options })`. Only `storage` is required. A key the store does not take is refused by
@@ -69,10 +74,10 @@ name.
69
74
 
70
75
  ## What it costs
71
76
 
72
- A cold `intersect` of two 500,000-id segments sharing 100 of their 1,999 chunks made 6 GETs and took 92.63 ms at the median, **$2.40 per
73
- million** at list prices: measured on S3 in-region, in the run of 2026-10-05, whose client had 128 sockets. On a spread layout it reads more
77
+ A cold `intersect` of two 500,000-id segments sharing 100 of their 1,999 chunks made 6 GETs and took 99.80 ms at the median, **$2.40 per
78
+ million** at list prices: measured on S3 in-region, in the run of 2026-10-06, whose client had 128 sockets. On a spread layout it reads more
74
79
  bytes than it needs, which cross-region can cost more than the requests it saves. A segment's first `store.load()` made
75
- 2 PUT + 3 GET, **$11.20 per million** at the default prices, pointer included: the requests are measured on S3 in-region, in the same run. A cold `count()` is one
80
+ 2 PUT + 3 GET, **$11.20 per million** at the default prices, pointer included: the requests are measured on S3 in-region, in the same run. A steady `store.load()` at `keep: 12` that deletes a generation by name made 2 PUT + 4 GET and a free delete, measured: **$11.60 per million**. A cold `count()` is one
76
81
  pointer read: it reads no payload and no object.
77
82
 
78
83
  The trade is stated plainly. A membership check that misses the cache costs a ranged GET against object storage,
@@ -93,7 +98,8 @@ The [production checklist](https://github.com/cloudbitmaps/cloudbitmaps/blob/mai
93
98
  ## Coming from Redis bitmaps?
94
99
 
95
100
  `GETBIT` is `has`, `BITCOUNT` is `count`, and `BITOP AND` / `OR` / `DIFF` are `intersect` / `union` / `andNot`, with
96
- `intersectInto`, `unionInto` and `andNotInto` publishing the result as a new generation. `EXPIRE` becomes
101
+ `intersectInto`, `unionInto` and `andNotInto` publishing the result as a new generation, and `materializeMany` publishing
102
+ many such results from one pass. `EXPIRE` becomes
97
103
  `setRetention` plus a `retireExpired` sweep you schedule, per segment, never per id. `SETBIT` has no equivalent:
98
104
  there is no per-id write. Build the set upstream and load it, and do not loop one id at a time. Remove one id
99
105
  everywhere with `eraseSubject`, a rewrite for compliance, not a hot-path verb. This is not a drop-in replacement, and