@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,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whole-segment erasure: crypto-shred (`destroySegment`, `eraseNamespace`) and disposal (`dropSegment`).
|
|
3
|
+
*
|
|
4
|
+
* GDPR "right to erasure" on immutable, backed-up storage: you can't delete a `.crbm` object from every
|
|
5
|
+
* backup, but you can **delete its key**. `destroySegment` drops the segment's wrapped DEK(s) from the registry
|
|
6
|
+
* (a CAS to a `destroyed` tombstone) — the encrypted Storage bytes are then permanently unreadable, everywhere,
|
|
7
|
+
* forever. **Immediate + irreversible**, so it's gated behind an explicit confirmation (name the exact
|
|
8
|
+
* segment/namespace). A `destroyed` segment reads as empty (its DEK is gone). Only works on an **encrypted**
|
|
9
|
+
* segment; a cleartext segment has no key to shred.
|
|
10
|
+
*
|
|
11
|
+
* The tombstone is also the fence every writer respects: a load refuses
|
|
12
|
+
* a `destroyed` row, so a load racing an erasure cannot resurrect the segment. A single id's erasure is a different
|
|
13
|
+
* operation — `eraseIdFromSegment` rewrites the generation without it.
|
|
14
|
+
*/
|
|
15
|
+
import { type IAuditSink } from './audit.js';
|
|
16
|
+
import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
|
|
17
|
+
export interface EraseDeps {
|
|
18
|
+
readonly registry: IRegistryDriver;
|
|
19
|
+
}
|
|
20
|
+
export interface DestroyResult {
|
|
21
|
+
readonly segment: string;
|
|
22
|
+
readonly namespace?: string;
|
|
23
|
+
/** True iff the segment is now a `destroyed` tombstone (incl. the idempotent already-destroyed case). */
|
|
24
|
+
readonly destroyed: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* True iff **this call** performed a genuine crypto-shred — an encrypted segment whose wrapped DEK(s) were
|
|
27
|
+
* dropped, so its Storage bytes are now permanently unreadable. False for a cleartext tombstone (bytes remain
|
|
28
|
+
* readable), for the idempotent already-destroyed case, and for an absent segment. This is what the
|
|
29
|
+
* `segment.erase` audit event keys off — a cleartext "erase" is not an irreversible destruction.
|
|
30
|
+
*/
|
|
31
|
+
readonly cryptoShredded: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Why this segment was not a fresh crypto-shred. `'absent'` (no registry row), `'already'` (already
|
|
34
|
+
* destroyed), `'cleartext'` (no DEK to shred) — and from `eraseNamespace` only, where per-segment faults are
|
|
35
|
+
* isolated so one failure cannot discard the ledger: `'contended'` (the row was rewritten during every CAS
|
|
36
|
+
* attempt) or `` `failed: <message>` `` (any other fault).
|
|
37
|
+
*
|
|
38
|
+
* The last two come with `destroyed: false` and mean **the segment still holds data**. A namespace erase
|
|
39
|
+
* returns them rather than throwing, so its entries have to be inspected — see {@link eraseNamespace}.
|
|
40
|
+
*/
|
|
41
|
+
readonly reason?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Crypto-shred one segment. **Irreversible.** `confirmSegment` must equal `ref.segment` (a guard against an
|
|
45
|
+
* accidental destroy — you must name the exact segment). A non-encrypted segment is rejected unless you opt in
|
|
46
|
+
* via `allowCleartext` (there's no key to shred; its Storage bytes stay readable — the tombstone only stops the
|
|
47
|
+
* segment resolving).
|
|
48
|
+
*/
|
|
49
|
+
export declare function destroySegment(ref: SegmentRef, deps: EraseDeps, options: {
|
|
50
|
+
confirmSegment: string;
|
|
51
|
+
allowCleartext?: boolean;
|
|
52
|
+
audit?: IAuditSink;
|
|
53
|
+
}): Promise<DestroyResult>;
|
|
54
|
+
/**
|
|
55
|
+
* Crypto-shred every segment in a namespace. **Irreversible.** `confirmNamespace` must equal `namespace`.
|
|
56
|
+
* Returns a per-segment result (skips cleartext segments unless `allowCleartext`).
|
|
57
|
+
*
|
|
58
|
+
* **Bounded.** It lists the whole namespace before it destroys anything, and holds the listing resident, so the
|
|
59
|
+
* listing is capped at `maxScanSegments` (default {@link DEFAULT_MAX_SCAN_SEGMENTS}, 250,000), the ceiling every
|
|
60
|
+
* other fleet scan keeps. A namespace over it throws `BudgetExceededError` **before any segment is destroyed**,
|
|
61
|
+
* never part-way through; raise `maxScanSegments` when the namespace really is that large and the memory is there.
|
|
62
|
+
* A `maxScanSegments` that is not a finite number >= 1 throws `ValidationError`, again before anything is destroyed.
|
|
63
|
+
*/
|
|
64
|
+
export declare function eraseNamespace(namespace: string, deps: EraseDeps, options: {
|
|
65
|
+
confirmNamespace: string;
|
|
66
|
+
allowCleartext?: boolean;
|
|
67
|
+
audit?: IAuditSink;
|
|
68
|
+
maxScanSegments?: number;
|
|
69
|
+
}): Promise<{
|
|
70
|
+
destroyed: DestroyResult[];
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* Deps for {@link dropSegment}. Adds `storage`, because unlike a crypto-shred this one deletes the objects.
|
|
74
|
+
*/
|
|
75
|
+
export interface DropDeps extends EraseDeps {
|
|
76
|
+
readonly storage: IStorageDriver;
|
|
77
|
+
}
|
|
78
|
+
export interface DropResult {
|
|
79
|
+
readonly segment: string;
|
|
80
|
+
readonly namespace?: string;
|
|
81
|
+
/**
|
|
82
|
+
* **True iff this segment is now empty as a result of this call — or already was.** This is the field a
|
|
83
|
+
* retention job should branch on: `false` means the call found nothing to do and your ref is probably wrong.
|
|
84
|
+
*/
|
|
85
|
+
readonly dropped: boolean;
|
|
86
|
+
/** Storage generations physically deleted, ascending. Empty on a dry run — see {@link DropResult.wouldDelete}. */
|
|
87
|
+
readonly generationsDeleted: readonly number[];
|
|
88
|
+
/**
|
|
89
|
+
* Generations still present in Storage when the sweep gave up, ascending. **Empty is the normal outcome** — a
|
|
90
|
+
* non-empty value means the storage was NOT fully reclaimed and the drop should be re-run.
|
|
91
|
+
*
|
|
92
|
+
* Without it, a drop would return `dropped: true` with a populated `generationsDeleted` and no `reason` even
|
|
93
|
+
* when an object holding the **complete set** had just been left in the bucket by a writer that was already
|
|
94
|
+
* mid-write when the tombstone landed (its publish is refused, but the object survives). For a cleartext
|
|
95
|
+
* segment those bytes are readable — and the collection only takes a destroyed segment's generations
|
|
96
|
+
* when something runs it. That result would look like a clean drop, and an operator would get no signal to
|
|
97
|
+
* re-run.
|
|
98
|
+
*/
|
|
99
|
+
readonly generationsRemaining: readonly number[];
|
|
100
|
+
/**
|
|
101
|
+
* On a dry run, the generations that **would** be deleted. `undefined` on a real run.
|
|
102
|
+
*
|
|
103
|
+
* This exists because the confirmation guard the erasure calls use — naming the segment twice — protects a
|
|
104
|
+
* hand-typed literal and nothing else. In the loop this operation is actually *for*
|
|
105
|
+
* (`for (const day of expired) drop({segment: day}, {confirmSegment: day})`) the same variable appears twice
|
|
106
|
+
* and the guard is pure ceremony. A dry run is the guard that still works when a machine is calling.
|
|
107
|
+
*
|
|
108
|
+
* It is a **snapshot**: a later real run can see a different set.
|
|
109
|
+
*/
|
|
110
|
+
readonly wouldDelete?: readonly number[];
|
|
111
|
+
/**
|
|
112
|
+
* On a dry run, whether the real drop would **also** crypto-shred (i.e. the segment is encrypted).
|
|
113
|
+
* `undefined` on a real run. This is the one fact in the preview that is irreversible *everywhere*, backups
|
|
114
|
+
* included — a dry run that did not surface it was previewing the recoverable half only.
|
|
115
|
+
*/
|
|
116
|
+
readonly wouldCryptoShred?: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* True iff the segment was encrypted and its wrapped DEK(s) were dropped as part of this call — so its bytes
|
|
119
|
+
* are unreadable *everywhere*, backups included, not merely deleted from the bucket. Deleting an object does
|
|
120
|
+
* not reach a noncurrent version, a replica, or a PITR snapshot; discarding the key does.
|
|
121
|
+
*/
|
|
122
|
+
readonly cryptoShredded: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* How the segment got to its current state. Absent on a fresh, ordinary drop (tombstone written, Storage swept).
|
|
125
|
+
*
|
|
126
|
+
* - `'already'` — it was already a tombstone. Idempotent; pairs with `dropped: true`. A re-drop is **not** a
|
|
127
|
+
* no-op — it re-sweeps Storage, so it is how a residual in `generationsRemaining` is collected.
|
|
128
|
+
* - `'absent'` — **nothing existed.** No registry row and no Storage objects. Pairs with `dropped: false`, and it
|
|
129
|
+
* is the one value worth alerting on: the usual cause is a mistyped name or an omitted `namespace`, both of
|
|
130
|
+
* which address a *different* segment than you meant.
|
|
131
|
+
*/
|
|
132
|
+
readonly reason?: string;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* **Dispose of a segment: tombstone it, then delete its Storage objects.** Irreversible.
|
|
136
|
+
*
|
|
137
|
+
* WHY THIS EXISTS, given {@link destroySegment} already erases. Because `destroySegment` answers a *compliance*
|
|
138
|
+
* question and this one answers an *operational* question, and they are not the same:
|
|
139
|
+
*
|
|
140
|
+
* - `destroySegment` **crypto-shreds** — it discards the key so the bytes are unreadable everywhere including
|
|
141
|
+
* immutable backups, which is the only erasure that survives WORM. But it **leaves the objects in the
|
|
142
|
+
* bucket**, still billed, and it *requires* encryption because a cleartext segment has no key to discard.
|
|
143
|
+
* - `dropSegment` **removes the storage**. It works on a cleartext segment, and on an encrypted one it *also*
|
|
144
|
+
* drops the DEKs, so it is a strict superset there.
|
|
145
|
+
*
|
|
146
|
+
* It is the supported way to delete a segment and stop paying for it. The obvious workaround — an object-store
|
|
147
|
+
* lifecycle rule on the key prefix — deletes the bytes while the registry still points at them. That is exactly the `missing-storage-generation` state the DR runbook says not to serve traffic
|
|
148
|
+
* on, and it surfaces *intermittently*, because a read consults the cache before Storage: cached chunks answer
|
|
149
|
+
* correctly and evicted ones throw. The whole value of this function is that the ordering below cannot be got
|
|
150
|
+
* wrong by a caller.
|
|
151
|
+
*
|
|
152
|
+
* **THE ORDER IS THE CONTRACT — registry first, then Storage.**
|
|
153
|
+
*
|
|
154
|
+
* 1. **Registry first.** After the tombstone nothing resolves a generation for this segment, so no reader can
|
|
155
|
+
* reach for bytes that are about to disappear, and no writer can publish onto it (a
|
|
156
|
+
* load refuses a `destroyed` row).
|
|
157
|
+
* 2. **Storage second, best-effort, and re-swept.** Once the pointer is a tombstone the segment resolves as empty,
|
|
158
|
+
* so a failure part-way through leaves **orphaned bytes, not a wrong answer.** Orphans cost money and are
|
|
159
|
+
* cleaned up by re-running; a torn pointer costs correctness and is not self-healing. Given the choice, leak
|
|
160
|
+
* bytes — but say so: whatever survives the sweep is reported in {@link DropResult.generationsRemaining}.
|
|
161
|
+
*
|
|
162
|
+
* **Why step 2 sweeps more than once.** A load that was already writing its object when the tombstone landed
|
|
163
|
+
* still finishes the write — its publish is then refused, but the object survives, and it holds the complete
|
|
164
|
+
* set. A single list-then-delete misses it entirely. The re-sweep converges because the tombstone *is* a hard
|
|
165
|
+
* fence on **publishing**, so only already-in-flight writes can appear and they are finite.
|
|
166
|
+
*
|
|
167
|
+
* **When "reads as empty" starts being true.** Not instantly, for a store that has already read this segment: a
|
|
168
|
+
* resolved generation is cached and decoded chunks sit in the cache, so an in-flight reader can answer from
|
|
169
|
+
* cache for a window. A fresh store, or any reader that had not touched the segment, sees empty at once.
|
|
170
|
+
*
|
|
171
|
+
* That window is bounded by `cache.genTtlMs` (default 2 s) **only for a reader whose storage source has both a clock
|
|
172
|
+
* and a registry and a positive TTL** — expiry needs all three. A source built without a clock or a registry, or
|
|
173
|
+
* with `cache.genTtlMs: 0`, has no timed refresh: it notices the drop only when a read has to fetch from a deleted
|
|
174
|
+
* generation or its reader cache evicts the segment. Because a cache-LRU hit never reaches Storage, such a reader
|
|
175
|
+
* can answer `true` for a dropped segment **indefinitely**, and must be invalidated or restarted. This is the *same* caching that makes the delete-bytes-first ordering fail intermittently rather
|
|
176
|
+
* than loudly — it cuts both ways.
|
|
177
|
+
*
|
|
178
|
+
* `confirmSegment` must equal `ref.segment`, matching `destroySegment`/`eraseNamespace`. For an automated
|
|
179
|
+
* caller that guard is ceremony — use `dryRun` first, which reports what would go without touching anything.
|
|
180
|
+
*/
|
|
181
|
+
export declare function dropSegment(ref: SegmentRef, deps: DropDeps, options: {
|
|
182
|
+
confirmSegment: string;
|
|
183
|
+
dryRun?: boolean;
|
|
184
|
+
audit?: IAuditSink;
|
|
185
|
+
}): Promise<DropResult>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed errors — callers learn *why* something failed, never by parsing strings. Retry is not the engine's job: a
|
|
3
|
+
* driver classifies its backend's failures into this vocabulary, and the store's read wrapper retries the
|
|
4
|
+
* transient ones through `withRetry`.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Registry-symbol brands, so an error stays classifiable when the class object is not shared.
|
|
8
|
+
*
|
|
9
|
+
* In an ordinary install there is ONE copy of these classes and `instanceof` holds everywhere — across the
|
|
10
|
+
* flavor, the driver packages and core itself — because every package leaves `@cloudbitmaps/*` external and
|
|
11
|
+
* resolves core at runtime rather than inlining it.
|
|
12
|
+
*
|
|
13
|
+
* What still breaks it is a SECOND copy: a version skew between our packages that the installer could not
|
|
14
|
+
* dedupe, or a bundler that emits core twice. Then a driver throws a class the catching side's `instanceof`
|
|
15
|
+
* does not match — and silently, because a `catch` that stops matching just falls through, defeating
|
|
16
|
+
* transient retry and the publish path's conflict handling with no error of its own.
|
|
17
|
+
*
|
|
18
|
+
* `Symbol.for` is identity-stable across copies, bundles and realms, so classify errors with the exported
|
|
19
|
+
* predicates below (never `instanceof`) anywhere an error may cross that boundary.
|
|
20
|
+
*/
|
|
21
|
+
declare const ERROR_BRAND: unique symbol;
|
|
22
|
+
declare const TRANSIENT_BRAND: unique symbol;
|
|
23
|
+
/** Base class for every error CloudRoaring throws. */
|
|
24
|
+
export declare class CloudRoaringError extends Error {
|
|
25
|
+
/** Cross-bundle brand — see the predicates ({@link isCloudRoaringError}, …). Non-enumerable-ish (symbol key ⇒ not in JSON). */
|
|
26
|
+
readonly [ERROR_BRAND]: true;
|
|
27
|
+
constructor(message: string);
|
|
28
|
+
}
|
|
29
|
+
/** Invalid caller input (bad id, segment name, options). Raised before any storage call. */
|
|
30
|
+
export declare class ValidationError extends CloudRoaringError {
|
|
31
|
+
}
|
|
32
|
+
/** An OCC conditional write/delete lost the race — the row changed since it was read. */
|
|
33
|
+
export declare class WriteConflictError extends CloudRoaringError {
|
|
34
|
+
}
|
|
35
|
+
/** Bytes from a tier are corrupt, oversized, or fail a checksum/format check. */
|
|
36
|
+
export declare class IntegrityError extends CloudRoaringError {
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A requested object/row does not exist. Part of the driver error vocabulary: every storage driver this
|
|
40
|
+
* library ships, the in-memory one included, throws it for a read of a generation object that is not there.
|
|
41
|
+
* A registry `get` of a row that is not there returns `null` instead.
|
|
42
|
+
*/
|
|
43
|
+
export declare class NotFoundError extends CloudRoaringError {
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* This build/configuration cannot perform the requested operation, though nothing is malformed. Two uses:
|
|
47
|
+
* (1) **format** — the bytes are well-formed but unreadable here (an unknown `.crbm` major version) —
|
|
48
|
+
* distinct from `IntegrityError` (corruption); and (2)
|
|
49
|
+
* **store configuration** — an operation this store's wiring doesn't support (e.g. a lifecycle helper like
|
|
50
|
+
* `eraseSubject`/`retireExpired` called on a store built without a storage backend). Raised at
|
|
51
|
+
* operation time, before any mutation.
|
|
52
|
+
*/
|
|
53
|
+
export declare class UnsupportedError extends CloudRoaringError {
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A driver cannot meet a capability the chosen topology requires (e.g. a Storage driver without range
|
|
57
|
+
* reads). Raised fail-fast at wiring time, never mid-operation.
|
|
58
|
+
*/
|
|
59
|
+
export declare class CapabilityError extends CloudRoaringError {
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* An operation would exceed its per-op **denial-of-wallet budget** — too many backend requests for a single
|
|
63
|
+
* `count`/`iterate`/`intersect`/`subjectReport`/`eraseSubject` call — so it is refused **before** fanning out
|
|
64
|
+
* (hard invariant 6). Default-on but generous (normal ops never hit it); tune it
|
|
65
|
+
* per store (`budget`) or per op, or disable with `budget: false`. Deterministic (never retried): the op is too
|
|
66
|
+
* big by policy, not by luck. Each request's bytes are separately capped (the safe-deserialize ceiling), so
|
|
67
|
+
* bounding the request count transitively bounds bytes. Carries the projected count + the limit, never data.
|
|
68
|
+
*/
|
|
69
|
+
export declare class BudgetExceededError extends CloudRoaringError {
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* An encrypted segment's data key (DEK) cannot be unwrapped because the keystore holds none of the
|
|
73
|
+
* key-encryption-keys (KEKs) its wrappings reference — the KEK was never configured, rotated away without
|
|
74
|
+
* keeping the old key, or lost. Deterministic (never retried): without a KEK the ciphertext is unreadable by
|
|
75
|
+
* design. The flip side of crypto-shred — when this is *intended* (a destroyed segment) the registry row is
|
|
76
|
+
* already a `destroyed` tombstone; when it's *not*, restore the missing KEK (or its recovery KEK). Carries no
|
|
77
|
+
* key material.
|
|
78
|
+
*/
|
|
79
|
+
export declare class KeyUnavailableError extends CloudRoaringError {
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* A **transient** infrastructure fault — throttling, a 5xx, a dropped connection, a client-side request
|
|
83
|
+
* timeout. Retrying a read that failed this way is safe; a write that failed this way may still have landed, so
|
|
84
|
+
* its caller re-runs the call rather than replaying the request. Drivers classify their backend's retryable faults and raise this (the
|
|
85
|
+
* SDK-specific knowledge stays in the SDK-specific driver); the retry layer (`core/retry`) retries **only**
|
|
86
|
+
* this class, never a deterministic error like {@link ValidationError}, {@link IntegrityError},
|
|
87
|
+
* {@link NotFoundError}, or {@link WriteConflictError} (retrying those is pointless or wrong). The original
|
|
88
|
+
* error is preserved in `cause` so callers can still inspect it.
|
|
89
|
+
*
|
|
90
|
+
* Note for logging hygiene: `cause` is the **raw SDK error**, which may carry operational
|
|
91
|
+
* metadata (endpoint host, request IDs, `$metadata`). The library's own `message` is identifier-only and safe
|
|
92
|
+
* to log; if you serialize the whole error *chain*, be aware you're including that metadata.
|
|
93
|
+
*/
|
|
94
|
+
export declare class TransientError extends CloudRoaringError {
|
|
95
|
+
/** A second brand so a transient fault is classifiable cross-bundle. */
|
|
96
|
+
readonly [TRANSIENT_BRAND]: true;
|
|
97
|
+
constructor(message: string, options?: {
|
|
98
|
+
cause?: unknown;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** Any error thrown by CloudRoaring (any tier, any bundle). */
|
|
102
|
+
export declare function isCloudRoaringError(err: unknown): err is CloudRoaringError;
|
|
103
|
+
/** An OCC conditional write/delete lost the race — retry the read-modify-write, don't fail. */
|
|
104
|
+
export declare function isWriteConflictError(err: unknown): err is WriteConflictError;
|
|
105
|
+
/** A retryable transient infrastructure fault. The retry layer keys on this. */
|
|
106
|
+
export declare function isTransientError(err: unknown): err is TransientError;
|
|
107
|
+
/** A requested object/row does not exist. */
|
|
108
|
+
export declare function isNotFoundError(err: unknown): err is NotFoundError;
|
|
109
|
+
/** Corrupt/oversized/failed-checksum bytes from a tier. */
|
|
110
|
+
export declare function isIntegrityError(err: unknown): err is IntegrityError;
|
|
111
|
+
/** Invalid caller input. */
|
|
112
|
+
export declare function isValidationError(err: unknown): err is ValidationError;
|
|
113
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
|
|
2
|
+
/** The two ports generation bookkeeping needs: the objects, and the pointer that says which one is current. */
|
|
3
|
+
export interface GenerationDeps {
|
|
4
|
+
readonly storage: IStorageDriver;
|
|
5
|
+
readonly registry: IRegistryDriver;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The generation number a writer should use for the segment's **next** object: one above the highest generation
|
|
9
|
+
* the registry points at *or* that is present in Storage — whichever is higher.
|
|
10
|
+
*
|
|
11
|
+
* Both are consulted because they can disagree. A load that wrote its object and crashed before publishing leaves
|
|
12
|
+
* an object *above* `currentGen`; a writer that consulted only the pointer would pick that same number and hit the
|
|
13
|
+
* write-once conflict on every retry. Skipping past it costs one `list` and keeps the retry trivial — the crashed
|
|
14
|
+
* object is an orphan {@link gcOrphanGenerations} collects once the pointer moves on. Reusing such an object
|
|
15
|
+
* instead of skipping it (verify-and-publish) is `load()`'s job, not a numbering helper's.
|
|
16
|
+
*
|
|
17
|
+
* A segment with no row and no objects starts at 0. An admin/write-path helper, never on the read path.
|
|
18
|
+
*/
|
|
19
|
+
export declare function nextGeneration(ref: SegmentRef, deps: GenerationDeps): Promise<number>;
|
|
20
|
+
/**
|
|
21
|
+
* Garbage-collect superseded Storage generations for a segment: everything strictly below `currentGen`, keeping
|
|
22
|
+
* the most recent `keep` of them as a grace window, so a read still fetching from a just-superseded
|
|
23
|
+
* generation need not re-resolve mid-call (**invariant 4**). It is a window, not a lock: a read whose
|
|
24
|
+
* generation is swept anyway re-resolves and retries once rather than failing (see `withFreshSnapshot`), so
|
|
25
|
+
* `keep` trades storage for round trips. Generations ≥ `currentGen` are never touched. Returns the generations
|
|
26
|
+
* deleted.
|
|
27
|
+
*
|
|
28
|
+
* **Except on a `destroyed` segment, where EVERY generation is garbage** and the grace window is meaningless.
|
|
29
|
+
* A tombstoned segment resolves no generation for any reader, pinned or not — a pin cannot be taken on one, and
|
|
30
|
+
* a pin taken while it was still active re-checks the status on every open — and nothing else in the library
|
|
31
|
+
* would ever collect them, so without this those objects are billed forever.
|
|
32
|
+
*
|
|
33
|
+
* **Throws {@link WriteConflictError} when it cannot act on the row it read**: the row is gone when it is re-read,
|
|
34
|
+
* after the listing or before a delete; on an active segment, the pointer has fallen below the cutoff before a
|
|
35
|
+
* delete; on a `destroyed` segment, the row's token has changed. Not to be confused with the empty array it still
|
|
36
|
+
* returns when there was genuinely nothing to collect (no row at all, or no pointer yet), which is why an empty
|
|
37
|
+
* array is not a receipt. The row is read before the listing and acted on after it, so a name that was purged and
|
|
38
|
+
* re-created in that window is a *different* segment wearing the same name, and its live object must not be
|
|
39
|
+
* collected on the strength of the old row: on an active segment the lower of the two pointers is what keeps it
|
|
40
|
+
* safe, and on a `destroyed` one the token. Refusing is safe — the objects are not going anywhere and the next
|
|
41
|
+
* pass reads a consistent row — but it has to be **distinguishable** from "there was nothing to collect", which
|
|
42
|
+
* an empty array is not: `eraseIdFromSegment` reads the returned list as the physical half of its erasure
|
|
43
|
+
* receipt, and would otherwise report `erased: true` over bytes still in the bucket. Re-run it.
|
|
44
|
+
*
|
|
45
|
+
* That state is reachable in practice: a `dropSegment` whose Storage sweep threw part-way, or a load that was
|
|
46
|
+
* already writing its object when the tombstone landed and finished the write afterwards. `dropSegment` re-sweeps
|
|
47
|
+
* and reports whatever it could not reclaim in `generationsRemaining`, but a drop that was never re-run leaves a
|
|
48
|
+
* residual, and this is what eventually collects it from the retention sweep.
|
|
49
|
+
*
|
|
50
|
+
* `keep: 0` is how a subject-erasure rewrite makes a bit **physically** gone on return: the generation that held
|
|
51
|
+
* it is collected the moment the rewrite is current. A live read still fetching from it heals forward to the
|
|
52
|
+
* rewrite, and a pin of it (`seg.pin()`) fails with `NotFoundError` for any chunk it has yet to read.
|
|
53
|
+
*/
|
|
54
|
+
export declare function gcOrphanGenerations(ref: SegmentRef, deps: GenerationDeps, options?: {
|
|
55
|
+
keep?: number;
|
|
56
|
+
}): Promise<number[]>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ChunkRef, SegmentRef } from './ports.js';
|
|
2
|
+
export declare function segmentKey(ref: SegmentRef): string;
|
|
3
|
+
export declare function segmentPrefix(ref: SegmentRef): string;
|
|
4
|
+
export declare function chunkRefKey(ref: ChunkRef): string;
|
|
5
|
+
/**
|
|
6
|
+
* Cache key for a chunk scoped to a specific **version** of the segment — {@link chunkRefKey} plus that
|
|
7
|
+
* version (space-delimited, injection-proof exactly as above — the name parts are already encoded). A publish bumps the version, so it naturally
|
|
8
|
+
* misses the cache instead of serving a stale superseded chunk, and the superseded entries age out under the
|
|
9
|
+
* LRU ceiling (no active purge needed).
|
|
10
|
+
*
|
|
11
|
+
* `version` is whatever the Storage source reports as identifying the bytes a read will see — its
|
|
12
|
+
* `currentVersion` where it has one, otherwise the bare generation number. **A generation number alone is not
|
|
13
|
+
* an identity**: a load's generation number restarts at 0 once a registry row is purged and the bucket emptied, so a
|
|
14
|
+
* retired-and-re-created name serves different data at the same `currentGen`, and a key built on the number
|
|
15
|
+
* hands the new incarnation the old one's decoded chunks.
|
|
16
|
+
*/
|
|
17
|
+
export declare function chunkGenKey(ref: ChunkRef, version: string | number): string;
|
|
18
|
+
/**
|
|
19
|
+
* Deterministic FNV-1a shard assignment for a segment key — dependency-free, stable across workers and
|
|
20
|
+
* restarts. One definition, so every sharded fleet operation (today the retention sweep) agrees on it exactly:
|
|
21
|
+
* two definitions that disagreed would make the union across workers neither disjoint nor complete.
|
|
22
|
+
*/
|
|
23
|
+
export declare function shardOf(key: string, totalShards: number): number;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `loadSegment` — the loaded store's primary write path, as one call.
|
|
3
|
+
*
|
|
4
|
+
* A load is always the same four steps: take the next generation number, write one immutable object, move the
|
|
5
|
+
* pointer, collect what the move superseded. Inside the library each is its own internal function, and the
|
|
6
|
+
* step a hand-composed load leaves out is the last one, so a store that composed it would accumulate superseded
|
|
7
|
+
* generations it keeps paying for.
|
|
8
|
+
* Composing it here, once, is the point.
|
|
9
|
+
*
|
|
10
|
+
* The other reason it is one call is the **guard**. A load REPLACES a segment: whatever the stream contains is
|
|
11
|
+
* what the segment contains afterwards. That makes an upstream query returning fewer rows than usual — or none —
|
|
12
|
+
* a silent, successful-looking wipe, and nothing downstream can tell it from a legitimate shrink. The guard is
|
|
13
|
+
* the place to say what a plausible result looks like, and it has to sit **between the write and the publish**,
|
|
14
|
+
* because that is the only moment where the new content is known and the old one is still authoritative.
|
|
15
|
+
*
|
|
16
|
+
* Refusing therefore has to clean up after itself. The object is already durable at that point, and it sits
|
|
17
|
+
* ABOVE `currentGen`, where generation collection deliberately never looks (it deletes strictly below the
|
|
18
|
+
* pointer), so a refused load deletes its own object before returning, while the segment's row is unchanged or
|
|
19
|
+
* gone. Once another write has changed the row, the object's number may name a re-created segment's live object,
|
|
20
|
+
* so it stays, and collection takes it like any other generation once one above it is current.
|
|
21
|
+
*/
|
|
22
|
+
import { type IAuditSink } from './audit.js';
|
|
23
|
+
import { type CodecInterface } from './codec.js';
|
|
24
|
+
import type { Clock } from './determinism.js';
|
|
25
|
+
import type { IKeystore } from './crypto.js';
|
|
26
|
+
import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
|
|
27
|
+
/** What {@link loadSegment} needs: the objects, the pointer, the codec, and key material if encrypted. */
|
|
28
|
+
export interface LoadDeps {
|
|
29
|
+
readonly storage: IStorageDriver;
|
|
30
|
+
readonly registry: IRegistryDriver;
|
|
31
|
+
readonly codec?: CodecInterface;
|
|
32
|
+
readonly keystore?: IKeystore;
|
|
33
|
+
readonly requireEncryption?: boolean;
|
|
34
|
+
readonly clock?: Clock;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* What a plausible result looks like, so an implausible one is refused instead of published.
|
|
38
|
+
*
|
|
39
|
+
* Both bounds are compared against the **current** generation's cardinality, read from the `.crbm` index rather
|
|
40
|
+
* than its payload — one object-header read, and only when a bound is actually set.
|
|
41
|
+
*/
|
|
42
|
+
export interface LoadGuard {
|
|
43
|
+
/** Refuse a generation with fewer than this many ids. Use for "this segment is never legitimately tiny". */
|
|
44
|
+
readonly minCardinality?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Refuse a generation that retains less than this fraction of the current one — `0.5` means "at least half the
|
|
47
|
+
* ids must survive". A *growing* load is never refused by this bound, and neither is the first load of a
|
|
48
|
+
* segment, which has nothing to shrink from.
|
|
49
|
+
*
|
|
50
|
+
* Phrased as a floor rather than a ceiling on the loss so that `0` means the same thing here as it does on
|
|
51
|
+
* `minCardinality` above: **no bound**. The inverse spelling (`maxShrink`) would make `0` mean "refuse any
|
|
52
|
+
* shrink at all" — maximally strict — sitting in the same object literal as a field where `0` is maximally
|
|
53
|
+
* permissive, which is where an inversion would eventually be written and not noticed.
|
|
54
|
+
*/
|
|
55
|
+
readonly minRetained?: number;
|
|
56
|
+
}
|
|
57
|
+
export interface LoadOptions {
|
|
58
|
+
/**
|
|
59
|
+
* Publish an empty generation over a non-empty one. Off by default: an empty result is far more often an
|
|
60
|
+
* upstream failure than an intent, and the failure is indistinguishable from success once it lands.
|
|
61
|
+
*/
|
|
62
|
+
readonly allowEmpty?: boolean;
|
|
63
|
+
readonly guard?: LoadGuard;
|
|
64
|
+
/**
|
|
65
|
+
* Generations to keep below the new pointer as a grace window (default 1). A non-negative integer: anything
|
|
66
|
+
* else (negative, fractional, `NaN`, infinite) throws `ValidationError` before anything is written. Keep at
|
|
67
|
+
* least one: a read that is
|
|
68
|
+
* still fetching chunks from the just-superseded generation would otherwise have it deleted out from under it
|
|
69
|
+
* and pay a re-resolve. A wider window costs storage, and it is what a pinned handle needs: a pin is never
|
|
70
|
+
* re-resolved, so a chunk a pinned read has not fetched fails with `NotFoundError` once its generation is
|
|
71
|
+
* collected. Keep at least one generation for every one that can be written above the pinned one while your
|
|
72
|
+
* longest pinned job runs, on every writer that loads the segment — see "Generations and `keep`" in the loading
|
|
73
|
+
* guide.
|
|
74
|
+
*/
|
|
75
|
+
readonly keep?: number;
|
|
76
|
+
readonly audit?: IAuditSink;
|
|
77
|
+
}
|
|
78
|
+
/** Why a load did not become current. */
|
|
79
|
+
export type LoadRefusal = 'empty' | 'min-cardinality' | 'min-retained'
|
|
80
|
+
/**
|
|
81
|
+
* Another writer got there first. Either another load wrote the same generation number first, so the write-once
|
|
82
|
+
* put refused this one and it wrote nothing (`size: 0`); or the segment's registry row changed while the load was
|
|
83
|
+
* writing: another load published, a retention change, a rollback or an erasure wrote the row, or the row was
|
|
84
|
+
* deleted. Once another write has changed the row, the object stays in the bucket, because its generation number
|
|
85
|
+
* may by then name another incarnation's live object; once the row is gone, the object is deleted.
|
|
86
|
+
*/
|
|
87
|
+
| 'superseded';
|
|
88
|
+
export interface LoadResult {
|
|
89
|
+
/**
|
|
90
|
+
* The generation written. Present even when refused: a refusal deletes that object while the segment's row is
|
|
91
|
+
* unchanged or gone, and leaves it in the bucket once another write has changed the row. A load that lost its
|
|
92
|
+
* generation number to another wrote nothing, and reports `size: 0`.
|
|
93
|
+
*/
|
|
94
|
+
readonly generation: number;
|
|
95
|
+
/** Whether this generation is now the segment's current one. */
|
|
96
|
+
readonly published: boolean;
|
|
97
|
+
/** Set only when `published` is false. */
|
|
98
|
+
readonly reason?: LoadRefusal;
|
|
99
|
+
readonly size: number;
|
|
100
|
+
readonly sha256: string;
|
|
101
|
+
readonly chunkCount: number;
|
|
102
|
+
/** Distinct ids in the generation that was written, post-dedup. */
|
|
103
|
+
readonly cardinality: number;
|
|
104
|
+
/**
|
|
105
|
+
* What the segment held when the guard judged it — `null` when there was no current generation, or when no
|
|
106
|
+
* bound needed it. Returned because an alert on `reason: 'min-retained'` is useless without it: "refused" is a
|
|
107
|
+
* page, "refused: 4 → 1" is a diagnosis, and re-reading the segment to find out costs a round trip and races
|
|
108
|
+
* whatever happens next.
|
|
109
|
+
*/
|
|
110
|
+
readonly cardinalityBefore: number | null;
|
|
111
|
+
/** The superseded generations collected after publishing. Empty when nothing was published. */
|
|
112
|
+
readonly collected: readonly number[];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Replace a segment's contents with `ids`, as one immutable generation, and make it current.
|
|
116
|
+
*
|
|
117
|
+
* Returns what was written and whether it became current. A refusal is **not** an exception: `published: false`
|
|
118
|
+
* with a `reason` is a normal outcome a caller branches on, in the same shape as a successful load, because the
|
|
119
|
+
* interesting cases (a guard tripped, a racing writer won) are operational facts rather than faults.
|
|
120
|
+
*/
|
|
121
|
+
export declare function loadSegment(ref: SegmentRef, ids: Iterable<number> | AsyncIterable<number>, deps: LoadDeps, options?: LoadOptions): Promise<LoadResult>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded LRU cache with optional TTL — the cache tier's memory ceiling.
|
|
3
|
+
*
|
|
4
|
+
* Time comes from an injected `Clock` (never `Date.now()`) so the cache is deterministic
|
|
5
|
+
* under simulation.
|
|
6
|
+
*
|
|
7
|
+
* The cache can be bounded by entry **count** (`maxEntries`, always) and, optionally, by an aggregate
|
|
8
|
+
* **byte weight** (`maxBytes`). The byte bound exists for values whose footprint is large and uneven — the
|
|
9
|
+
* storage reader cache, where one entry pins a fully-parsed `.crbm` index that can be ~6 MB for a wide segment
|
|
10
|
+
* yet a few hundred bytes for a small one (a count-only bound lets 1024 wide indices blow a Lambda's
|
|
11
|
+
* memory even though the count is "in bounds"). Weights arrive **after** insertion (a cached reader is a
|
|
12
|
+
* pending promise; its size is known only once it resolves), so weight is reported via {@link setWeight} and
|
|
13
|
+
* eviction re-runs then.
|
|
14
|
+
*/
|
|
15
|
+
import type { Clock } from './determinism.js';
|
|
16
|
+
export interface LruOptions {
|
|
17
|
+
/** Hard ceiling on entries; the least-recently-used is evicted past it. */
|
|
18
|
+
readonly maxEntries: number;
|
|
19
|
+
/**
|
|
20
|
+
* Optional aggregate byte ceiling across all entries' reported weights (see {@link BoundedLru.setWeight}).
|
|
21
|
+
* When set, the least-recently-used entries are evicted until the total is within budget — in addition to
|
|
22
|
+
* the `maxEntries` bound. Omit for a pure count-bounded cache (weights stay 0, no byte eviction).
|
|
23
|
+
*/
|
|
24
|
+
readonly maxBytes?: number;
|
|
25
|
+
/** Optional time-to-live in ms; an entry older than this is treated as absent. */
|
|
26
|
+
readonly ttlMs?: number;
|
|
27
|
+
/** Only `now()` is used (never `sleep`), so a count-only cache can pass a minimal time source. */
|
|
28
|
+
readonly clock: Pick<Clock, 'now'>;
|
|
29
|
+
}
|
|
30
|
+
export declare class BoundedLru<K, V> {
|
|
31
|
+
private readonly map;
|
|
32
|
+
private readonly maxEntries;
|
|
33
|
+
private readonly maxBytes;
|
|
34
|
+
private readonly ttlMs;
|
|
35
|
+
private readonly clock;
|
|
36
|
+
private totalBytes;
|
|
37
|
+
constructor(options: LruOptions);
|
|
38
|
+
get(key: K): V | undefined;
|
|
39
|
+
/** Read a value **without** counting as an access (recency unchanged, no purge on expiry). */
|
|
40
|
+
peek(key: K): V | undefined;
|
|
41
|
+
set(key: K, value: V): void;
|
|
42
|
+
/**
|
|
43
|
+
* Report an entry's byte weight (typically once a cached promise resolves and its size is known). Updates
|
|
44
|
+
* the running total and evicts the least-recently-used entries if the aggregate now exceeds `maxBytes`.
|
|
45
|
+
* Recency is **not** changed. The `key` being weighted is protected from this eviction pass (we just
|
|
46
|
+
* populated it) — so a single entry larger than the whole budget is kept rather than immediately dropped.
|
|
47
|
+
*/
|
|
48
|
+
setWeight(key: K, bytes: number): void;
|
|
49
|
+
private evictToBudget;
|
|
50
|
+
/** The least-recently-used key that is not `protectKey`, or undefined if none other exists. */
|
|
51
|
+
private oldestExcept;
|
|
52
|
+
private dropKey;
|
|
53
|
+
private drop;
|
|
54
|
+
/** Membership test that does NOT count as an access (recency is unchanged). Purges if expired. */
|
|
55
|
+
has(key: K): boolean;
|
|
56
|
+
delete(key: K): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Delete every entry whose key matches `pred`, returning how many went. Used to drop one segment's decoded
|
|
59
|
+
* chunks when something destroys the generation they came from — a deletion the generation-keyed cache
|
|
60
|
+
* cannot express on its own, because a *destroyed* segment has no newer generation whose key would miss.
|
|
61
|
+
* Linear in the cache size; called on admin paths (erasure, drop, shred, retirement), never on a read.
|
|
62
|
+
*/
|
|
63
|
+
deleteWhere(pred: (key: K) => boolean): number;
|
|
64
|
+
clear(): void;
|
|
65
|
+
/** Current entry count (may include not-yet-evicted expired entries). */
|
|
66
|
+
get size(): number;
|
|
67
|
+
/** Current aggregate reported weight (bytes) across all entries. */
|
|
68
|
+
get weightBytes(): number;
|
|
69
|
+
}
|