@cloudbitmaps/core 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +4 -4
- package/README.md +33 -16
- package/dist/chunk-5OMC5U7U.js +508 -0
- package/dist/chunk-5OMC5U7U.js.map +7 -0
- package/dist/core/audit.d.ts +128 -0
- package/dist/core/bit-route.d.ts +21 -0
- package/dist/core/blob.d.ts +34 -0
- package/dist/core/budget.d.ts +68 -0
- package/dist/core/codec.d.ts +122 -0
- package/dist/core/concurrency.d.ts +15 -0
- package/dist/core/consistency.d.ts +73 -0
- package/dist/core/cooperative.d.ts +78 -0
- package/dist/core/cost.d.ts +254 -0
- package/dist/core/crbm/crc32c.d.ts +10 -0
- package/dist/core/crbm/format.d.ts +114 -0
- package/dist/core/crbm/reader.d.ts +86 -0
- package/dist/core/crbm/varint.d.ts +13 -0
- package/dist/core/crbm/writer.d.ts +39 -0
- package/dist/core/crbm-storage-source.d.ts +522 -0
- package/dist/core/crypto.d.ts +80 -0
- package/dist/core/determinism.d.ts +42 -0
- package/dist/core/discover.d.ts +77 -0
- package/dist/core/due-index.d.ts +53 -0
- package/dist/core/engine.d.ts +246 -0
- package/dist/core/erase-id.d.ts +176 -0
- package/dist/core/erasure.d.ts +185 -0
- package/dist/core/errors.d.ts +113 -0
- package/dist/core/generation-gc.d.ts +56 -0
- package/dist/core/keys.d.ts +23 -0
- package/dist/core/load.d.ts +121 -0
- package/dist/core/lru.d.ts +69 -0
- package/dist/core/metrics.d.ts +136 -0
- package/dist/core/name-codec.d.ts +69 -0
- package/dist/core/pinned-storage-source.d.ts +90 -0
- package/dist/core/ports.d.ts +423 -0
- package/dist/core/reader-defaults.d.ts +18 -0
- package/dist/core/registry-scan.d.ts +47 -0
- package/dist/core/retention-sweep.d.ts +198 -0
- package/dist/core/retention.d.ts +67 -0
- package/dist/core/retry.d.ts +69 -0
- package/dist/core/rollback.d.ts +75 -0
- package/dist/core/validate.d.ts +44 -0
- package/dist/driver-kit.d.ts +9 -0
- package/dist/driver-kit.js +275 -0
- package/dist/driver-kit.js.map +7 -0
- package/dist/drivers/_shared/keys.d.ts +26 -0
- package/dist/drivers/_shared/object-registry-keys.d.ts +28 -0
- package/dist/drivers/_shared/object-registry.d.ts +89 -0
- package/dist/drivers/_shared/registry.d.ts +66 -0
- package/dist/drivers/backends.d.ts +43 -0
- package/dist/drivers/crypto.d.ts +38 -0
- package/dist/drivers/localfs/fs-util.d.ts +21 -0
- package/dist/drivers/localfs/paths.d.ts +33 -0
- package/dist/drivers/localfs/registry.d.ts +28 -0
- package/dist/drivers/localfs/storage.d.ts +19 -0
- package/dist/drivers/memory.d.ts +49 -0
- package/dist/drivers/retry/retrying-chunk-source.d.ts +59 -0
- package/dist/export/index.d.ts +93 -0
- package/dist/index.d.ts +76 -2760
- package/dist/index.js +3527 -3133
- package/dist/index.js.map +7 -1
- package/dist/testing/fuzz-core.d.ts +14 -0
- package/package.json +19 -115
- package/dist/azure/index.cjs +0 -368
- package/dist/azure/index.cjs.map +0 -1
- package/dist/azure/index.d.cts +0 -48
- package/dist/azure/index.d.ts +0 -48
- package/dist/azure/index.js +0 -304
- package/dist/azure/index.js.map +0 -1
- package/dist/cassandra/index.cjs +0 -265
- package/dist/cassandra/index.cjs.map +0 -1
- package/dist/cassandra/index.d.cts +0 -52
- package/dist/cassandra/index.d.ts +0 -52
- package/dist/cassandra/index.js +0 -204
- package/dist/cassandra/index.js.map +0 -1
- package/dist/chunk-7LMLYSVJ.js +0 -43
- package/dist/chunk-7LMLYSVJ.js.map +0 -1
- package/dist/chunk-BD22WDAN.js +0 -230
- package/dist/chunk-BD22WDAN.js.map +0 -1
- package/dist/chunk-NUIDEEFZ.js +0 -91
- package/dist/chunk-NUIDEEFZ.js.map +0 -1
- package/dist/chunk-OZ6LAFV4.js +0 -6
- package/dist/chunk-OZ6LAFV4.js.map +0 -1
- package/dist/chunk-SNJVZ227.js +0 -35
- package/dist/chunk-SNJVZ227.js.map +0 -1
- package/dist/dynamodb/index.cjs +0 -767
- package/dist/dynamodb/index.cjs.map +0 -1
- package/dist/dynamodb/index.d.cts +0 -106
- package/dist/dynamodb/index.d.ts +0 -106
- package/dist/dynamodb/index.js +0 -469
- package/dist/dynamodb/index.js.map +0 -1
- package/dist/gcs/index.cjs +0 -343
- package/dist/gcs/index.cjs.map +0 -1
- package/dist/gcs/index.d.cts +0 -46
- package/dist/gcs/index.d.ts +0 -46
- package/dist/gcs/index.js +0 -279
- package/dist/gcs/index.js.map +0 -1
- package/dist/index.cjs +0 -4990
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -2761
- package/dist/mongodb/index.cjs +0 -260
- package/dist/mongodb/index.cjs.map +0 -1
- package/dist/mongodb/index.d.cts +0 -45
- package/dist/mongodb/index.d.ts +0 -45
- package/dist/mongodb/index.js +0 -199
- package/dist/mongodb/index.js.map +0 -1
- package/dist/mysql/index.cjs +0 -281
- package/dist/mysql/index.cjs.map +0 -1
- package/dist/mysql/index.d.cts +0 -56
- package/dist/mysql/index.d.ts +0 -56
- package/dist/mysql/index.js +0 -211
- package/dist/mysql/index.js.map +0 -1
- package/dist/ports-CvcvJWf6.d.cts +0 -395
- package/dist/ports-CvcvJWf6.d.ts +0 -395
- package/dist/postgres/index.cjs +0 -273
- package/dist/postgres/index.cjs.map +0 -1
- package/dist/postgres/index.d.cts +0 -54
- package/dist/postgres/index.d.ts +0 -54
- package/dist/postgres/index.js +0 -203
- package/dist/postgres/index.js.map +0 -1
- package/dist/redis/index.cjs +0 -257
- package/dist/redis/index.cjs.map +0 -1
- package/dist/redis/index.d.cts +0 -42
- package/dist/redis/index.d.ts +0 -42
- package/dist/redis/index.js +0 -197
- package/dist/redis/index.js.map +0 -1
- package/dist/s3/index.cjs +0 -918
- package/dist/s3/index.cjs.map +0 -1
- package/dist/s3/index.d.cts +0 -112
- package/dist/s3/index.d.ts +0 -112
- package/dist/s3/index.js +0 -555
- package/dist/s3/index.js.map +0 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CrbmCrypto } from '../crypto.js';
|
|
2
|
+
import type { BlobSink } from '../blob.js';
|
|
3
|
+
export interface CrbmWriterOptions {
|
|
4
|
+
/** Self-describing generation number (also encoded in the object key). */
|
|
5
|
+
readonly generation: number;
|
|
6
|
+
/**
|
|
7
|
+
* Which codec produced the chunk payloads. Defaults to {@link PAYLOAD_CODEC_ROARING_PORTABLE}.
|
|
8
|
+
*
|
|
9
|
+
* A codec id rather than a roaring-specific one, because `.crbm` is a shared container and a second codec is
|
|
10
|
+
* expected before the format freezes.
|
|
11
|
+
*/
|
|
12
|
+
readonly payloadCodecId?: number;
|
|
13
|
+
readonly elementWidth?: number;
|
|
14
|
+
/** When set, payloads + index are AES-256-GCM-encrypted, bound to `(segment, generation, scope)` via AAD. */
|
|
15
|
+
readonly crypto?: CrbmCrypto;
|
|
16
|
+
}
|
|
17
|
+
export declare class CrbmWriter {
|
|
18
|
+
private readonly sink;
|
|
19
|
+
private readonly options;
|
|
20
|
+
private readonly entries;
|
|
21
|
+
private offset;
|
|
22
|
+
private lastKey;
|
|
23
|
+
private totalCardinality;
|
|
24
|
+
private preambleWritten;
|
|
25
|
+
private finished;
|
|
26
|
+
constructor(sink: BlobSink, options: CrbmWriterOptions);
|
|
27
|
+
/**
|
|
28
|
+
* Append one chunk's payload (the roaring-serialized bytes) with its cardinality. `chunkKey` must be
|
|
29
|
+
* strictly greater than the previous one (ascending, no duplicates); `cardinality` is `[1, 65536]`.
|
|
30
|
+
*/
|
|
31
|
+
addChunk(chunkKey: number, payload: Uint8Array, cardinality: number): Promise<void>;
|
|
32
|
+
/** Seal one chunk payload into the on-disk frame `nonce ‖ ciphertext ‖ tag`, bound to its chunkKey. */
|
|
33
|
+
private sealChunk;
|
|
34
|
+
/** Write the index region + the fixed footer. After this the object is complete and immutable. */
|
|
35
|
+
finish(): Promise<void>;
|
|
36
|
+
private ensurePreamble;
|
|
37
|
+
private encodeIndex;
|
|
38
|
+
private buildFooter;
|
|
39
|
+
}
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { type IAuditSink } from './audit.js';
|
|
2
|
+
import type { Yielder } from './cooperative.js';
|
|
3
|
+
import type { Clock } from './determinism.js';
|
|
4
|
+
import type { CrbmCrypto, IKeystore, WrappedDek } from './crypto.js';
|
|
5
|
+
import type { ChunkRef, StorageChunkSource, GenKey, IStorageDriver, IRegistryDriver, SegmentRef, SegmentSize, Token } from './ports.js';
|
|
6
|
+
import { CrbmReader } from './crbm/reader.js';
|
|
7
|
+
import type { CrbmReaderOptions } from './crbm/reader.js';
|
|
8
|
+
import type { CodecBitmap, CodecInterface } from './codec.js';
|
|
9
|
+
export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Optional {@link IRegistryDriver}. When provided, the current generation is resolved via the registry's
|
|
12
|
+
* authoritative `currentGen` (one cheap strong read) instead of a `list` scan of every generation — the
|
|
13
|
+
* retirement of that scan. When absent, the source falls back to the `list`-scan (so the
|
|
14
|
+
* in-memory / simple setups keep working with no registry). The resolved generation is cached and
|
|
15
|
+
* **re-resolved on a short TTL** ({@link currentGenTtlMs}, needs a {@link clock}) so a long-lived source
|
|
16
|
+
* observes another process's load within the TTL, not only when something else happens to re-resolve it.
|
|
17
|
+
*/
|
|
18
|
+
readonly registry?: IRegistryDriver;
|
|
19
|
+
/**
|
|
20
|
+
* Optional {@link IKeystore} for reading **encrypted** segments. When a segment's registry record
|
|
21
|
+
* carries wrapped DEK(s), the source unwraps the DEK via this keystore and decrypts each chunk/index. Reading
|
|
22
|
+
* an encrypted segment without a keystore throws {@link KeyUnavailableError}; cleartext segments ignore it.
|
|
23
|
+
* Requires a `registry` (that's where the wrapped DEKs live).
|
|
24
|
+
*/
|
|
25
|
+
readonly keystore?: IKeystore;
|
|
26
|
+
/**
|
|
27
|
+
* Enforce that every segment read is encrypted. When true, resolving a **cleartext** segment (no
|
|
28
|
+
* wrapped DEKs) throws {@link KeyUnavailableError} — a guard against silently reading a segment that should
|
|
29
|
+
* have been encrypted. Off by default (encryption is opt-in).
|
|
30
|
+
*/
|
|
31
|
+
readonly requireEncryption?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Time source for the current-generation TTL refresh — the determinism seam; `core/` never reads
|
|
34
|
+
* ambient time. Refresh needs **both** a clock and a `registry`. Without either there is no timed refresh, and
|
|
35
|
+
* that is the only difference: a segment is still re-resolved when its reader is evicted, when a fetch finds its
|
|
36
|
+
* generation swept, and on {@link CrbmStorageChunkSource.invalidate}. What goes is the bound on how long another
|
|
37
|
+
* process's publish takes to arrive. The `CloudRoaring` facade passes its clock automatically, so wiring a
|
|
38
|
+
* `registry` is enough to get the bounded refresh.
|
|
39
|
+
*/
|
|
40
|
+
readonly clock?: Pick<Clock, 'now'>;
|
|
41
|
+
/**
|
|
42
|
+
* How long (ms) a resolved `currentGen` is trusted before the next read re-resolves it (default 2000) — the
|
|
43
|
+
* bound on read staleness after a load publishes: a reader may serve the prior generation for up to this long,
|
|
44
|
+
* then converges. Lazy (checked on read — no timer); ≤ one cheap registry read per segment per window, and a
|
|
45
|
+
* new {@link CrbmReader} is opened only when the generation actually changed. `0` turns the refresh off, as a
|
|
46
|
+
* missing clock or registry does: {@link clock} says what that changes, and what it does not.
|
|
47
|
+
*/
|
|
48
|
+
readonly currentGenTtlMs?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Hard ceiling on how many segments' readers (each holding a fully-parsed `.crbm` index) are cached at once
|
|
51
|
+
* (default 1024) — a memory bound for a long-running server. Past it, the least-recently-used
|
|
52
|
+
* segment's reader is evicted; the next read of an evicted segment re-opens it (one cheap tail GET, since
|
|
53
|
+
* generations are immutable). Raise it for a big cache working set of small segments.
|
|
54
|
+
*/
|
|
55
|
+
readonly maxOpenSegments?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Aggregate byte ceiling on the parsed `.crbm` indices resident in the reader cache (default 64 MiB) — the
|
|
58
|
+
* **second half of that memory bound**. `maxOpenSegments` alone bounds by *count*, but a wide/dense segment's
|
|
59
|
+
* parsed index can be several MB, so 1024 wide indices could pin ~GBs and blow a small heap (e.g. a 128 MB
|
|
60
|
+
* Lambda) while the count is nominally "in bounds". This caps the summed {@link CrbmReader.retainedIndexBytes}
|
|
61
|
+
* across cached readers; the least-recently-used reader is evicted once the total would exceed it — whichever
|
|
62
|
+
* of the count/byte bounds binds first. Lower it for memory-tight deployments with wide segments; a single
|
|
63
|
+
* segment whose index alone exceeds the budget is still cached (it can't be shrunk) but nothing else alongside.
|
|
64
|
+
*/
|
|
65
|
+
readonly maxOpenIndexBytes?: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* What a pin holds of the object it pinned: the version its cache entries are keyed by, and the object's
|
|
69
|
+
* fingerprint, which a pinned read checks each time it opens the generation. `pinGeneration` always records one.
|
|
70
|
+
* A pin built by hand without one is not checked: it reads whatever object is under its generation's key.
|
|
71
|
+
*/
|
|
72
|
+
export interface PinnedObject {
|
|
73
|
+
readonly version: string;
|
|
74
|
+
readonly fingerprint?: string;
|
|
75
|
+
}
|
|
76
|
+
export declare class CrbmStorageChunkSource implements StorageChunkSource {
|
|
77
|
+
private readonly driver;
|
|
78
|
+
/**
|
|
79
|
+
* One resolved reader per segment, re-resolved on a short TTL ({@link CrbmStorageChunkSourceOptions.currentGenTtlMs},
|
|
80
|
+
* needs a clock). Within the TTL a segment's Storage bytes are treated as an immutable snapshot; when the TTL
|
|
81
|
+
* elapses the next read cheaply re-resolves `currentGen` and, only if it advanced (a load published),
|
|
82
|
+
* opens the new generation — so a long-lived source observes new generations within the TTL. The engine pairs
|
|
83
|
+
* this with a **generation-keyed** cache so a bump never serves a stale decoded chunk. Without a clock or a
|
|
84
|
+
* registry there is no timed refresh, and a segment's reader is replaced only when it is evicted, when a read
|
|
85
|
+
* has to fetch from a generation a sweep deleted, or when {@link invalidate} drops it. A segment with no generation yet is not memoized, so
|
|
86
|
+
* it's re-checked until one exists. **Bounded** by a {@link BoundedLru} ({@link CrbmStorageChunkSourceOptions.maxOpenSegments}, default
|
|
87
|
+
* 1024): past the ceiling the least-recently-used segment's reader (and its parsed index) is evicted — the
|
|
88
|
+
* steady-state memory bound; re-opening an evicted segment is one cheap tail GET.
|
|
89
|
+
*/
|
|
90
|
+
private readonly snapshots;
|
|
91
|
+
/**
|
|
92
|
+
* Pins ({@link heldKey}) whose object has been found replaced: another object is under its generation's key. Kept,
|
|
93
|
+
* and bounded as the readers are, so that a pin's later chunk reads and any reopen fail at once rather than pay for
|
|
94
|
+
* the check again. A replacement can be undone, by a restore that puts the pinned object back, so
|
|
95
|
+
* {@link invalidate} forgets the segment's, and a pin of the same version that opens the object forgets the one
|
|
96
|
+
* against it. An object found gone is not kept: that is the open's own `NotFoundError`, and a 404 can pass. A
|
|
97
|
+
* reader still memoised keeps answering from the index it holds, so the pin's `count()` does, until a later pin of
|
|
98
|
+
* that version opens the object now under the key, which takes its place.
|
|
99
|
+
*/
|
|
100
|
+
private readonly replacedPins;
|
|
101
|
+
/**
|
|
102
|
+
* A check of a pinned object still under way, by {@link heldKey}, so the reads that ask at once share one footer
|
|
103
|
+
* read. One forgotten before it ends, by {@link invalidate} or a pin, keeps nothing it finds.
|
|
104
|
+
*/
|
|
105
|
+
private readonly checking;
|
|
106
|
+
/**
|
|
107
|
+
* How many times {@link invalidate} has run, so a read that waited can tell whether the store was told something
|
|
108
|
+
* meanwhile. An eviction from the reader cache is not an invalidation, and what a read found stands through one. It
|
|
109
|
+
* counts every segment's invalidations: a read that waited through another segment's only costs a later read the
|
|
110
|
+
* check again.
|
|
111
|
+
*/
|
|
112
|
+
private invalidations;
|
|
113
|
+
private readonly registry;
|
|
114
|
+
private readonly keystore;
|
|
115
|
+
private readonly requireEncryption;
|
|
116
|
+
private readonly readerOptions;
|
|
117
|
+
private readonly clock;
|
|
118
|
+
private readonly currentGenTtlMs;
|
|
119
|
+
constructor(driver: IStorageDriver, options?: CrbmStorageChunkSourceOptions);
|
|
120
|
+
/** The current resolved reader for a segment, refreshing on the TTL. Cheap within the TTL window. */
|
|
121
|
+
private resolvedReader;
|
|
122
|
+
/**
|
|
123
|
+
* A refresh failed transiently and the prior reader keeps serving: make this snapshot lapse after
|
|
124
|
+
* {@link REFRESH_RETRY_MS} (never longer than the TTL) rather than after a whole TTL, so the next read after
|
|
125
|
+
* that asks the registry again. It costs no read of its own; it only changes when the next refresh happens.
|
|
126
|
+
*/
|
|
127
|
+
private retrySoon;
|
|
128
|
+
/** TTL elapsed: cheaply re-resolve `currentGen`; reopen only if it actually changed, else reuse the prior reader. */
|
|
129
|
+
private computeRefreshedReader;
|
|
130
|
+
/**
|
|
131
|
+
* Memoize a reader-promise as the segment's snapshot at `now()`, forgetting it later if it resolves to
|
|
132
|
+
* null / throws (identity-guarded, so a stale cleanup never clobbers a fresher snapshot). Installing the
|
|
133
|
+
* (pending) promise **synchronously** is what lets concurrent callers coalesce onto one in-flight resolve.
|
|
134
|
+
*/
|
|
135
|
+
private install;
|
|
136
|
+
private now;
|
|
137
|
+
/**
|
|
138
|
+
* How often this source re-reads a segment's pointer while the segment is being read, in ms: its TTL, or 0 when
|
|
139
|
+
* it has no timed refresh (no clock, no registry, or a TTL of 0). The grounded cost report prices the refresh at
|
|
140
|
+
* this.
|
|
141
|
+
*/
|
|
142
|
+
get pointerRefreshMs(): number;
|
|
143
|
+
private expired;
|
|
144
|
+
/**
|
|
145
|
+
* The segment's current generation number — the engine keys its chunk cache by this so a generation bump
|
|
146
|
+
* is observed instead of serving a stale decoded chunk. Served from the (TTL-refreshed)
|
|
147
|
+
* snapshot, so no extra backend read within the TTL window. `null` if the segment has no committed generation.
|
|
148
|
+
*
|
|
149
|
+
* Heals a swept generation exactly like {@link withFreshSnapshot}: a caller that resolves the generation once
|
|
150
|
+
* per op, before any chunk fetch, would otherwise fail the whole operation rather than one chunk. The engine
|
|
151
|
+
* keys by {@link currentVersion} instead, which heals the same way, and falls back to this for a source that
|
|
152
|
+
* cannot report a version. It is spelled out rather than delegated because a lookup like this runs once per
|
|
153
|
+
* operand of every `has`/`count`/`iterate`/`intersect`, almost always served from the cached snapshot with
|
|
154
|
+
* no backend call at all. Routing it through the generic helper cost ~115 ns/op on that path (a second async
|
|
155
|
+
* frame, a per-call closure, and an `await` on a plain number) for a race that fires only during a
|
|
156
|
+
* concurrent sweep. One retry, then propagate — same contract, same eviction rule.
|
|
157
|
+
*/
|
|
158
|
+
currentGeneration(ref: SegmentRef): Promise<number | null>;
|
|
159
|
+
/**
|
|
160
|
+
* Forget everything derived from `ref`: the resolved snapshot and the open reader behind it (and with the
|
|
161
|
+
* reader, the DEK it unwrapped at open time), and what its pins' objects were found to be. The next read
|
|
162
|
+
* resolves the segment again.
|
|
163
|
+
*
|
|
164
|
+
* Needed because the TTL refresh only ever answers "has the pointer moved?", and every *destructive* verb —
|
|
165
|
+
* an erasure that deletes the generation holding the bit, a `dropSegment`, a crypto-shred, a retirement —
|
|
166
|
+
* leaves the pointer's answer unchanged from this source's point of view while making the snapshot wrong.
|
|
167
|
+
* Until this is called the source keeps serving that snapshot with no backend read at all, so nothing on the
|
|
168
|
+
* storage side can close the window; with no registry, no clock or `cache.genTtlMs: 0`, nothing bounds it at all.
|
|
169
|
+
*/
|
|
170
|
+
invalidate(ref: SegmentRef): void;
|
|
171
|
+
/**
|
|
172
|
+
* `<generation>` for a registry-less source, or `<generation>:<row token>` with one. The token moves on every row
|
|
173
|
+
* write, so an unrelated write (a `setRetention`) costs the segment's decoded chunks once — bounded, and on an
|
|
174
|
+
* admin path. Exactness in the direction that matters: with a registry, two incarnations can never share a version
|
|
175
|
+
* string. Without one they can: a name purged and loaded again out of band restarts at the same bare number, and
|
|
176
|
+
* a live read that reopens it after an eviction reads the new object's chunks beside the old one's cached ones. A
|
|
177
|
+
* store with no registry cannot write, so that takes a change made from outside it; a pin is kept apart from it by
|
|
178
|
+
* the fingerprint it records.
|
|
179
|
+
*
|
|
180
|
+
* This is the call the engine makes **once per operand of every read**, before any chunk fetch, to key its
|
|
181
|
+
* chunk cache, so it heals a swept generation exactly as {@link currentGeneration} does: unhealed, a cold read
|
|
182
|
+
* racing a publish and a `keep: 0` sweep failed the whole operation with `NotFoundError`, where the same race
|
|
183
|
+
* on a chunk fetch heals. Spelled out rather than delegated for the reason `currentGeneration` gives: it is
|
|
184
|
+
* almost always served from the cached snapshot with no backend call at all. One retry, then propagate.
|
|
185
|
+
*/
|
|
186
|
+
currentVersion(ref: SegmentRef): Promise<string | null>;
|
|
187
|
+
/**
|
|
188
|
+
* Resolve the segment **once** and report what a pin should hold: the generation, and the version that
|
|
189
|
+
* identifies those exact bytes. `null` when the segment resolves to no generation — there is nothing to pin,
|
|
190
|
+
* and a caller must treat that as "this handle reads empty", not "pinning is unsupported here".
|
|
191
|
+
*
|
|
192
|
+
* Heals a generation swept between the registry read and the open, as {@link currentVersion} does: a publish
|
|
193
|
+
* and a `keep: 0` sweep can land in that gap, and nothing has been handed out yet, so the generation current
|
|
194
|
+
* once the swept one is gone is exactly what a pin taken now should hold. One retry, then propagate.
|
|
195
|
+
*/
|
|
196
|
+
pinGeneration(ref: SegmentRef): Promise<({
|
|
197
|
+
generation: number;
|
|
198
|
+
} & Required<PinnedObject>) | null>;
|
|
199
|
+
private pinOnce;
|
|
200
|
+
/**
|
|
201
|
+
* Where a pinned reader of one object is memoised. With a registry that is its version, which names the object:
|
|
202
|
+
* a name purged and loaded again gets a new row, and with it a new token. Without one the version is the bare
|
|
203
|
+
* generation number, which two incarnations share, so the object's fingerprint is added to tell them apart.
|
|
204
|
+
*/
|
|
205
|
+
private pinnedKey;
|
|
206
|
+
/**
|
|
207
|
+
* One pin's hold on one segment: its version, and the fingerprint of the object it pinned. A check of that object
|
|
208
|
+
* is shared under it, and what the check finds is kept under it, with a registry or without one: pins of one
|
|
209
|
+
* version can hold different objects, since a pin built by hand can name any, and an object replaced from outside
|
|
210
|
+
* the store leaves the row, and so the version, as it was.
|
|
211
|
+
*/
|
|
212
|
+
private heldKey;
|
|
213
|
+
/**
|
|
214
|
+
* A reader for one **specific** generation, memoized in the same bounded LRU as the live snapshots under a
|
|
215
|
+
* generation-qualified key.
|
|
216
|
+
*
|
|
217
|
+
* Sharing the LRU is the point, not an implementation detail. A pinned reader held in a private field is
|
|
218
|
+
* outside the memory ceiling the library advertises — measured at 10.34 MiB per live pin, 32 pins holding
|
|
219
|
+
* 111.7 MiB against an 8 MiB configured bound. A pinned *generation number* has no such problem: an object is
|
|
220
|
+
* immutable, so eviction is harmless, and re-opening at the same number reopens the same bytes unless the name
|
|
221
|
+
* was purged and loaded again, which the pin's fingerprint then says. It also removes a memoized-rejection bug by
|
|
222
|
+
* construction — `install` forgets a promise that rejects,
|
|
223
|
+
* where a hand-rolled `this.reader ??= open()` cached the rejection for the life of the handle and made a pin
|
|
224
|
+
* the one read path with no resilience.
|
|
225
|
+
*
|
|
226
|
+
* **`status` is re-checked here**, on every open rather than once at pin time. A pin taken before a
|
|
227
|
+
* crypto-shred must not keep unwrapping a DEK the shred destroyed; a destroyed segment resolves no generation
|
|
228
|
+
* for anyone, and a pin's read of one fails. That holds from the pin's next open: a reader already open keeps
|
|
229
|
+
* the key it unwrapped until it is evicted or invalidated.
|
|
230
|
+
*
|
|
231
|
+
* **And the object is checked**, when the caller says which one it pinned. A generation number is not an
|
|
232
|
+
* identity (invariant 1): once a name is purged and loaded again, its new segment starts again at generation 0,
|
|
233
|
+
* so a pin of the old one would open the new one's object as its own, and read it beside the chunks it had
|
|
234
|
+
* already cached from the old one. The row's token cannot tell them apart, since it moves on every write, a
|
|
235
|
+
* publish included; the object's fingerprint can, and a pin whose object has been replaced fails, as one whose
|
|
236
|
+
* generation has been swept does. The memo is keyed so that two pins of one generation number in two
|
|
237
|
+
* incarnations never share a reader, with a registry or without one ({@link pinnedKey}).
|
|
238
|
+
*/
|
|
239
|
+
private readerAt;
|
|
240
|
+
private openAt;
|
|
241
|
+
/**
|
|
242
|
+
* Read one chunk of a specific generation — the pinned read path. `held`, when given, is what a pin holds of the
|
|
243
|
+
* object, and the read fails if the generation is now another object.
|
|
244
|
+
*/
|
|
245
|
+
getChunkAt(ref: ChunkRef, generation: number, held?: PinnedObject): Promise<Uint8Array | null>;
|
|
246
|
+
/**
|
|
247
|
+
* Throws the pin's NotFoundError if the object under `generation`'s key is no longer the one the pin holds. Its
|
|
248
|
+
* footer says, read with no key and no row ({@link CrbmReader.sameObject}), so an object written under a key this
|
|
249
|
+
* store lacks answers the same way, and so does one gone from its key. A replacement is kept
|
|
250
|
+
* ({@link replacedPins}), and a check under way is shared by the reads that ask at once; neither "gone" nor "the
|
|
251
|
+
* same" is kept, since either can change. A transient fault is rethrown, for the store's retries to see for what it
|
|
252
|
+
* is. A footer that cannot be read otherwise says nothing about which object is there, so this returns, and the
|
|
253
|
+
* caller's own error stands.
|
|
254
|
+
*/
|
|
255
|
+
private throwIfReplaced;
|
|
256
|
+
/** Chunk keys of a specific generation — the pinned shape read. `held` as for {@link getChunkAt}. */
|
|
257
|
+
listChunkKeysAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<number[]>;
|
|
258
|
+
/**
|
|
259
|
+
* Per-chunk cardinalities of a specific generation — powers a pinned `count()` with no payload reads. `held` as
|
|
260
|
+
* for {@link getChunkAt}.
|
|
261
|
+
*/
|
|
262
|
+
cardinalitiesAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<ReadonlyMap<number, number> | null>;
|
|
263
|
+
/** Grounded size of a specific generation. `held` as for {@link getChunkAt}. */
|
|
264
|
+
sizeOfAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<SegmentSize | null>;
|
|
265
|
+
/**
|
|
266
|
+
* Whether the segment is a registered name at all. With a registry that is exactly "a row exists" — including
|
|
267
|
+
* a row with no generation yet (minted by `setRetention` before the first load) and a `destroyed` tombstone,
|
|
268
|
+
* both of which are names somebody deliberately created. Without a registry there is no registration to
|
|
269
|
+
* consult, so the honest answer is whether the bucket holds any generation of it.
|
|
270
|
+
*
|
|
271
|
+
* Deliberately NOT served from the snapshot memo: the memo resolves to `null` for both states this exists to
|
|
272
|
+
* tell apart.
|
|
273
|
+
*/
|
|
274
|
+
exists(ref: SegmentRef): Promise<boolean>;
|
|
275
|
+
/**
|
|
276
|
+
* Evict a snapshot we just failed to read from — matched by its reader *promise*, so a concurrent call's
|
|
277
|
+
* fresher snapshot is never clobbered. The identity guard is the whole point: `install` replaces the entry
|
|
278
|
+
* wholesale, so comparing anything else would drop a good snapshot on the floor.
|
|
279
|
+
*/
|
|
280
|
+
private dropStale;
|
|
281
|
+
private resolveLatest;
|
|
282
|
+
/** Open a {@link CrbmReader} for an already-resolved generation target (decrypting if the segment is encrypted). */
|
|
283
|
+
private openForTarget;
|
|
284
|
+
/**
|
|
285
|
+
* The current generation + its DEK wrappings: the registry's authoritative record (one strong read), or a
|
|
286
|
+
* `list` scan for the max generation when there's no registry (a registry-less source can only read cleartext
|
|
287
|
+
* — there's nowhere a wrapped DEK could live).
|
|
288
|
+
*/
|
|
289
|
+
private resolveTarget;
|
|
290
|
+
/** Build the per-generation decryption context for an encrypted segment (undefined for cleartext). */
|
|
291
|
+
private cryptoForRead;
|
|
292
|
+
getChunk(ref: ChunkRef): Promise<Uint8Array | null>;
|
|
293
|
+
listChunkKeys(ref: SegmentRef): Promise<number[]>;
|
|
294
|
+
sizeOf(ref: SegmentRef): Promise<SegmentSize | null>;
|
|
295
|
+
cardinalities(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
|
|
296
|
+
/**
|
|
297
|
+
* Run `read` against the pinned snapshot, healing the torn-read window generation GC can open: if the
|
|
298
|
+
* generation we resolved was superseded *and* swept (the grace window elapsed), the Storage driver throws
|
|
299
|
+
* {@link NotFoundError}. Surfacing that as a query failure would turn a benign, recoverable race into a
|
|
300
|
+
* caller-visible error, so instead we drop the stale snapshot,
|
|
301
|
+
* re-resolve `currentGen`, and retry once — the read then serves the newer (committed, immutable) generation,
|
|
302
|
+
* a monotonic move forward — within one incarnation of the row. A name that was retired and re-created is a
|
|
303
|
+
* different segment and can resolve to a LOWER generation, which is why the snapshot carries the row's token
|
|
304
|
+
* as its lineage rather than trusting the generation number (invariant 1).
|
|
305
|
+
*
|
|
306
|
+
* **Resolution and open are inside the retry, not before it.** Resolving `currentGen` and opening that
|
|
307
|
+
* generation's object are two backend round trips with a gap between them, so the object can be swept after
|
|
308
|
+
* the registry named it and before its tail is read — and that miss surfaces from awaiting the snapshot, not
|
|
309
|
+
* from `read`. Awaiting outside the retry left the *first* attempt unhealed on the one path GC actually
|
|
310
|
+
* races: `keep: 0`, which every id erasure passes, sweeps microseconds after the publish.
|
|
311
|
+
*
|
|
312
|
+
* A *second* NotFound is pathological (GC outrunning resolution) and propagates rather than fabricating an
|
|
313
|
+
* absent answer — never return a wrong result. `ifGone` is returned when the segment has no committed
|
|
314
|
+
* generation to serve: storage is empty, or the row was dropped or crypto-shredded, in which case reading empty
|
|
315
|
+
* is the documented outcome rather than a failure.
|
|
316
|
+
*
|
|
317
|
+
* **Bounded to exactly two resolve-and-open round trips.** That is a cost contract, not a detail: the retry
|
|
318
|
+
* now re-reads the *registry*, so an unbounded one would hammer the shared, throttle-prone resource in a
|
|
319
|
+
* tight loop on a segment whose pointer names an object that is permanently absent — and an N-way
|
|
320
|
+
* `intersect` would do it N times. Gated in `tests/core/storage-source-heal-open.test.ts` by counting calls.
|
|
321
|
+
*/
|
|
322
|
+
private withFreshSnapshot;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Write one immutable generation from in-memory bitmaps. Chunks are
|
|
326
|
+
* sorted ascending and empty bitmaps skipped (empty chunks are never stored). Returns the driver's
|
|
327
|
+
* `{ size, sha256 }` for the written object. Pass `options.crypto` to AES-256-GCM-encrypt the generation
|
|
328
|
+
* (built from the segment's DEK, with associated data bound to `(segment, generation)`).
|
|
329
|
+
*/
|
|
330
|
+
export declare function writeCrbmGeneration(driver: IStorageDriver, key: GenKey, chunks: Iterable<{
|
|
331
|
+
chunkKey: number;
|
|
332
|
+
bitmap: CodecBitmap;
|
|
333
|
+
}>, options?: {
|
|
334
|
+
crypto?: CrbmCrypto;
|
|
335
|
+
clock?: Yielder;
|
|
336
|
+
}): Promise<{
|
|
337
|
+
size: number;
|
|
338
|
+
sha256: string;
|
|
339
|
+
}>;
|
|
340
|
+
/** What {@link writeCrbmGenerationStream} wrote: the driver's `{ size, sha256 }` + a tally of the generation. */
|
|
341
|
+
export interface StreamWriteResult {
|
|
342
|
+
readonly size: number;
|
|
343
|
+
readonly sha256: string;
|
|
344
|
+
/** Non-empty chunk keys written, in ascending order (for the post-write verify, since the stream is consumed). */
|
|
345
|
+
readonly chunkKeys: number[];
|
|
346
|
+
/** Total ids written. */
|
|
347
|
+
readonly cardinality: number;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Streaming variant of {@link writeCrbmGeneration} for a **constant-memory** rewrite (the erasure rewrite, a
|
|
351
|
+
* sorted load): consumes an **already-ascending** async stream of `{ chunkKey, bitmap }`, feeding each to the
|
|
352
|
+
* codec and freeing it, instead of materializing the whole generation. Paired with a streaming storage sink (S3
|
|
353
|
+
* multipart / LocalFs temp file), peak memory is ~one chunk + one part. Returns a tally so the caller can verify
|
|
354
|
+
* the re-opened object without re-iterating the (now-consumed) stream. Input **must** be ascending by `chunkKey`
|
|
355
|
+
* — the codec rejects an out-of-order chunk; empty bitmaps are skipped.
|
|
356
|
+
*/
|
|
357
|
+
export declare function writeCrbmGenerationStream(driver: IStorageDriver, key: GenKey, chunks: AsyncIterable<{
|
|
358
|
+
chunkKey: number;
|
|
359
|
+
bitmap: CodecBitmap;
|
|
360
|
+
}>, options?: {
|
|
361
|
+
crypto?: CrbmCrypto;
|
|
362
|
+
clock?: Yielder;
|
|
363
|
+
}): Promise<StreamWriteResult>;
|
|
364
|
+
/**
|
|
365
|
+
* Point a segment's registry `currentGen` at `key.generation` — the publish step that makes a freshly-written
|
|
366
|
+
* generation the authoritative latest (so registry-aware readers see it). **Forward-only and idempotent:**
|
|
367
|
+
* if the registry has no row it creates one; if it's already at/ahead of `key.generation` it's a no-op (an
|
|
368
|
+
* out-of-order/duplicate publish never regresses the pointer); otherwise it advances via compare-and-swap,
|
|
369
|
+
* retrying a few times under contention. Separated from the Storage write so callers can publish atomically
|
|
370
|
+
* after the immutable object is durable (write-then-publish).
|
|
371
|
+
*
|
|
372
|
+
* **Throws** rather than returning, in two cases, because each would otherwise create a state no reader can
|
|
373
|
+
* recover from: a `destroyed` (crypto-shredded) row, where advancing the pointer would name an object encrypted
|
|
374
|
+
* under a shredded key; and an advance carrying **new** `wrappedDeks`, which would either strand that key or make
|
|
375
|
+
* the row advertise encryption over cleartext generations. Contention that never clears throws
|
|
376
|
+
* {@link WriteConflictError} after the retries.
|
|
377
|
+
*
|
|
378
|
+
* **`expectFrom` turns forward-only into read-modify-write.** Forward-only is the right rule for a writer whose
|
|
379
|
+
* content does not depend on what was current — a load computes its ids upstream, so publishing over a newer
|
|
380
|
+
* generation loses nothing the loader knew about. It is the WRONG rule for a writer that *derived* its content
|
|
381
|
+
* from a particular generation: the erasure rewrite streams generation `from` and clears one bit, so publishing
|
|
382
|
+
* it over a newer generation silently discards whatever that generation added. Passing `expectFrom` makes the
|
|
383
|
+
* publish land only while the pointer is still exactly there, and return `false` otherwise, so the caller can
|
|
384
|
+
* report `superseded` and re-derive. The compare-and-swap itself carries the token read in the same iteration,
|
|
385
|
+
* so the check and the write see the same row.
|
|
386
|
+
*
|
|
387
|
+
* **`expectToken` fences the LINEAGE, and a derived writer needs both.** A generation number identifies a
|
|
388
|
+
* generation only within one incarnation of a name: `nextGeneration` restarts at `0` once the row is purged and
|
|
389
|
+
* the bucket empty, so a name that is retired and re-created has a *different* segment wearing the *same*
|
|
390
|
+
* `currentGen`. `expectFrom` alone matched it — and an erasure rewrite then published one incarnation's content
|
|
391
|
+
* over another's, deleted the live objects with its `keep: 0` collection, and returned `erased: true`. The row's
|
|
392
|
+
* OCC token is the identity that survives this: {@link IRegistryDriver.delete} tombstones rather than unlinks,
|
|
393
|
+
* so "a later `create` still gets a fresh, greater token" and no token is ever reused across incarnations.
|
|
394
|
+
*
|
|
395
|
+
* Pass the token read alongside `expectFrom` and the publish lands only on the same row it was derived from.
|
|
396
|
+
* The check is deliberately **conservative**: the token also changes on writes that are not supersessions at
|
|
397
|
+
* all (a `setRetention`, a due-index reindex), so such a write makes a derived publish report `superseded` and
|
|
398
|
+
* the caller re-derive. That costs a re-run on a rare, unrelated write; the alternative costs a segment.
|
|
399
|
+
*/
|
|
400
|
+
export declare function publishGeneration(registry: IRegistryDriver, key: GenKey, options?: {
|
|
401
|
+
wrappedDeks?: readonly WrappedDek[];
|
|
402
|
+
expectFrom?: number;
|
|
403
|
+
expectToken?: Token;
|
|
404
|
+
/**
|
|
405
|
+
* Publish only while the segment still has **no registry row**.
|
|
406
|
+
*
|
|
407
|
+
* The fence for a caller whose decision rests on the row being ABSENT. `expectFrom` and `expectToken`
|
|
408
|
+
* cannot express it: both compare against a value read from a row, so when there was no row there is
|
|
409
|
+
* nothing to compare and both are simply omitted — leaving the publish a bare forward-only advance that
|
|
410
|
+
* happily lands over whatever appeared in the meantime.
|
|
411
|
+
*
|
|
412
|
+
* That gap was a silent wipe, not a theoretical one. A guarded load into a segment that did not exist yet
|
|
413
|
+
* read "no row", so its empty/`minRetained` bounds had nothing to judge and passed vacuously; a
|
|
414
|
+
* concurrent writer then created the row and published a thousand ids; and the guarded load published an
|
|
415
|
+
* EMPTY generation over them, reporting `published: true` with no reason. Reproduced through both
|
|
416
|
+
* `loadSegment` and the `*Into` verbs, which hit it far more often because materialising into a
|
|
417
|
+
* destination that does not exist yet is the ordinary first run of a pipeline.
|
|
418
|
+
*/
|
|
419
|
+
expectAbsent?: boolean;
|
|
420
|
+
}): Promise<boolean>;
|
|
421
|
+
/** What a bulk-load wrote — the driver's `{ size, sha256 }` plus a summary of the built generation. */
|
|
422
|
+
export interface BulkLoadResult {
|
|
423
|
+
/** Bytes written to the Storage object. */
|
|
424
|
+
readonly size: number;
|
|
425
|
+
/** The driver's content hash of the object. */
|
|
426
|
+
readonly sha256: string;
|
|
427
|
+
/** Distinct non-empty chunks written (≤ 65536). */
|
|
428
|
+
readonly chunkCount: number;
|
|
429
|
+
/**
|
|
430
|
+
* The DEK freshly minted for this generation, wrapped — present only when the segment is encrypted AND this
|
|
431
|
+
* call minted one (an existing segment reuses its key, which is already on the row).
|
|
432
|
+
*
|
|
433
|
+
* Returned so a caller that deliberately does **not** pass `registry` here — writing now and publishing later,
|
|
434
|
+
* which is how a load applies a guard while the old generation is still authoritative — can carry the key onto
|
|
435
|
+
* its own `publishGeneration`. Without it that publish stores no DEK and the generation it makes current is
|
|
436
|
+
* unreadable: the bytes are encrypted with a key nothing recorded.
|
|
437
|
+
*/
|
|
438
|
+
readonly wrappedDeks?: readonly WrappedDek[];
|
|
439
|
+
/** Total distinct ids in the generation (post-dedup). */
|
|
440
|
+
readonly cardinality: number;
|
|
441
|
+
/**
|
|
442
|
+
* Whether this generation is now the segment's **current** one — `undefined` when no `registry` was wired, so
|
|
443
|
+
* there is no pointer and no publish step to report on.
|
|
444
|
+
*
|
|
445
|
+
* `false` means the object is durable but a **concurrent writer published a higher generation first**, so this
|
|
446
|
+
* one is an orphan that no reader will ever resolve. The write succeeded and the *load* did not: whatever this
|
|
447
|
+
* call was asked to make the segment contain, the segment does not contain. That is not a detail a caller can
|
|
448
|
+
* be left to infer — a `*Into` materialisation that reported the generation it wrote as "the destination's new
|
|
449
|
+
* current generation" on this path would state a plain untruth — so the flag is on the result and the verbs that
|
|
450
|
+
* promise a published generation check it.
|
|
451
|
+
*/
|
|
452
|
+
readonly becameCurrent?: boolean;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* **Bulk-load** a whole immutable generation from a flat id source — the batch "seed/rebuild" entry point.
|
|
456
|
+
* Unlike {@link writeCrbmGeneration} (which takes pre-grouped bitmaps), this
|
|
457
|
+
* consumes an arbitrary, **unsorted** stream of u32 ids (sync or async) and routes each into its chunk's
|
|
458
|
+
* Roaring bitmap as it goes. The input is consumed lazily and ids dedup on insert, so memory is bounded by
|
|
459
|
+
* the **built generation** — the in-memory Roaring representation of the *distinct* set (≤ one bitmap per
|
|
460
|
+
* non-empty chunk) — not by the input length: you can stream a billion duplicate-heavy ids and hold only the
|
|
461
|
+
* distinct result. Note this is `O(distinct set)`, **not** window-bounded like {@link SegmentEngine.intersect}:
|
|
462
|
+
* bulk-load holds the whole generation in RAM, which suits batch seed/rebuild jobs but not an
|
|
463
|
+
* unbounded-cardinality stream (a segment larger than RAM needs external-merge / pre-sorted input — later phase).
|
|
464
|
+
*
|
|
465
|
+
* Each id must be an integer in `[0, 2³²)` ({@link splitId} throws {@link ValidationError} otherwise) — the
|
|
466
|
+
* source is consumed lazily, so a bad id aborts mid-stream without writing a partial object (the driver
|
|
467
|
+
* commits only after the callback resolves). An empty source writes a valid empty generation.
|
|
468
|
+
*
|
|
469
|
+
* Writes a fresh full snapshot of a segment. The caller picks the generation number in `key` — `nextGeneration`
|
|
470
|
+
* computes the right one from the registry and the bucket — and re-using an existing generation throws
|
|
471
|
+
* {@link WriteConflictError} (write-once). Without a registry a `StorageChunkSource` serves the **highest**
|
|
472
|
+
* generation present, so a too-high number silently shadows real data; with one, `publishGeneration` decides.
|
|
473
|
+
*/
|
|
474
|
+
export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenKey, ids: Iterable<number> | AsyncIterable<number>, options?: {
|
|
475
|
+
registry?: IRegistryDriver;
|
|
476
|
+
keystore?: IKeystore;
|
|
477
|
+
requireEncryption?: boolean;
|
|
478
|
+
audit?: IAuditSink;
|
|
479
|
+
/** Bitmap codec. Optional in the type; a **flavor** package binds it ({@link requireCodec}). */
|
|
480
|
+
codec?: CodecInterface;
|
|
481
|
+
/**
|
|
482
|
+
* Injected clock. Supplying one makes bulk-load **cooperative**: it yields the event loop periodically so a
|
|
483
|
+
* long load does not stall everything else on the process. Without it the load still completes, just
|
|
484
|
+
* without yielding — which is the pre-existing behaviour, kept so this is purely additive.
|
|
485
|
+
*
|
|
486
|
+
* `@cloudbitmaps/roaring` supplies a real clock by default, so flavor users get cooperative behaviour with
|
|
487
|
+
* no wiring. `core/` cannot default it: it is timer-free by lint, which is exactly why waiting goes through
|
|
488
|
+
* this seam rather than `setTimeout`.
|
|
489
|
+
*/
|
|
490
|
+
clock?: Clock;
|
|
491
|
+
/**
|
|
492
|
+
* Write the object but do **not** advance the pointer (default: publish when a `registry` is wired).
|
|
493
|
+
*
|
|
494
|
+
* For a caller that has to inspect what it wrote before deciding whether it should become current — a
|
|
495
|
+
* guarded {@link loadSegment} is the one in-tree case — because the check is only meaningful while the old
|
|
496
|
+
* generation is still authoritative. The `registry` is still required for an encrypted segment and still
|
|
497
|
+
* consulted: it is where an existing segment's DEK lives, and reusing that key is not optional. The wrapped
|
|
498
|
+
* DEK comes back on the result so the deferred publish can store it.
|
|
499
|
+
*
|
|
500
|
+
* A caller that defers the publish owns what it wrote: an unpublished object sits ABOVE `currentGen`, where
|
|
501
|
+
* generation collection deliberately never looks, so nothing reclaims it until a later generation above it is
|
|
502
|
+
* current.
|
|
503
|
+
*/
|
|
504
|
+
publish?: boolean;
|
|
505
|
+
}): Promise<BulkLoadResult>;
|
|
506
|
+
/**
|
|
507
|
+
* Open a {@link CrbmReader} on one generation over the storage driver's range/tail reads (decrypting if `crypto`).
|
|
508
|
+
* The reader the write paths use to re-read what they wrote, and the erasure rewrite uses to stream the old
|
|
509
|
+
* generation; the engine's read path goes through {@link CrbmStorageChunkSource} instead, which caches these.
|
|
510
|
+
*/
|
|
511
|
+
export declare function openGenerationReader(storage: IStorageDriver, key: GenKey, crypto: CrbmCrypto | undefined, options?: Omit<CrbmReaderOptions, 'crypto'>): Promise<CrbmReader>;
|
|
512
|
+
/**
|
|
513
|
+
* Re-open a freshly written generation and assert it round-trips exactly what was streamed into it: the same
|
|
514
|
+
* per-chunk key set *and* the same total cardinality (on top of the codec's own per-chunk CRC + footer checks).
|
|
515
|
+
* `expected` is the streaming writer's tally (the stream is consumed, so it can't be re-iterated) — the key-set
|
|
516
|
+
* comparison catches a dropped/extra chunk that a cardinality-only check could miss when two errors cancel out.
|
|
517
|
+
* Throws {@link IntegrityError}: the object is on disk but must not be published.
|
|
518
|
+
*/
|
|
519
|
+
export declare function verifyGeneration(storage: IStorageDriver, key: GenKey, expected: {
|
|
520
|
+
readonly chunkKeys: readonly number[];
|
|
521
|
+
readonly cardinality: number;
|
|
522
|
+
}, crypto: CrbmCrypto | undefined): Promise<void>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption seams — the **pure** interfaces `core/` depends on so the codec can encrypt/decrypt without
|
|
3
|
+
* importing `node:crypto` (lint-banned here, like `Clock`/`Rng`/drivers). Concrete AES-256-GCM lives outside
|
|
4
|
+
* `core/` (`src/drivers/crypto.ts`) and is injected. The envelope scheme: a per-segment DEK wrapped under an
|
|
5
|
+
* operator-held KEK, so discarding the DEK crypto-shreds the segment. Per-chunk AEAD over the `.crbm` payloads + index; a per-segment **DEK** wrapped (envelope
|
|
6
|
+
* encryption) under one or more **KEK**s by an {@link IKeystore}; the wrapped DEK(s) live in the registry, so
|
|
7
|
+
* crypto-shred = delete them.
|
|
8
|
+
*/
|
|
9
|
+
/** One AEAD result: a fresh per-message nonce, the ciphertext, and the authentication tag. */
|
|
10
|
+
export interface AeadSealed {
|
|
11
|
+
readonly nonce: Uint8Array;
|
|
12
|
+
readonly ciphertext: Uint8Array;
|
|
13
|
+
readonly tag: Uint8Array;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Authenticated encryption bound to a single key (a segment's DEK). `aad` (associated data) is authenticated
|
|
17
|
+
* but not encrypted — the codec passes each chunk's `(namespace, segment, generation, chunkKey)` identity
|
|
18
|
+
* (the library derives it; an `Aead` implementation receives it), so a chunk's ciphertext can't be silently
|
|
19
|
+
* relocated to another segment/generation/chunk or
|
|
20
|
+
* the index, even by someone who can rewrite Storage objects. The seam is symmetric and self-framing-agnostic:
|
|
21
|
+
* the caller decides where the nonce/tag go (inline with a chunk, or in the footer for the index).
|
|
22
|
+
*/
|
|
23
|
+
export interface Aead {
|
|
24
|
+
/** Encrypt `plaintext`, authenticating `aad`. Returns a fresh nonce + ciphertext + tag. */
|
|
25
|
+
seal(plaintext: Uint8Array, aad: Uint8Array): AeadSealed;
|
|
26
|
+
/**
|
|
27
|
+
* Decrypt + verify. Throws {@link IntegrityError} on **any** mismatch (wrong key, tampered ciphertext/tag,
|
|
28
|
+
* or `aad` that doesn't match what was sealed) — never returns wrong-but-plausible plaintext.
|
|
29
|
+
*/
|
|
30
|
+
open(sealed: AeadSealed, aad: Uint8Array): Uint8Array;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A DEK wrapped under one KEK. A segment stores a **list** of these (one per KEK it was wrapped under — e.g.
|
|
34
|
+
* an active KEK plus an offline recovery KEK), so losing one KEK isn't fatal and KEK rotation needs no
|
|
35
|
+
* data re-encryption. `keyId` names which KEK; `wrapped` is the opaque wrapped-key blob (base64). Carries no
|
|
36
|
+
* plaintext key material — safe to store in the registry next to the (encrypted) data.
|
|
37
|
+
*/
|
|
38
|
+
export interface WrappedDek {
|
|
39
|
+
readonly keyId: string;
|
|
40
|
+
readonly wrapped: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Envelope key management — the injected seam between the codec and however the operator holds key material
|
|
44
|
+
* (in-process BYOK by default; KMS/Vault as optional adapters later). Async so a remote keystore (KMS) fits
|
|
45
|
+
* the same interface. Never exposes raw key bytes to `core/`: it returns an {@link Aead} bound to the DEK.
|
|
46
|
+
*/
|
|
47
|
+
export interface IKeystore {
|
|
48
|
+
/**
|
|
49
|
+
* Mint a fresh per-segment DEK, wrapped under the active KEK (and any recovery KEK). Returns the wrapping(s)
|
|
50
|
+
* to persist in the registry plus an {@link Aead} to encrypt this segment's chunks with.
|
|
51
|
+
*/
|
|
52
|
+
createDek(): Promise<{
|
|
53
|
+
wrapped: WrappedDek[];
|
|
54
|
+
aead: Aead;
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Reconstruct the {@link Aead} from a segment's stored wrappings by unwrapping with any KEK currently held.
|
|
58
|
+
* Throws {@link KeyUnavailableError} if it holds none of the referenced KEKs (lost key, or a shredded
|
|
59
|
+
* segment whose wrappings were deleted).
|
|
60
|
+
*/
|
|
61
|
+
openDek(wrapped: readonly WrappedDek[]): Promise<Aead>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* What the encrypting codec needs: the {@link Aead} for this segment's DEK plus a way to build the AAD for a
|
|
65
|
+
* given scope (`'index'` or a chunkKey). Built per (segment, generation) by the storage-source bridge so the
|
|
66
|
+
* codec itself stays segment-agnostic.
|
|
67
|
+
*/
|
|
68
|
+
export interface CrbmCrypto {
|
|
69
|
+
readonly aead: Aead;
|
|
70
|
+
aadFor(scope: number | 'index'): Uint8Array;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build the AEAD associated-data binding a chunk (or the index) to its exact location:
|
|
74
|
+
* `v1 ‖ len(namespace) ‖ namespace ‖ len(segment) ‖ segment ‖ generation ‖ scope ‖ chunkKey`. Length-prefixed
|
|
75
|
+
* so distinct `(namespace, segment)` pairs can never collide. Pure (no crypto) — just the authenticated label.
|
|
76
|
+
*/
|
|
77
|
+
export declare function aadFor(ref: {
|
|
78
|
+
readonly namespace?: string;
|
|
79
|
+
readonly segment: string;
|
|
80
|
+
}, generation: number, scope: number | 'index'): Uint8Array;
|