@cloudbitmaps/roaring 0.9.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.
Files changed (80) hide show
  1. package/NOTICE +3 -3
  2. package/PRIVACY.md +268 -76
  3. package/README.md +85 -95
  4. package/dist/bin/export-segments.d.ts +21 -0
  5. package/dist/bin/export-segments.js +1972 -30
  6. package/dist/bin/export-segments.js.map +7 -1
  7. package/dist/index.d.ts +969 -458
  8. package/dist/index.js +1461 -354
  9. package/dist/index.js.map +7 -1
  10. package/dist/open-charging-storage.d.ts +34 -0
  11. package/dist/option-keys.d.ts +19 -0
  12. package/dist/portable/decode.d.ts +53 -0
  13. package/dist/portable/layout.d.ts +32 -0
  14. package/dist/reserved-namespace.d.ts +3 -0
  15. package/dist/roaring-codec.d.ts +68 -0
  16. package/dist/system-clock.d.ts +39 -0
  17. package/dist/testing/conformance.d.ts +54 -0
  18. package/dist/testing/fuzz-codec.d.ts +23 -0
  19. package/package.json +11 -117
  20. package/dist/azure/index.cjs +0 -14
  21. package/dist/azure/index.cjs.map +0 -1
  22. package/dist/azure/index.d.cts +0 -9
  23. package/dist/azure/index.d.ts +0 -9
  24. package/dist/azure/index.js +0 -3
  25. package/dist/azure/index.js.map +0 -1
  26. package/dist/bin/compact-segments.js +0 -201
  27. package/dist/bin/compact-segments.js.map +0 -1
  28. package/dist/cassandra/index.cjs +0 -14
  29. package/dist/cassandra/index.cjs.map +0 -1
  30. package/dist/cassandra/index.d.cts +0 -1
  31. package/dist/cassandra/index.d.ts +0 -1
  32. package/dist/cassandra/index.js +0 -3
  33. package/dist/cassandra/index.js.map +0 -1
  34. package/dist/chunk-D6LIJSGF.js +0 -906
  35. package/dist/chunk-D6LIJSGF.js.map +0 -1
  36. package/dist/dynamodb/index.cjs +0 -14
  37. package/dist/dynamodb/index.cjs.map +0 -1
  38. package/dist/dynamodb/index.d.cts +0 -1
  39. package/dist/dynamodb/index.d.ts +0 -1
  40. package/dist/dynamodb/index.js +0 -3
  41. package/dist/dynamodb/index.js.map +0 -1
  42. package/dist/gcs/index.cjs +0 -14
  43. package/dist/gcs/index.cjs.map +0 -1
  44. package/dist/gcs/index.d.cts +0 -1
  45. package/dist/gcs/index.d.ts +0 -1
  46. package/dist/gcs/index.js +0 -3
  47. package/dist/gcs/index.js.map +0 -1
  48. package/dist/index.cjs +0 -894
  49. package/dist/index.cjs.map +0 -1
  50. package/dist/index.d.cts +0 -734
  51. package/dist/mongodb/index.cjs +0 -14
  52. package/dist/mongodb/index.cjs.map +0 -1
  53. package/dist/mongodb/index.d.cts +0 -1
  54. package/dist/mongodb/index.d.ts +0 -1
  55. package/dist/mongodb/index.js +0 -3
  56. package/dist/mongodb/index.js.map +0 -1
  57. package/dist/mysql/index.cjs +0 -14
  58. package/dist/mysql/index.cjs.map +0 -1
  59. package/dist/mysql/index.d.cts +0 -1
  60. package/dist/mysql/index.d.ts +0 -1
  61. package/dist/mysql/index.js +0 -3
  62. package/dist/mysql/index.js.map +0 -1
  63. package/dist/postgres/index.cjs +0 -14
  64. package/dist/postgres/index.cjs.map +0 -1
  65. package/dist/postgres/index.d.cts +0 -1
  66. package/dist/postgres/index.d.ts +0 -1
  67. package/dist/postgres/index.js +0 -3
  68. package/dist/postgres/index.js.map +0 -1
  69. package/dist/redis/index.cjs +0 -14
  70. package/dist/redis/index.cjs.map +0 -1
  71. package/dist/redis/index.d.cts +0 -1
  72. package/dist/redis/index.d.ts +0 -1
  73. package/dist/redis/index.js +0 -3
  74. package/dist/redis/index.js.map +0 -1
  75. package/dist/s3/index.cjs +0 -14
  76. package/dist/s3/index.cjs.map +0 -1
  77. package/dist/s3/index.d.cts +0 -1
  78. package/dist/s3/index.d.ts +0 -1
  79. package/dist/s3/index.js +0 -3
  80. package/dist/s3/index.js.map +0 -1
