@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.
- package/NOTICE +3 -3
- package/PRIVACY.md +268 -76
- package/README.md +85 -95
- package/dist/bin/export-segments.d.ts +21 -0
- package/dist/bin/export-segments.js +1972 -30
- package/dist/bin/export-segments.js.map +7 -1
- package/dist/index.d.ts +969 -458
- package/dist/index.js +1461 -354
- package/dist/index.js.map +7 -1
- package/dist/open-charging-storage.d.ts +34 -0
- package/dist/option-keys.d.ts +19 -0
- package/dist/portable/decode.d.ts +53 -0
- package/dist/portable/layout.d.ts +32 -0
- package/dist/reserved-namespace.d.ts +3 -0
- package/dist/roaring-codec.d.ts +68 -0
- package/dist/system-clock.d.ts +39 -0
- package/dist/testing/conformance.d.ts +54 -0
- package/dist/testing/fuzz-codec.d.ts +23 -0
- package/package.json +11 -117
- package/dist/azure/index.cjs +0 -14
- package/dist/azure/index.cjs.map +0 -1
- package/dist/azure/index.d.cts +0 -9
- package/dist/azure/index.d.ts +0 -9
- package/dist/azure/index.js +0 -3
- package/dist/azure/index.js.map +0 -1
- package/dist/bin/compact-segments.js +0 -201
- package/dist/bin/compact-segments.js.map +0 -1
- package/dist/cassandra/index.cjs +0 -14
- package/dist/cassandra/index.cjs.map +0 -1
- package/dist/cassandra/index.d.cts +0 -1
- package/dist/cassandra/index.d.ts +0 -1
- package/dist/cassandra/index.js +0 -3
- package/dist/cassandra/index.js.map +0 -1
- package/dist/chunk-D6LIJSGF.js +0 -906
- package/dist/chunk-D6LIJSGF.js.map +0 -1
- package/dist/dynamodb/index.cjs +0 -14
- package/dist/dynamodb/index.cjs.map +0 -1
- package/dist/dynamodb/index.d.cts +0 -1
- package/dist/dynamodb/index.d.ts +0 -1
- package/dist/dynamodb/index.js +0 -3
- package/dist/dynamodb/index.js.map +0 -1
- package/dist/gcs/index.cjs +0 -14
- package/dist/gcs/index.cjs.map +0 -1
- package/dist/gcs/index.d.cts +0 -1
- package/dist/gcs/index.d.ts +0 -1
- package/dist/gcs/index.js +0 -3
- package/dist/gcs/index.js.map +0 -1
- package/dist/index.cjs +0 -894
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -734
- package/dist/mongodb/index.cjs +0 -14
- package/dist/mongodb/index.cjs.map +0 -1
- package/dist/mongodb/index.d.cts +0 -1
- package/dist/mongodb/index.d.ts +0 -1
- package/dist/mongodb/index.js +0 -3
- package/dist/mongodb/index.js.map +0 -1
- package/dist/mysql/index.cjs +0 -14
- package/dist/mysql/index.cjs.map +0 -1
- package/dist/mysql/index.d.cts +0 -1
- package/dist/mysql/index.d.ts +0 -1
- package/dist/mysql/index.js +0 -3
- package/dist/mysql/index.js.map +0 -1
- package/dist/postgres/index.cjs +0 -14
- package/dist/postgres/index.cjs.map +0 -1
- package/dist/postgres/index.d.cts +0 -1
- package/dist/postgres/index.d.ts +0 -1
- package/dist/postgres/index.js +0 -3
- package/dist/postgres/index.js.map +0 -1
- package/dist/redis/index.cjs +0 -14
- package/dist/redis/index.cjs.map +0 -1
- package/dist/redis/index.d.cts +0 -1
- package/dist/redis/index.d.ts +0 -1
- package/dist/redis/index.js +0 -3
- package/dist/redis/index.js.map +0 -1
- package/dist/s3/index.cjs +0 -14
- package/dist/s3/index.cjs.map +0 -1
- package/dist/s3/index.d.cts +0 -1
- package/dist/s3/index.d.ts +0 -1
- package/dist/s3/index.js +0 -3
- package/dist/s3/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,119 +1,109 @@
|
|
|
1
1
|
# @cloudbitmaps/roaring
|
|
2
2
|
|
|
3
|
-
**
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
and
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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>;
|