@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 +38 -23
- package/README.md +27 -9
- package/dist/bin/export-segments.js +683 -260
- package/dist/bin/export-segments.js.map +4 -4
- package/dist/index.d.ts +395 -171
- package/dist/index.js +633 -279
- package/dist/index.js.map +4 -4
- package/dist/roaring-codec.d.ts +23 -22
- package/package.json +4 -3
- package/dist/bin/export-segments.d.ts +0 -25
- package/dist/lease-guards.d.ts +0 -46
- package/dist/open-charging-storage.d.ts +0 -37
- package/dist/option-keys.d.ts +0 -19
- package/dist/options.d.ts +0 -30
- package/dist/portable/decode.d.ts +0 -54
- package/dist/portable/layout.d.ts +0 -52
- package/dist/reserved-namespace.d.ts +0 -3
- package/dist/system-clock.d.ts +0 -39
- package/dist/testing/conformance.d.ts +0 -118
- package/dist/testing/fuzz-codec.d.ts +0 -23
- package/dist/wire-store.d.ts +0 -38
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.
|
|
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
|
|
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
|
|
70
|
-
rewrite, and deletes every generation that held
|
|
71
|
-
|
|
72
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
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
|
|
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.
|
|
373
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
79
|
-
million** at list prices: measured on S3 in-region, in the run of 2026-10-
|
|
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,
|
|
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
|
-
-
|
|
93
|
-
|
|
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
|