package/README.md CHANGED
@@ -1,119 +1,109 @@
1
1
  # @cloudbitmaps/roaring
2
2
 
3
- **Distributed, cloud-native Roaring Bitmaps.** Query and intersect billion-scale integer sets over tiered cloud
4
- storage (RAM → NoSQL → object store) at a fraction of an always-on cache — with the fast, familiar in-memory API:
5
- `add`, `has`, `remove`, `count`, `intersect`.
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.
6
6
 
7
- This is the **flagship flavor** of the [CloudBitmaps](https://github.com/cloudbitmaps/cloudbitmaps) family: the
8
- roaring codec (CRoaring, via `roaring`) plus the `CloudRoaring` facade, on top of the codec-agnostic
9
- [`@cloudbitmaps/core`](https://www.npmjs.com/package/@cloudbitmaps/core) engine — which arrives **transitively**,
10
- so this is the only package you install.
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
+
10
+ > Pre-1.0: the API and the on-disk format can still change. These docs describe `main`, ahead of the npm release.
11
+
12
+ ## Install
11
13
 
12
14
  ```bash
13
- npm i @cloudbitmaps/roaring @aws-sdk/client-s3 @aws-sdk/client-dynamodb # + only the SDK(s) you use
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
14
17
  ```
15
18
 
16
- ```ts
17
- import { CloudRoaring } from '@cloudbitmaps/roaring';
18
- import { S3ColdDriver } from '@cloudbitmaps/roaring/s3';
19
- import { DynamoDbWarmDriver } from '@cloudbitmaps/roaring/dynamodb';
20
- ```
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).
21
20
 
22
- Every storage driver is re-exported on a matching subpath (`/s3`, `/gcs`, `/azure`, `/dynamodb`, `/postgres`,
23
- `/redis`, `/mongodb`, `/cassandra`, `/mysql`); each backend SDK is an optional peer dependency, so the main entry
24
- stays lean. Ships two CLIs: `compact-segments` and `export-segments`.
21
+ ## Use
25
22
 
26
- ## Measured, not modeled
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`:
27
25
 
28
- Benchmarked against **real** S3 + DynamoDB in `us-east-1`, not an emulator: **$0.75 per million** incremental
29
- writes, **$0.14 per million** `count()`s, **$5.88 per million** segment publishes — against an always-on Redis-HA
30
- line of **$346/month, standing**. DynamoDB capacity is taken from AWS's own `ConsumedCapacity` rather than
31
- estimated. `count()` on a published segment does **0 payload reads**, and intersecting two 2,000,000-id segments
32
- fetches only the shared chunks.
26
+ ```js
27
+ import { CloudRoaring, MemoryStorage } from '@cloudbitmaps/roaring';
33
28
 
34
- The trade is stated plainly rather than buried: a membership check that misses the hot cache costs a network
35
- round trip to your warm tier, where an in-process RAM store costs a memory read. If you need a sub-millisecond
36
- p99 on a working set that fits a bounded hot cache, use Redis. If your sets are large, mostly read, and shouldn't cost $346/month to keep warm, use this.
29
+ const store = new CloudRoaring({ storage: new MemoryStorage() });
37
30
 
38
- ## Coming from Redis bitmaps?
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]);
33
+
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
+ ```
39
+
40
+ To persist, pass a different **backend** as `storage`. Nothing else changes:
41
+
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) |
39
49
 
40
- **A durable alternative to Redis bitmaps for set-shaped work** — audiences, dedup, suppression, membership —
41
- where the sets are large, mostly idle, must survive a restart, and are written in **batches**. You are not giving
42
- up the bitmap either: past **4,096 ids** in a 65,536-id chunk — 6.25% of it — Roaring stores that chunk *as* a flat
43
- bit array, byte for byte what you have now. It just stops paying for the chunks you never wrote to.
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.
44
52
 
45
- | Redis | Here |
53
+ ## Options
54
+
55
+ `new CloudRoaring({ storage, ...options })`. Only `storage` is required. A key the store does not take is refused by
56
+ name.
57
+
58
+ | Option | What it holds |
46
59
  |---|---|
47
- | `SETBIT` / `GETBIT` | `add` / `addMany` · `remove` / `removeMany` · `has` |
48
- | `prior = SETBIT` | `claimMany(ids)` — adds a batch and returns only the ids that were **not** already there, so an *"already sent to this id?"* check is exactly-once |
49
- | `BITCOUNT` | `count()` — exact, served from the index with no payload reads |
50
- | `BITOP AND` / `OR` / `DIFF` | `intersect` / `union` / `andNot` — and `intersect` skips chunks that cannot contribute |
51
- | `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. And never enable your backend's own row expiry on the warm table — those rows are un-compacted deltas, so it discards writes silently |
52
-
53
- **It is not a drop-in replacement, and two limits are worth knowing before you port anything.**
54
-
55
- **1. Do not port a per-id write loop.** `SETBIT` flips one bit in place and is genuinely O(1). Here a warm write
56
- re-serializes a whole 65,536-id chunk, so **5,000 ids added one at a time cost 5,000 writes and 23,762 KB, against
57
- 1 write and 8 KB for the same ids in one call** — roughly 3,000× the bytes. Batch and you are far cheaper than
58
- Redis; port the loop literally and you are far more expensive. On a per-request-metered store (DynamoDB on-demand)
59
- that is a line item; on an instance-priced one (ElastiCache, RDS) it surfaces as IOPS and latency headroom instead,
60
- which makes it easier to miss rather than cheaper. `claimMany` takes a batch for exactly this reason.
61
-
62
- **2. What does not carry over: the raw bytes.** A `.crbm` object is not a flat bit array, so anything reading your
63
- Redis bitmap's underlying string — a job that `GET`s the key and indexes into it, a byte-for-byte backup —
64
- will not read ours. `BITFIELD`, `BITPOS`, `BITOP NOT` and byte-range `BITCOUNT` have no equivalent either: this
65
- is a set of ids, not an addressable bit buffer, and `NOT` in particular has no bounded universe to complement
66
- against. Raw bit-position import/export is unbuilt;
67
- [say so in an issue](https://github.com/cloudbitmaps/cloudbitmaps/issues) if you need it, because that is what
68
- decides whether it gets built.
69
-
70
- ## Retiring data: a per-segment expiry, and a sweep you schedule
71
-
72
- A **segment** can expire; an individual **id** cannot (a bitmap stores ids, not `(id, timestamp)` pairs — a
73
- timestamp per id costs more than the compression saves).
74
-
75
- ```ts
76
- const DAY = 86_400_000;
77
- const ref = { namespace: 'active-daily', segment: '2026-08-05' };
78
-
79
- await store.setRetention(ref, { expiresAt: Date.now() + 30 * DAY }); // once, when you create the bucket
80
- ```
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 |
81
67
 
82
- `expiresAt` is an absolute instant **you** compute, not a duration derived from anything observed: compaction
83
- rewrites every basis such a duration could use, so "30 days since the last write" would keep a busy bucket alive
84
- precisely *because* the daemon was keeping it cheap.
68
+ ## What it costs
85
69
 
86
- Then, from whatever schedule your deployment already has — an EventBridge rule, a Kubernetes `CronJob`, `cron`, a
87
- queue job — run the sweep. **This library starts no background timer**, deliberately: the same code has to behave
88
- identically in a Lambda, an edge isolate and a long-lived server, and a timer that only works in one of those is
89
- worse than none.
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.
90
74
 
91
- ```ts
92
- const swept = await store.retireExpired({ namespace: 'active-daily' });
93
- for (const e of swept.entries) if (e.action === 'skipped') console.warn(e.segment, e.reason);
94
- if (swept.limited) scheduleAnotherPassSoon(); // the per-cycle cap deferred some; re-run
95
- ```
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
- Each retirement goes through `dropSegment`, so the Warm → registry → Cold ordering is one implementation rather
98
- than two. The sweep is bounded (`limit`, default 100), previewable (`dryRun`), and returns a per-segment ledger
99
- rather than throwing — a fault on one segment must not decide the fate of the other ninety-nine. Once a day is
100
- enough for daily buckets. Full walkthrough:
101
- [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).
80
+ ## Before production
102
81
 
103
- ## No seed step, and compaction is optional
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.
87
+
88
+ The [production checklist](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/production.md) covers each item.
89
+
90
+ ## Coming from Redis bitmaps?
104
91
 
105
- A brand-new segment is usable the moment you construct the store: `addMany`, `has`, `remove`, `count`, `iterate`
106
- and the whole set algebra work against a segment with **no cold generation and no registry row**, because a read
107
- merges `(cold ∪ warm.adds) \ warm.removes` and an absent cold tier just makes that merge trivial. Bulk-load is an
108
- **import** path for data you already have elsewhere, not an initialization step.
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.
109
99
 
110
- Compaction is a **cost** optimization, never a correctness requirement — it buys cheaper storage, smaller rewrites,
111
- and a free index-only `count()`. For a write-once dated bucket you retire (with `dropSegment`, or by recording an
112
- expiry as above), skip it entirely.
100
+ ## Documentation
113
101
 
114
- Redis stays first-class as a **warm tier** underneath this (`@cloudbitmaps/roaring/redis`) — the point above is
115
- about replacing `SETBIT`-on-one-giant-key as your *data model*, not replacing Redis as infrastructure.
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
116
108
 
117
- Full README, guides, [benchmarks](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/benchmarks.md) (with
118
- the method and what the numbers do *not* establish), and the design corpus live in the
119
- [repository](https://github.com/cloudbitmaps/cloudbitmaps). Licensed Apache-2.0.
109
+ Apache-2.0
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import type { ExportFormat, ExportManifest, ExportSink } from '../index.js';
3
+ export interface ExportConfig {
4
+ readonly root: string;
5
+ readonly out: string;
6
+ readonly format: ExportFormat;
7
+ readonly namespace?: string;
8
+ }
9
+ /** Parse + validate config from an environment map. Throws a clear `Error` on misconfiguration. */
10
+ export declare function parseConfig(env: Record<string, string | undefined>): ExportConfig;
11
+ /**
12
+ * A filesystem {@link ExportSink}. Writes each segment to a **unique** `<segment><ext>.<uuid>.part` temp
13
+ * (`O_EXCL` create, mode `0o600`) and atomically renames it into `<out>/<namespace>/<segment><ext>` on `close()`,
14
+ * so a half-written file never masquerades as complete and concurrent exports to the same dir can't clobber one
15
+ * another. Uses a `FileHandle` (not `createWriteStream`) so an I/O fault **rejects the write** rather than
16
+ * emitting an unhandled `'error'` event that would crash the process, and loops on short writes so a partial
17
+ * `write()` never truncates the output. Artifacts are **owner-only** (dir `0o700`, files `0o600`) — cleartext.
18
+ */
19
+ export declare function fsSink(out: string): ExportSink;
20
+ /** CLI entry: build a LocalFs store, export every enumerated segment, then write the manifest. */
21
+ export declare function main(env?: Record<string, string | undefined>, now?: () => number): Promise<ExportManifest>;