@cloudbitmaps/roaring 0.8.2 → 0.10.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 +198 -72
- package/README.md +148 -50
- package/dist/bin/export-segments.d.ts +21 -0
- package/dist/bin/export-segments.js +1508 -30
- package/dist/bin/export-segments.js.map +7 -1
- package/dist/codec-bound.d.ts +38 -0
- package/dist/index.d.ts +875 -425
- package/dist/index.js +997 -306
- package/dist/index.js.map +7 -1
- package/dist/moved-options.d.ts +14 -0
- package/dist/portable/decode.d.ts +54 -0
- package/dist/roaring-codec.d.ts +61 -0
- package/dist/system-clock.d.ts +40 -0
- package/dist/testing/conformance.d.ts +34 -0
- package/dist/testing/fuzz-codec.d.ts +9 -0
- package/package.json +11 -116
- 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 -134
- 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-4NVOQJGJ.js +0 -801
- package/dist/chunk-4NVOQJGJ.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 -789
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -642
- 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/dist/index.d.ts
CHANGED
|
@@ -1,277 +1,205 @@
|
|
|
1
|
-
import { bulkLoadCrbmGeneration as bulkLoadCrbmGeneration$1, compactSegment as compactSegment$1, runCompactionCycle as runCompactionCycle$1, runExport as runExport$1, CodecBitmap, CodecInterface, BudgetOption, IColdDriver, ColdChunkSource, IWarmDriver, IRegistryDriver, IKeystore, Clock, Rng, RetryPolicy, IMetricsSink, SegmentEngine, SegmentRef, PricingProfile, Workload, Topology, CostReport, IAuditSink, CompactionOptions, CompactionResult, DropResult, ConsistencyReport, ExportSink, ExportOptions, ExportManifest, EstimateInput } from '@cloudbitmaps/core';
|
|
2
|
-
export * from '@cloudbitmaps/core';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Codec-bound wrappers over the public `@cloudbitmaps/core` entry points that need a bitmap codec.
|
|
6
|
-
*
|
|
7
|
-
* `core` is codec-agnostic and therefore cannot default the codec (the concrete codec lives *here*, in a package
|
|
8
|
-
* that depends on core — a default over there would invert the dependency arrow). But an application using
|
|
9
|
-
* `@cloudbitmaps/roaring` should never have to pass one. So this module re-binds each such entry point with
|
|
10
|
-
* {@link roaringCodec} and the facade re-exports these **explicitly**, which shadows the same names coming from
|
|
11
|
-
* `export * from '@cloudbitmaps/core'` (an explicit export always wins over a star export). Net effect: every
|
|
12
|
-
* call signature is unchanged from before the family split — you can still call
|
|
13
|
-
* `bulkLoadCrbmGeneration(driver, key, ids)` with no options at all.
|
|
14
|
-
*
|
|
15
|
-
* A caller who *wants* a different codec passes it explicitly; the binding only fills an absent one — via
|
|
16
|
-
* `?? roaringCodec` rather than spread order, so an explicit `codec: undefined` still gets the binding
|
|
17
|
-
* (`exactOptionalPropertyTypes` is off, so that call typechecks and must not fall through to the throw).
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
type BulkLoad = typeof bulkLoadCrbmGeneration$1;
|
|
21
|
-
type CompactSegment = typeof compactSegment$1;
|
|
22
|
-
type RunCompactionCycle = typeof runCompactionCycle$1;
|
|
23
|
-
type RunExport = typeof runExport$1;
|
|
24
1
|
/**
|
|
25
|
-
*
|
|
2
|
+
* CloudRoaring — distributed, cloud-native Roaring Bitmaps, read from object storage.
|
|
26
3
|
*
|
|
27
|
-
* The
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* one in a simulation, say) keeps it.
|
|
33
|
-
*/
|
|
34
|
-
declare const bulkLoadCrbmGeneration: BulkLoad;
|
|
35
|
-
/** {@link coreCompactSegment} with the roaring codec pre-bound. */
|
|
36
|
-
declare const compactSegment: CompactSegment;
|
|
37
|
-
/** {@link coreRunCompactionCycle} with the roaring codec pre-bound. */
|
|
38
|
-
declare const runCompactionCycle: RunCompactionCycle;
|
|
39
|
-
/** {@link coreRunExport} with the roaring codec pre-bound (only the `'roaring'` format needs it). */
|
|
40
|
-
declare const runExport: RunExport;
|
|
41
|
-
|
|
42
|
-
declare class SafeBitmap implements CodecBitmap {
|
|
43
|
-
private readonly bitmap;
|
|
44
|
-
private constructor();
|
|
45
|
-
static empty(): SafeBitmap;
|
|
46
|
-
static fromValues(values: Iterable<number>): SafeBitmap;
|
|
47
|
-
/**
|
|
48
|
-
* S1: validate size, then deserialize with the **portable** (validated) format.
|
|
49
|
-
* Throws `IntegrityError` if the input exceeds `maxBytes` or fails to decode — the
|
|
50
|
-
* native addon is never handed unbounded or unsafe-format input.
|
|
51
|
-
*/
|
|
52
|
-
static safeDeserialize(bytes: Uint8Array, maxBytes: number): SafeBitmap;
|
|
53
|
-
serialize(): Uint8Array;
|
|
54
|
-
/**
|
|
55
|
-
* Convert containers to run-encoding where that is smaller. Representation only — membership is untouched, and
|
|
56
|
-
* the portable format still round-trips through {@link RoaringCodec.safeDeserialize}.
|
|
57
|
-
*
|
|
58
|
-
* Roaring's third container type is a RUN, and no implementation selects it on its own: it takes an explicit
|
|
59
|
-
* `runOptimize()` pass. Nothing here made that call, so run-shaped ids paid array or bitset prices — measured
|
|
60
|
-
* at **570×** for a contiguous 1M-id range (128.1 KiB → 0.2 KiB) and **63×** for a 2,000-run shape
|
|
61
|
-
* (536.5 KiB → 8.5 KiB). Sparse ids come out byte-identical, so this is never a losing trade; roaring keeps
|
|
62
|
-
* whichever encoding is smaller per container.
|
|
63
|
-
*
|
|
64
|
-
* Those two factors are THIS CALL's own before/after on the same ids. They are not the ones the site quotes
|
|
65
|
-
* (543× and 63×) — those compare the optimized encoding against the best *fixed* representation at its floor,
|
|
66
|
-
* a different baseline that happens to land near 63× on the run shape too. `pnpm bench:encoding` prints both
|
|
67
|
-
* columns; don't carry a figure between them without checking which denominator it had.
|
|
68
|
-
*
|
|
69
|
-
* Called by the engine only when writing an immutable cold generation — see {@link CodecBitmap.optimize} for
|
|
70
|
-
* why the per-operation warm path deliberately does not.
|
|
71
|
-
*/
|
|
72
|
-
optimize(): void;
|
|
73
|
-
/**
|
|
74
|
-
* Largest value, or `undefined` when empty. O(1) — roaring reads it off the container index, so the engine's
|
|
75
|
-
* per-chunk range assertion costs one call per chunk rather than a walk per id.
|
|
76
|
-
*/
|
|
77
|
-
maximum(): number | undefined;
|
|
78
|
-
add(value: number): void;
|
|
79
|
-
addMany(values: Iterable<number>): void;
|
|
80
|
-
remove(value: number): void;
|
|
81
|
-
removeMany(values: Iterable<number>): void;
|
|
82
|
-
has(value: number): boolean;
|
|
83
|
-
get size(): number;
|
|
84
|
-
get isEmpty(): boolean;
|
|
85
|
-
clone(): SafeBitmap;
|
|
86
|
-
/** In-place union: `this = this ∪ other`. */
|
|
87
|
-
orInPlace(other: CodecBitmap): void;
|
|
88
|
-
/** In-place difference: `this = this \ other`. */
|
|
89
|
-
andNotInPlace(other: CodecBitmap): void;
|
|
90
|
-
/** In-place intersection: `this = this ∩ other`. */
|
|
91
|
-
andInPlace(other: CodecBitmap): void;
|
|
92
|
-
/** Ascending iterator over the set values. */
|
|
93
|
-
[Symbol.iterator](): IterableIterator<number>;
|
|
94
|
-
toArray(): number[];
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* The roaring {@link CodecInterface} — the flagship codec, delegating to {@link SafeBitmap}'s statics. This is
|
|
98
|
-
* the default the `CloudRoaring` facade injects into the engine; `core/` itself never hard-references it once a
|
|
99
|
-
* caller supplies a codec. Moves to `@cloudbitmaps/roaring` when the package split lands.
|
|
100
|
-
*/
|
|
101
|
-
declare const roaringCodec: CodecInterface;
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* CloudRoaring — distributed, cloud-native Roaring Bitmaps.
|
|
4
|
+
* The `CloudRoaring` class is the read engine over **loaded** segments: write-once `.crbm` generations in an
|
|
5
|
+
* object store, behind one registry pointer per segment. You wire storage **once**, as a single config object:
|
|
6
|
+
* pass a **backend** as `storage` — `S3Storage`, `GcsStorage`, `AzureBlobStorage`, `LocalFsStorage` or
|
|
7
|
+
* `MemoryStorage` — and it carries both halves, the generations and the pointer, from one bucket and one
|
|
8
|
+
* prefix. Add a `keystore` for encryption-at-rest / crypto-shred.
|
|
105
9
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* cold `list`-scan) and a `keystore` (encryption-at-rest / crypto-shred). Pass a **raw** {@link IColdDriver}
|
|
110
|
-
* as `cold` (e.g. `S3ColdDriver`, `LocalFsColdDriver`, `MemoryColdDriver`) and the store assembles the `.crbm`
|
|
111
|
-
* cold source ({@link CrbmColdChunkSource}) for you — so each driver is named exactly once. Or pass an
|
|
112
|
-
* already-built {@link ColdChunkSource} to control advanced reader options yourself.
|
|
10
|
+
* Two narrower shapes are also accepted for `storage`: a bare {@link IStorageDriver}, which has no pointer and
|
|
11
|
+
* so resolves generations by list-scanning storage (**cleartext and read-only**), and an already-built
|
|
12
|
+
* {@link StorageChunkSource} for advanced reader options you configure yourself.
|
|
113
13
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
14
|
+
* **Data gets in by loading a generation**, never by mutating one: `bulkLoadCrbmGeneration` streams a set of ids
|
|
15
|
+
* into one immutable object and publishes it forward-only. Every other write in the library is a load in
|
|
16
|
+
* disguise — `intersectInto`/`unionInto`/`andNotInto` write a new generation of their destination, and
|
|
17
|
+
* `eraseSubject` rewrites a generation without one id. Reads (`has`/`count`/`iterate`/`intersect`/`union`/`andNot`)
|
|
18
|
+
* see whole, checksum-verified generations and nothing else.
|
|
118
19
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* you already have elsewhere, not an initialization step, and compaction is a **cost** optimization, never a
|
|
123
|
-
* correctness requirement. See the README and the getting-started guide.
|
|
20
|
+
* In-process lifecycle helpers — `eraseSubject`, `subjectReport`, `dropSegment`, `retireExpired`,
|
|
21
|
+
* `checkConsistency`, `exportSegments` — reuse the store's own drivers, so you never re-pass them (they need the
|
|
22
|
+
* store built with a backend). See the README and the getting-started guide.
|
|
124
23
|
*/
|
|
125
|
-
|
|
24
|
+
import { SegmentEngine } from '@cloudbitmaps/core';
|
|
25
|
+
import type { BudgetOption, GenerationEntry, LoadGuard, LoadOptions, LoadRefusal, LoadResult, RollbackResult, Clock, StorageChunkSource, ConsistencyReport, CostReport, DropResult, EstimateInput, ExportManifest, ExportOptions, ExportSink, IAuditSink, IStorageDriver, IKeystore, IMetricsSink, StorageBackend, PricingProfile, RetentionPolicy, RetireExpiredOptions, RetireExpiredResult, RetryPolicy, Rng, SetRetentionResult, PinnedAt, SegmentRef, Workload } from '@cloudbitmaps/core';
|
|
26
|
+
import type { SegmentInfo } from '@cloudbitmaps/core';
|
|
126
27
|
/**
|
|
127
|
-
* Wiring for a {@link CloudRoaring} store. **Only `
|
|
128
|
-
* `new CloudRoaring({
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
28
|
+
* Wiring for a {@link CloudRoaring} store. **Only `storage` is required** — the minimal call is
|
|
29
|
+
* `new CloudRoaring({ storage: backend })`. A backend resolves generations with one strong read, reads
|
|
30
|
+
* encrypted segments, and unlocks every lifecycle helper; the narrower shapes `storage` also accepts are
|
|
31
|
+
* described on the option itself. Everything else is **optional
|
|
32
|
+
* tuning with sensible defaults** — resilience/retries are already on, the cache is bounded, metrics are a
|
|
33
|
+
* no-op — so reach for them only when you need to.
|
|
132
34
|
*/
|
|
133
|
-
interface CloudRoaringOptions {
|
|
35
|
+
export interface CloudRoaringOptions {
|
|
134
36
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
37
|
+
* Where the store keeps everything. **The only required option.**
|
|
38
|
+
*
|
|
39
|
+
* Pass a {@link StorageBackend} — `S3Storage`, `GcsStorage`, `AzureBlobStorage`, `LocalFsStorage`,
|
|
40
|
+
* `MemoryStorage` — and you are done: it carries both halves, the generations and the `currentGen` pointer,
|
|
41
|
+
* configured from one bucket and one prefix. That is the whole wiring, and it is the shape to reach for.
|
|
42
|
+
*
|
|
43
|
+
* **Pass the backend itself, not `backend.storage`.** They differ by one property access and the mistake is
|
|
44
|
+
* silent: `storage: backend` is the store, while `storage: backend.storage` hands over the *driver half*
|
|
45
|
+
* alone, which constructs without complaint and gives you a cleartext, read-only store that fails later on
|
|
46
|
+
* the first write or lifecycle call. The free functions take `backend.storage`; the store takes `backend`.
|
|
47
|
+
*
|
|
48
|
+
* Two lower-level shapes stay accepted for wiring the facade does not cover:
|
|
49
|
+
*
|
|
50
|
+
* - a **raw {@link IStorageDriver}**, which the store wraps in a {@link CrbmStorageChunkSource}. There is no
|
|
51
|
+
* registry in this shape, so the store resolves generations by list-scanning storage: **cleartext and
|
|
52
|
+
* read-only**. Encrypted segments, the `*Into` verbs and every lifecycle helper need a backend.
|
|
53
|
+
* - an already-built {@link StorageChunkSource}, used as-is, for advanced reader options.
|
|
139
54
|
*/
|
|
140
|
-
readonly
|
|
141
|
-
/** Warm tier: the per-chunk delta store under OCC (`DynamoDbWarmDriver`, `LocalFsWarmDriver`, `MemoryWarmDriver`). */
|
|
142
|
-
readonly warm: IWarmDriver;
|
|
55
|
+
readonly storage: StorageBackend | IStorageDriver | StorageChunkSource;
|
|
143
56
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* and (b) lets the store read **encrypted** segments (that's where wrapped DEKs live). Optional — a
|
|
147
|
-
* registry-less store reads the highest generation by list-scanning Cold (cleartext only). When you pass a
|
|
148
|
-
* pre-built `ColdChunkSource`, that source resolves its own generations, so a top-level `registry` is inert
|
|
149
|
-
* there and rejected as a wiring mistake — configure it on the source instead.
|
|
57
|
+
* Memory and staleness bounds for the read path. Every key is optional and every default is already sane;
|
|
58
|
+
* reach for these when a deployment's shape differs from "a long-running server with room to breathe".
|
|
150
59
|
*/
|
|
151
|
-
readonly
|
|
60
|
+
readonly cache?: CacheOptions;
|
|
152
61
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* pre-built {@link ColdChunkSource}, configure the keystore on that source instead.
|
|
62
|
+
* Encryption at rest. Omit it entirely for a cleartext store — encryption is opt-in, and a segment's
|
|
63
|
+
* encryption is decided at its **first** generation.
|
|
156
64
|
*/
|
|
157
|
-
readonly
|
|
65
|
+
readonly encryption?: EncryptionOptions;
|
|
158
66
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
67
|
+
* Resilience: by default every storage read retries **transient** faults (throttling, 5xx, dropped
|
|
68
|
+
* connections) with bounded, jittered exponential backoff (see {@link DEFAULT_RETRY_POLICY}). Pass a partial
|
|
69
|
+
* policy to tune it — anything you leave out keeps its default — or `false` to disable the transient-retry
|
|
70
|
+
* wrapper entirely (e.g. if your injected client already retries). Deterministic errors
|
|
71
|
+
* (`ValidationError`/`IntegrityError`/`WriteConflictError`/…) are never retried by this layer.
|
|
161
72
|
*/
|
|
162
|
-
readonly
|
|
163
|
-
/**
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
readonly
|
|
169
|
-
/**
|
|
170
|
-
|
|
73
|
+
readonly retry?: RetryOptions | false;
|
|
74
|
+
/**
|
|
75
|
+
* Observability sink: receives typed metric events (storage GET/bytes, cache hit/miss, retries, intersection
|
|
76
|
+
* efficiency, op latency). Defaults to a no-op — emission is skipped entirely when unused (near-zero
|
|
77
|
+
* overhead). Any exception the sink throws is swallowed — metrics can never break a read.
|
|
78
|
+
*/
|
|
79
|
+
readonly metrics?: IMetricsSink;
|
|
80
|
+
/**
|
|
81
|
+
* Per-op **denial-of-wallet** budget: the max backend requests a single
|
|
82
|
+
* `count`/`iterate`/`intersect`/`union`/`andNot`/`subjectReport`/`eraseSubject` may fan out into before it's
|
|
83
|
+
* refused with {@link BudgetExceededError} — so one runaway op can't drive unbounded GET cost on a shared
|
|
84
|
+
* backend. **On by default, generous** ({@link DEFAULT_BUDGET}: 1,000,000 requests — a normal op never hits it).
|
|
85
|
+
* Tune with `{ maxRequests }`, override per op (on the combines / `subjectReport` / `eraseSubject`), or set
|
|
86
|
+
* `false` to disable. The check is O(1) (before fan-out), so the hot path is untouched; per-request bytes are
|
|
87
|
+
* separately size-capped, so bounding requests transitively bounds bytes.
|
|
88
|
+
*/
|
|
89
|
+
readonly budget?: BudgetOption;
|
|
90
|
+
/**
|
|
91
|
+
* Determinism seams, for tests and replayable jobs. Production stores leave this out and get a system clock
|
|
92
|
+
* and a `Math.random`-backed source.
|
|
93
|
+
*/
|
|
94
|
+
readonly seams?: SeamOptions;
|
|
95
|
+
}
|
|
96
|
+
/** {@link CloudRoaringOptions.cache} — the memory and staleness bounds. */
|
|
97
|
+
export interface CacheOptions {
|
|
98
|
+
/** Ceiling on decoded Storage chunks held in RAM (default 1024). */
|
|
99
|
+
readonly maxChunks?: number;
|
|
100
|
+
/** Optional TTL on cached chunks (ms). Omit to keep a chunk until it is evicted by the count bound. */
|
|
101
|
+
readonly ttlMs?: number;
|
|
171
102
|
/**
|
|
172
103
|
* How long (ms) the store trusts a segment's resolved `currentGen` before re-resolving it on the next read
|
|
173
|
-
* (default 2000) — the bound on read staleness after a
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* window, opening a new reader only when the
|
|
104
|
+
* (default 2000) — the bound on read staleness after a load publishes a new generation. Applies when
|
|
105
|
+
* `storage` is a **backend**, whose registry supplies the cheap `currentGen` read the refresh needs. A store
|
|
106
|
+
* wired with a bare `IStorageDriver` has no registry at all and pins the generation for the source's
|
|
107
|
+
* lifetime. Lazy — no timer; ≤ one registry read per segment per window, opening a new reader only when the
|
|
108
|
+
* generation actually advanced.
|
|
177
109
|
*/
|
|
178
|
-
readonly
|
|
110
|
+
readonly genTtlMs?: number;
|
|
179
111
|
/**
|
|
180
112
|
* Ceiling on how many segments' `.crbm` readers (each holding a parsed index) the store keeps open at once
|
|
181
|
-
* (default 1024) — the steady-state memory bound for a long-running server that reads across many segments
|
|
182
|
-
*
|
|
183
|
-
*
|
|
113
|
+
* (default 1024) — the steady-state memory bound for a long-running server that reads across many segments.
|
|
114
|
+
* Past it the least-recently-used segment's reader is evicted; re-opening it later is one cheap tail GET.
|
|
115
|
+
* Applies whenever the store builds its own read path — a backend or a bare `IStorageDriver`. A pre-built
|
|
116
|
+
* `StorageChunkSource` manages its own reader cache.
|
|
184
117
|
*/
|
|
185
|
-
readonly
|
|
118
|
+
readonly readerMax?: number;
|
|
186
119
|
/**
|
|
187
|
-
* Aggregate byte ceiling on the parsed `.crbm` indices the open readers hold (default 64 MiB) — the byte
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
120
|
+
* Aggregate byte ceiling on the parsed `.crbm` indices the open readers hold (default 64 MiB) — the byte half
|
|
121
|
+
* of the memory bound, complementing the {@link CacheOptions.readerMax} *count* bound. A wide/dense segment's
|
|
122
|
+
* parsed index can be several MB, so a count-only bound could let the open readers pin ~GBs and blow a small
|
|
123
|
+
* heap (e.g. a 128 MB Lambda); this evicts the least-recently-used reader once the summed index footprint
|
|
124
|
+
* would exceed the ceiling — whichever of the count/byte bounds binds first. Lower it for memory-tight
|
|
125
|
+
* deployments that read across wide segments. Applies whenever the store builds its own read path.
|
|
193
126
|
*/
|
|
194
|
-
readonly
|
|
127
|
+
readonly readerMaxBytes?: number;
|
|
128
|
+
}
|
|
129
|
+
/** {@link CloudRoaringOptions.encryption} — encryption at rest and crypto-shred. */
|
|
130
|
+
export interface EncryptionOptions {
|
|
195
131
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* transient-retry wrappers entirely (e.g. if your injected client already retries). Deterministic errors
|
|
200
|
-
* (`ValidationError`/`IntegrityError`/`WriteConflictError`/…) are never retried by this layer.
|
|
132
|
+
* The keystore that mints and unwraps per-segment DEKs. Needs a **backend**: the wrapped DEK lives in the
|
|
133
|
+
* registry, so there is nowhere to put it on a store wired with a bare driver, and that is refused at
|
|
134
|
+
* construction rather than at the first read.
|
|
201
135
|
*/
|
|
202
|
-
readonly
|
|
203
|
-
/**
|
|
204
|
-
|
|
136
|
+
readonly keystore?: IKeystore;
|
|
137
|
+
/**
|
|
138
|
+
* Refuse to touch a **cleartext** segment — a guard against silently reading, or writing, data that should be
|
|
139
|
+
* encrypted. Needs a backend, for the same reason as {@link EncryptionOptions.keystore}. Off by default.
|
|
140
|
+
*
|
|
141
|
+
* It refuses **writes** as well as reads, which is easy to miss: the `*Into` verbs and `eraseSubject` both
|
|
142
|
+
* carry it into their write path, so on a cleartext segment a materialisation throws and an erasure records
|
|
143
|
+
* `note: 'error: requireEncryption: …'` in its ledger rather than erasing. And since a segment's encryption is
|
|
144
|
+
* decided at its **first** generation, this cannot be switched on for a segment that already has one — load
|
|
145
|
+
* into a new encrypted segment and drop the old one.
|
|
146
|
+
*/
|
|
147
|
+
readonly required?: boolean;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* {@link CloudRoaringOptions.retry} — a partial {@link RetryPolicy} plus the retry callback.
|
|
151
|
+
*
|
|
152
|
+
* Partial on purpose: the flat form this replaces took a **whole** `RetryPolicy`, so tuning one field meant
|
|
153
|
+
* restating all five. Anything omitted here keeps its {@link DEFAULT_RETRY_POLICY} value.
|
|
154
|
+
*/
|
|
155
|
+
export interface RetryOptions extends Partial<RetryPolicy> {
|
|
205
156
|
/** Observability: called before each transient-retry backoff wait. */
|
|
206
157
|
readonly onRetry?: (info: {
|
|
207
158
|
attempt: number;
|
|
208
159
|
delayMs: number;
|
|
209
160
|
err: unknown;
|
|
210
161
|
}) => void;
|
|
162
|
+
}
|
|
163
|
+
/** {@link CloudRoaringOptions.seams} — injected for deterministic tests and replayable jobs. */
|
|
164
|
+
export interface SeamOptions {
|
|
165
|
+
/** Defaults to a system clock. */
|
|
166
|
+
readonly clock?: Clock;
|
|
167
|
+
/** Defaults to `Math.random`-backed. Drives transient-retry jitter. */
|
|
168
|
+
readonly rng?: Rng;
|
|
169
|
+
}
|
|
170
|
+
export interface SegmentOptions {
|
|
171
|
+
readonly namespace?: string;
|
|
211
172
|
/**
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* entirely when unused (near-zero overhead). Any exception the sink throws is swallowed — metrics can
|
|
215
|
-
* never break a read/write.
|
|
216
|
-
*/
|
|
217
|
-
readonly metrics?: IMetricsSink;
|
|
218
|
-
/**
|
|
219
|
-
* Warm read consistency for the READ paths (`has`/`count`/`iterate`/`intersect`). Default `'strong'`
|
|
220
|
-
* (read-your-writes). `'eventual'` trades read-after-write for **~½ the DynamoDB read cost** (a strong read
|
|
221
|
-
* bills 2× RCU) — a good fit for read-heavy, staleness-tolerant workloads (mirrors the cold tier's bounded
|
|
222
|
-
* eventual reads). The compaction/OCC write path is always strongly consistent regardless. No effect on the
|
|
223
|
-
* in-memory/LocalFs drivers (always strong). (Audit gap #9.)
|
|
224
|
-
*/
|
|
225
|
-
readonly warmReadConsistency?: 'strong' | 'eventual';
|
|
226
|
-
/**
|
|
227
|
-
* Max Warm chunk writes in flight per `addMany`/`removeMany` (the bounded flusher). Default **4**
|
|
228
|
-
* (`DEFAULT_WRITE_CONCURRENCY`); set it to `1` for strictly serial writes.
|
|
173
|
+
* **When this segment stops being readable** — an absolute epoch-ms instant, declared where the segment is
|
|
174
|
+
* named instead of in a separate `setRetention` call.
|
|
229
175
|
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* flusher simply degrades toward serial.
|
|
176
|
+
* Every read through this handle checks it first: past the deadline, `has` is `false`, `count` is `0`, and
|
|
177
|
+
* `iterate` yields nothing — **one integer compare against the injected clock, no I/O, on every backend**.
|
|
178
|
+
* That is Redis's lazy expiry, and it is what makes an expiry *correct* rather than *eventually correct*: a
|
|
179
|
+
* deployment whose sweep is late — or which has no sweep at all, like a Lambda-only reader — still stops
|
|
180
|
+
* serving the data on time.
|
|
236
181
|
*
|
|
237
|
-
*
|
|
238
|
-
* concurrency changes is only how much of the batch may already have landed when the first error surfaces.
|
|
239
|
-
*/
|
|
240
|
-
readonly writeConcurrency?: number;
|
|
241
|
-
/**
|
|
242
|
-
* Hard ceiling on the warm-delta bytes one segment scan may hold resident. Default **64 MiB**.
|
|
182
|
+
* **Two things this does NOT do, both deliberate:**
|
|
243
183
|
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
184
|
+
* - It does not reclaim the bytes. That is {@link CloudRoaring.retireExpired}, and until it runs the data is
|
|
185
|
+
* still stored and still billed. `count()` reporting 0 while objects exist is the expected state in that
|
|
186
|
+
* window, not a bug.
|
|
187
|
+
* - It does not apply to *other* handles. The deadline lives on this handle; a second handle opened without
|
|
188
|
+
* the option reads the segment normally. Record the policy with {@link CloudRoaring.setRetention} to make
|
|
189
|
+
* it durable, fleet-visible, and reclaimable.
|
|
250
190
|
*
|
|
251
|
-
*
|
|
252
|
-
|
|
253
|
-
readonly maxWarmScanBytes?: number;
|
|
254
|
-
/**
|
|
255
|
-
* Per-op **denial-of-wallet** budget (Decision #3 / invariant T3): the max backend requests a
|
|
256
|
-
* single `count`/`iterate`/`intersect`/`subjectReport`/`eraseSubject` may fan out into before it's refused
|
|
257
|
-
* with {@link BudgetExceededError} — so one runaway op can't drive unbounded RCU/GET cost on a shared backend.
|
|
258
|
-
* **On by default, generous** ({@link DEFAULT_BUDGET}: 1,000,000 requests — a normal op never hits it). Tune
|
|
259
|
-
* with `{ maxRequests }`, override per op (on `intersect`/`subjectReport`/`eraseSubject`), or set `false` to
|
|
260
|
-
* disable. The check is O(1) (before fan-out), so the hot path is untouched; per-request bytes are separately
|
|
261
|
-
* size-capped, so bounding requests transitively bounds bytes.
|
|
191
|
+
* Must be **milliseconds** since the epoch and at or after {@link MIN_EXPIRES_AT_MS} — a seconds value would
|
|
192
|
+
* land in 1970 and make the segment permanently unreadable, so it is refused rather than honoured.
|
|
262
193
|
*/
|
|
263
|
-
readonly
|
|
264
|
-
}
|
|
265
|
-
interface SegmentOptions {
|
|
266
|
-
readonly namespace?: string;
|
|
194
|
+
readonly expiresAt?: number;
|
|
267
195
|
}
|
|
268
196
|
/** A segment reference in a subject report / erasure ledger. */
|
|
269
|
-
interface SubjectSegmentRef {
|
|
197
|
+
export interface SubjectSegmentRef {
|
|
270
198
|
readonly segment: string;
|
|
271
199
|
readonly namespace?: string;
|
|
272
200
|
}
|
|
273
201
|
/** Result of {@link CloudRoaring.subjectReport} — the segments an id is a member of (over registered segments). */
|
|
274
|
-
interface SubjectReport {
|
|
202
|
+
export interface SubjectReport {
|
|
275
203
|
readonly id: number;
|
|
276
204
|
/** The registered segments the id is currently a member of. */
|
|
277
205
|
readonly segments: SubjectSegmentRef[];
|
|
@@ -279,65 +207,186 @@ interface SubjectReport {
|
|
|
279
207
|
readonly scannedSegments: number;
|
|
280
208
|
}
|
|
281
209
|
/** One segment's entry in an erasure ledger (see {@link EraseSubjectResult}). */
|
|
282
|
-
interface SubjectErasureEntry {
|
|
210
|
+
export interface SubjectErasureEntry {
|
|
283
211
|
readonly segment: string;
|
|
284
212
|
readonly namespace?: string;
|
|
285
|
-
/**
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
*
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
213
|
+
/**
|
|
214
|
+
* True iff the id **was** a member and a generation without it is now current — and the generation that held
|
|
215
|
+
* the bit has been deleted from the bucket. The physical half is inherent: an erasure is a rewrite, and the
|
|
216
|
+
* rewrite's predecessor is collected before this entry is returned (see {@link CloudRoaring.eraseSubject}).
|
|
217
|
+
*/
|
|
218
|
+
readonly erased: boolean;
|
|
219
|
+
/** The generation the id was found in (present whenever the segment was read). */
|
|
220
|
+
readonly fromGeneration?: number;
|
|
221
|
+
/** The generation written without the id (present whenever one was written). */
|
|
222
|
+
readonly generation?: number;
|
|
223
|
+
/**
|
|
224
|
+
* Why the id was NOT erased from this segment, when `erased` is false. `'superseded'` — a newer generation
|
|
225
|
+
* was published while the rewrite was in flight, by a load or by another erasure, so **this call** did not
|
|
226
|
+
* erase the id; re-run against the new generation, which erases it if it is still there and reports nothing
|
|
227
|
+
* for the segment if the racing writer already removed it. `` `error: <message>` `` — an isolated per-segment
|
|
228
|
+
* fault (per-segment faults are recorded so one segment can't discard the whole ledger); re-run after fixing
|
|
229
|
+
* the fault. A fault that landed once part of the work was already done — a Storage `delete` fault, or a collect
|
|
230
|
+
* that could not prove the segment was still the same one, whether or not a rewrite was published first —
|
|
231
|
+
* also re-runs, but **read what the re-run says**: it usually reports `erased: true` against the superseded generation it found the id in, it reports
|
|
232
|
+
* nothing at all if a racing collector took that generation first (the bit is gone, but no run holds a
|
|
233
|
+
* receipt for it), and if the segment's row has since been purged it is no longer scanned at all — anything
|
|
234
|
+
* left in its bucket is an orphan for `checkConsistency` / `gcOrphanGenerations`. **Segments the id is not in
|
|
235
|
+
* are not listed, and neither are segments that no longer have a registry row** — an empty ledger is not by
|
|
236
|
+
* itself proof the id is gone.
|
|
237
|
+
*/
|
|
297
238
|
readonly note?: string;
|
|
298
239
|
}
|
|
299
240
|
/**
|
|
300
241
|
* The erasure ledger returned by {@link CloudRoaring.eraseSubject} — your proof-of-deletion artifact. It is a
|
|
301
242
|
* return value only (no library-side persistence): persist it / route it to your audit sink as you see fit.
|
|
302
243
|
*/
|
|
303
|
-
interface EraseSubjectResult {
|
|
244
|
+
export interface EraseSubjectResult {
|
|
304
245
|
readonly id: number;
|
|
305
|
-
/** Per-segment records for the segments the id was
|
|
246
|
+
/** Per-segment records for the segments the id was found in (absent segments are not listed). */
|
|
306
247
|
readonly erasedFrom: SubjectErasureEntry[];
|
|
307
248
|
/** How many registered segments were scanned. */
|
|
308
249
|
readonly scannedSegments: number;
|
|
309
250
|
}
|
|
310
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Why a materialisation did not become current.
|
|
253
|
+
*
|
|
254
|
+
* {@link LoadRefusal} minus `'superseded'`: that one is a lost race, and the `*Into` verbs throw
|
|
255
|
+
* {@link WriteConflictError} for it rather than reporting it. Narrowing the type rather than saying so only
|
|
256
|
+
* in prose means the compiler enforces it, and a caller's exhaustive `switch` has no dead branch.
|
|
257
|
+
*/
|
|
258
|
+
export type MaterializeRefusal = Exclude<LoadRefusal, 'superseded'>;
|
|
259
|
+
/** What an `*Into` verb wrote: the new generation of the destination, and whether it became current. */
|
|
260
|
+
export interface MaterializeResult {
|
|
261
|
+
/** The generation written. Present even when refused — it is what was written and then deleted again. */
|
|
262
|
+
readonly generation: number;
|
|
263
|
+
/**
|
|
264
|
+
* Whether this generation is now the destination's current one.
|
|
265
|
+
*
|
|
266
|
+
* Before the guard existed this was always true, because a materialisation always published. Branch on it:
|
|
267
|
+
* a refusal is reported, not thrown, so a caller that ignores it sees a successful-looking result for a
|
|
268
|
+
* write that deliberately did not happen.
|
|
269
|
+
*/
|
|
270
|
+
readonly published: boolean;
|
|
271
|
+
/** Set only when `published` is false. A lost race throws {@link WriteConflictError} rather than appearing here. */
|
|
272
|
+
readonly reason?: MaterializeRefusal;
|
|
273
|
+
/** Ids in the generation. */
|
|
274
|
+
readonly cardinality: number;
|
|
275
|
+
/**
|
|
276
|
+
* What the destination held when the guard judged it — `null` when it had no current generation, **or when
|
|
277
|
+
* no bound needed the read**. The read costs an object-header fetch, so it is taken only when a bound will
|
|
278
|
+
* use it: `allowEmpty: true` with no `guard.minRetained` skips it, and this is `null` even though `dest`
|
|
279
|
+
* was non-empty.
|
|
280
|
+
*/
|
|
281
|
+
readonly cardinalityBefore: number | null;
|
|
282
|
+
/** Non-empty chunks in the generation. */
|
|
283
|
+
readonly chunkCount: number;
|
|
284
|
+
/** Bytes of the written object. */
|
|
285
|
+
readonly size: number;
|
|
286
|
+
/**
|
|
287
|
+
* The superseded generations collected after publishing. Empty when nothing was published — and **also
|
|
288
|
+
* empty on a successful publish** unless you passed `keep`, because a materialisation collects nothing by
|
|
289
|
+
* default. See {@link MaterializeOptions.keep}.
|
|
290
|
+
*/
|
|
291
|
+
readonly collected: readonly number[];
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Options that moved into a group, and where each one went.
|
|
295
|
+
*
|
|
296
|
+
* TypeScript rejects these at the call site, which covers most callers. It does not cover a plain-JS caller, a
|
|
297
|
+
* config object that arrived as JSON, or anything that reached the constructor through an `as` cast — and for
|
|
298
|
+
* this particular set, being ignored is worse than being rejected, because **every one of them is a knob whose
|
|
299
|
+
* absence is silent and wrong**: a dropped `requireEncryption` reads cleartext when the caller demanded
|
|
300
|
+
* encryption, a dropped `clock` makes a "deterministic" job non-deterministic, and a dropped
|
|
301
|
+
* `coldReaderCacheMaxBytes` restores a 64 MiB ceiling someone had deliberately lowered for a small heap.
|
|
302
|
+
* None of those announces itself; each looks like the store simply working.
|
|
303
|
+
*/
|
|
304
|
+
/**
|
|
305
|
+
* How a `0.9.x` option is answered: it moved into a group, it was renamed, or it is gone.
|
|
306
|
+
*
|
|
307
|
+
* The category is DATA, not inferred from how the guidance happens to be punctuated. The first version
|
|
308
|
+
* decided by testing whether the replacement text looked like an identifier, which got two entries wrong in
|
|
309
|
+
* opposite directions: `registry` HAS a successor and was announced as "removed", and `cold` → `storage` was
|
|
310
|
+
* announced as "moved into a group" when `storage` is the one required flat option, not a group. A reader
|
|
311
|
+
* told to look in a group that does not exist is the failure this whole guard is about.
|
|
312
|
+
*/
|
|
313
|
+
export declare class CloudRoaring {
|
|
311
314
|
private readonly engine;
|
|
315
|
+
private readonly cache;
|
|
316
|
+
private readonly crbmSource;
|
|
312
317
|
private readonly clock;
|
|
313
318
|
private readonly metrics;
|
|
314
|
-
private readonly
|
|
315
|
-
private readonly warmDriver;
|
|
319
|
+
private readonly storageDriver;
|
|
316
320
|
private readonly registry;
|
|
317
321
|
private readonly keystore;
|
|
318
322
|
private readonly requireEncryption;
|
|
319
323
|
/** Resolved store-level per-op budget (null = disabled); the admin scans use it, with a per-op override. */
|
|
320
324
|
private readonly budget;
|
|
325
|
+
/**
|
|
326
|
+
* Refuse an option that moved into a group, naming where it went.
|
|
327
|
+
*
|
|
328
|
+
* Silently ignoring one would be the exact failure this release exists to remove — see {@link MOVED_OPTIONS}
|
|
329
|
+
* for why each of these is unsafe to drop rather than merely untidy.
|
|
330
|
+
*/
|
|
331
|
+
private static rejectMovedOptions;
|
|
321
332
|
constructor(options: CloudRoaringOptions);
|
|
322
333
|
/**
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
* free
|
|
334
|
+
* The write-side deps, from the store's own drivers, for the lifecycle helpers and the `*Into` verbs. Requires
|
|
335
|
+
* the store to have been constructed with a **backend**, which supplies both halves: an `IStorageDriver` to
|
|
336
|
+
* write generations through (a pre-built `StorageChunkSource` has none) and the registry holding the pointer
|
|
337
|
+
* every write publishes.
|
|
338
|
+
* Out-of-process callers use the free functions with explicit deps.
|
|
328
339
|
*/
|
|
329
|
-
private
|
|
330
|
-
/** Get a handle to a segment. Validates the name
|
|
340
|
+
private lifecycleDeps;
|
|
341
|
+
/** Get a handle to a segment. Validates the name (non-empty, well-formed, within the encoded-length cap). */
|
|
331
342
|
segment(name: string, options?: SegmentOptions): Segment;
|
|
332
343
|
/**
|
|
333
|
-
*
|
|
344
|
+
* Write `ids` as a **new generation of `dest`** and publish it forward-only — the shared body of the `*Into`
|
|
345
|
+
* verbs. The destination's previous generation stays readable until the publish lands (readers re-resolve
|
|
346
|
+
* within `cache.genTtlMs`).
|
|
347
|
+
*
|
|
348
|
+
* This routes through `loadSegment` rather than writing the generation itself, and that is the whole point
|
|
349
|
+
* of it. A materialisation is a load whose ids happen to come from a combine instead of from upstream, so
|
|
350
|
+
* everything `load()` learned the hard way applies unchanged: the generation is written UNPUBLISHED, the
|
|
351
|
+
* guard runs while the old generation is still authoritative, the publish is fenced (on the pointer it
|
|
352
|
+
* judged, on the row's identity, and — where it judged an ABSENT segment — on that absence), and a refused
|
|
353
|
+
* object is reclaimed only after re-reading the row and finding the same incarnation (hard invariant 1:
|
|
354
|
+
* deleting it after a purge-and-recreate would put a live row over a missing generation).
|
|
355
|
+
*
|
|
356
|
+
* That last check narrows the window rather than closing it: the row read and the delete are two round
|
|
357
|
+
* trips, and `IStorageDriver` has no conditional delete to make them one. `gcOrphanGenerations` carries
|
|
358
|
+
* the same residual and says so. The failure it leaves is an orphan object, which costs storage until
|
|
359
|
+
* something collects it — deliberately the cheaper side of the trade.
|
|
360
|
+
*
|
|
361
|
+
* Materialising used to do none of that. It wrote and published in one step, so an empty combine — a typo'd
|
|
362
|
+
* operand, an `exclude` that swallowed everything, an operand that had not loaded yet — silently replaced
|
|
363
|
+
* `dest` with an empty generation. That is the same failure `load()`'s guard exists to prevent, on the same
|
|
364
|
+
* data, and it was reachable without passing any option at all.
|
|
334
365
|
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
366
|
+
* **A lost race still throws.** `loadSegment` reports one as `reason: 'superseded'`; the `*Into` verbs have
|
|
367
|
+
* always thrown {@link WriteConflictError} for it, and a caller who wrote `catch (WriteConflictError)` must
|
|
368
|
+
* keep working. So that one refusal is translated back into the throw, and `MaterializeResult.reason` never
|
|
369
|
+
* carries it.
|
|
370
|
+
*
|
|
371
|
+
* **A `WriteConflictError` does not by itself mean nothing was published**, and that is worth knowing
|
|
372
|
+
* before you write the retry. `'superseded'` covers four different causes — the write-once PUT collided,
|
|
373
|
+
* the pointer moved, the row's token changed, the row was purged — and only the first two are the
|
|
374
|
+
* "somebody beat us" the name suggests. A token can also change on a write that is not a supersession at
|
|
375
|
+
* all, such as a `setRetention` on the destination. On top of that, the collection pass that runs AFTER a
|
|
376
|
+
* successful publish can raise the same error. So: treat it as "re-read the destination and decide",
|
|
377
|
+
* never as "the write did not happen".
|
|
378
|
+
*
|
|
379
|
+
* **And it still collects nothing**, unlike `load()`. See the `keep` default below.
|
|
380
|
+
*/
|
|
381
|
+
private materialize;
|
|
382
|
+
/**
|
|
383
|
+
* **Subject access (GDPR Art. 15 / CCPA right-to-know): which segments is this id a member of?**
|
|
384
|
+
*
|
|
385
|
+
* Enumerates the **registered** segments (via the store's own `registry`) and does a `has(id)` on each — no
|
|
386
|
+
* drivers to re-pass. Complete only over registered segments (every loaded segment has a row, so register the
|
|
387
|
+
* registry the loads used). There is deliberately **no `id → segments` reverse index** — that would tax every
|
|
388
|
+
* load for a rare request; this admin scan is `O(registered segments)` and touches no hot path. Requires a
|
|
389
|
+
* `registry` in the store config (throws {@link UnsupportedError} otherwise).
|
|
341
390
|
*/
|
|
342
391
|
subjectReport(id: number, options?: {
|
|
343
392
|
namespace?: string;
|
|
@@ -346,42 +395,53 @@ declare class CloudRoaring {
|
|
|
346
395
|
budget?: BudgetOption;
|
|
347
396
|
}): Promise<SubjectReport>;
|
|
348
397
|
/**
|
|
349
|
-
* **Subject erasure (GDPR Art. 17): remove an id from every segment it's in, with
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
* For each **registered** segment the id is a member of
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
* persist it / route it to your audit sink as the proof of deletion
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
398
|
+
* **Subject erasure (GDPR Art. 17): remove an id from every segment it's in, with the bit physically gone
|
|
399
|
+
* from the bucket on return.**
|
|
400
|
+
*
|
|
401
|
+
* For each **registered** segment the id is a member of, `eraseIdFromSegment` rewrites the current generation
|
|
402
|
+
* without the id — every chunk streamed through, one bit cleared — publishes the rewrite **fenced on the
|
|
403
|
+
* generation it was derived from**, and
|
|
404
|
+
* collects the generation that held the bit. The returned per-segment record is your **erasure ledger** —
|
|
405
|
+
* persist it / route it to your audit sink as the proof of deletion (a `segment.rewrite` audit event is also
|
|
406
|
+
* emitted per rewrite when you pass `audit`).
|
|
407
|
+
*
|
|
408
|
+
* Uses the backend's **own** two halves, so the membership check and the rewrite provably run
|
|
409
|
+
* over the same generation. Requires the store built with a **backend** (throws
|
|
410
|
+
* {@link UnsupportedError} otherwise; a pre-built `StorageChunkSource` store has no `IStorageDriver` to write
|
|
411
|
+
* through — use the `eraseIdFromSegment` free function there).
|
|
412
|
+
*
|
|
413
|
+
* **One contract remains** (an integrator obligation the library cannot check): **do not load the segment
|
|
414
|
+
* while erasing from it.** A load that lands after the rewrite carries whatever its source held, and the
|
|
415
|
+
* library cannot know that source was meant to exclude the id. Quiesce loads of the affected segments for the
|
|
416
|
+
* duration, or fix the source first and load after. A writer that lands *during* the rewrite is caught: the
|
|
417
|
+
* rewrite's publish is refused **by the fence** — `publishGeneration`'s `expectFrom`, which lands the CAS only
|
|
418
|
+
* while the pointer is still on the generation the rewrite streamed — and the entry says `note: 'superseded'`,
|
|
419
|
+
* so re-run. Forward-only alone would NOT refuse it: `nextGeneration` numbers above everything in the bucket,
|
|
420
|
+
* so the rewrite would out-rank the newer generation and then collect it.
|
|
421
|
+
*
|
|
422
|
+
* A racing **erasure** is caught before that, and reported the same way. It collects with `keep: 0`, taking
|
|
423
|
+
* every generation below its new pointer — the one this rewrite is streaming, and the object this rewrite
|
|
424
|
+
* just wrote — so the loser can find its own inputs deleted mid-flight. That surfaces as a reason rather than
|
|
425
|
+
* an error, read off the row: a moved pointer is `'superseded'`, a concurrent `dropSegment` `'destroyed'`,
|
|
426
|
+
* a retention sweep that purged the row `'absent'`. Re-running is the fix in every case.
|
|
427
|
+
*
|
|
428
|
+
* **Read `note` on any `erased: false` entry — the two reasons mean different things.** `'superseded'` means
|
|
429
|
+
* another writer (a load, or another erasure) moved the pointer mid-rewrite, so **this call** did not erase
|
|
430
|
+
* the id. Re-run: it erases the id if it is still there, and lists nothing for the segment if a racing
|
|
431
|
+
* erasure of the same id already removed it. Do not read `'superseded'` as "the id is still present" —
|
|
432
|
+
* read it as "not done by this call, and the re-run settles it".
|
|
433
|
+
* `` `error: …` `` is a per-segment fault (caught so one segment can't discard the whole ledger) and it can land
|
|
434
|
+
* on either side of the publish: if the rewrite had not published, the id is still there and a re-run erases
|
|
435
|
+
* it; if the publish succeeded and only the **collection** of the old generation failed, the id is already
|
|
436
|
+
* absent from every read and what remains is an object in the bucket that still contains the bit. A re-run
|
|
437
|
+
* then reports nothing for that segment (the id is not in the current generation), so **that residual is
|
|
438
|
+
* collected by `gcOrphanGenerations(ref, deps, { keep: 0 })` or the next retention sweep**, not by another
|
|
439
|
+
* `eraseSubject`. Re-running is otherwise safe and idempotent: a segment the id is no longer in is not listed. Admin-only path;
|
|
379
440
|
* `O(registered segments)`, no hot-path cost. Per-subject crypto-shred is infeasible (a subject's bit is
|
|
380
441
|
* co-mingled in a shared container), so this is the single-subject erasure route; whole-segment/tenant erasure
|
|
381
|
-
* is the `destroySegment`/`eraseNamespace` free functions.
|
|
442
|
+
* is `dropSegment` / the `destroySegment`/`eraseNamespace` free functions.
|
|
382
443
|
*/
|
|
383
|
-
eraseSubject(id: number, options
|
|
384
|
-
owner: string;
|
|
444
|
+
eraseSubject(id: number, options?: {
|
|
385
445
|
namespace?: string;
|
|
386
446
|
allNamespaces?: boolean;
|
|
387
447
|
audit?: IAuditSink;
|
|
@@ -389,20 +449,141 @@ declare class CloudRoaring {
|
|
|
389
449
|
budget?: BudgetOption;
|
|
390
450
|
}): Promise<EraseSubjectResult>;
|
|
391
451
|
/**
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
452
|
+
* Every generation still in the bucket for this segment, ascending, with the current one marked.
|
|
453
|
+
*
|
|
454
|
+
* What the bucket holds, not what the segment has ever been — collection deletes superseded objects, so this is
|
|
455
|
+
* the grace window plus whatever has not been collected yet. It is the set {@link CloudRoaring.rollback} can
|
|
456
|
+
* choose from, which is the reason to look at it. One `list` call; it does not open the objects.
|
|
457
|
+
*
|
|
458
|
+
* Needs a backend.
|
|
459
|
+
*/
|
|
460
|
+
generations(ref: SegmentRef): Promise<GenerationEntry[]>;
|
|
461
|
+
/**
|
|
462
|
+
* Whether a read of this segment would find anything — the "do I already have this?" question, as one
|
|
463
|
+
* registry point read.
|
|
464
|
+
*
|
|
465
|
+
* There is no `create` in this library: {@link CloudRoaring.segment} is a validated address and does no I/O,
|
|
466
|
+
* so naming a segment can never collide with an existing one. A segment starts existing when something is
|
|
467
|
+
* first loaded into it, and this is how you ask whether that has happened.
|
|
468
|
+
*
|
|
469
|
+
* **Not the same as `count() > 0`.** A segment loaded with no ids exists and counts zero. Distinguishing
|
|
470
|
+
* "never loaded" from "loaded, and genuinely empty" is the thing `count()` cannot do and the reason this
|
|
471
|
+
* exists. It is `false` for a segment whose row was minted ahead of its first load (by `setRetention`) and
|
|
472
|
+
* for a `destroyed` tombstone, because a read answers empty in both cases.
|
|
473
|
+
*
|
|
474
|
+
* Two states answer `true` where a read still gives you nothing: a torn restore (a live pointer whose object
|
|
475
|
+
* was deleted) makes reads *throw* rather than answer empty — `checkConsistency` is the call for that — and
|
|
476
|
+
* a handle carrying an expired `expiresAt` reads empty by a rule that lives on the handle, not the row.
|
|
477
|
+
*
|
|
478
|
+
* Not a lock: the answer can change the moment it returns. If it has to hold, use the fence built for that —
|
|
479
|
+
* `load`'s `guard`, or `expectFrom`/`expectToken` on a publish. Needs a `registry`.
|
|
480
|
+
*
|
|
481
|
+
* ```ts
|
|
482
|
+
* if (!(await store.exists({ segment: 'users' }))) {
|
|
483
|
+
* await store.load({ segment: 'users' }, idsFromUpstream);
|
|
484
|
+
* }
|
|
485
|
+
* ```
|
|
486
|
+
*/
|
|
487
|
+
exists(ref: SegmentRef): Promise<boolean>;
|
|
488
|
+
/**
|
|
489
|
+
* Every segment the registry knows about, streamed — optionally scoped to one namespace.
|
|
490
|
+
*
|
|
491
|
+
* The registry is already the list of your segments, which is why you should not keep a second one beside it:
|
|
492
|
+
* a hand-maintained list is a source of truth that drifts from this one the first time a load fails halfway.
|
|
493
|
+
*
|
|
494
|
+
* **An admin/discovery call, not a request-path one.** This is the registry's own enumeration — a paged
|
|
495
|
+
* LIST over the `registry/` prefix — so its cost grows with the size of the fleet rather than with what you
|
|
496
|
+
* are looking for.
|
|
497
|
+
*
|
|
498
|
+
* Scoping to a namespace narrows the LIST prefix, so it really is the difference between reading one tenant
|
|
499
|
+
* and reading all of them.
|
|
500
|
+
*
|
|
501
|
+
* It streams, and stopping the iteration stops the scan — except behind a driver that buffers its
|
|
502
|
+
* enumeration to retry it as a unit, which `RetryingRegistryDriver` does: wrapped in that, the whole scan is
|
|
503
|
+
* paid for and resident before the first row arrives.
|
|
504
|
+
*
|
|
505
|
+
* Yields `destroyed` tombstones and rows with `currentGen: null`, because a filtered enumeration that looks
|
|
506
|
+
* complete is worse than an honest one — filter on `status`/`currentGen` yourself, or ask
|
|
507
|
+
* {@link CloudRoaring.exists} the narrower question. Needs a `registry`.
|
|
508
|
+
*
|
|
509
|
+
* ```ts
|
|
510
|
+
* for await (const s of store.segments({ namespace: 'active-daily' })) {
|
|
511
|
+
* console.log(s.segment, s.currentGen, s.status);
|
|
512
|
+
* }
|
|
513
|
+
* ```
|
|
514
|
+
*/
|
|
515
|
+
segments(options?: {
|
|
516
|
+
namespace?: string;
|
|
517
|
+
}): AsyncIterable<SegmentInfo>;
|
|
518
|
+
/**
|
|
519
|
+
* Move this segment's pointer **back** to a generation still in the bucket — the one write in the library that
|
|
520
|
+
* is not forward-only.
|
|
521
|
+
*
|
|
522
|
+
* Forward-only is right for a writer: a load whose ids came from upstream loses nothing by being out-raced, and
|
|
523
|
+
* letting it regress would let a slow loader silently undo a fast one. It is wrong for an operator who has
|
|
524
|
+
* looked at the segment, decided the current generation is wrong, and knows which one they want. So this is
|
|
525
|
+
* reachable only by asking for it by name — no sweep, retry or reconciliation calls it — and it is audited
|
|
526
|
+
* (`segment.rollback`), because every other pointer move can be reconstructed from "a load happened" and this
|
|
527
|
+
* one cannot.
|
|
528
|
+
*
|
|
529
|
+
* It refuses rather than guesses: a generation not in the bucket (collected, or never written) throws
|
|
530
|
+
* `NotFoundError` naming what *is* available, and a crypto-shredded segment throws
|
|
531
|
+
* {@link ValidationError} because every generation of it is unreadable. Rolling to the generation already
|
|
532
|
+
* current is a no-op that reports itself.
|
|
533
|
+
*
|
|
534
|
+
* It deletes nothing. The generations above the new pointer stay put — which is what makes this reversible —
|
|
535
|
+
* and are then *above* `currentGen`, where collection never looks, so they remain until a later load raises the
|
|
536
|
+
* pointer past them. An operator who has just undone a bad load should not have the evidence collected out from
|
|
537
|
+
* under them.
|
|
538
|
+
*
|
|
539
|
+
* Needs a backend.
|
|
540
|
+
*/
|
|
541
|
+
rollback(ref: SegmentRef, toGeneration: number, options?: {
|
|
542
|
+
audit?: IAuditSink;
|
|
543
|
+
}): Promise<RollbackResult>;
|
|
544
|
+
/**
|
|
545
|
+
* **Replace this segment's contents** with `ids`, as one new immutable generation, and make it current.
|
|
546
|
+
*
|
|
547
|
+
* The whole write path in one call: take the next generation number, write the object, check the result is
|
|
548
|
+
* plausible, move the pointer, collect what the move superseded. Composed by hand those are four functions and
|
|
549
|
+
* the one that gets left out is the last, so segments quietly accumulate superseded generations nobody pays
|
|
550
|
+
* attention to and everybody pays for.
|
|
551
|
+
*
|
|
552
|
+
* ```ts
|
|
553
|
+
* const r = await store.load('audience:active', idsFromWarehouse, {
|
|
554
|
+
* guard: { maxShrink: 0.5 }, // refuse a load that drops more than half the segment
|
|
555
|
+
* });
|
|
556
|
+
* if (!r.published) console.warn(`load refused: ${r.reason}`);
|
|
557
|
+
* ```
|
|
558
|
+
*
|
|
559
|
+
* **A load REPLACES.** Whatever the stream contains is what the segment contains afterwards, so an upstream
|
|
560
|
+
* query that returns fewer rows than usual is a shrink nobody asked for and an empty one is a wipe — both
|
|
561
|
+
* reported as a successful write, because at the storage layer they are one. That is what `guard` and the
|
|
562
|
+
* default empty refusal are for, and why they run **between** the write and the publish: the only moment where
|
|
563
|
+
* the new content is known and the old one is still authoritative.
|
|
564
|
+
*
|
|
565
|
+
* **Branch on `published`.** A refusal is a normal outcome, not an exception — `published: false` with a
|
|
566
|
+
* `reason`, in the same shape as a success — so a discarded result is a load that silently did nothing, and
|
|
567
|
+
* with the empty guard on by default this verb refuses more readily than any other. Note the sibling `*Into`
|
|
568
|
+
* verbs *throw* on the same superseded condition rather than reporting it; this one follows
|
|
569
|
+
* `eraseIdFromSegment` instead, because three of its four refusals are expected guard outcomes rather than
|
|
570
|
+
* faults.
|
|
571
|
+
*
|
|
572
|
+
* The object written for a refused load is deleted again before returning — it sits above `currentGen`, where
|
|
573
|
+
* generation collection deliberately never looks, so nothing else would reclaim it. The exception is a load
|
|
574
|
+
* that finds the segment **re-created** underneath it: the generation number it holds may then name the new
|
|
575
|
+
* incarnation's live object, so it leaves the orphan rather than risk deleting live data.
|
|
576
|
+
*
|
|
577
|
+
* Two things it does **throw** for, rather than report: a crypto-shredded segment (`ValidationError` — there
|
|
578
|
+
* is no key to write under), and a collection pass that could not prove the segment was still the same one
|
|
579
|
+
* (`WriteConflictError`). The second can be raised **after** the publish already landed, so a throw does not
|
|
580
|
+
* by itself mean the load did not take effect — re-read the pointer rather than assuming.
|
|
581
|
+
*
|
|
582
|
+
* Needs a backend (throws {@link UnsupportedError} otherwise).
|
|
402
583
|
*/
|
|
403
|
-
|
|
584
|
+
load(ref: SegmentRef, ids: Iterable<number> | AsyncIterable<number>, options?: LoadOptions): Promise<LoadResult>;
|
|
404
585
|
/**
|
|
405
|
-
* **Dispose of a segment — tombstone it,
|
|
586
|
+
* **Dispose of a segment — tombstone it, then delete its storage objects.** Irreversible.
|
|
406
587
|
*
|
|
407
588
|
* The operation a rolling window needs: `destroySegment` crypto-shreds (bytes unreadable everywhere including
|
|
408
589
|
* backups, but still sitting in your bucket and still billed, and it requires encryption), while this one
|
|
@@ -414,7 +595,8 @@ declare class CloudRoaring {
|
|
|
414
595
|
*
|
|
415
596
|
* ```ts
|
|
416
597
|
* for (const day of expiredDays) {
|
|
417
|
-
* //
|
|
598
|
+
* // The family in the namespace, the date in the segment — `registry.list(namespace)` then enumerates
|
|
599
|
+
* // exactly this family's buckets, which a flat `active:2026-08-01` name cannot do without string-matching.
|
|
418
600
|
* const ref = { namespace: 'active-daily', segment: day };
|
|
419
601
|
* await store.dropSegment(ref, { confirmSegment: ref.segment });
|
|
420
602
|
* }
|
|
@@ -424,24 +606,19 @@ declare class CloudRoaring {
|
|
|
424
606
|
* `{ dropped: false, reason: 'absent' }`, not a throw, so a retention loop with that mistake deletes nothing
|
|
425
607
|
* forever and quietly. Branch on `dropped`, and treat `reason: 'absent'` as the alert.
|
|
426
608
|
*
|
|
427
|
-
* **Works on an accumulator too.** A segment you created by writing to it — never bulk-loaded, never compacted,
|
|
428
|
-
* so it has no registry row and no Cold objects — is retired by deleting its Warm rows alone. That reports
|
|
429
|
-
* `{ dropped: true, reason: 'warm-only' }`: there is no tombstone to write, and deliberately none is written,
|
|
430
|
-
* because a `destroyed` row per retired daily bucket would be registry litter. The data really is gone.
|
|
431
|
-
*
|
|
432
609
|
* **Inspect `generationsRemaining`.** Empty is the normal outcome; non-empty means the storage was NOT fully
|
|
433
|
-
* reclaimed and the drop should be re-run. A
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
* Reads become empty within `
|
|
438
|
-
* segment may answer from its cached generation +
|
|
439
|
-
* touched it sees empty at once. **That bound needs a clock and `
|
|
440
|
-
* clock, or with `
|
|
610
|
+
* reclaimed and the drop should be re-run. A load that was already writing when the tombstone landed still
|
|
611
|
+
* finishes its object, so a single sweep can miss it — this call re-sweeps and then reports whatever it still
|
|
612
|
+
* could not remove rather than returning a result that looks like a clean drop.
|
|
613
|
+
*
|
|
614
|
+
* Reads become empty within `cache.genTtlMs` (default 2 s), not instantly: a store that had already read this
|
|
615
|
+
* segment may answer from its cached generation + cached chunks until that window lapses. A reader that never
|
|
616
|
+
* touched it sees empty at once. **That bound needs a clock and `cache.genTtlMs > 0`** — a store built without a
|
|
617
|
+
* clock, or with `cache.genTtlMs: 0` ("pin forever"), holds its resolved snapshot for its own lifetime and can
|
|
441
618
|
* keep answering `true` for a dropped segment indefinitely; restart it.
|
|
442
619
|
*
|
|
443
|
-
* Needs the store built with a **
|
|
444
|
-
* because it has to enumerate and delete generations — a pre-built `
|
|
620
|
+
* Needs the store built with a **backend** (throws {@link UnsupportedError} otherwise),
|
|
621
|
+
* because it has to enumerate and delete generations — a pre-built `StorageChunkSource` only reads.
|
|
445
622
|
*/
|
|
446
623
|
dropSegment(ref: SegmentRef, options: {
|
|
447
624
|
confirmSegment: string;
|
|
@@ -449,54 +626,211 @@ declare class CloudRoaring {
|
|
|
449
626
|
audit?: IAuditSink;
|
|
450
627
|
}): Promise<DropResult>;
|
|
451
628
|
/**
|
|
452
|
-
* **
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
629
|
+
* **Record when this segment becomes eligible for retirement.** One registry write; nothing is deleted here,
|
|
630
|
+
* and nothing starts running. `retireExpired` is what acts on the policy, and **you** decide when that runs —
|
|
631
|
+
* an EventBridge rule, a CronJob, a queue consumer, whatever your deployment already has. This library starts
|
|
632
|
+
* no background thread (it has to work identically in a Lambda, an edge isolate and a long-lived server).
|
|
633
|
+
*
|
|
634
|
+
* `expiresAt` is an **absolute epoch-ms you compute**, not a duration the library derives. A relative TTL would
|
|
635
|
+
* have to be anchored to something the library knows — `updatedAt`, or the current generation — and every load
|
|
636
|
+
* rewrites both, so "expire 30 days after the last write" would keep a busy daily bucket alive forever
|
|
637
|
+
* precisely because it is being reloaded.
|
|
638
|
+
*
|
|
639
|
+
* ```ts
|
|
640
|
+
* const DAY = 86_400_000;
|
|
641
|
+
* const ref = { namespace: 'active-daily', segment: '2026-08-05' };
|
|
642
|
+
* await store.setRetention(ref, { expiresAt: Date.now() + 30 * DAY });
|
|
643
|
+
* ```
|
|
644
|
+
*
|
|
645
|
+
* **Works before the first load**: a segment with no registry row yet gets one (`createdRow: true` in the
|
|
646
|
+
* result) with **no Storage generation**, so the policy is recorded ahead of the data and the segment is already
|
|
647
|
+
* enumerable by the sweep; the first load then publishes onto that row.
|
|
648
|
+
*
|
|
649
|
+
* A value in the past is legal and means "eligible on the next sweep" — backfilling a policy onto existing
|
|
650
|
+
* buckets is normal. A value below `MIN_EXPIRES_AT_MS` (2001-09-09) is rejected: it is almost certainly epoch
|
|
651
|
+
* **seconds**, which would read as long-expired and retire the segment on the next pass. Needs a `registry`
|
|
652
|
+
* in the store config (throws {@link UnsupportedError} otherwise), and refuses a crypto-shredded segment.
|
|
653
|
+
*/
|
|
654
|
+
setRetention(ref: SegmentRef, policy: RetentionPolicy): Promise<SetRetentionResult>;
|
|
655
|
+
/**
|
|
656
|
+
* **The stored retention policy**, or `null` if the segment has none (or has no registry row, or is a
|
|
657
|
+
* tombstone). Returns the string `'invalid'` for a row whose `expiresAt` is present but unusable — a
|
|
658
|
+
* hand-edited row, or one from a restore — so a malformed policy is visible rather than silently reading as
|
|
659
|
+
* "never expires" on a segment someone believes is expiring.
|
|
660
|
+
*/
|
|
661
|
+
getRetention(ref: SegmentRef): Promise<RetentionPolicy | null | 'invalid'>;
|
|
662
|
+
/**
|
|
663
|
+
* **Cancel a segment's expiry** so no sweep retires it. Returns whether a policy was actually removed (`false`
|
|
664
|
+
* when there was none). A separate verb from `setRetention` on purpose: "never expire" as a magic value passed
|
|
665
|
+
* to the setter is how a typo becomes a deletion.
|
|
666
|
+
*/
|
|
667
|
+
clearRetention(ref: SegmentRef): Promise<boolean>;
|
|
668
|
+
/**
|
|
669
|
+
* **Run the retention sweep: retire every segment whose `expiresAt` has passed.** This is the call that acts on
|
|
670
|
+
* the policies `setRetention` records — and it is a **call, not a daemon**. Nothing schedules it; you run it from
|
|
671
|
+
* whatever heartbeat your deployment already has (an EventBridge rule, a Kubernetes CronJob, a queue consumer,
|
|
672
|
+
* the job that runs your loads). A library that started a timer would behave differently in a Lambda, an edge
|
|
673
|
+
* isolate and a long-lived server, which is worse than not having one.
|
|
674
|
+
*
|
|
675
|
+
* Each retirement goes through `dropSegment`, so the registry → Storage ordering, the re-sweep for an object a
|
|
676
|
+
* load was still writing, and the `generationsRemaining` report all come from one implementation rather than
|
|
677
|
+
* two.
|
|
678
|
+
*
|
|
679
|
+
* ```ts
|
|
680
|
+
* // In your scheduled handler. Start with a preview in a new deployment.
|
|
681
|
+
* const preview = await store.retireExpired({ namespace: 'active-daily', dryRun: true });
|
|
682
|
+
* console.log(`would retire ${preview.retired} of ${preview.scanned} (limited: ${preview.limited})`);
|
|
683
|
+
*
|
|
684
|
+
* const swept = await store.retireExpired({ namespace: 'active-daily' });
|
|
685
|
+
* for (const e of swept.entries) {
|
|
686
|
+
* if (e.action === 'skipped') console.warn(`${e.segment}: ${e.reason}`); // invalid-policy / limit / failed: …
|
|
687
|
+
* if (e.action === 'retired' && e.result.generationsRemaining.length > 0) {
|
|
688
|
+
* console.warn(`${e.segment}: storage not fully reclaimed — re-run`);
|
|
689
|
+
* }
|
|
690
|
+
* }
|
|
691
|
+
* if (swept.limited) scheduleAnotherPassSoon(); // more are still eligible
|
|
692
|
+
* ```
|
|
693
|
+
*
|
|
694
|
+
* **Read the ledger.** A per-segment *fault* is an entry, not an exception — a throw from the middle of a fleet
|
|
695
|
+
* sweep would leave the caller unable to say which segments were retired, having already retired some. (A bad
|
|
696
|
+
* argument does throw, and so does a fleet larger than `maxScanSegments`.) `limited: true` means the per-cycle
|
|
697
|
+
* `limit` (default 100) cut the pass short and more are eligible. That cap is charged on **attempts**, not
|
|
698
|
+
* successes, which is what makes it a real bound: `dropSegment` writes the tombstone before sweeping Storage, so a
|
|
699
|
+
* fault in the Storage phase is a segment that is already retired. Retirements are sequential, so `limit` is a
|
|
700
|
+
* wall-clock knob too, and `retired` counts deletions only — a dry run reports `wouldRetire` instead, so a
|
|
701
|
+
* dashboard summing `retired` can never show a phantom deletion.
|
|
702
|
+
*
|
|
703
|
+
* It also **deletes the tombstone rows its own past retirements left**, after `tombstoneGraceMs` (default 24 h)
|
|
704
|
+
* and only once that segment's Storage generations are provably gone — collecting a straggler generation itself
|
|
705
|
+
* first, since nothing else ever would for a tombstoned segment. Attribution is a **positive marker the sweep
|
|
706
|
+
* stamps on its own retirements**, not an inference from "destroyed + an expired policy": a crypto-shred leaves
|
|
707
|
+
* `retention` untouched, so setting a policy and then honouring a right-to-erasure request mid-window produces
|
|
708
|
+
* exactly that row, and deleting it would destroy the Art. 17 attestation and un-fence the name. Pass
|
|
709
|
+
* `purgeTombstones: false` to keep every tombstone.
|
|
710
|
+
*
|
|
711
|
+
* Needs the store built with a **backend** (throws {@link UnsupportedError} otherwise),
|
|
712
|
+
* because retiring a segment deletes its storage objects. `now` defaults to the store's clock.
|
|
713
|
+
*/
|
|
714
|
+
retireExpired(options?: Omit<RetireExpiredOptions, 'now'> & {
|
|
715
|
+
now?: number;
|
|
716
|
+
}): Promise<RetireExpiredResult>;
|
|
717
|
+
/**
|
|
718
|
+
* **Forget everything this store has cached about a segment.** Use it after destroying or retiring a segment
|
|
719
|
+
* through a path this store did not perform itself.
|
|
720
|
+
*
|
|
721
|
+
* A store keeps two layers of derived state — a resolved snapshot per segment (an open `.crbm` reader, plus
|
|
722
|
+
* the DEK it unwrapped) and decoded chunks keyed by generation. Both are built for one event: **a publish
|
|
723
|
+
* that advances `currentGen`**, which the snapshot TTL notices and the generation-keyed cache misses on.
|
|
724
|
+
* Neither notices an event that *destroys* what they were derived from.
|
|
725
|
+
*
|
|
726
|
+
* The verbs on this class handle themselves — `eraseSubject`, `dropSegment` and `retireExpired` invalidate
|
|
727
|
+
* what they touch. This method is for the cases they cannot see:
|
|
728
|
+
*
|
|
729
|
+
* - **`destroySegment` / `eraseNamespace`**, which are free functions over raw drivers rather than methods
|
|
730
|
+
* here, so a crypto-shred performed beside this store leaves it holding an open reader and an unwrapped
|
|
731
|
+
* DEK. Until it is told, it keeps decrypting — including chunks it had never fetched before the shred.
|
|
732
|
+
* - **Another process.** Erasing on one box invalidates nothing on the others; each store bounds its own
|
|
733
|
+
* staleness by `cache.genTtlMs`, and a store built with no clock or `cache.genTtlMs: 0` ("pin forever") never
|
|
734
|
+
* converges at all. If a compliance deadline depends on every reader converging, you need to signal them —
|
|
735
|
+
* this is the call to make when your own fan-out delivers.
|
|
736
|
+
*
|
|
737
|
+
* Synchronous, best-effort, and safe to call for a segment this store has never read.
|
|
738
|
+
*
|
|
739
|
+
* ```ts
|
|
740
|
+
* await destroySegment(ref, { storage, registry, keystore }, { confirmSegment: ref.segment });
|
|
741
|
+
* store.invalidate(ref); // this process
|
|
742
|
+
* await bus.publish('cloudbitmaps.invalidate', ref); // and every other one
|
|
743
|
+
* ```
|
|
744
|
+
*/
|
|
745
|
+
invalidate(ref: SegmentRef): void;
|
|
746
|
+
/**
|
|
747
|
+
* Build a handle held at the generation `ref` resolves to right now. See {@link Segment.pin}.
|
|
748
|
+
*
|
|
749
|
+
* The pinned handle gets its own engine but **shares the store's chunk cache**, which is safe precisely
|
|
750
|
+
* because the cache is keyed by the source's version rather than the generation number: the pinned view
|
|
751
|
+
* reports the version captured at pin time, so its decoded chunks cannot collide with the live generation's.
|
|
752
|
+
* Sharing it on a generation-only key was how a pinned read could resurrect an id already reported
|
|
753
|
+
* physically gone.
|
|
754
|
+
*/
|
|
755
|
+
private pinSegment;
|
|
756
|
+
/**
|
|
757
|
+
* An engine reading every segment in `pins` at its pinned generation and everything else live.
|
|
758
|
+
*
|
|
759
|
+
* It **shares the store's chunk cache**, which is safe only because that cache is keyed by the source's
|
|
760
|
+
* version rather than the generation number: a pinned view reports the version captured at pin time, so its
|
|
761
|
+
* decoded chunks cannot collide with the live generation's.
|
|
762
|
+
*/
|
|
763
|
+
private engineWithPins;
|
|
764
|
+
/**
|
|
765
|
+
* The engine a combine should run on, given the handles involved.
|
|
766
|
+
*
|
|
767
|
+
* A pinned handle passed as an **operand** must still be read at its pin. Routing by "which handle the call
|
|
768
|
+
* was made on" reads it live instead — `snap.intersect([other])` honours the pin while
|
|
769
|
+
* `other.intersect([snap])` silently does not, and the two are the same question. So the combine collects
|
|
770
|
+
* every pin in play and runs on an engine that honours all of them. With no pins anywhere this is the
|
|
771
|
+
* store's own engine and costs nothing.
|
|
772
|
+
*/
|
|
773
|
+
private engineForCombine;
|
|
774
|
+
/** The store's registry, or a typed error naming the operation that needs one. */
|
|
775
|
+
private requireRegistry;
|
|
776
|
+
/**
|
|
777
|
+
* **Cross-tier DR consistency check.** After a restore/failover, verify every registered segment's `currentGen`
|
|
778
|
+
* actually has its `.crbm` present in Storage — catching a **torn restore** where the registry (`currentGen`) came
|
|
779
|
+
* back ahead of the object store, so a pointer references a generation that isn't there (reads would then
|
|
780
|
+
* throw). Read-only, bounded fan-out; run it at startup after a restore. Returns `{ checked, inconsistent }` —
|
|
781
|
+
* `inconsistent` empty ⇒ coherent; otherwise it names the segments to recover (restore the object store, or
|
|
782
|
+
* roll the registry back to a coherent point). Needs the store built with a **backend**
|
|
783
|
+
* (throws {@link UnsupportedError} otherwise). `destroyed` (crypto-shredded) segments are skipped. Pair it with
|
|
784
|
+
* the DR runbook (docs/guide/disaster-recovery.md).
|
|
460
785
|
*/
|
|
461
786
|
checkConsistency(options?: {
|
|
462
787
|
namespace?: string;
|
|
463
788
|
concurrency?: number;
|
|
464
789
|
}): Promise<ConsistencyReport>;
|
|
465
790
|
/**
|
|
466
|
-
* **Export ("eject") every registered segment's current
|
|
467
|
-
*
|
|
468
|
-
*
|
|
791
|
+
* **Export ("eject") every registered segment's current generation** through the injected `sink`, using only
|
|
792
|
+
* public read APIs — so your data is readable **without CloudRoaring** (the exit path; see the README's "Your
|
|
793
|
+
* data stays yours"). `format: 'roaring'` (default) writes one **portable RoaringBitmap32** per segment
|
|
469
794
|
* (loadable by any roaring library); `'ndjson'` writes newline-delimited ids (zero-dependency, streaming).
|
|
470
795
|
* Enumerates via the store's **own** registry (needs one — throws {@link UnsupportedError} otherwise) so the
|
|
471
|
-
* enumeration and the read path provably share one registry
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
796
|
+
* enumeration and the read path provably share one registry. Encrypted segments are decrypted transparently
|
|
797
|
+
* **iff** this store was wired with their keystore — the export is therefore **cleartext**; protect it.
|
|
798
|
+
* Crypto-shredded segments are skipped. A segment that can't be read is isolated into the manifest's `failed[]`
|
|
799
|
+
* (the run continues), so "a manifest exists" means the run finished — check `failed`. The `export-segments`
|
|
800
|
+
* CLI wraps this with a filesystem sink.
|
|
476
801
|
*/
|
|
477
802
|
exportSegments(sink: ExportSink, options?: ExportOptions): Promise<ExportManifest>;
|
|
478
803
|
/**
|
|
479
|
-
* Planning cost estimate
|
|
480
|
-
*
|
|
804
|
+
* Planning cost estimate — pure, no instance/data needed: sizing, sales, what-if. For a real, grounded report
|
|
805
|
+
* from live segment sizes, use `store.segment(name).costReport()`. See {@link CostReport}.
|
|
481
806
|
*/
|
|
482
807
|
static estimateCost(input: EstimateInput): CostReport;
|
|
483
808
|
}
|
|
484
|
-
/**
|
|
485
|
-
* A handle bound to one segment — the read/write ops.
|
|
486
|
-
*
|
|
487
|
-
* **IDs must be integers in `[0, 2^32)`** (dense 32-bit). A non-integer / negative / out-of-range id
|
|
488
|
-
* throws {@link ValidationError}.
|
|
489
|
-
*
|
|
490
|
-
* **`addMany`/`removeMany`/`intersectInto`/`unionInto`/`andNotInto` are not atomic across chunks**: ids are grouped by chunk and
|
|
491
|
-
* applied one chunk at a time, so if a later chunk fails (e.g. {@link WriteConflictError} after retries)
|
|
492
|
-
* earlier chunks are already applied. Within a single chunk the update is atomic.
|
|
493
|
-
*/
|
|
494
809
|
/** Options common to every chunk-aligned combine (`intersect` / `union` / `andNot`). */
|
|
495
|
-
interface BaseCombineOptions {
|
|
496
|
-
/** Max chunk keys resolved concurrently — bounds the
|
|
810
|
+
export interface BaseCombineOptions {
|
|
811
|
+
/** Max chunk keys resolved concurrently — bounds the Storage footprint. A positive integer. */
|
|
497
812
|
readonly concurrency?: number;
|
|
498
813
|
/** Override the store's per-op denial-of-wallet budget for this call (`false` lifts it). */
|
|
499
814
|
readonly budget?: BudgetOption;
|
|
815
|
+
/**
|
|
816
|
+
* Allow an operand naming a segment that does not exist. Default `false`: a combine **refuses** one, because
|
|
817
|
+
* a misspelled or mis-namespaced operand is indistinguishable from a correct one in the result.
|
|
818
|
+
*
|
|
819
|
+
* Reading a segment that does not exist answers empty, and that is right — but passing one as an operand is
|
|
820
|
+
* different. A suppression list with nobody on it correctly suppresses nothing; one whose `namespace` you
|
|
821
|
+
* omitted *silently* suppresses nothing, and the result is not obviously-empty, it is the full audience and
|
|
822
|
+
* plausibly right. The failure mode is mailing the people who opted out.
|
|
823
|
+
*
|
|
824
|
+
* ```ts
|
|
825
|
+
* // `global-opt-out` lives in the `suppression` namespace. This addresses a DIFFERENT segment:
|
|
826
|
+
* audience.andNot([store.segment('global-opt-out')]); // throws ValidationError
|
|
827
|
+
* audience.andNot([store.segment('global-opt-out', { namespace: 'suppression' })]); // correct
|
|
828
|
+
* ```
|
|
829
|
+
*
|
|
830
|
+
* Set `true` when you genuinely intend to combine against a name that may not exist yet. Checked only for an
|
|
831
|
+
* operand that resolved to no chunks at all, so a normal combine pays nothing for it.
|
|
832
|
+
*/
|
|
833
|
+
readonly allowAbsentOperands?: boolean;
|
|
500
834
|
}
|
|
501
835
|
/**
|
|
502
836
|
* A combine that can also subtract.
|
|
@@ -505,96 +839,215 @@ interface BaseCombineOptions {
|
|
|
505
839
|
* chunk-aligned pass, and each exclude is read **only at the keys that survived** — so a large global opt-out
|
|
506
840
|
* list costs reads proportional to the audience, not to itself.
|
|
507
841
|
*/
|
|
508
|
-
interface CombineOptions extends BaseCombineOptions {
|
|
842
|
+
export interface CombineOptions extends BaseCombineOptions {
|
|
509
843
|
/** Segments whose ids are subtracted from the result. */
|
|
510
844
|
readonly exclude?: Segment[];
|
|
511
845
|
}
|
|
512
|
-
/**
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
846
|
+
/**
|
|
847
|
+
* A combine that WRITES its result — the extra options `intersect`/`union`/`andNot` have no use for.
|
|
848
|
+
*
|
|
849
|
+
* Separate from {@link CombineOptions} on purpose: `allowEmpty` and `guard` decide whether a generation is
|
|
850
|
+
* published, and a read verb publishes nothing. Offering them on `intersect()` would be offering a parameter
|
|
851
|
+
* that cannot do anything.
|
|
852
|
+
*/
|
|
853
|
+
export interface MaterializeOptions extends CombineOptions {
|
|
854
|
+
/**
|
|
855
|
+
* Audit sink. A materialisation publishes a generation, and a publish is an auditable event
|
|
856
|
+
* (`segment.publish`, exactly as a load emits) — as is a refusal (`segment.load-refused`).
|
|
857
|
+
*
|
|
858
|
+
* It is on the call rather than on the store because that is where every other auditable operation takes it
|
|
859
|
+
* (`eraseSubject`, `dropSegment`, `retireExpired`): the caller who performs the act decides where the record
|
|
860
|
+
* goes. Without it a `*Into` was the one write path in the library that could make a generation current and
|
|
861
|
+
* leave no trace in the compliance trail.
|
|
862
|
+
*
|
|
863
|
+
* It sits HERE rather than on {@link BaseCombineOptions}, where it used to, for the reason this type exists:
|
|
864
|
+
* the streaming verbs write nothing, so an audit sink on `intersect()` was a parameter that could not do
|
|
865
|
+
* anything. Same rule, now applied to itself.
|
|
866
|
+
*/
|
|
867
|
+
readonly audit?: IAuditSink;
|
|
868
|
+
/**
|
|
869
|
+
* Publish an empty result over a non-empty destination. Off by default, exactly as on {@link CloudRoaring.load}:
|
|
870
|
+
* an empty combine is far more often a mistake upstream — a typo'd operand, a segment that has not loaded
|
|
871
|
+
* yet, an `exclude` that swallowed everything — than an intent, and once it lands it is indistinguishable
|
|
872
|
+
* from a correct run.
|
|
873
|
+
*/
|
|
874
|
+
readonly allowEmpty?: boolean;
|
|
875
|
+
/** Refuse an implausible result rather than publish it. Same bounds, and same meaning, as on `load()`. */
|
|
876
|
+
readonly guard?: LoadGuard;
|
|
877
|
+
/**
|
|
878
|
+
* Generations to keep below the new pointer — see {@link LoadOptions.keep}.
|
|
879
|
+
*
|
|
880
|
+
* **Defaults to keeping everything**, unlike `load()`, which keeps 1 and collects the rest. A
|
|
881
|
+
* materialisation has never collected, and an operator's recovery story can depend on that: `rollbackSegment`
|
|
882
|
+
* refuses a target that has been collected. Pass a number to collect on the way through; `0` keeps only the
|
|
883
|
+
* generation this call publishes.
|
|
884
|
+
*/
|
|
885
|
+
readonly keep?: number;
|
|
516
886
|
}
|
|
517
|
-
|
|
887
|
+
/**
|
|
888
|
+
* What `andNotInto` takes: the write options without `exclude`, because its `excludes` argument IS the
|
|
889
|
+
* subtraction — a second one in the options would be two spellings of one thing.
|
|
890
|
+
*
|
|
891
|
+
* Named rather than left as an inline `Omit` so it can be imported and annotated. The read sibling `andNot`
|
|
892
|
+
* solves the same problem the same way, by taking {@link BaseCombineOptions}.
|
|
893
|
+
*/
|
|
894
|
+
export type AndNotIntoOptions = Omit<MaterializeOptions, 'exclude'>;
|
|
895
|
+
/** How a `Segment` hands a result stream back to its store to become a new generation of `dest`. */
|
|
896
|
+
/** Build a pinned twin of a handle — injected into `Segment` so it stays free of store wiring. */
|
|
897
|
+
type Pin = (ref: SegmentRef, expiresAt?: number) => Promise<Segment>;
|
|
898
|
+
/**
|
|
899
|
+
* The engine a combine should run on, given every handle involved — `undefined` when none is pinned and the
|
|
900
|
+
* store's own engine will do. Injected so `Segment` stays free of store wiring.
|
|
901
|
+
*/
|
|
902
|
+
type CombineEngine = (handles: readonly Segment[]) => SegmentEngine | undefined;
|
|
903
|
+
type Materialize = (dest: SegmentRef, ids: AsyncIterable<number>, op: string, options?: MaterializeOptions) => Promise<MaterializeResult>;
|
|
904
|
+
/**
|
|
905
|
+
* A handle bound to one segment — the read verbs, plus the three `*Into` verbs that write a **new generation**
|
|
906
|
+
* of another segment.
|
|
907
|
+
*
|
|
908
|
+
* **IDs must be integers in `[0, 2^32)`** (dense 32-bit). A non-integer / negative / out-of-range id
|
|
909
|
+
* throws {@link ValidationError}.
|
|
910
|
+
*
|
|
911
|
+
* There is no `add`/`remove` on a handle: data enters a segment as a whole generation (`bulkLoadCrbmGeneration`,
|
|
912
|
+
* or one of the `*Into` verbs), and leaves it the same way (`eraseSubject`, `dropSegment`).
|
|
913
|
+
*/
|
|
914
|
+
export declare class Segment {
|
|
518
915
|
private readonly engine;
|
|
519
916
|
private readonly ref;
|
|
520
917
|
private readonly clock;
|
|
521
918
|
private readonly metrics;
|
|
919
|
+
private readonly materialize;
|
|
920
|
+
/** Build a pinned twin of this handle — injected so `Segment` stays free of store wiring. */
|
|
921
|
+
private readonly pinned;
|
|
922
|
+
private readonly combineEngine;
|
|
923
|
+
/** Absolute epoch-ms deadline from {@link SegmentOptions.expiresAt}; `undefined` ⇒ this handle never expires. */
|
|
924
|
+
readonly expiresAt?: number | undefined;
|
|
925
|
+
/**
|
|
926
|
+
* The generation this handle is held at, when it came from {@link Segment.pin}. Read by the store so a
|
|
927
|
+
* pinned handle passed as an **operand** is still read at its pin rather than live.
|
|
928
|
+
*/
|
|
929
|
+
readonly pinnedAt?: PinnedAt | undefined;
|
|
522
930
|
private readonly metricsOn;
|
|
523
|
-
constructor(engine: SegmentEngine, ref: SegmentRef, clock: Clock, metrics: IMetricsSink
|
|
931
|
+
constructor(engine: SegmentEngine, ref: SegmentRef, clock: Clock, metrics: IMetricsSink, materialize: Materialize,
|
|
932
|
+
/** Build a pinned twin of this handle — injected so `Segment` stays free of store wiring. */
|
|
933
|
+
pinned: Pin, combineEngine: CombineEngine,
|
|
934
|
+
/** Absolute epoch-ms deadline from {@link SegmentOptions.expiresAt}; `undefined` ⇒ this handle never expires. */
|
|
935
|
+
expiresAt?: number | undefined,
|
|
524
936
|
/**
|
|
525
|
-
*
|
|
526
|
-
*
|
|
937
|
+
* The generation this handle is held at, when it came from {@link Segment.pin}. Read by the store so a
|
|
938
|
+
* pinned handle passed as an **operand** is still read at its pin rather than live.
|
|
527
939
|
*/
|
|
528
|
-
|
|
529
|
-
/** Add an id (integer in `[0, 2^32)`). Throws {@link ValidationError} on a bad id. */
|
|
530
|
-
add(id: number): Promise<void>;
|
|
940
|
+
pinnedAt?: PinnedAt | undefined);
|
|
531
941
|
/**
|
|
532
|
-
*
|
|
942
|
+
* **Hold this segment at the generation that is current right now**, for as long as you keep the handle.
|
|
943
|
+
*
|
|
944
|
+
* An ordinary handle re-resolves on `cache.genTtlMs`, so a publish part-way through a long job means its second
|
|
945
|
+
* half describes a different instant than its first — every chunk whole and verified, but the answer covering
|
|
946
|
+
* two moments, with nothing in the result saying so. That is fine for a dashboard and wrong for an export, a
|
|
947
|
+
* reconciliation, or a send that has to match the count you reported. A pin is how you get one instant.
|
|
948
|
+
*
|
|
949
|
+
* ```ts
|
|
950
|
+
* const snap = await store.segment('active-30d').pin();
|
|
951
|
+
* const total = await snap.count(); // the number you report
|
|
952
|
+
* for await (const id of snap.iterate()) { … } // …and the ids it counted, however long this takes
|
|
953
|
+
* ```
|
|
954
|
+
*
|
|
955
|
+
* **Only this segment is pinned.** `snap.intersect([other])` reads `snap` at its pinned generation and
|
|
956
|
+
* `other` at whatever is current — pin each segment if you want the whole query held. And a pinned handle
|
|
957
|
+
* used as an *operand* is still read at its pin, never live.
|
|
533
958
|
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
959
|
+
* **It is a hold, not a lease.** Nothing here stops `gcOrphanGenerations` deleting the generation underneath
|
|
960
|
+
* you: a pinned read deliberately does **not** heal forward, because silently serving a different generation
|
|
961
|
+
* is the one thing a pin exists to prevent, so it fails instead. Size `keep` to cover your longest pinned
|
|
962
|
+
* job — see [Sizing `keep`](../../docs/guide/getting-started.md#sizing-keep) — or take the pin on a segment
|
|
963
|
+
* you are not collecting.
|
|
538
964
|
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
965
|
+
* A segment with no current generation pins nothing and reads empty, exactly as it would unpinned. A pin
|
|
966
|
+
* taken before a crypto-shred stops reading when the shred lands: the destroyed row is re-checked every time
|
|
967
|
+
* the pinned reader opens, so a pin cannot outlive the key it was using.
|
|
968
|
+
*
|
|
969
|
+
* Needs a store built on the `.crbm` storage source (the default when you pass a backend or a raw driver). Throws
|
|
970
|
+
* {@link UnsupportedError} on a store wired with a pre-built source that cannot pin.
|
|
543
971
|
*/
|
|
544
|
-
|
|
545
|
-
/**
|
|
546
|
-
|
|
547
|
-
/** Remove many ids, from a **sync or async** iterable. Not atomic across chunks — see {@link addMany}. */
|
|
548
|
-
removeMany(ids: Iterable<number> | AsyncIterable<number>): Promise<void>;
|
|
972
|
+
pin(): Promise<Segment>;
|
|
973
|
+
/** This handle's segment, as the cache/pin key — `ref` stays private; the encapsulation is worth the method. */
|
|
974
|
+
key(): string;
|
|
549
975
|
/**
|
|
550
|
-
*
|
|
976
|
+
* Has this handle's deadline passed? **The lazy half of expiry** — the whole check is one comparison against
|
|
977
|
+
* the injected clock, so it costs nothing on a handle with no deadline and no I/O on one that has expired.
|
|
551
978
|
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
|
|
979
|
+
* Deliberately evaluated per call rather than cached: a long-lived handle created before its deadline must
|
|
980
|
+
* start reading empty the moment the deadline passes, without the caller re-creating it.
|
|
981
|
+
*/
|
|
982
|
+
private expired;
|
|
983
|
+
/**
|
|
984
|
+
* Refuse a materialisation that involves an **expired** handle. Called by the three `*Into` verbs only.
|
|
555
985
|
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
* ```
|
|
986
|
+
* On the read verbs an expired handle answers empty, which is the point of lazy expiry. On a *write* the same
|
|
987
|
+
* rule would be destructive in a way nobody asks for: `a.intersectInto(dest, [b])` where `b`'s deadline has
|
|
988
|
+
* quietly passed publishes an **empty generation over `dest`** — a wipe, reported as a successful write, with
|
|
989
|
+
* the cause (a deadline on a handle somewhere) nowhere in the result. Reads degrade to empty; writes must not.
|
|
561
990
|
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
* distinct chunk, giving you Redis's *semantics* without Redis's per-id *cost shape*.
|
|
991
|
+
* So every handle in the call is checked, `dest` included: an expired `dest` does not change the bytes written,
|
|
992
|
+
* but a caller who put a deadline on the thing they are writing into has said something contradictory and is
|
|
993
|
+
* better told than guessed at. Open a handle without `expiresAt` to write, or drop the deadline.
|
|
566
994
|
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
995
|
+
* (The broader guard — refusing to publish an empty or implausible generation over a non-empty one, with an
|
|
996
|
+
* `allowEmpty` override — now covers these verbs too: they route through the same guarded write path as
|
|
997
|
+
* {@link CloudRoaring.load}. The two stay separate because they differ in kind. That one is a REPORTED
|
|
998
|
+
* refusal a caller may legitimately override; an expired handle is a wiring mistake, so it THROWS, before
|
|
999
|
+
* any object is written — and `allowEmpty: true` does not reach it.)
|
|
571
1000
|
*
|
|
572
|
-
*
|
|
1001
|
+
* The verbs are `async` so this surfaces as a **rejected promise**, like every other validation in the facade —
|
|
1002
|
+
* a synchronous throw out of a promise-returning method escapes a caller who attached `.catch()` instead of
|
|
1003
|
+
* awaiting.
|
|
1004
|
+
*/
|
|
1005
|
+
private refuseIfExpired;
|
|
1006
|
+
/**
|
|
1007
|
+
* Time an op with the injected clock and emit an `op` metric on completion (success or throw). Skipped
|
|
1008
|
+
* entirely when no sink is wired, so the default path pays nothing.
|
|
573
1009
|
*/
|
|
574
|
-
|
|
1010
|
+
private timed;
|
|
1011
|
+
/** Membership: one chunk — the cache, else one ranged GET. Throws {@link ValidationError} on a bad id. */
|
|
575
1012
|
has(id: number): Promise<boolean>;
|
|
1013
|
+
/** Cardinality — summed from the `.crbm` index with **zero payload reads** on a loaded segment. */
|
|
576
1014
|
count(): Promise<number>;
|
|
1015
|
+
/** Every id, ascending, streamed one chunk at a time. */
|
|
577
1016
|
iterate(): AsyncIterable<number>;
|
|
578
|
-
/**
|
|
579
|
-
* Chunk-skipping intersection: stream the ids in **this** segment AND every segment in `others`, ascending.
|
|
580
|
-
* Fetches only the Cold chunks present in *all* operands (a key absent from any operand contributes nothing
|
|
581
|
-
* and is never downloaded), streaming under a bounded in-flight window — so the Cold footprint stays small
|
|
582
|
-
* (Lambda-friendly) regardless of segment size. Pass `concurrency` to tune that window (a positive integer).
|
|
583
|
-
* AND is commutative, so `a.intersect([b])` and `b.intersect([a])` yield the same ids. (Warm state is read
|
|
584
|
-
* up front, so total memory also carries each operand's Warm size — negligible under Topology-A.) Pass
|
|
585
|
-
* `budget` to override the store's per-op denial-of-wallet budget for this call (or `false` to lift it).
|
|
586
|
-
*/
|
|
587
1017
|
/**
|
|
588
1018
|
* Map the facade's `Segment` handles in `exclude` down to the plain refs `core` takes. A method rather than
|
|
589
1019
|
* a module function because `ref` is class-private — the encapsulation is worth more than the free function.
|
|
590
1020
|
*/
|
|
591
1021
|
private refsIn;
|
|
1022
|
+
/**
|
|
1023
|
+
* Chunk-skipping intersection: stream the ids in **this** segment AND every segment in `others`, ascending.
|
|
1024
|
+
* Fetches only the Storage chunks present in *all* operands (a key absent from any operand contributes nothing
|
|
1025
|
+
* and is never downloaded), streaming under a bounded in-flight window — so the Storage footprint stays small
|
|
1026
|
+
* (Lambda-friendly) regardless of segment size. Pass `concurrency` to tune that window (a positive integer).
|
|
1027
|
+
* AND is commutative, so `a.intersect([b])` and `b.intersect([a])` yield the same ids. Pass `budget` to
|
|
1028
|
+
* override the store's per-op denial-of-wallet budget for this call (or `false` to lift it).
|
|
1029
|
+
*/
|
|
592
1030
|
intersect(others: Segment[], options?: CombineOptions): AsyncIterable<number>;
|
|
593
1031
|
/**
|
|
594
|
-
* Materialize `this ∩ others…` (minus `exclude`) **
|
|
595
|
-
* bounded-memory
|
|
1032
|
+
* Materialize `this ∩ others…` (minus `exclude`) as a **new generation of `dest`** — `dest`'s previous contents
|
|
1033
|
+
* are superseded, not added to. Streaming + bounded-memory; the result is one immutable object published
|
|
1034
|
+
* forward-only, so readers of `dest` see either the old generation or the new one, never a partial. Needs the
|
|
1035
|
+
* store built with a backend (throws {@link UnsupportedError} otherwise).
|
|
1036
|
+
*
|
|
1037
|
+
* **An empty result does NOT overwrite a non-empty destination.** A combine that comes out empty is far more
|
|
1038
|
+
* often a mistake upstream — a typo'd operand, an `exclude` that swallowed everything, an operand that has
|
|
1039
|
+
* not loaded yet — than an intent, and once it publishes it is indistinguishable from a correct run. So the
|
|
1040
|
+
* write is **refused and reported**: `published: false`, `reason: 'empty'`, and `dest` keeps what it had.
|
|
1041
|
+
* Pass `allowEmpty: true` when emptying the destination is the point.
|
|
1042
|
+
*
|
|
1043
|
+
* `guard` adds the same plausibility bounds `load()` takes — `minCardinality` and `minRetained` — judged
|
|
1044
|
+
* against what `dest` held before. A refusal is **reported, not thrown**, exactly as on `load()`; branch on
|
|
1045
|
+
* `published`. A lost race is the one outcome that still throws ({@link WriteConflictError}), because a
|
|
1046
|
+
* materialisation that silently did not take effect is the one thing a caller cannot detect on its own.
|
|
1047
|
+
*
|
|
1048
|
+
* A call involving an **expired handle** is refused before any of this. See {@link refuseIfExpired}.
|
|
596
1049
|
*/
|
|
597
|
-
intersectInto(dest: Segment, others: Segment[], options?:
|
|
1050
|
+
intersectInto(dest: Segment, others: Segment[], options?: MaterializeOptions): Promise<MaterializeResult>;
|
|
598
1051
|
/**
|
|
599
1052
|
* `this ∪ others…`, minus `options.exclude` — streamed ascending.
|
|
600
1053
|
*
|
|
@@ -603,11 +1056,11 @@ declare class Segment {
|
|
|
603
1056
|
* `intersect` prunes any key missing from any operand; union has nothing to prune. It is charged against the
|
|
604
1057
|
* same per-op budget, so a wide union is refused rather than quietly billed — pass `budget` to raise it
|
|
605
1058
|
* deliberately. If you find yourself unioning the same segments on every read, materializing the combined
|
|
606
|
-
* segment once (`unionInto`, or a
|
|
1059
|
+
* segment once (`unionInto`, or a load) is the cheaper shape.
|
|
607
1060
|
*/
|
|
608
1061
|
union(others: Segment[], options?: CombineOptions): AsyncIterable<number>;
|
|
609
|
-
/** Materialize `this ∪ others…` (minus `exclude`) **
|
|
610
|
-
unionInto(dest: Segment, others: Segment[], options?:
|
|
1062
|
+
/** Materialize `this ∪ others…` (minus `exclude`) as a **new generation of `dest`** — see {@link intersectInto}. */
|
|
1063
|
+
unionInto(dest: Segment, others: Segment[], options?: MaterializeOptions): Promise<MaterializeResult>;
|
|
611
1064
|
/**
|
|
612
1065
|
* `this \ (excludes…)` — streamed ascending. Suppression on its own.
|
|
613
1066
|
*
|
|
@@ -620,23 +1073,20 @@ declare class Segment {
|
|
|
620
1073
|
* the suppression folds into the same pass rather than materializing an intermediate segment first.
|
|
621
1074
|
*/
|
|
622
1075
|
andNot(excludes: Segment[], options?: BaseCombineOptions): AsyncIterable<number>;
|
|
623
|
-
/** Materialize `this \ (excludes…)` **
|
|
624
|
-
andNotInto(dest: Segment, excludes: Segment[], options?:
|
|
625
|
-
batchSize?: number;
|
|
626
|
-
}): Promise<void>;
|
|
1076
|
+
/** Materialize `this \ (excludes…)` as a **new generation of `dest`** — see {@link intersectInto}. */
|
|
1077
|
+
andNotInto(dest: Segment, excludes: Segment[], options?: AndNotIntoOptions): Promise<MaterializeResult>;
|
|
627
1078
|
/**
|
|
628
|
-
* Grounded cost report for this segment
|
|
629
|
-
*
|
|
630
|
-
*
|
|
1079
|
+
* Grounded cost report for this segment: storage cost from its **real** `.crbm` size (exact, no payload
|
|
1080
|
+
* reads); request cost from the supplied `workload` rates. A segment with no Storage generation reports zero
|
|
1081
|
+
* storage. See {@link CostReport} — it always includes a verdict (incl. the lose-zone).
|
|
631
1082
|
*/
|
|
632
1083
|
costReport(options?: {
|
|
633
1084
|
pricing?: PricingProfile;
|
|
634
1085
|
workload?: Workload;
|
|
635
|
-
topology?: Topology;
|
|
636
1086
|
}): Promise<CostReport>;
|
|
637
1087
|
}
|
|
638
|
-
|
|
1088
|
+
export * from '@cloudbitmaps/core';
|
|
1089
|
+
export { bulkLoadCrbmGeneration, eraseIdFromSegment, loadSegment, runExport } from './codec-bound.js';
|
|
1090
|
+
export { SafeBitmap, roaringCodec } from './roaring-codec.js';
|
|
639
1091
|
/** Package version marker. Kept in sync with package.json at release. */
|
|
640
|
-
declare const VERSION = "0.
|
|
641
|
-
|
|
642
|
-
export { type BaseCombineOptions, CloudRoaring, type CloudRoaringOptions, type CombineIntoOptions, type CombineOptions, type EraseSubjectResult, SafeBitmap, Segment, type SegmentOptions, type SubjectErasureEntry, type SubjectReport, type SubjectSegmentRef, VERSION, bulkLoadCrbmGeneration, compactSegment, roaringCodec, runCompactionCycle, runExport };
|
|
1092
|
+
export declare const VERSION = "0.10.0";
|