@cloudbitmaps/roaring 0.14.0 → 0.16.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 +3 -3
- package/README.md +6 -5
- package/dist/bin/export-segments.js +76 -25
- package/dist/bin/export-segments.js.map +2 -2
- package/dist/index.d.ts +22 -11
- package/dist/index.js +76 -25
- package/dist/index.js.map +2 -2
- package/package.json +2 -2
package/PRIVACY.md
CHANGED
|
@@ -44,7 +44,7 @@ from constructing a cross-region topology. The points where personal data moves
|
|
|
44
44
|
|---|---|---|
|
|
45
45
|
| **Storage** (object store) | immutable `.crbm` generations — every generation a segment has had, until a superseded one is collected. Azure Blob objects, and GCS objects above the simple-upload threshold, also carry a random write id in their metadata (`cbwid`): 128 random bits, no data from the bitmap or the source | the region of the bucket you wire |
|
|
46
46
|
| **Registry** (S3 / GCS / Azure Blob / local) | one row per segment: the current-generation pointer, wrapped keys, retention metadata, and the current generation's id count and your metadata (sealed when the segment is encrypted) — no IDs, unless you put one in the metadata, which you must not. An Azure Blob row also carries the random write id in its metadata | the region of the bucket you wire |
|
|
47
|
-
| **cache** (process RAM) | decoded chunks, bounded LRU | **wherever your process/Lambda runs** — an EU segment queried from a US function is processed in the US |
|
|
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, `eraseSubject`) | read your source (or existing generations), write a new generation | 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 |
|
|
50
50
|
|
|
@@ -126,7 +126,7 @@ after it returns, and re-run any erasure a rollback interrupted.
|
|
|
126
126
|
Erasure and crypto-shred are **immediate in storage and immediate in the store whose verb performed them**, for
|
|
127
127
|
every read that starts after the verb returns. A read already in progress in that store can still yield an erased id
|
|
128
128
|
from a chunk it had requested before the verb ran: up to 32 chunks for `iterate` and `count`, and up to `concurrency`
|
|
129
|
-
keys (32 by default) for a combine. They are not immediate in any *other* store, in the same process or another, and this library ships nothing that could
|
|
129
|
+
keys (32 by default) for a combine. A combine or `iterate` reads ranges of the object, and resolves the segment again before it serves each chunk, as a read of that chunk alone does, so the ranges it had already requested are dropped, not served. They are not immediate in any *other* store, in the same process or another, and this library ships nothing that could
|
|
130
130
|
make them so — there is no daemon, no bus, and no connection between two stores that happen to point at the same
|
|
131
131
|
bucket.
|
|
132
132
|
|
|
@@ -136,7 +136,7 @@ bucket.
|
|
|
136
136
|
| 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 |
|
|
137
137
|
| 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) |
|
|
138
138
|
| another store with **no registry** (a bare `IStorageDriver`), with `cache: { genTtlMs: 0 }`, or on a storage source built with **no clock** | **no bound** — only when its caches happen to let the segment go, or something tells it |
|
|
139
|
-
| a pinned handle (`seg.pin()`) in another store | **no bound** — until that store's reader cache evicts the pin's reader and its chunk cache evicts the chunks the pin decoded, or something tells it |
|
|
139
|
+
| 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 |
|
|
140
140
|
|
|
141
141
|
**An outage of the registry extends that bound.** A refresh that cannot read the row because of a transient fault
|
|
142
142
|
(throttling, a 5xx, a dropped connection) keeps serving the generation the reader already holds, and the key it
|
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ name.
|
|
|
60
60
|
| Option | What it holds |
|
|
61
61
|
|---|---|
|
|
62
62
|
| `storage` (required) | a backend (see above) |
|
|
63
|
-
| `cache` | `maxChunks`, `genTtlMs` (how soon a reader sees a new load, default 2000 ms), `readerMax`, `readerMaxBytes` |
|
|
63
|
+
| `cache` | `maxChunks`, `ttlMs` (an optional age limit on a cached chunk; omitted, a chunk stays until evicted), `genTtlMs` (how soon a reader sees a new load, default 2000 ms), `readerMax`, `readerMaxBytes` |
|
|
64
64
|
| `encryption` | `{ keystore, required }`: encrypt segments at rest with your own key |
|
|
65
65
|
| `retry` | tune the retry of reads, or `false` to turn it off |
|
|
66
66
|
| `metrics` | a metrics sink; off by default |
|
|
@@ -69,10 +69,11 @@ name.
|
|
|
69
69
|
|
|
70
70
|
## What it costs
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
A cold `intersect` of two 500,000-id segments sharing 100 of their 1,999 chunks made 6 GETs and took 95.03 ms at the median, **$2.40 per
|
|
73
|
+
million** at list prices: measured on S3 in-region, in the run of 2026-10-04, whose client had 128 sockets. On a spread layout it reads more
|
|
74
|
+
bytes than it needs, which cross-region can cost more than the requests it saves. A segment's first `store.load()` is
|
|
75
|
+
expected at **$11.20 per million**, pointer included: counted by running the engine and priced at list, not yet measured on a cloud. A cold `count()` is one
|
|
76
|
+
pointer read: it reads no payload and no object.
|
|
76
77
|
|
|
77
78
|
The trade is stated plainly. A membership check that misses the cache costs a ranged GET against object storage,
|
|
78
79
|
where an in-process store costs a memory read. If you need sub-millisecond answers on a working set that fits a
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
estimateCost,
|
|
34
34
|
groundedReport,
|
|
35
35
|
loadSegment,
|
|
36
|
+
loadSegmentChunks,
|
|
36
37
|
mapWithConcurrency,
|
|
37
38
|
resolveBudget,
|
|
38
39
|
resolvePerOpBudget,
|
|
@@ -460,7 +461,8 @@ function bitmapAsLoadInput(input) {
|
|
|
460
461
|
var roaringCodec = {
|
|
461
462
|
empty: () => SafeBitmap.empty(),
|
|
462
463
|
fromValues: (values) => SafeBitmap.fromValues(values),
|
|
463
|
-
safeDeserialize: (bytes, maxBytes, options) => SafeBitmap.safeDeserialize(bytes, maxBytes, options)
|
|
464
|
+
safeDeserialize: (bytes, maxBytes, options) => SafeBitmap.safeDeserialize(bytes, maxBytes, options),
|
|
465
|
+
owns: (bitmap) => bitmap instanceof SafeBitmap
|
|
464
466
|
};
|
|
465
467
|
|
|
466
468
|
// src/system-clock.ts
|
|
@@ -885,7 +887,7 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
885
887
|
const metadata = options?.metadata;
|
|
886
888
|
let result;
|
|
887
889
|
try {
|
|
888
|
-
|
|
890
|
+
const loadOptions = {
|
|
889
891
|
...options?.allowEmpty === void 0 ? {} : { allowEmpty: options.allowEmpty },
|
|
890
892
|
...options?.guard === void 0 ? {} : { guard: options.guard },
|
|
891
893
|
...metadata === void 0 ? {} : { metadata },
|
|
@@ -898,7 +900,8 @@ var CloudRoaring = class _CloudRoaring {
|
|
|
898
900
|
// as a side effect of adding a guard whose entire purpose is preventing data loss. Opt in with `keep`.
|
|
899
901
|
keep: options?.keep ?? KEEP_EVERY_GENERATION,
|
|
900
902
|
...options?.audit === void 0 ? {} : { audit: options.audit }
|
|
901
|
-
}
|
|
903
|
+
};
|
|
904
|
+
result = ids instanceof CombineChunks ? await loadSegmentChunks(dest, ids.chunks, deps, loadOptions) : await loadSegment(dest, ids, deps, loadOptions);
|
|
902
905
|
} finally {
|
|
903
906
|
this.engine.invalidate(dest);
|
|
904
907
|
}
|
|
@@ -1702,6 +1705,32 @@ var EMPTY_IDS = {
|
|
|
1702
1705
|
async *batches() {
|
|
1703
1706
|
}
|
|
1704
1707
|
};
|
|
1708
|
+
var AS_IDS = {
|
|
1709
|
+
none: EMPTY_IDS,
|
|
1710
|
+
failing,
|
|
1711
|
+
intersect: (engine, refs, opts) => withBatches(engine.intersect(refs, opts), () => engine.intersectBatches(refs, opts)),
|
|
1712
|
+
union: (engine, refs, opts) => withBatches(engine.union(refs, opts), () => engine.unionBatches(refs, opts)),
|
|
1713
|
+
andNot: (engine, base, refs, opts) => withBatches(engine.andNot(base, refs, opts), () => engine.andNotBatches(base, refs, opts))
|
|
1714
|
+
};
|
|
1715
|
+
var NO_CHUNKS = {
|
|
1716
|
+
async *[Symbol.asyncIterator]() {
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
var CombineChunks = class {
|
|
1720
|
+
constructor(chunks) {
|
|
1721
|
+
this.chunks = chunks;
|
|
1722
|
+
}
|
|
1723
|
+
chunks;
|
|
1724
|
+
};
|
|
1725
|
+
var AS_CHUNKS = {
|
|
1726
|
+
none: NO_CHUNKS,
|
|
1727
|
+
failing: (err) => ({
|
|
1728
|
+
[Symbol.asyncIterator]: () => ({ next: () => Promise.reject(err) })
|
|
1729
|
+
}),
|
|
1730
|
+
intersect: (engine, refs, opts) => engine.intersectChunks(refs, opts),
|
|
1731
|
+
union: (engine, refs, opts) => engine.unionChunks(refs, opts),
|
|
1732
|
+
andNot: (engine, base, refs, opts) => engine.andNotChunks(base, refs, opts)
|
|
1733
|
+
};
|
|
1705
1734
|
var KEEP_EVERY_GENERATION = Number.MAX_SAFE_INTEGER;
|
|
1706
1735
|
var makeSegment;
|
|
1707
1736
|
var minting = false;
|
|
@@ -1993,23 +2022,27 @@ var Segment = class _Segment {
|
|
|
1993
2022
|
/**
|
|
1994
2023
|
* Chunk-skipping intersection: stream the ids in **this** segment AND every segment in `others`, ascending.
|
|
1995
2024
|
* Fetches only the Storage chunks present in *all* operands (a key absent from any operand contributes nothing
|
|
1996
|
-
* and is never downloaded),
|
|
1997
|
-
* (Lambda-friendly) regardless of segment size. Pass `concurrency` to tune that window (
|
|
2025
|
+
* and is never downloaded), reading each operand's as coalesced ranges under a bounded in-flight window — so the Storage
|
|
2026
|
+
* footprint stays small (Lambda-friendly) regardless of segment size. Pass `concurrency` to tune that window (the range
|
|
2027
|
+
* requests held ahead per operand: a positive integer, 32 by default).
|
|
1998
2028
|
* AND is commutative, so `a.intersect([b])` and `b.intersect([a])` yield the same ids. Pass `budget` to
|
|
1999
2029
|
* override the store's per-op denial-of-wallet budget for this call (or `false` to lift it).
|
|
2000
2030
|
*/
|
|
2001
2031
|
intersect(others, options) {
|
|
2002
|
-
|
|
2032
|
+
return this.intersectAs(AS_IDS, others, options);
|
|
2033
|
+
}
|
|
2034
|
+
intersectAs(out, others, options) {
|
|
2035
|
+
if (this.expired() || others.some((o) => o.expired())) return out.none;
|
|
2003
2036
|
const exclude = this.liveExcludes(options);
|
|
2004
2037
|
let engine;
|
|
2005
2038
|
try {
|
|
2006
2039
|
engine = this.combineEngine([this, ...others, ...exclude]) ?? this.engine;
|
|
2007
2040
|
} catch (err) {
|
|
2008
|
-
return failing(err);
|
|
2041
|
+
return out.failing(err);
|
|
2009
2042
|
}
|
|
2010
2043
|
const refs = [this.ref, ...others.map((o) => o.ref)];
|
|
2011
2044
|
const opts = this.refsIn(options, exclude);
|
|
2012
|
-
return
|
|
2045
|
+
return out.intersect(engine, refs, opts);
|
|
2013
2046
|
}
|
|
2014
2047
|
/**
|
|
2015
2048
|
* Materialize `this ∩ others…` (minus `exclude`) as a **new generation of `dest`** — `dest`'s previous contents
|
|
@@ -2036,7 +2069,12 @@ var Segment = class _Segment {
|
|
|
2036
2069
|
this.combineEngine([this, ...others, ...options?.exclude ?? []]);
|
|
2037
2070
|
return this.timed(
|
|
2038
2071
|
"intersectInto",
|
|
2039
|
-
() => this.materialize(
|
|
2072
|
+
() => this.materialize(
|
|
2073
|
+
dest.ref,
|
|
2074
|
+
new CombineChunks(this.intersectAs(AS_CHUNKS, others, options)),
|
|
2075
|
+
"intersectInto",
|
|
2076
|
+
options
|
|
2077
|
+
)
|
|
2040
2078
|
);
|
|
2041
2079
|
}
|
|
2042
2080
|
/**
|
|
@@ -2050,26 +2088,29 @@ var Segment = class _Segment {
|
|
|
2050
2088
|
* segment once (`unionInto`, or a load) is the cheaper shape.
|
|
2051
2089
|
*/
|
|
2052
2090
|
union(others, options) {
|
|
2091
|
+
return this.unionAs(AS_IDS, others, options);
|
|
2092
|
+
}
|
|
2093
|
+
unionAs(out, others, options) {
|
|
2053
2094
|
const live = others.filter((o) => !o.expired());
|
|
2054
2095
|
if (this.expired()) {
|
|
2055
|
-
if (live.length === 0) return
|
|
2056
|
-
return live[0].
|
|
2096
|
+
if (live.length === 0) return out.none;
|
|
2097
|
+
return live[0].unionAs(out, live.slice(1), options);
|
|
2057
2098
|
}
|
|
2058
2099
|
if (live.length === 0 && others.length > 0) {
|
|
2059
2100
|
const exclude2 = this.liveExcludes(options);
|
|
2060
|
-
return exclude2.length > 0 ? this.
|
|
2101
|
+
return exclude2.length > 0 ? this.andNotAs(out, [...exclude2], options) : this.unionAs(out, [], options);
|
|
2061
2102
|
}
|
|
2062
|
-
if (live.length !== others.length) return this.
|
|
2103
|
+
if (live.length !== others.length) return this.unionAs(out, live, options);
|
|
2063
2104
|
const exclude = this.liveExcludes(options);
|
|
2064
2105
|
let engine;
|
|
2065
2106
|
try {
|
|
2066
2107
|
engine = this.combineEngine([this, ...others, ...exclude]) ?? this.engine;
|
|
2067
2108
|
} catch (err) {
|
|
2068
|
-
return failing(err);
|
|
2109
|
+
return out.failing(err);
|
|
2069
2110
|
}
|
|
2070
2111
|
const refs = [this.ref, ...others.map((o) => o.ref)];
|
|
2071
2112
|
const opts = this.refsIn(options, exclude);
|
|
2072
|
-
return
|
|
2113
|
+
return out.union(engine, refs, opts);
|
|
2073
2114
|
}
|
|
2074
2115
|
/** Materialize `this ∪ others…` (minus `exclude`) as a **new generation of `dest`** — see {@link intersectInto}. */
|
|
2075
2116
|
async unionInto(dest, others, options) {
|
|
@@ -2077,7 +2118,12 @@ var Segment = class _Segment {
|
|
|
2077
2118
|
this.combineEngine([this, ...others, ...options?.exclude ?? []]);
|
|
2078
2119
|
return this.timed(
|
|
2079
2120
|
"unionInto",
|
|
2080
|
-
() => this.materialize(
|
|
2121
|
+
() => this.materialize(
|
|
2122
|
+
dest.ref,
|
|
2123
|
+
new CombineChunks(this.unionAs(AS_CHUNKS, others, options)),
|
|
2124
|
+
"unionInto",
|
|
2125
|
+
options
|
|
2126
|
+
)
|
|
2081
2127
|
);
|
|
2082
2128
|
}
|
|
2083
2129
|
/**
|
|
@@ -2096,25 +2142,25 @@ var Segment = class _Segment {
|
|
|
2096
2142
|
* the suppression folds into the same pass rather than materializing an intermediate segment first.
|
|
2097
2143
|
*/
|
|
2098
2144
|
andNot(excludes, options) {
|
|
2099
|
-
|
|
2145
|
+
return this.andNotAs(AS_IDS, excludes, options);
|
|
2146
|
+
}
|
|
2147
|
+
andNotAs(out, excludes, options) {
|
|
2148
|
+
if (this.expired()) return out.none;
|
|
2100
2149
|
const liveExcludes = excludes.filter((e) => !e.expired());
|
|
2101
2150
|
if (liveExcludes.length === 0 && excludes.length > 0) {
|
|
2102
|
-
return this.
|
|
2151
|
+
return this.unionAs(out, [], options == null ? void 0 : readOptions(options));
|
|
2103
2152
|
}
|
|
2104
|
-
if (liveExcludes.length !== excludes.length) return this.
|
|
2153
|
+
if (liveExcludes.length !== excludes.length) return this.andNotAs(out, liveExcludes, options);
|
|
2105
2154
|
let engine;
|
|
2106
2155
|
try {
|
|
2107
2156
|
engine = this.combineEngine([this, ...excludes]) ?? this.engine;
|
|
2108
2157
|
} catch (err) {
|
|
2109
|
-
return failing(err);
|
|
2158
|
+
return out.failing(err);
|
|
2110
2159
|
}
|
|
2111
2160
|
const base = this.ref;
|
|
2112
2161
|
const refs = excludes.map((o) => o.ref);
|
|
2113
2162
|
const opts = options == null ? void 0 : readOptions(options);
|
|
2114
|
-
return
|
|
2115
|
-
engine.andNot(base, refs, opts),
|
|
2116
|
-
() => engine.andNotBatches(base, refs, opts)
|
|
2117
|
-
);
|
|
2163
|
+
return out.andNot(engine, base, refs, opts);
|
|
2118
2164
|
}
|
|
2119
2165
|
/** Materialize `this \ (excludes…)` as a **new generation of `dest`** — see {@link intersectInto}. */
|
|
2120
2166
|
async andNotInto(dest, excludes, options) {
|
|
@@ -2122,7 +2168,12 @@ var Segment = class _Segment {
|
|
|
2122
2168
|
this.combineEngine([this, ...excludes]);
|
|
2123
2169
|
return this.timed(
|
|
2124
2170
|
"andNotInto",
|
|
2125
|
-
() => this.materialize(
|
|
2171
|
+
() => this.materialize(
|
|
2172
|
+
dest.ref,
|
|
2173
|
+
new CombineChunks(this.andNotAs(AS_CHUNKS, excludes, options)),
|
|
2174
|
+
"andNotInto",
|
|
2175
|
+
options
|
|
2176
|
+
)
|
|
2126
2177
|
);
|
|
2127
2178
|
}
|
|
2128
2179
|
/**
|