@cloudbitmaps/roaring 0.10.0 → 0.11.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 +139 -73
- package/README.md +80 -154
- package/dist/bin/export-segments.js +784 -320
- package/dist/bin/export-segments.js.map +4 -4
- package/dist/index.d.ts +357 -204
- package/dist/index.js +815 -294
- package/dist/index.js.map +4 -4
- package/dist/open-charging-storage.d.ts +34 -0
- package/dist/option-keys.d.ts +19 -0
- package/dist/portable/decode.d.ts +6 -7
- package/dist/portable/layout.d.ts +32 -0
- package/dist/reserved-namespace.d.ts +3 -0
- package/dist/roaring-codec.d.ts +13 -6
- package/dist/system-clock.d.ts +3 -4
- package/dist/testing/conformance.d.ts +21 -1
- package/dist/testing/fuzz-codec.d.ts +19 -5
- package/package.json +2 -3
- package/dist/codec-bound.d.ts +0 -38
- package/dist/moved-options.d.ts +0 -14
package/README.md
CHANGED
|
@@ -1,183 +1,109 @@
|
|
|
1
1
|
# @cloudbitmaps/roaring
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
> a CommonJS project needs `"module": "nodenext"` or `"node20"`. See the
|
|
7
|
-
> [repository README](https://github.com/cloudbitmaps/cloudbitmaps#install--entry-points) for the details.
|
|
3
|
+
**Large sets of integer ids, stored in your own object storage and read from anywhere.** Load a set as one immutable
|
|
4
|
+
file, then ask `has`, `count`, `iterate`, `intersect`, `union` and `andNot`, with no server or cache to run. Part of
|
|
5
|
+
[CloudBitmaps](https://github.com/cloudbitmaps/cloudbitmaps); this is the package you import from.
|
|
8
6
|
|
|
7
|
+
> **ESM-only, Node ≥ 22.12.** Use `import`; for `require()`, Jest and TypeScript, see
|
|
8
|
+
> [CommonJS, Jest and TypeScript](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#commonjs-jest-and-typescript).
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
object storage — a bounded RAM cache over immutable `.crbm` objects in your own bucket — at a fraction of an
|
|
12
|
-
always-on cache, with the familiar bitmap API: `has`, `count`, `iterate`, `intersect`, `union`, `andNot`.
|
|
10
|
+
> Pre-1.0: the API and the on-disk format can still change. These docs describe `main`, ahead of the npm release.
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
roaring codec (CRoaring, via `roaring`) plus the `CloudRoaring` facade, on top of the codec-agnostic
|
|
16
|
-
[`@cloudbitmaps/core`](https://www.npmjs.com/package/@cloudbitmaps/core) engine, which is a dependency of
|
|
17
|
-
this package and of the storage package you pair it with — so you never install or name core yourself.
|
|
12
|
+
## Install
|
|
18
13
|
|
|
19
14
|
```bash
|
|
20
|
-
pnpm add @cloudbitmaps/roaring
|
|
21
|
-
|
|
15
|
+
pnpm add @cloudbitmaps/roaring # the store, with in-memory and local-disk backends
|
|
16
|
+
pnpm add @cloudbitmaps/s3 # the storage you have: or @cloudbitmaps/gcs, or @cloudbitmaps/azure-blob
|
|
22
17
|
```
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
> the `roaring` native addon never downloads and the package throws at `import` — while the install
|
|
26
|
-
> itself prints a warning and **exits 0**. Add this to your `package.json`, then install:
|
|
27
|
-
>
|
|
28
|
-
> ```json
|
|
29
|
-
> { "pnpm": { "onlyBuiltDependencies": ["roaring"] } }
|
|
30
|
-
> ```
|
|
31
|
-
>
|
|
32
|
-
> pnpm 9 and npm run it already. [Full symptoms and fixes](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#cannot-find-module-buildreleaseroaringnode-after-a-successful-install).
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
import { CloudRoaring } from '@cloudbitmaps/roaring';
|
|
36
|
-
import { S3Storage } from '@cloudbitmaps/s3';
|
|
37
|
-
```
|
|
19
|
+
On pnpm 10 and later, allow the one build script first, or the package throws at `import` while the install exits 0. Put this in your `package.json`: `{ "pnpm": { "onlyBuiltDependencies": ["roaring"] } }`. npm and pnpm 9 need nothing extra. [Details](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#cannot-find-module-buildreleaseroaringnode-after-a-successful-install).
|
|
38
20
|
|
|
39
|
-
|
|
40
|
-
**`@cloudbitmaps/gcs`** or **`@cloudbitmaps/azure-blob`**. Each depends on its cloud SDK for real, so nothing
|
|
41
|
-
is an optional peer and no install carries an SDK you do not use. Ships one CLI: `export-segments`.
|
|
21
|
+
## Use
|
|
42
22
|
|
|
43
|
-
|
|
23
|
+
No setup needed: `MemoryStorage` keeps everything in the process, which is right for a first look and for tests.
|
|
24
|
+
Save this as `first-run.mjs` and run it with `node`:
|
|
44
25
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
query, a batch job, a combine of other segments) and `store.load` streams it into a single immutable object,
|
|
48
|
-
checks the result is plausible, then advances the pointer forward-only. Nothing mutates a stored bitmap, so a crash before the publish
|
|
49
|
-
leaves the previous generation authoritative, a rerun is idempotent, and there is no partially-visible write.
|
|
50
|
-
Reads (`has`, `count`, `iterate`, `intersect`, `union`, `andNot`) see one whole, checksum-verified generation.
|
|
26
|
+
```js
|
|
27
|
+
import { CloudRoaring, MemoryStorage } from '@cloudbitmaps/roaring';
|
|
51
28
|
|
|
52
|
-
|
|
53
|
-
// One bucket is the whole deployment: the generations and the pointer, stated once.
|
|
54
|
-
const backend = new S3Storage({ bucket: 'bitmaps', region: 'us-east-1' });
|
|
29
|
+
const store = new CloudRoaring({ storage: new MemoryStorage() });
|
|
55
30
|
|
|
56
|
-
|
|
31
|
+
await store.load({ segment: 'shoppers' }, [5, 99_999, 1_234_567_890]); // a load replaces the segment
|
|
32
|
+
await store.load({ segment: 'active' }, [5, 7, 1_234_567_890]);
|
|
57
33
|
|
|
58
|
-
|
|
59
|
-
await
|
|
34
|
+
const shoppers = store.segment('shoppers');
|
|
35
|
+
console.log(await shoppers.has(99_999)); // true
|
|
36
|
+
console.log(await shoppers.count()); // 3
|
|
37
|
+
for await (const id of shoppers.intersect([store.segment('active')])) console.log(id); // 5, then 1234567890
|
|
38
|
+
```
|
|
60
39
|
|
|
61
|
-
|
|
40
|
+
To persist, pass a different **backend** as `storage`. Nothing else changes:
|
|
62
41
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
42
|
+
| Backend | From | Use it for |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `MemoryStorage` | `@cloudbitmaps/roaring` | tests and a first look |
|
|
45
|
+
| `LocalFsStorage('./.cloudbitmaps')` | `@cloudbitmaps/roaring` | one process on one folder: a laptop or a CI job |
|
|
46
|
+
| `S3Storage({ bucket, prefix })` | `@cloudbitmaps/s3` | production: the validated one |
|
|
47
|
+
| `GcsStorage({ bucket, prefix })` | `@cloudbitmaps/gcs` | Google Cloud Storage; outside the [validated envelope](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/ROADMAP.md#the-validated-envelope--whats-proven-and-what-isnt) |
|
|
48
|
+
| `AzureBlobStorage({ connectionString, container })` | `@cloudbitmaps/azure-blob` | Azure Blob Storage; outside the [validated envelope](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/ROADMAP.md#the-validated-envelope--whats-proven-and-what-isnt) |
|
|
69
49
|
|
|
70
|
-
|
|
71
|
-
|
|
50
|
+
There is no `add` or `remove`: a segment changes only by loading a new generation. A load is a batch job, not a
|
|
51
|
+
request handler. `store.exists(ref)` says whether a segment has been loaded.
|
|
72
52
|
|
|
73
|
-
##
|
|
53
|
+
## Options
|
|
74
54
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
**$0.03/month** for 1.2 GiB of segments at rest. Request counts are read off the AWS SDK layer rather than
|
|
78
|
-
estimated from sizes. `count()` on a published segment does **0 payload reads**, and intersecting two
|
|
79
|
-
2,000,000-id segments fetches only the shared chunks. That run kept the generation pointer in a NoSQL
|
|
80
|
-
registry that no longer ships, so both figures are the object-store half rather than today's total — the
|
|
81
|
-
pointer is now an object request that is not in them.
|
|
55
|
+
`new CloudRoaring({ storage, ...options })`. Only `storage` is required. A key the store does not take is refused by
|
|
56
|
+
name.
|
|
82
57
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
58
|
+
| Option | What it holds |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `storage` (required) | a backend (see above) |
|
|
61
|
+
| `cache` | `maxChunks`, `genTtlMs` (how soon a reader sees a new load, default 2000 ms), `readerMax`, `readerMaxBytes` |
|
|
62
|
+
| `encryption` | `{ keystore, required }`: encrypt segments at rest with your own key |
|
|
63
|
+
| `retry` | tune the retry of reads, or `false` to turn it off |
|
|
64
|
+
| `metrics` | a metrics sink; off by default |
|
|
65
|
+
| `budget` | `{ maxRequests }` or `false`: the per-call request ceiling |
|
|
66
|
+
| `seams` | `{ clock, rng }`: determinism, for tests |
|
|
87
67
|
|
|
88
|
-
##
|
|
68
|
+
## What it costs
|
|
89
69
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
the
|
|
93
|
-
|
|
70
|
+
Measured against real S3 in `us-east-1`, with the pointer in the same bucket as the data: the median cold `intersect`
|
|
71
|
+
of two 500,000-id segments sharing 100 of their 1,999 chunks made 206 GETs, **$82.40 per million**, requesting only
|
|
72
|
+
the shared chunks. Inside the region it is expected at 204 GETs, $81.60 per million. Writing and publishing a segment
|
|
73
|
+
is **$11.20 per million**, pointer included, from its measured requests at list prices. `count()` reads no payload.
|
|
94
74
|
|
|
95
|
-
The
|
|
75
|
+
The trade is stated plainly. A membership check that misses the cache costs a ranged GET against object storage,
|
|
76
|
+
where an in-process store costs a memory read. If you need sub-millisecond answers on a working set that fits a
|
|
77
|
+
bounded cache, use Redis. [The benchmarks](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/benchmarks.md)
|
|
78
|
+
give the method and what the numbers do not establish.
|
|
96
79
|
|
|
97
|
-
|
|
98
|
-
|---|---|
|
|
99
|
-
| `GETBIT` | `has(id)` |
|
|
100
|
-
| `BITCOUNT` | `count()` — exact, served from the index with no payload reads |
|
|
101
|
-
| `BITOP AND` / `OR` / `DIFF` | `intersect` / `union` / `andNot` — streamed, and `intersect` skips chunks that cannot contribute. `intersectInto` / `unionInto` / `andNotInto` publish the result as a new generation of the destination |
|
|
102
|
-
| `SETBIT` | **no equivalent — there is no per-id write.** Build the set upstream and load it; remove one id everywhere with `eraseSubject` (a generation rewrite for compliance, not a hot-path verb) |
|
|
103
|
-
| `EXPIRE` | **`setRetention(ref, { expiresAt })` + `retireExpired()`** — a per-segment expiry the writer sets, and a sweep **you** schedule (this library starts no timer, so it works the same in a Lambda and a server). No per-**id** TTL: a bitmap stores ids, not timestamps |
|
|
104
|
-
|
|
105
|
-
**It is not a drop-in replacement, and the difference is the write model.** Redis mutates one bit in place per
|
|
106
|
-
call; here a segment changes only by getting a new generation. So a `SETBIT` loop has nothing to port to: if your
|
|
107
|
-
set is defined by a query, run the query and load the result; if it is defined by events arriving one at a time,
|
|
108
|
-
accumulate them where they arrive (Redis is good at that) and load the set on a cadence. A load bills per
|
|
109
|
-
**object**, not per id — ten million ids are a handful of PUTs.
|
|
110
|
-
|
|
111
|
-
**What does not carry over: the raw bytes.** A `.crbm` object is not a flat bit array, so anything reading your
|
|
112
|
-
Redis bitmap's underlying string — a job that `GET`s the key and indexes into it, a byte-for-byte backup — will
|
|
113
|
-
not read ours. `BITFIELD`, `BITPOS`, `BITOP NOT` and byte-range `BITCOUNT` have no equivalent either: this is a
|
|
114
|
-
set of ids, not an addressable bit buffer, and `NOT` in particular has no bounded universe to complement against.
|
|
115
|
-
Raw bit-position import/export is unbuilt;
|
|
116
|
-
[say so in an issue](https://github.com/cloudbitmaps/cloudbitmaps/issues) if you need it, because that is what
|
|
117
|
-
decides whether it gets built.
|
|
118
|
-
|
|
119
|
-
## Compliance is built in, not bolted on
|
|
120
|
-
|
|
121
|
-
- `subjectReport(id)` — which segments an id is in (GDPR Art. 15).
|
|
122
|
-
- `eraseSubject(id)` — rewrites every segment holding the id without it and deletes the generation that held the
|
|
123
|
-
bit, so it is **physically gone from the bucket when the call returns**; you get an erasure ledger back and a
|
|
124
|
-
`segment.rewrite` audit event per segment it had to rewrite (Art. 17; an id erased out of a retained
|
|
125
|
-
superseded generation is collected rather than rewritten, so that one is attested by the ledger entry alone).
|
|
126
|
-
- `destroySegment` / `eraseNamespace` — **crypto-shred**: drop the segment's wrapped data key so its encrypted
|
|
127
|
-
bytes are unreadable *everywhere, including backups*.
|
|
128
|
-
- `dropSegment(ref, { confirmSegment, dryRun })` — retire a segment and reclaim its storage.
|
|
129
|
-
- `exists(ref)` — whether this segment's pointer resolves a generation, as one registry read. There is no
|
|
130
|
-
`create`: `segment(name)` is a validated address and does no I/O, so a name can never collide. Not the
|
|
131
|
-
same as `count() > 0` — a segment loaded with no ids exists and counts zero.
|
|
132
|
-
- `segments({ namespace })` — everything the registry holds, streamed, so you do not keep your own list of
|
|
133
|
-
segment names beside the store. An admin/dashboard call, not a request-path one.
|
|
134
|
-
- `checkConsistency()` — after a restore, verify every pointer's object is actually present.
|
|
135
|
-
- `exportSegments(sink, { format })` — eject every segment to portable `roaring` or `ndjson`. Your exit path.
|
|
136
|
-
|
|
137
|
-
## Retiring data: a per-segment expiry, and a sweep you schedule
|
|
138
|
-
|
|
139
|
-
A **segment** can expire; an individual **id** cannot (a bitmap stores ids, not `(id, timestamp)` pairs — a
|
|
140
|
-
timestamp per id costs more than the compression saves).
|
|
141
|
-
|
|
142
|
-
```ts
|
|
143
|
-
const DAY = 86_400_000;
|
|
144
|
-
const ref = { namespace: 'active-daily', segment: '2026-08-05' };
|
|
145
|
-
|
|
146
|
-
await store.setRetention(ref, { expiresAt: Date.now() + 30 * DAY }); // once, when you create the bucket
|
|
147
|
-
```
|
|
80
|
+
## Before production
|
|
148
81
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
82
|
+
- Grant the permissions the library issues and add a lifecycle rule to abort incomplete uploads.
|
|
83
|
+
- Use a bucket that honors conditional writes, and `@aws-sdk/client-s3` 3.645.0 or later if you pass your own S3 client.
|
|
84
|
+
- Set a request timeout on your storage client. Reads retry by themselves; a write that fails transiently is yours to re-run.
|
|
85
|
+
- Back up the registry with the data, and the keystore too if you encrypt. If you lose the key, the data is gone.
|
|
86
|
+
- Schedule your loads, and `retireExpired` if you use retention. Run loads off the request path.
|
|
152
87
|
|
|
153
|
-
|
|
154
|
-
queue job — run the sweep. **This library starts no background timer**, deliberately: the same code has to behave
|
|
155
|
-
identically in a Lambda, an edge isolate and a long-lived server, and a timer that only works in one of those is
|
|
156
|
-
worse than none.
|
|
88
|
+
The [production checklist](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md) covers each item.
|
|
157
89
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
90
|
+
## Coming from Redis bitmaps?
|
|
91
|
+
|
|
92
|
+
`GETBIT` is `has`, `BITCOUNT` is `count`, and `BITOP AND` / `OR` / `DIFF` are `intersect` / `union` / `andNot`, with
|
|
93
|
+
`intersectInto`, `unionInto` and `andNotInto` publishing the result as a new generation. `EXPIRE` becomes
|
|
94
|
+
`setRetention` plus a `retireExpired` sweep you schedule, per segment, never per id. `SETBIT` has no equivalent:
|
|
95
|
+
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
|
|
96
|
+
everywhere with `eraseSubject`, a rewrite for compliance, not a hot-path verb. This is not a drop-in replacement, and
|
|
97
|
+
what does not carry over is the write model and the raw bytes: `exportSegments` writes portable Roaring and ndjson, not Redis's layout. [The full mapping](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#coming-from-redis-bitmaps)
|
|
98
|
+
says what else differs.
|
|
99
|
+
|
|
100
|
+
## Documentation
|
|
101
|
+
|
|
102
|
+
- [Getting started](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md)
|
|
103
|
+
- [Before production](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md)
|
|
104
|
+
- [API reference](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/api-reference.md)
|
|
105
|
+
- [Changelog](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/CHANGELOG.md)
|
|
106
|
+
- [Privacy and shared responsibility](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/PRIVACY.md)
|
|
107
|
+
## License
|
|
163
108
|
|
|
164
|
-
|
|
165
|
-
than two. The sweep is bounded (`limit`, default 100), previewable (`dryRun`), and returns a per-segment ledger
|
|
166
|
-
rather than throwing — a fault on one segment must not decide the fate of the other ninety-nine. Once a day is
|
|
167
|
-
enough for daily buckets. Full walkthrough:
|
|
168
|
-
[getting-started §13.5](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#135-retention-ttl-and-pruning--what-exists-and-what-doesnt).
|
|
169
|
-
|
|
170
|
-
## No background process, and nothing to seed
|
|
171
|
-
|
|
172
|
-
There is no daemon, no compaction pass and no lifecycle worker to run: a segment exists once you have loaded a
|
|
173
|
-
generation into it, and the only scheduled work is the retention sweep above (plus `gcOrphanGenerations` if you
|
|
174
|
-
want superseded generations collected sooner than the sweep does it). Pass a **backend** — `S3Storage`,
|
|
175
|
-
`GcsStorage` or `AzureBlobStorage` from the storage package you installed, or `LocalFsStorage` /
|
|
176
|
-
`MemoryStorage` from this one — and you get all of it: generations resolved with one strong read, encrypted
|
|
177
|
-
segments, and the lifecycle helpers. `storage` also accepts a bare driver or a pre-built chunk source for
|
|
178
|
-
read-only wiring, which carries no registry and so offers none of those. There is no separate `registry`
|
|
179
|
-
option any more; passing one is refused by name.
|
|
180
|
-
|
|
181
|
-
Full README, guides, [benchmarks](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/benchmarks.md) (with
|
|
182
|
-
the method and what the numbers do *not* establish), and the design corpus live in the
|
|
183
|
-
[repository](https://github.com/cloudbitmaps/cloudbitmaps). Licensed Apache-2.0.
|
|
109
|
+
Apache-2.0
|