@cloudbitmaps/roaring 0.21.0 → 0.23.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
@@ -42,8 +42,8 @@ from constructing a cross-region topology. The points where personal data moves
42
42
 
43
43
  | Location | What's there | Residency note |
44
44
  |---|---|---|
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
- | **Registry** (S3 / GCS / Azure Blob / local) | one row per segment: its namespace and name, its status and timestamps, the current-generation pointer, wrapped keys and any key id, its `retention` and `residency` metadata, the current generation's id count, its object's size and footer checksum and your metadata (those three sealed when the segment is encrypted), the generations its loads keep, any leases, and a token and `pointerId` — no IDs, unless you put one in the metadata or a name, which you must not. An Azure Blob row also carries the random write id in its metadata | the region of the bucket you wire |
45
+ | **Storage** (object store) | immutable `.crbm` generations — every generation a segment has had, until a superseded one is collected. Every generation object a cloud driver writes (S3, GCS and Azure Blob) also carries a random write id in its metadata (`cbwid`; `x-amz-meta-cbwid` on S3): 128 random bits, no data from the bitmap or the source. A write that fails or is killed part way can leave the uploaded part of a generation behind (S3 multipart parts, Azure Blob uncommitted blocks, an unfinished GCS resumable session, a `LocalFsStorage` `.tmp` file); no listing shows it, so no erasure or drop reaches it. What each backend leaves, and when it is removed, is in [the backup checklist](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/disaster-recovery.md#backup-checklist) | the region of the bucket you wire |
46
+ | **Registry** (S3 / GCS / Azure Blob / local) | one row per segment: its namespace and name, its status and timestamps, the current-generation pointer, wrapped keys and any key id, its `retention` and `residency` metadata, the current generation's id count, its object's size, footer checksum and tail size, and your metadata (those sealed when the segment is encrypted), a ledger of the generations the segment knows (each one's number and its object's footer checksum, in the clear even on an encrypted segment), any leases, and a token and `pointerId` — no IDs, unless you put one in the metadata or a name, which you must not. An Azure Blob row also carries the random write id in its metadata. On `LocalFsStorage`, a write killed part way leaves a `.tmp` copy of the row beside it, wrapped keys included, until a later write in that directory removes it once it is more than a day old, so a crypto-shred there can be outlived by that copy for that long | 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
48
  | **Loads and rewrites** (`store.load()`, the `*Into` verbs, `store.materializeMany()`, `eraseSubject`) | read your source (or existing generations), write a new generation; a fed 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 |
@@ -60,16 +60,18 @@ CloudBitmaps gives you three levers with different guarantees. Use them delibera
60
60
 
61
61
  | Lever | API | Guarantee | Use for |
62
62
  |---|---|---|---|
63
- | **Subject erasure** | `store.eraseSubject(id, { namespace })` | *Physical on return* — the segment's current generation is rewritten without the id (every chunk streamed through, one bit cleared), published **fenced on the generation it streamed**, and **the generation that held the bit is deleted before the call returns**. So is every other generation still holding the id: a retained *superseded* one (an ex-member dropped by a re-seed), and each one above the pointer after a `rollback` — generations a rollback can move the pointer onto again — while, when the current generation does not hold the id, the ones there that never held it stay as rollback targets, but for any object no read of the segment can open, which goes whatever the id. (When the current generation holds the id, the rewrite is numbered above everything, so its `keep: 0` collection takes every older generation, above the pointer or below.) Before it says `erased: true` the call lists the bucket and reads what is left, so **`erased: true` means no generation of the segment holds the id**, above the pointer or below it. `eraseSubject` reports a per-segment fault as an `error: …` ledger entry rather than throwing it, and a call that a racing writer overtook (a load, another erasure, or a rollback) as `erased: false, note: 'superseded'`, so "on return" is a claim about every segment whose entry says **`erased: true`** (a fault is an `error: …` entry, never `erased: true`). The store that performs the call stops serving the id immediately; **other processes converge on their own read TTL** — see [One process, and the rest of your fleet](#one-process-and-the-rest-of-your-fleet). Does **not** reach backups, replicas or noncurrent object versions — those hold the old generation until their own lifecycle removes it. It carries a generation's metadata over unchanged and does **not** scan it, so **never put a subject's id in metadata**. | "forget this person" — GDPR Art. 17 |
63
+ | **Subject erasure** | `store.eraseSubject(id, { namespace })` | *Physical on return* — the segment's current generation is rewritten without the id (every chunk streamed through, one bit cleared), published **fenced on the generation it streamed**, and **the generation that held the bit is deleted before the call returns**. So is every other generation still holding the id: a retained *superseded* one (an ex-member dropped by a re-seed), and each one above the pointer after a `rollback` — generations a rollback can move the pointer onto again — while, when the current generation does not hold the id, the ones there that never held it stay as rollback targets, but for any object no read of the segment can open, which goes whatever the id. (When the current generation holds the id, the rewrite is numbered above everything, so its `keep: 0` collection takes every older generation, above the pointer or below. A holder found only above the pointer likewise takes every generation below the pointer with it, clean ones included ([every holder goes](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/correctness.md#every-holder-goes)); above the pointer, a generation with no copy of the id stays, and so does the current one.) Before it says `erased: true` the call lists the bucket and reads what is left, so **`erased: true` means no generation of the segment holds the id**, above the pointer or below it. `eraseSubject` reports a per-segment fault as an `error: …` ledger entry rather than throwing it, and a call that a racing writer overtook (a load, another erasure, or a rollback) as `erased: false, note: 'superseded'`, so "on return" is a claim about every segment whose entry says **`erased: true`** (a fault is an `error: …` entry, never `erased: true`). The store that performs the call stops serving the id immediately; **other processes converge on their own read TTL** — see [One process, and the rest of your fleet](#one-process-and-the-rest-of-your-fleet). Does **not** reach backups, replicas or noncurrent object versions — those hold the old generation until their own lifecycle removes it. It carries a generation's metadata over unchanged and does **not** scan it, so **never put a subject's id in metadata**. The scope is one `namespace` or `{ allNamespaces: true }`, never both (both throws `ValidationError`); a segment loaded with no `namespace` is reached only by `{ allNamespaces: true }`, and `'_default'` is an ordinary namespace name, not those segments. | "forget this person" — GDPR Art. 17 |
64
64
  | **Dispose** | `store.dropSegment(ref, { confirmSegment })` | *Immediate* — the segment is tombstoned and its Storage generations deleted, reclaiming the space. **Check `generationsRemaining`:** if it is non-empty the space was *not* fully reclaimed and the drop should be re-run (a load that had read the segment before the tombstone landed, even one still consuming its ids, or an `eraseSubject` rewrite already under way, can write its object after the drop's sweep; it deletes that object itself once its publish is refused, and only one whose process stops in between, or whose publish fails without a definite answer (a lost response, a timeout), leaves it, for a re-run of the drop). Works on cleartext; on an encrypted segment it *also* crypto-shreds, on the terms in the next row. Does **not** reach noncurrent versions / replicas / PITR snapshots — deleting an object is weaker than removing a key. | retiring a dated bucket; rolling-window retention |
65
65
  | **Crypto-shred** | `destroySegment` / `eraseNamespace` | *At rest, complete once no copy of the wrapped key survives* — removes the segment's wrapped DEK(s) from its registry row in one compare-and-swap, without touching the bytes or any KEK. **Every** copy of the objects (current, prior generations, backups, WORM-locked objects) is then unreadable once no retained copy of that row still holds the wrapped key — noncurrent object versions, backups and PITR copies of the row included — or once every KEK that wrapped it is destroyed. Until then, a retained copy of the row and a KEK that wrapped it decrypt the segment, and a registry restore to a point before the shred makes it readable again; see *Erasure vs. backups / WORM* below, and [an optional registry expiry rule](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/disaster-recovery.md#optional-make-a-shred-durable-with-a-registry-expiry-rule) that makes a shred durable after a set number of days. Requires the segment to be encrypted. A process that had already opened the segment holds the **unwrapped** key in memory and keeps reading until it is told — call `store.invalidate(ref)` there; see [One process, and the rest of your fleet](#one-process-and-the-rest-of-your-fleet). | whole-segment / tenant offboarding; erasure under immutable backups (see below) |
66
66
 
67
67
  **Subject-wide erasure** (GDPR Art. 17 — "forget this person everywhere") is
68
- `store.eraseSubject(id, { namespace })` — or `{ allNamespaces: true }` to sweep every tenant deliberately. For
69
- every **registered** segment the id is in, it rewrites the current generation without the id, publishes the
70
- rewrite, and deletes every generation that held the bit — the one it replaced, a retained superseded one, and
71
- any above the pointer that a rollback left — so the bit is physically gone from the bucket on return, for idle
72
- and archival segments as much as busy ones. It checks rather than assumes: before an entry says `erased: true`,
68
+ `store.eraseSubject(id, { namespace })` — or `{ allNamespaces: true }` to sweep every tenant deliberately; giving both throws `ValidationError`. A segment loaded with no `namespace` is in no namespace you can name: only `{ allNamespaces: true }` reaches it, and `'_default'`, the label the export tool and the disaster-recovery samples print, and the bucket's object keys use, for those segments, is an ordinary namespace name here, so passing it scans a namespace of that name, not them. For
69
+ every **registered** segment the id is in, it [rewrites the current generation without the
70
+ id](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/correctness.md#erasure-is-a-rewrite), publishes the rewrite, and deletes [every generation that held
71
+ the bit](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/correctness.md#every-holder-goes) — the one it replaced, a retained superseded one, and any
72
+ above the pointer that a rollback left — so the bit is physically gone from the bucket on return, for idle
73
+ and archival segments as much as busy ones (a noncurrent version, a replica or a backup keeps the old object until
74
+ its own lifecycle removes it; see *Honest limits* below). It checks rather than assumes: before an entry says `erased: true`,
73
75
  the call lists the segment's generations and reads each one it has not already seen without the id. There is no logical-then-physical gap and no
74
76
  scheduled step to wait for: an erasure *is* a new generation, the same shape as every other write in the library.
75
77
  It reuses the store's own drivers (so build the store with a backend). It returns an
@@ -86,11 +88,11 @@ not in are not listed. A segment where the erasure found the id in nothing it co
86
88
  no read of it can open, is not listed either: the audit sink gets `segment.collect { collected }` for it, with no
87
89
  `fromGeneration`, and that event is the only record of the deletion. `store.subjectReport(id, { namespace })` answers
88
90
  the read side (Art. 15 — which segments
89
- an id is in).
91
+ an id is in, and which destroyed segments still hold it in objects left in the bucket).
90
92
 
91
93
  An erasure that finds the id in no generation but an older one also writes the segment's registry row once, before it
92
94
  deletes anything, when the row's ledger of known generations names a generation the call is about to delete: it takes
93
- those names out of the ledger first (see [how it stays correct](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/erasure.md#how-it-stays-correct)). That write
95
+ those names out of the ledger first (see [how it stays correct](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/correctness.md#how-an-erasure-stays-correct)). That write
94
96
  is a timing signal. Someone who can read the registry and knows when an erasure ran can tell that this segment's row
95
97
  changed then, and so that the segment held the subject in a generation older than its current one. The row also
96
98
  changes at erasure time when the segment's current generation held the id, or one above the pointer, a first load's
@@ -111,7 +113,7 @@ replaced, so it still holds the winner's id, and when it sits above the winner's
111
113
  returning, unless writes other than lease writes and other erasures' renewals beat its renewal of the segment's
112
114
  row 8 times: it then keeps that object, one `rollback({ allowUnknown: true })` from being served, until a later call
113
115
  removes it: an erasure that rewrites the segment (the loser's re-run, while the loser's id is still in the current
114
- generation), an erasure of the winner's id, or the collection of loads that number past it ([how it stays correct](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/erasure.md#how-it-stays-correct)). **Read the ledger**: an `erased: false` entry means *this run* did not erase the id from that segment,
116
+ generation), an erasure of the winner's id, or the collection of loads that number past it ([how it stays correct](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/correctness.md#how-an-erasure-stays-correct)). **Read the ledger**: an `erased: false` entry means *this run* did not erase the id from that segment,
115
117
  which is not the same as the id still being present — if the racing writer was another erasure of the same id, it
116
118
  is already gone. For `'superseded'` re-run `eraseSubject`: it is idempotent, it erases the id if the id is still
117
119
  there, and a segment the id is no longer in is simply not listed — though "not listed" alone does not prove the
@@ -131,7 +133,7 @@ same one, a generation still holding the id — can also come from the collect-o
131
133
  only outside the current generation and nothing was published; there the pointer has not moved, and a re-run
132
134
  simply repeats the attempt.) **Read what the re-run says.** Usually it reports `erased: true` against the
133
135
  generation it found the id in, which is the receipt the failed call could not give you. If a racing collector took
134
- that generation first it reports nothing for the segment: the bit is gone, but no run holds a receipt for it, so
136
+ that generation first, or this call's own delete of it landed after a fault the call could not settle, it reports nothing for the segment: the bit is gone, but no run holds a receipt for it, so
135
137
  keep the failed call's error alongside your ledger. And if the segment's registry row has since been purged, it is
136
138
  not a segment any more and `eraseSubject` does not scan it at all — anything left in its bucket is an **orphan**,
137
139
  which neither `store.checkConsistency()` (it visits registry rows only) nor the collection a load and the retention
@@ -146,7 +148,7 @@ object is refused at its publish and never names an object that is gone. Each de
146
148
  read: on a storage driver that reports `conditionalDelete`, another erasure of the id that deletes it first, followed
147
149
  by a load that takes the number freed and publishes, does not lose that load's generation to this erasure's delete; on
148
150
  one that does not, it can, and the row then names a missing object, which `store.checkConsistency()` reports
149
- ([a number taken again](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/erasure.md#how-it-stays-correct)). On an encrypted store such an object cannot
151
+ ([a number taken again](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/correctness.md#how-an-erasure-stays-correct)). On an encrypted store such an object cannot
150
152
  be searched until its load publishes the key, so it is deleted whatever id is erased: an erasure of any id refuses an
151
153
  encrypted first load in flight that has written its object onto a segment whose row was made ahead of its data, and
152
154
  that load is re-run ([the erasure guide](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/erasure.md#three-rules-while-you-erase)). On an encrypted segment,
@@ -180,7 +182,7 @@ bucket.
180
182
  | storage | on return — the generation holding it is deleted, or the wrapped DEK is removed from the segment's current registry row (a crypto-shred is complete once no retained copy of that row holds it, or every KEK that wrapped it is destroyed; see *Erasure vs. backups / WORM* below) |
181
183
  | 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 |
182
184
  | 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) |
183
- | 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 |
185
+ | another store with **no registry** (a bare `IStorageDriver`), with `cache: { genTtlMs: 0 }`, or on a `StorageChunkSource` of your own, which the store times no refresh on | **no bound** — only when its caches happen to let the segment go, or something tells it |
184
186
  | `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 rows for the store whose verb made the call and for a pinned handle in another store |
185
187
  | 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` |
186
188
  | 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 |
@@ -229,7 +231,7 @@ registry row is a random holder id, a generation number and an instant, with no
229
231
 
230
232
  **Your exit path** (and a building block for a **data-portability / Art. 20** response): `store.exportSegments(sink,
231
233
  { format })` (and the `export-segments` CLI) dumps the current generation of every registered segment that is not
232
- destroyed (crypto-shredded or dropped) to a portable
234
+ destroyed (crypto-shredded, destroyed in the clear or dropped) to a portable
233
235
  file — `roaring` (loadable by any roaring library) or `ndjson` (newline ids) — readable without CloudBitmaps. A destroyed segment resolves no generation, so it is not exported: the manifest's `skipped` lists it, and `segments`, `failed` and `skipped` together account for every segment row the registry listed.
234
236
  It's a **controller-side bulk dump** (all segments, opaque ids), not a per-subject deliverable — the per-subject
235
237
  rights are `subjectReport` (Art. 15) / `eraseSubject` (Art. 17). Encrypted segments are decrypted transparently
@@ -360,7 +362,9 @@ So the two are complements, not alternatives: **`dropSegment` for "stop paying f
360
362
  them included), its timestamps and its token. It holds no id, and on an encrypted segment no wrapped key, since the
361
363
  drop shreds it. The due index holds a pointer to it whose name spells out the same namespace and name. Both stay
362
364
  for `tombstoneGraceMs` (24 h by default) after the retirement, and for as long as the segment's storage cannot be
363
- proven gone. Then the sweep's purge removes the row, and the pointers it read to it, from the bucket, where the registry
365
+ proven gone, except a segment that held no generation, whose row the same pass deletes (with `purgeTombstones` on, the default; from the bucket where the
366
+ registry reports `conditionalDelete`; elsewhere as a deleted marker that keeps the same fields), so it never waits out
367
+ the grace. Then the sweep's purge removes the row, and the pointers it read to it, from the bucket, where the registry
364
368
  reports `conditionalDelete`: the default for S3 when its client sends to an AWS S3 host,
365
369
  for Azure Blob, the local filesystem and memory. **A pointer can outlive its row**, and with it the name in its key: a
366
370
  purge that ran with another `tombstoneGraceMs` than the sweep that filed the pointer, a delete that landed and lost its
@@ -369,13 +373,15 @@ response, a removal the registry refused (`purgeFaults` counts it), or a pointer
369
373
  it reads, and an unscoped fleet scan, one given no `namespace`, from every day. A deployment that scopes every sweep to a
370
374
  namespace, or runs only index scans, keeps such a pointer until an unscoped fleet scan runs. Where the registry does not
371
375
  report `conditionalDelete` (an S3 client that sends to an S3-compatible store or an emulator, or a GCS client, by default, or
372
- `conditionalDelete: false`), the purge leaves a tombstone instead: every one of those fields, in the bucket, indefinitely, read by every full listing. A registry that does not
373
- report it files no pointer to begin with. A removed
376
+ `conditionalDelete: false`), the purge leaves a tombstone instead: every one of those fields, in the bucket, indefinitely, read by every full listing. Such a
377
+ registry files no pointer for a retirement's purge, but `setRetention` files its expiry-day pointer on every registry,
378
+ and removing it there (at the retirement, a `clearRetention`, or a moved expiry) leaves a deleted marker under the
379
+ reserved `cbm.due.` namespace whose key spells out the segment's namespace and name, also indefinitely. A removed
374
380
  row stays recoverable wherever the storage keeps a copy of it: with object versioning on, its earlier versions stay until
375
381
  a noncurrent-version rule expires them, as an overwritten row's do; with soft delete on, it stays for the retention
376
382
  window (GCS: on by default for a new bucket, 7 days; Azure Blob: where enabled, for the days set), whatever the registry
377
- did. A tombstone a hand-run `dropSegment` or a
378
- crypto-shred left is never purged by the library.
383
+ did. A [tombstone a hand-run `dropSegment`, a
384
+ crypto-shred or a cleartext destroy left is never purged by the library](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/correctness.md#tombstones-purged-narrowly).
379
385
 
380
386
  Full detail, including the dated-bucket pattern and the pitfalls, is in the retention section of the
381
387
  [retention guide](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/retention.md).
@@ -422,13 +428,13 @@ rewrite: `fromGeneration` → `generation`), `segment.collect` (a subject erasur
422
428
  no reader resolves: the generations holding the id, when it found the id only outside the current generation, or, with
423
429
  no `fromGeneration`, only objects no read of the segment can open, which it deletes whatever the id), `segment.erase`
424
430
  (a genuine
425
- crypto-shred), `segment.dispose` (a `dropSegment`, including every retirement the sweep performs) and
431
+ crypto-shred), `segment.tombstone` (a cleartext tombstone from `allowCleartext`, which deletes nothing), `segment.dispose` (a `dropSegment`, including every retirement the sweep performs) and
426
432
  `namespace.erase` — for your audit log / SIEM. Each `segment.*` event names the segment's `incarnation` when its
427
433
  registry token carries one, so a segment created again under the same name is told apart from the one before.
428
434
  It is off by default and exception-safe. See the [dashboards guide](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/dashboards.md), which also says
429
435
  which event is the receipt for which claim. An erasure *ledger* — per-subject-request proof of physical deletion
430
- — is returned by **`eraseSubject`**. `subjectReport` is the read side and returns only which segments an id is
431
- in; it performs no deletion and issues no ledger.
436
+ — is returned by **`eraseSubject`**. `subjectReport` is the read side and returns which segments an id is
437
+ in, and which destroyed segments' leftover objects hold it; it performs no deletion and issues no ledger.
432
438
 
433
439
  **Log hygiene:** `segment` / `namespace` are *your* strings and may encode sensitive purpose; IDs are personal
434
440
  data. The library never logs bitmap contents or raw IDs, but **you** should treat segment names and IDs as PII
@@ -437,6 +443,15 @@ segment names for sensitive segments (keeping the human label in your own classi
437
443
  address, free text, or anything else an upstream system hands you. That makes this advice stronger rather than
438
444
  weaker: the library will store what you name, and will not sanitise it for you.
439
445
 
446
+ The library's own error messages name the segment and namespace; some messages also give a chunk key (the high 16 bits
447
+ of the ids in that chunk), id counts, metadata keys, key ids, the bucket or container name, or the account host, and an
448
+ object's key (the store's `prefix`, the namespace and the encoded segment name); an error the cloud SDK raised reaches
449
+ you as the SDK wrote it, except that the S3, GCS and Azure Blob drivers first remove the credentials they recognise in
450
+ it (the request and response it kept, and a credential the service quotes back or its text quotes), and a credential
451
+ in another form reaches you as the SDK wrote it; and `eraseSubject` copies each message into its ledger's `note`.
452
+ Treat an error message, and the ledger, as carrying those names wherever you report or keep them.
453
+
454
+
440
455
  ## Shared-responsibility matrix
441
456
 
442
457
  | Concern | CloudBitmaps provides | You (the integrator) must |
package/README.md CHANGED
@@ -17,7 +17,16 @@ pnpm add @cloudbitmaps/s3 # the storage you have: or @cloudbit
17
17
  pnpm add @cloudbitmaps/tools # optional: price a store, offline
18
18
  ```
19
19
 
20
- On npm 12 and pnpm 10 and later, allow `roaring`'s one install script first, or the install exits 0 and the package throws at `import`. Put this in your `package.json`: `{ "allowScripts": { "roaring": true }, "pnpm": { "onlyBuiltDependencies": ["roaring"] } }`. npm 11 runs the script but warns until you allow it the same way; pnpm 9 needs nothing extra. [Details](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#cannot-find-module-buildreleaseroaringnode-after-a-successful-install).
20
+ Install the same version of every `@cloudbitmaps/*` package: they release together, and each pins the exact version of `core` it was built with.
21
+
22
+ `roaring` needs its one install script to fetch its native binary, and newer clients run a dependency's script only if you allow it. Allow it before you install:
23
+
24
+ - **npm 12:** add `{ "allowScripts": { "roaring": true } }` to your `package.json`. Without it the install exits 0 and the package throws at `import`. npm 11 runs the script but warns until you allow it the same way.
25
+ - **pnpm 11 and later:** put `allowBuilds: { roaring: true }` in a `pnpm-workspace.yaml` beside your `package.json`. Those versions no longer read the `package.json` `pnpm` key. Without it the install exits 1 with `ERR_PNPM_IGNORED_BUILDS`.
26
+ - **pnpm 10:** add `{ "pnpm": { "onlyBuiltDependencies": ["roaring"] } }` to your `package.json`. Without it the install exits 0 and the package throws at `import`.
27
+ - **pnpm 9:** nothing extra.
28
+
29
+ [Details](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#cannot-find-module-buildreleaseroaringnode-after-a-successful-install).
21
30
 
22
31
  ## Use
23
32
 
@@ -51,7 +60,7 @@ To persist, pass a different **backend** as `storage`. Nothing else changes:
51
60
  There is no `add` or `remove`: a segment changes only by loading a new generation. A load is a batch job, not a
52
61
  request handler. `store.exists(ref)` says whether a segment has been loaded. A load takes ids, or a bitmap you already
53
62
  hold: `store.load(ref, { bitmap })` for a `RoaringBitmap32`, or `{ serialized }` for portable Roaring bytes, checked
54
- before anything is written and loaded with no per-id work, into the same bytes its ids would write.
63
+ before anything is written and loaded with no per-id work, into a generation holding chunk for chunk what its ids would write (its footer carries its own write identity).
55
64
 
56
65
  To write many results over the same stored operands, `store.materializeMany` reads each operand once for a whole batch of
57
66
  `and` / `or` / `andNot` outputs instead of once per `*Into` call, and a set you already hold in memory streams in as its
@@ -71,14 +80,14 @@ name.
71
80
  | `retry` | tune the retry of reads, or `false` to turn it off |
72
81
  | `metrics` | a metrics sink; off by default |
73
82
  | `budget` | `{ maxRequests }` or `false`: the per-call request ceiling |
74
- | `seams` | `{ clock, rng }`: determinism, for tests |
83
+ | `seams` | `{ clock, rng, entropy }`: determinism, for tests and replayable jobs; the source of each generation's write identity is the last |
75
84
 
76
85
  ## What it costs
77
86
 
78
- 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
79
- 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
80
- bytes than it needs, which cross-region can cost more than the requests it saves. A segment's first `store.load()` made
81
- 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 costs **$12.00 per million**, for 2 PUT + 5 GET and a free delete, counted by the engine's tests. A cold `count()` is one
87
+ A cold `intersect` of two 500,000-id segments sharing 100 of their 1,999 chunks made 6 GETs and took 104.88 ms at the median, **$2.40 per
88
+ million** at list prices: measured on S3 in-region, in the run of 2026-10-13, whose client had 128 sockets. On a spread layout it reads more
89
+ bytes than it needs, which cross-region can cost more than the requests it saves. A segment's first single-part `store.load()` made
90
+ 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 single-part `store.load()` at `keep: 12` that deletes a generation by name costs **$12.00 per million**, for 2 PUT + 5 GET and a free delete, the requests measured on S3 in-region, in the same run. A cold `count()` is one
82
91
  pointer read: it reads no payload and no object.
83
92
 
84
93
  The trade is stated plainly. A membership check that misses the cache costs a ranged GET against object storage,
@@ -89,8 +98,16 @@ give the method and what the numbers do not establish. To price your own store,
89
98
 
90
99
  ## Before production
91
100
 
92
- - Grant the permissions the library issues and add a lifecycle rule to abort incomplete uploads.
93
- - Use a bucket that honors conditional writes, and `@aws-sdk/client-s3` 3.700.0 or later if you pass your own S3 client.
101
+ - Run `await store.preflight()` once before the first load. It reads the registry, writes, tries to write again, reads, lists and
102
+ deletes one probe object and checks the backend's lifecycle rules, and answers `{ ok, checks }`; see [`store.preflight()`](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md#check-the-setup-with-storepreflight).
103
+ It does not check `s3:AbortMultipartUpload`, the registry's `If-Match`, or a read after a delete, among others: [the full list](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md#what-preflight-does-not-check).
104
+ - Grant the permissions the library issues and add a lifecycle rule to abort incomplete uploads. The S3 and GCS
105
+ backends' `checkLifecycle()` read the bucket's rules and say whether one would delete or archive what the store
106
+ keeps; Azure Blob's answers `unchecked` and says how to check by hand; `MemoryStorage` and `LocalFsStorage` have no
107
+ rules and answer `ok`.
108
+ - Use a bucket that honors conditional writes and is strongly consistent (a read or a listing made after a write or a
109
+ delete sees it), and `@aws-sdk/client-s3` 3.700.0 or later if you pass your own S3 client. Preflight reports a second write that succeeds (which is how an SDK of 3.640.0 or older shows) and a read or listing
110
+ that lags a write, but not an SDK from 3.641.0 to 3.699.0, which keeps `If-None-Match` but drops `If-Match`: the SDK floor stays yours to check.
94
111
  - Set a request timeout on your storage client. Reads retry by themselves, and a read can be timed on every backend (`readTimeoutMs`); a write that fails transiently is yours to re-run.
95
112
  - Back up the registry with the data, and the keystore too if you encrypt. If you lose the key, the data is gone.
96
113
  - Schedule your loads, and `retireExpired` if you use retention. Run loads off the request path.
@@ -115,6 +132,7 @@ says what else differs.
115
132
  - [API reference](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/api-reference.md)
116
133
  - [Changelog](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/CHANGELOG.md)
117
134
  - [Privacy and shared responsibility](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/PRIVACY.md)
135
+
118
136
  ## License
119
137
 
120
138
  Apache-2.0