@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,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The determinism seam.
|
|
3
|
+
*
|
|
4
|
+
* `core/` is a pure function of its inputs + injected dependencies — it never reaches for
|
|
5
|
+
* ambient time or randomness. Production wiring supplies real implementations; the deterministic
|
|
6
|
+
* tests supply controlled, seeded ones, so any run can be replayed exactly from a seed.
|
|
7
|
+
*
|
|
8
|
+
* These are interfaces only: the seam the whole library depends on for reproducibility. Every module that
|
|
9
|
+
* needs time, randomness or a yield takes one of these rather than reaching for a global.
|
|
10
|
+
*/
|
|
11
|
+
/** Injected time source. `core/` must never call `Date.now()` directly. */
|
|
12
|
+
export interface Clock {
|
|
13
|
+
/** Milliseconds since the Unix epoch. */
|
|
14
|
+
now(): number;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve after at least `ms` have elapsed. The retry/backoff path's only way to wait — `core/` must
|
|
17
|
+
* never reach for `setTimeout` directly (it's timer-free, lint-enforced). Production wiring supplies a
|
|
18
|
+
* `setTimeout`-backed implementation; a test supplies a virtual-time one so a backoff schedule is
|
|
19
|
+
* replayable. `ms <= 0` resolves on a microtask without scheduling a timer.
|
|
20
|
+
*/
|
|
21
|
+
sleep(ms: number): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Hand the event loop back once, without asking for any elapsed time.
|
|
24
|
+
*
|
|
25
|
+
* This is **not** `sleep(0)`, and the difference is the reason the member exists. `sleep(0)` resolves on a
|
|
26
|
+
* microtask, and microtasks drain before the loop advances a phase — so awaiting it inside a CPU-bound loop
|
|
27
|
+
* yields to nothing at all, and a co-resident server stays blocked for the loop's full duration. A real yield
|
|
28
|
+
* needs a macrotask. `sleep(1)` is one, but it buys the relief at ~1 ms of dead wall-clock per yield.
|
|
29
|
+
*
|
|
30
|
+
* **Optional**, so a minimal `Clock` — `now` and `sleep` alone — still satisfies the interface. Callers must
|
|
31
|
+
* therefore degrade rather than assume: `clock.yieldNow?.() ?? clock.sleep(1)` — correct on any clock, cheap
|
|
32
|
+
* on one that implements this. `core/` cannot supply it itself (it is timer-free, lint-enforced); production
|
|
33
|
+
* wiring backs it with `setImmediate`, and a test can make it a no-op so virtual time is not perturbed
|
|
34
|
+
* by what is purely a scheduling courtesy.
|
|
35
|
+
*/
|
|
36
|
+
yieldNow?(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
/** Injected randomness. `core/` must never call `Math.random()` directly. Seedable for simulation. */
|
|
39
|
+
export interface Rng {
|
|
40
|
+
/** A float in the half-open interval [0, 1). */
|
|
41
|
+
next(): number;
|
|
42
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `segmentExists` and `listSegments` — ask the registry what is there.
|
|
3
|
+
*
|
|
4
|
+
* The registry has always known which segments exist; it is what `checkConsistency`, the retention sweep and
|
|
5
|
+
* `exportSegments` each enumerate. It was simply never exposed, so a user asking "do I already have this?" had
|
|
6
|
+
* to infer it: `count()` returns `0` for a segment that was never loaded AND for one loaded with no ids, and
|
|
7
|
+
* `generations()` costs a bucket listing to answer a question the pointer alone settles.
|
|
8
|
+
*
|
|
9
|
+
* Inferring it is the expensive kind of wrong. The alternative a user reaches for is keeping their own list of
|
|
10
|
+
* segment names alongside the store — a second source of truth, updated by hand, drifting from the first the
|
|
11
|
+
* moment a load fails halfway. The registry is already the list; these two functions read it.
|
|
12
|
+
*/
|
|
13
|
+
import type { IRegistryDriver, RegistryRecord, SegmentRef } from './ports.js';
|
|
14
|
+
/** One segment the registry knows about. */
|
|
15
|
+
export interface SegmentInfo extends SegmentRef {
|
|
16
|
+
/**
|
|
17
|
+
* The generation the **pointer names** — not what a read would resolve. On a `destroyed` tombstone this is
|
|
18
|
+
* still a number while reads answer empty, so read it together with {@link SegmentInfo.status} rather than
|
|
19
|
+
* alone. `null` means the row exists with no Storage generation yet.
|
|
20
|
+
*/
|
|
21
|
+
readonly currentGen: number | null;
|
|
22
|
+
/**
|
|
23
|
+
* `destroyed` is a tombstone: the row stays so sweeps can still find it, and the generations are gone — on an
|
|
24
|
+
* encrypted segment the DEK wrappings went with them, which is what makes the bytes unrecoverable rather than
|
|
25
|
+
* merely deleted.
|
|
26
|
+
*/
|
|
27
|
+
readonly status: RegistryRecord['status'];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Whether this segment's pointer resolves a generation — the question behind "does it already exist?".
|
|
31
|
+
*
|
|
32
|
+
* One registry point read, on every backend. Deliberately narrower than "is there a row": it is **false** for a
|
|
33
|
+
* row that exists with `currentGen: null` (a retention policy recorded ahead of the first load) and for a
|
|
34
|
+
* `destroyed` tombstone, because a read answers empty in both. The predicate mirrors generation resolution
|
|
35
|
+
* clause for clause, which is what keeps the two from disagreeing.
|
|
36
|
+
*
|
|
37
|
+
* It is NOT the same question as `count() > 0`. A segment loaded with no ids exists and counts zero — the
|
|
38
|
+
* distinction between "never loaded" and "loaded, and genuinely empty" is the one `count()` cannot make and
|
|
39
|
+
* the main reason this function is worth having.
|
|
40
|
+
*
|
|
41
|
+
* Two states answer `true` where a read still gives you nothing, and neither is this call's job to detect: a
|
|
42
|
+
* **torn restore** (`missing-storage-generation` — a live pointer whose object was deleted) makes reads *throw*
|
|
43
|
+
* `NotFoundError` rather than answer empty, and `runConsistencyCheck` is the call that looks for it; and a
|
|
44
|
+
* handle carrying an expired `expiresAt` reads as empty by a rule that lives on the handle, not on the row.
|
|
45
|
+
*
|
|
46
|
+
* Not a lock. A segment can be created or dropped between this call and whatever you do next; if the answer
|
|
47
|
+
* has to hold, use the fences that exist for it — `load`'s guard, or `expectFrom`/`expectToken` on a publish.
|
|
48
|
+
*/
|
|
49
|
+
export declare function segmentExists(ref: SegmentRef, registry: IRegistryDriver): Promise<boolean>;
|
|
50
|
+
/**
|
|
51
|
+
* Every segment the registry holds, streamed — optionally scoped to one namespace.
|
|
52
|
+
*
|
|
53
|
+
* **This is an admin/discovery call, not a request-path one.** It is the registry's own enumeration — a paged
|
|
54
|
+
* LIST over the `registry/` prefix — so cost grows with the fleet, not with what you are looking for. The
|
|
55
|
+
* fleet-wide sweeps that *drain* it bound it (`maxScanSegments`); this one streams, so the bound is yours.
|
|
56
|
+
*
|
|
57
|
+
* **Scoping to a namespace narrows the LIST prefix**, so it really is the difference between reading one
|
|
58
|
+
* tenant and reading all of them.
|
|
59
|
+
*
|
|
60
|
+
* Streams, so a large fleet need not be held at once, and stopping the iteration stops the scan — as long as the
|
|
61
|
+
* registry driver's own `list` streams. One that collects its whole enumeration before yielding defeats both
|
|
62
|
+
* properties, since the scan is then paid for and resident before the first row reaches you, so a custom driver's
|
|
63
|
+
* `list` should yield each row as it reads it. The registry drivers this library ships all stream; on S3, GCS and
|
|
64
|
+
* Azure Blob the granularity is a page, so one LIST page and its in-flight row reads complete regardless. Nothing
|
|
65
|
+
* here retries the scan: a transient fault part-way through ends the iteration with that error, and calling
|
|
66
|
+
* `listSegments` again scans from the start, since the iterable one call returns is used up.
|
|
67
|
+
*
|
|
68
|
+
* Yields `destroyed` tombstones, and rows whose `currentGen` is `null`, because hiding either would make this
|
|
69
|
+
* disagree with the registry it reports on — a filtered enumeration that looks complete is how a retention
|
|
70
|
+
* sweep ends up permanently skipping rows nobody can see. Filter on `status`/`currentGen` yourself, or use
|
|
71
|
+
* {@link segmentExists} for the "does the pointer resolve" question. Internal bookkeeping rows are the one
|
|
72
|
+
* exclusion, and only on an unscoped scan: they are not segments, and a caller naming their namespace still
|
|
73
|
+
* sees them.
|
|
74
|
+
*/
|
|
75
|
+
export declare function listSegments(registry: IRegistryDriver, options?: {
|
|
76
|
+
namespace?: string;
|
|
77
|
+
}): AsyncIterable<SegmentInfo>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { MAX_NAME_LENGTH } from './validate.js';
|
|
2
|
+
import type { RegistryRecord, SegmentRef } from './ports.js';
|
|
3
|
+
/** Bucket width. One day: small enough that a cycle reads little, coarse enough that the index stays tiny. */
|
|
4
|
+
export declare const DUE_BUCKET_MS = 86400000;
|
|
5
|
+
/**
|
|
6
|
+
* The name-length cap, and an index row encodes **both** of the original ref's parts into one name — so a ref
|
|
7
|
+
* whose encoding would exceed it is simply not indexed (see {@link canIndex}).
|
|
8
|
+
*
|
|
9
|
+
* Re-exported from the validator rather than restated. The two were separate constants with the same value
|
|
10
|
+
* until the validator started measuring the **encoded** length; `canIndex` went on counting raw characters and
|
|
11
|
+
* began answering `true` for refs the registry then refused, turning a documented graceful degradation into a
|
|
12
|
+
* guaranteed throw on the retention fast path.
|
|
13
|
+
*/
|
|
14
|
+
export { MAX_NAME_LENGTH };
|
|
15
|
+
/** Which bucket an expiry falls in. Total and reversible; no calendar, no timezone, no ambient time. */
|
|
16
|
+
export declare function dueBucket(expiresAt: number): number;
|
|
17
|
+
/** The namespace holding one bucket's pointers. */
|
|
18
|
+
export declare function dueNamespace(bucket: number): string;
|
|
19
|
+
/** Is this record a due-index pointer rather than a segment? */
|
|
20
|
+
export declare function isDueIndexRow(record: Pick<RegistryRecord, 'namespace'>): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Encode a ref into one index-row name, unambiguously.
|
|
23
|
+
*
|
|
24
|
+
* `${namespaceLength}.${namespace}${segment}` — a decimal length, a dot, then the two parts concatenated. The
|
|
25
|
+
* length prefix is what makes it reversible: a name may contain any character, the dot included, so no
|
|
26
|
+
* separator character could ever be unambiguous on its own. Reading the digits
|
|
27
|
+
* up to the first dot tells the parser exactly where the namespace ends.
|
|
28
|
+
*/
|
|
29
|
+
export declare function encodeDueName(ref: SegmentRef): string;
|
|
30
|
+
/**
|
|
31
|
+
* Can this ref be indexed at all? False when the encoded name would exceed the 256-character name cap —
|
|
32
|
+
* possible only for a ref whose namespace and segment are together near the limit.
|
|
33
|
+
*
|
|
34
|
+
* **Not indexable is not "not retired".** The repair scan still sees the segment's own row, so the consequence
|
|
35
|
+
* is that it expires on the repair cadence instead of the fast one. Callers must not treat `false` as an error.
|
|
36
|
+
*/
|
|
37
|
+
export declare function canIndex(ref: SegmentRef): boolean;
|
|
38
|
+
/** The registry ref of the pointer for `ref` in `bucket`. Throws if the ref cannot be encoded — check first. */
|
|
39
|
+
export declare function dueIndexRef(bucket: number, ref: SegmentRef): SegmentRef;
|
|
40
|
+
/**
|
|
41
|
+
* Recover the original ref from an index row's name. `null` for anything we did not write — a foreign row in
|
|
42
|
+
* the reserved namespace is ignored rather than acted upon.
|
|
43
|
+
*/
|
|
44
|
+
export declare function decodeDueName(name: string): SegmentRef | null;
|
|
45
|
+
/**
|
|
46
|
+
* Which buckets a cycle must read at `now`: everything from `since` up to and including the current one.
|
|
47
|
+
*
|
|
48
|
+
* Past buckets are included because a sweep that did not run — a scaled-to-zero worker, a failed deploy, a
|
|
49
|
+
* paused schedule — must not leave its buckets stranded. `lookbackBuckets` bounds how far back a first run
|
|
50
|
+
* reaches, so a long outage costs a bounded number of list calls per cycle rather than one per day since the
|
|
51
|
+
* epoch; anything older is the repair scan's job.
|
|
52
|
+
*/
|
|
53
|
+
export declare function dueBucketsAt(now: number, lookbackBuckets: number): number[];
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import type { CodecBitmap, CodecInterface } from './codec.js';
|
|
2
|
+
import type { Budget, BudgetOption } from './budget.js';
|
|
3
|
+
import type { Clock } from './determinism.js';
|
|
4
|
+
import type { BoundedLru } from './lru.js';
|
|
5
|
+
import type { IMetricsSink } from './metrics.js';
|
|
6
|
+
import type { StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
|
|
7
|
+
export interface EngineDeps {
|
|
8
|
+
readonly storage: StorageChunkSource;
|
|
9
|
+
/** Optional cache of decoded (immutable) Storage chunks. */
|
|
10
|
+
readonly cache?: BoundedLru<string, CodecBitmap>;
|
|
11
|
+
readonly maxBitmapBytes?: number;
|
|
12
|
+
/**
|
|
13
|
+
* The bitmap codec — **required**. `core/` is codec-agnostic: it can have no default, because the concrete codec
|
|
14
|
+
* lives in a *flavor* package that depends on core (a default here would invert that arrow). A flavor's facade
|
|
15
|
+
* injects it — `@cloudbitmaps/roaring` passes the roaring codec — so applications never see this.
|
|
16
|
+
*/
|
|
17
|
+
readonly codec: CodecInterface;
|
|
18
|
+
/** Time source for the `storage.get` latency metric only; defaults to a clock that reads 0. */
|
|
19
|
+
readonly clock?: Pick<Clock, 'now'>;
|
|
20
|
+
/** Observability sink; defaults to a no-op. Assumed exception-safe (the facade wraps it). */
|
|
21
|
+
readonly metrics?: IMetricsSink;
|
|
22
|
+
/**
|
|
23
|
+
* Per-op denial-of-wallet budget, already resolved by the facade: a {@link Budget} to enforce, or `null` to
|
|
24
|
+
* disable. Undefined ⇒ {@link DEFAULT_BUDGET} (a direct engine construction still gets the generous default).
|
|
25
|
+
* Read ops refuse before fan-out if they'd exceed it.
|
|
26
|
+
*/
|
|
27
|
+
readonly budget?: Budget | null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A range of ids for a read to yield: those greater than `after` and no greater than `through`, `(after, through]`.
|
|
31
|
+
*
|
|
32
|
+
* Built for keyset paging: `after` is the last id the caller already has, `through` the end of its window. Each is
|
|
33
|
+
* optional: left out, the read starts at the first id or runs to the last. Each is an integer in `0..4294967295`, or
|
|
34
|
+
* the read throws {@link ValidationError} when it is first read. `after >= through` is an empty range, not an error,
|
|
35
|
+
* because a cursor that has reached the end of its window is normal; an empty range reads nothing, not even whether
|
|
36
|
+
* a combine's operands exist.
|
|
37
|
+
*
|
|
38
|
+
* A read with a range fetches only the chunks the range overlaps, on a combine for every operand and every
|
|
39
|
+
* `exclude`. The per-op budget is charged once, before the first fetch, for every chunk in the range, so with
|
|
40
|
+
* `after` alone it is charged to the end of the segment however early the caller stops; pass `through` to bound it.
|
|
41
|
+
*/
|
|
42
|
+
export interface IdRange {
|
|
43
|
+
/** Exclusive lower bound: the read yields only ids greater than this. */
|
|
44
|
+
readonly after?: number | undefined;
|
|
45
|
+
/** Inclusive upper bound: the read yields only ids up to and including this. */
|
|
46
|
+
readonly through?: number | undefined;
|
|
47
|
+
}
|
|
48
|
+
/** Options common to the chunk-aligned combines. */
|
|
49
|
+
export interface CombineOptions extends IdRange {
|
|
50
|
+
/** Max chunk keys resolved concurrently — bounds the Storage footprint. A positive integer. */
|
|
51
|
+
readonly concurrency?: number;
|
|
52
|
+
/** Override the store's per-op budget for this call (`false` lifts it). */
|
|
53
|
+
readonly budget?: BudgetOption;
|
|
54
|
+
/** Segments whose ids are subtracted from the result. */
|
|
55
|
+
readonly exclude?: readonly SegmentRef[];
|
|
56
|
+
/**
|
|
57
|
+
* Allow an operand that names a segment which does not exist. Default `false` — a combine refuses one,
|
|
58
|
+
* because a misspelled or mis-namespaced operand is indistinguishable from a correct one in the result.
|
|
59
|
+
* Set `true` when you genuinely intend to combine against a name that may not have been created yet.
|
|
60
|
+
*/
|
|
61
|
+
readonly allowAbsentOperands?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export declare class SegmentEngine {
|
|
64
|
+
private readonly storage;
|
|
65
|
+
private readonly cache;
|
|
66
|
+
private readonly codec;
|
|
67
|
+
private readonly maxBitmapBytes;
|
|
68
|
+
private readonly clock;
|
|
69
|
+
private readonly metrics;
|
|
70
|
+
private readonly metricsOn;
|
|
71
|
+
/** Resolved per-op budget (null = disabled); undefined deps ⇒ the generous default. */
|
|
72
|
+
private readonly budget;
|
|
73
|
+
constructor(deps: EngineDeps);
|
|
74
|
+
/** Membership: one chunk lookup — the cache, else one Storage fetch of that chunk. */
|
|
75
|
+
has(seg: SegmentRef, id: number): Promise<boolean>;
|
|
76
|
+
/**
|
|
77
|
+
* Cardinality. When the Storage source can serve per-chunk cardinality from its `.crbm` index (the `.crbm`
|
|
78
|
+
* source can), the count is summed from the index with **zero payload reads**. A source without that
|
|
79
|
+
* capability (the in-memory source) falls back to fetching every chunk.
|
|
80
|
+
*
|
|
81
|
+
* The index sum is trusted, not confirmed: the reader checked the index for internal consistency when it opened
|
|
82
|
+
* the object, and a corrupt index that is still internally consistent yields a wrong count. `iterate()` and the
|
|
83
|
+
* combines decode the payloads.
|
|
84
|
+
*/
|
|
85
|
+
count(seg: SegmentRef): Promise<number>;
|
|
86
|
+
/** Whether the Storage source can measure segment size (for grounded cost); false ⇒ storage isn't grounded. */
|
|
87
|
+
get supportsStorageSize(): boolean;
|
|
88
|
+
/** How often the Storage source re-reads a pointer while reading (cost reporting); undefined if it does not say. */
|
|
89
|
+
get pointerRefreshMs(): number | undefined;
|
|
90
|
+
/** Grounded Storage size of a segment's current generation (cost reporting), or null if it has no generation. */
|
|
91
|
+
segmentSize(seg: SegmentRef): Promise<SegmentSize | null>;
|
|
92
|
+
/**
|
|
93
|
+
* Every id, ascending, one chunk at a time; with `range`, only the ids in `(after, through]`, fetching only the
|
|
94
|
+
* chunks the range overlaps (see {@link IdRange}).
|
|
95
|
+
*
|
|
96
|
+
* Two generators, not one with a branch: a second `yield` site in the full read's generator grows its frame, and
|
|
97
|
+
* every id of a read that asked for no range paid for it (measured at about 5% per id). The full read keeps the
|
|
98
|
+
* loop it always had.
|
|
99
|
+
*/
|
|
100
|
+
iterate(seg: SegmentRef, range?: IdRange): AsyncGenerator<number>;
|
|
101
|
+
private iterateAll;
|
|
102
|
+
private iterateRange;
|
|
103
|
+
/**
|
|
104
|
+
* `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
|
|
105
|
+
*
|
|
106
|
+
* The `exclude` operands are what make suppression **compose without materialising**. Applying suppression
|
|
107
|
+
* afterwards (intersect into a temp segment, then subtract) writes an intermediate segment that nobody wants;
|
|
108
|
+
* applying it here folds it into the same chunk-aligned pass. It is also cheaper than it looks: an exclude
|
|
109
|
+
* operand is read **only at the keys that survived the intersection** — the cost is bounded by the
|
|
110
|
+
* audience's surviving key count and **never scales with the size of the suppression list**. Subtracting a
|
|
111
|
+
* 61,000-chunk global opt-out list costs at most one read per surviving key, not 61,000.
|
|
112
|
+
*
|
|
113
|
+
* ---
|
|
114
|
+
*
|
|
115
|
+
* Chunk-skipping intersection. Aligns each segment's chunk-key set (read from the `.crbm` index, no payload),
|
|
116
|
+
* keeps only keys present in **all** operands (a key missing from any operand can't contribute → its Storage
|
|
117
|
+
* chunks are never fetched — the core saving), then for each surviving key fetches the operands' chunks in
|
|
118
|
+
* parallel and hands them to the codec for the AND, streaming results through a bounded in-flight window.
|
|
119
|
+
*
|
|
120
|
+
* **Memory:** the Storage payload footprint is bounded by the window (`concurrency × operands × chunk`), not by
|
|
121
|
+
* segment size — that's the Lambda-friendly property.
|
|
122
|
+
*
|
|
123
|
+
* Generation-consistent within the call (normal case): each operand's current generation is resolved **once**
|
|
124
|
+
* up front (before the fan-out) and threaded into every chunk read, so a concurrent load can't corrupt or tear
|
|
125
|
+
* the result — every chunk read is a whole, checksum-verified, immutable generation. The edge a *long* call can
|
|
126
|
+
* hit is invariant 3's: if it straddles a mid-call `cache.genTtlMs` boundary and a load has published, an
|
|
127
|
+
* operand's not-yet-read chunks may re-resolve forward to the newer generation (a generation hop within one long
|
|
128
|
+
* call) — the call never crashes or returns a torn object, but may mix generations. Three things hop it without
|
|
129
|
+
* waiting for the TTL, so a shorter call can meet them too: **the reader cache evicting an operand mid-call**
|
|
130
|
+
* (`maxOpenSegments`), whose re-read re-resolves fresh; a sweep deleting the generation it was reading, which
|
|
131
|
+
* heals the read forward; and an invalidation, which this store's own `load`, `rollback` and `eraseSubject` make
|
|
132
|
+
* and `invalidate()` makes on request. Still whole/immutable per read, never torn.
|
|
133
|
+
*/
|
|
134
|
+
intersect(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<number>;
|
|
135
|
+
/**
|
|
136
|
+
* `segs[0] ∪ segs[1] ∪ …`, minus every id in `options.exclude` — streamed ascending.
|
|
137
|
+
*
|
|
138
|
+
* **Union is the one composite read with no chunk-skipping**, and that is a property of the operation rather
|
|
139
|
+
* than of this implementation: an id in *any* operand belongs to the result, so every chunk of every operand
|
|
140
|
+
* has to be read. `intersect` can prune a key absent from any operand; union cannot prune anything. It is
|
|
141
|
+
* budgeted exactly like `intersect` so a wide union is refused rather than quietly billed.
|
|
142
|
+
*/
|
|
143
|
+
union(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<number>;
|
|
144
|
+
/**
|
|
145
|
+
* `seg \ (excludes[0] ∪ excludes[1] ∪ …)` — streamed ascending. Suppression on its own.
|
|
146
|
+
*
|
|
147
|
+
* Reads every chunk of `seg` (any of them may survive) but each exclude **only where it overlaps `seg`**, so
|
|
148
|
+
* the cost scales with the segment being filtered rather than with the size of the suppression list.
|
|
149
|
+
*/
|
|
150
|
+
andNot(seg: SegmentRef, excludes: readonly SegmentRef[], options?: Omit<CombineOptions, 'exclude'>): AsyncGenerator<number>;
|
|
151
|
+
private combine;
|
|
152
|
+
/**
|
|
153
|
+
* The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
|
|
154
|
+
* Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
|
|
155
|
+
*
|
|
156
|
+
* The accumulator is a **clone** of the first operand's chunk, and that clone is load-bearing: every other
|
|
157
|
+
* bitmap here is the cached, shared Storage instance and must never be mutated, or the cache is poisoned for
|
|
158
|
+
* every later reader. The remaining operands and the excludes are only *read* by the in-place ops, so they are
|
|
159
|
+
* used as-is — one clone per key, not one per operand.
|
|
160
|
+
*/
|
|
161
|
+
private combineChunk;
|
|
162
|
+
/** The segment's chunk keys, ascending — a shape read off the index, no payload. Keys are untrusted (invariant 5). */
|
|
163
|
+
private chunkKeys;
|
|
164
|
+
/**
|
|
165
|
+
* Tier-derived keys are untrusted (invariant 5) — fail fast on a corrupt/out-of-range key rather than
|
|
166
|
+
* letting it flow into the id-routing step and produce a bogus id.
|
|
167
|
+
*/
|
|
168
|
+
private assertChunkKeyInRange;
|
|
169
|
+
/**
|
|
170
|
+
* The other half of invariant 5: a chunk payload holds **remainders** — 16-bit offsets within one chunk — so
|
|
171
|
+
* every value must be `<= 0xffff`.
|
|
172
|
+
*
|
|
173
|
+
* Nothing upstream establishes that. The byte cap bounds *size*, and CRC/AEAD prove the bytes are the bytes
|
|
174
|
+
* that were written — which anyone able to write the bucket satisfies trivially. A value `>= 65536` would then
|
|
175
|
+
* reach the id-routing step, which masks it (`remainder & 0xffff`) and emits a **fabricated id belonging to a different
|
|
176
|
+
* chunk's id space**: indistinguishable from real data, inflating `count()` and creating spurious `intersect`
|
|
177
|
+
* matches.
|
|
178
|
+
*
|
|
179
|
+
* Costs one `maximum()` per chunk, not per id — `maximum` is optional on the codec seam precisely so a codec
|
|
180
|
+
* that cannot answer in O(1) opts out instead of making the read path walk every value.
|
|
181
|
+
*
|
|
182
|
+
* One value stands for all of them only because the decode is structurally checked. Roaring answers `maximum()`
|
|
183
|
+
* from its last container, so a payload listing its containers out of order would report the wrong one's
|
|
184
|
+
* largest value and pass here while holding values above 65,535. The codec's `safeDeserialize` refuses that
|
|
185
|
+
* payload, and every other shape that would make this answer wrong, before it reaches this check.
|
|
186
|
+
*/
|
|
187
|
+
private assertChunkPayloadInRange;
|
|
188
|
+
/**
|
|
189
|
+
* Refuse a combine whose operand names a segment that was never created.
|
|
190
|
+
*
|
|
191
|
+
* A segment that resolves to nothing is ambiguous in a way that matters only here. Read it directly and
|
|
192
|
+
* "empty" is the right answer either way. Pass it as an **operand** and the two states diverge: a
|
|
193
|
+
* suppression list with nobody on it correctly suppresses nothing, while one whose namespace you omitted
|
|
194
|
+
* *silently* suppresses nothing — and the result is not empty and obviously wrong, it is the full audience
|
|
195
|
+
* and plausibly right. That direction is the dangerous one: the failure mode is mailing the people who
|
|
196
|
+
* opted out.
|
|
197
|
+
*
|
|
198
|
+
* The asymmetry is why this is checked rather than left to the caller. A mistyped **include** collapses an
|
|
199
|
+
* intersect to nothing, which you notice; a mistyped **exclude** removes a safeguard, which you do not. Both
|
|
200
|
+
* are refused, because both are wiring errors and one rule is easier to trust than two.
|
|
201
|
+
*
|
|
202
|
+
* **Costs nothing on a normal combine.** `exists` is consulted only for an operand that resolved to zero
|
|
203
|
+
* chunks — a segment with data is self-evidently registered — so the check is a registry read exactly when
|
|
204
|
+
* a combine was about to do something suspicious, and no reads at all otherwise. A source that cannot answer
|
|
205
|
+
* `exists` (it has no notion of registration) skips the check rather than guessing. An operand is judged by
|
|
206
|
+
* all its chunks, not those inside a range: one with nothing in range but data elsewhere is not suspicious.
|
|
207
|
+
*/
|
|
208
|
+
private refuseAbsentOperands;
|
|
209
|
+
/**
|
|
210
|
+
* Drop every piece of state this engine derived from `ref`, and tell the Storage source to do the same.
|
|
211
|
+
*
|
|
212
|
+
* The decoded-chunk cache is keyed by generation, which handles a *publish* (the new generation misses) but
|
|
213
|
+
* not a *destruction*: a segment that was erased from, dropped, shredded or retired has no newer generation
|
|
214
|
+
* whose key would miss, so its decoded chunks stay resident and keep answering. That is why the erasing
|
|
215
|
+
* process itself could report `erased: true` and then still answer `true` for the same id, out of RAM, with
|
|
216
|
+
* no storage read to intercept.
|
|
217
|
+
*
|
|
218
|
+
* Called by the destructive verbs on the facade. Synchronous and best-effort — forgetting cannot fail.
|
|
219
|
+
*/
|
|
220
|
+
invalidate(ref: SegmentRef): void;
|
|
221
|
+
/** The segment's current generation, resolved once per op (`undefined` ⇒ source can't report it). */
|
|
222
|
+
private currentGen;
|
|
223
|
+
/**
|
|
224
|
+
* The cache-key component identifying **which bytes** this op will read: the source's `currentVersion` when
|
|
225
|
+
* it has one, else the generation alone.
|
|
226
|
+
*
|
|
227
|
+
* The generation alone is not an identity. A load's generation number restarts at 0 once a row is purged
|
|
228
|
+
* and the bucket emptied, so a retired-and-re-created name serves different data at the same `currentGen` — and a
|
|
229
|
+
* decoded chunk cached under `(segment, chunk, 0)` is handed straight back to the new incarnation. That is an
|
|
230
|
+
* erased id reappearing with no read to intercept it, which is why this is keyed on the version rather than
|
|
231
|
+
* the number.
|
|
232
|
+
*/
|
|
233
|
+
private cacheVersion;
|
|
234
|
+
/**
|
|
235
|
+
* Decode a Storage chunk. `gen` is the segment's current generation, resolved **once per op** by the caller (not
|
|
236
|
+
* per chunk — that would put a registry re-resolve on every chunk of a count/intersect). The cache is
|
|
237
|
+
* keyed by it, so a load that advances the generation misses the cache and re-reads the new bytes instead of
|
|
238
|
+
* serving a stale decoded chunk (an erased id can't resurrect from a cached superseded chunk). `gen === null`
|
|
239
|
+
* ⇒ the source reports no current generation ⇒ no storage bytes for any chunk, so skip the fetch entirely.
|
|
240
|
+
* `gen === undefined` ⇒ the source can't report a generation (it only ever serves one) ⇒ the key stays
|
|
241
|
+
* generation-free. Superseded-generation entries age out under the LRU ceiling — no active purge.
|
|
242
|
+
*
|
|
243
|
+
* The returned instance is **shared** (it may be the cached one): callers read it or clone it, never mutate it.
|
|
244
|
+
*/
|
|
245
|
+
private storageChunk;
|
|
246
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subject erasure on a loaded segment (GDPR Art. 17): remove **one id** from a segment by rewriting its current
|
|
3
|
+
* generation without that id.
|
|
4
|
+
*
|
|
5
|
+
* There is no per-id delete on an immutable object, and there is no mutable tier to hold a tombstone, so erasure
|
|
6
|
+
* is what every other write in this library is: a new generation. The current generation is streamed chunk by
|
|
7
|
+
* chunk through the ascending writer — every chunk decoded, range-checked and re-encoded, the one holding the id
|
|
8
|
+
* with that bit cleared — then published fenced on the generation it streamed, and that generation is collected immediately
|
|
9
|
+
* (`keep: 0`), so the bit is **physically gone from the bucket when this returns**. Constant memory: one chunk in
|
|
10
|
+
* flight, never the whole segment.
|
|
11
|
+
*
|
|
12
|
+
* **`erased: true` is a claim about every generation of the segment, not only the one it replaced.** A rollback
|
|
13
|
+
* leaves generations above the pointer that were once current and can be made current again, so a holder can sit
|
|
14
|
+
* above the pointer as well as below it, and there can be several. Before it reports `erased: true` the call
|
|
15
|
+
* therefore lists the bucket once more and reads every generation it has not already seen to be clean; any that
|
|
16
|
+
* still holds the id makes it report or throw instead. In the ordinary case that list returns one generation, the
|
|
17
|
+
* current one, and costs one `list`.
|
|
18
|
+
*
|
|
19
|
+
* What it does NOT reach: backups, replicas and noncurrent object versions hold the old object until their own
|
|
20
|
+
* lifecycle removes it. For an at-rest guarantee that survives those, use encryption plus crypto-shred
|
|
21
|
+
* (`destroySegment`), which is per segment — a per-id shred is infeasible because one DEK covers the whole segment.
|
|
22
|
+
*
|
|
23
|
+
* Concurrency. This is a **read-modify-write**, not a plain forward-only publish, and that distinction is where
|
|
24
|
+
* its correctness lives: the new generation is `from` minus one bit, so it is a valid successor to `from` and to
|
|
25
|
+
* nothing else. The publish is therefore fenced on `from` **and on the row it was read from**
|
|
26
|
+
* (`publishGeneration`'s `expectFrom` + `expectToken`) and lands only while the pointer is still exactly there,
|
|
27
|
+
* on that same row. Both halves are needed: a generation number identifies a generation only within one
|
|
28
|
+
* incarnation of a name, and a name that is retired and re-created restarts at `0`, so the number alone matched
|
|
29
|
+
* a different segment entirely. Anything else — a load that published in the meantime, or another
|
|
30
|
+
* erasure that got there first — is reported as `reason: 'superseded'`, `erased: false`, and the caller re-runs
|
|
31
|
+
* against the new generation. A writer that claims the same generation *number* first surfaces as a
|
|
32
|
+
* `WriteConflictError` (write-once).
|
|
33
|
+
*
|
|
34
|
+
* The same answer covers the read half, and for the same reason. An erasure collects with `keep: 0`, which takes
|
|
35
|
+
* every generation below its new pointer — so a racing erasure can delete the generation this call is streaming,
|
|
36
|
+
* and (once it is below the winner's pointer) the object this call just wrote. All three exposed round trips (the
|
|
37
|
+
* reader open, any chunk of the whole-segment rewrite, the verify) therefore report a *reason* rather than a bare
|
|
38
|
+
* `NotFoundError`, which was the same event wearing an unactionable face: through `eraseSubject` it became a note
|
|
39
|
+
* the facade documents as ambiguous about which side of the publish it landed on, telling an operator to triage
|
|
40
|
+
* where the truth was to re-run.
|
|
41
|
+
*
|
|
42
|
+
* Which reason comes from **re-reading the row**, not from assuming a supersession: a moved pointer is
|
|
43
|
+
* `'superseded'`, a tombstoned row `'destroyed'`, a purged row `'absent'`, a row with no pointer
|
|
44
|
+
* `'no-generation'`. A pointer still on `from` means the object it names is genuinely gone — the forbidden
|
|
45
|
+
* `missing-storage-generation` state — and that throws, because no re-run fixes it. That `NotFoundError` is the only
|
|
46
|
+
* signal of that state, so a faulting re-read rethrows it rather than replacing it with a transient-looking
|
|
47
|
+
* registry error.
|
|
48
|
+
*
|
|
49
|
+
* **A refused rewrite deletes its own object when it would outlive the winner above the pointer.** `putImmutable`
|
|
50
|
+
* commits atomically, so a rewrite whose stream throws leaves no object at all. One that completed is either
|
|
51
|
+
* below the winner's pointer, where the winner's collection takes it, or above it: the case where this call took
|
|
52
|
+
* its number after the winner's object was already in the bucket. Up there nothing collects it, and it was derived
|
|
53
|
+
* from a generation the winner has replaced, so when the winner was another erasure it still holds the id that
|
|
54
|
+
* erasure has just reported gone. That object is deleted before this call returns — see `discardRefused`.
|
|
55
|
+
*
|
|
56
|
+
* Without that fence the failure was silent and severe, and both halves were reproduced: `nextGeneration` picks a
|
|
57
|
+
* number above everything in the bucket, so a forward-only publish here always won — discarding a concurrent
|
|
58
|
+
* load's whole set, and letting two concurrent erasures each return `erased: true` while the second one's
|
|
59
|
+
* generation put the first one's id back and collected the generation that had evidenced its removal. A false
|
|
60
|
+
* Art. 17 receipt is the worst output this module can produce.
|
|
61
|
+
*
|
|
62
|
+
* A live read still fetching from the collected generation heals forward to the rewrite, and a pin of it fails with
|
|
63
|
+
* `NotFoundError` for any chunk it has yet to read: that is the documented cost of physical deletion on return. **Do not re-load the id while erasing it**: a load that lands after this rewrite
|
|
64
|
+
* carries whatever its source held, and the library cannot know that source was meant to exclude the id.
|
|
65
|
+
* A load already in flight is the same case: it writes its object above the pointer before it publishes, and an
|
|
66
|
+
* erasure that finds the id in that object deletes it as it does any holder there, so a load that then publishes
|
|
67
|
+
* leaves the pointer naming a missing object.
|
|
68
|
+
*/
|
|
69
|
+
import { type IAuditSink } from './audit.js';
|
|
70
|
+
import type { CodecInterface } from './codec.js';
|
|
71
|
+
import type { Yielder } from './cooperative.js';
|
|
72
|
+
import type { IKeystore } from './crypto.js';
|
|
73
|
+
import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
|
|
74
|
+
/** What {@link eraseIdFromSegment} needs: the objects, the pointer, the codec, and the key material if encrypted. */
|
|
75
|
+
export interface EraseIdDeps {
|
|
76
|
+
readonly storage: IStorageDriver;
|
|
77
|
+
readonly registry: IRegistryDriver;
|
|
78
|
+
/**
|
|
79
|
+
* Bitmap codec — the rewrite decodes and re-encodes every chunk through it. Optional in the type so this stays
|
|
80
|
+
* call-compatible public API; a **flavor** package binds it (see {@link requireCodec}).
|
|
81
|
+
*/
|
|
82
|
+
readonly codec?: CodecInterface;
|
|
83
|
+
/**
|
|
84
|
+
* Keystore for an **encrypted** segment: its registry record carries wrapped DEK(s), and the rewrite reuses
|
|
85
|
+
* that DEK — unwrapping it to decrypt the old generation and encrypt the new one under a `(segment, generation)`
|
|
86
|
+
* -bound AAD. Rewriting an encrypted segment without a keystore throws {@link KeyUnavailableError}.
|
|
87
|
+
*/
|
|
88
|
+
readonly keystore?: IKeystore;
|
|
89
|
+
/** When true, refuse to rewrite a **cleartext** segment (the same guard the read path and a load offer). */
|
|
90
|
+
readonly requireEncryption?: boolean;
|
|
91
|
+
/** Supplying a clock that can yield makes the rewrite cooperative, as it makes a load. */
|
|
92
|
+
readonly clock?: Yielder;
|
|
93
|
+
/** Per-chunk decode ceiling (invariant 5); defaults to 1 MiB. */
|
|
94
|
+
readonly maxBitmapBytes?: number;
|
|
95
|
+
}
|
|
96
|
+
export interface EraseIdResult {
|
|
97
|
+
readonly segment: string;
|
|
98
|
+
readonly namespace?: string;
|
|
99
|
+
/**
|
|
100
|
+
* True iff the id **was** in the segment — in its current generation, or in any other generation still in the
|
|
101
|
+
* bucket, above the pointer or below it — and **no generation of the segment holds it now**. The current
|
|
102
|
+
* generation is one without it, every generation that held it has been deleted (see `collected`), and the call
|
|
103
|
+
* listed the bucket and read what was left before saying so. This is the field an erasure ledger records.
|
|
104
|
+
*/
|
|
105
|
+
readonly erased: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Why the id was not erased, when `erased` is false. `'absent'` (no registry row), `'destroyed'` (a crypto-shred
|
|
108
|
+
* tombstone — already unreadable), `'no-generation'` (a row with no Storage data yet), `'not-member'` (no
|
|
109
|
+
* generation in the bucket holds the id — the common case across a fleet scan), or `'superseded'`.
|
|
110
|
+
*
|
|
111
|
+
* **`'superseded'` means this call did not erase the id, not that the id is still there.** Another writer — a
|
|
112
|
+
* load, another erasure, or a rollback — moved the pointer off the generation this call read, so what it was
|
|
113
|
+
* doing no longer follows from what is current: a rewrite derived from that generation is not a valid successor
|
|
114
|
+
* to the new one, and a generation it meant to delete above the pointer may be the one the pointer now names.
|
|
115
|
+
* Re-run against the new generation: if the id is still present it is erased then; if the racing writer was
|
|
116
|
+
* another erasure of the *same* id, the re-run reports `'not-member'` because it is already gone. Either way the
|
|
117
|
+
* re-run settles it, which is why it is the documented action for this reason and for no other.
|
|
118
|
+
*
|
|
119
|
+
* The first three reasons are also returned when the row changes *underneath* a call already in flight, not
|
|
120
|
+
* only when it is read up front — a concurrent `dropSegment` gives `'destroyed'`, a retention sweep that purges
|
|
121
|
+
* the row gives `'absent'`. A caller branching on `reason` never has to care which it was.
|
|
122
|
+
*/
|
|
123
|
+
readonly reason?: 'absent' | 'destroyed' | 'no-generation' | 'not-member' | 'superseded';
|
|
124
|
+
/**
|
|
125
|
+
* The generation the id was found in (present whenever one was read) — the newest of them, when the current
|
|
126
|
+
* generation did not hold it and several others did.
|
|
127
|
+
*/
|
|
128
|
+
readonly fromGeneration?: number;
|
|
129
|
+
/**
|
|
130
|
+
* The generation written without the id (present whenever one was written, even if `superseded`). A refused
|
|
131
|
+
* rewrite whose object sits above the winner's pointer deletes that object again before it returns.
|
|
132
|
+
*/
|
|
133
|
+
readonly generation?: number;
|
|
134
|
+
/**
|
|
135
|
+
* Generations this call deleted. On a rewrite that is normally `[fromGeneration]`, plus any older orphans. When
|
|
136
|
+
* the current generation did not hold the id, it is every generation below the pointer (`keep: 0` takes them
|
|
137
|
+
* together) and each one above the pointer that held the id; the ones above it that did not hold the id stay,
|
|
138
|
+
* because they are an operator's rollback targets. Empty when nothing was deleted.
|
|
139
|
+
*
|
|
140
|
+
* **It is what THIS call deleted, not the proof that the id is gone** — those differ: a concurrent collector can
|
|
141
|
+
* take a generation holding the id first, and then `erased: true` is returned without it in `collected`, because
|
|
142
|
+
* the claim is about the bucket rather than about who emptied it. Treat a non-empty `collected` as evidence and
|
|
143
|
+
* a missing holder as "someone else got there", never as a failure.
|
|
144
|
+
*
|
|
145
|
+
* A call that cannot establish the claim **throws** `WriteConflictError` or the underlying fault rather than
|
|
146
|
+
* report `erased: true` over bytes that are still there. Four ways that happens: a Storage `delete` fault; a
|
|
147
|
+
* collection pass that could not prove the segment was still the same one (which an ordinary retirement landing
|
|
148
|
+
* mid-call is enough to cause); the same refusal on the path where the current generation did not hold the id
|
|
149
|
+
* and nothing was published at all; and a generation still holding the id when the bucket is listed at the end
|
|
150
|
+
* — one an operator rolled the pointer onto while a rewrite was collecting, say.
|
|
151
|
+
*
|
|
152
|
+
* **Re-run it**, and read what the re-run says rather than assuming it finished the job. The re-run looks for
|
|
153
|
+
* the id in every generation in the bucket, not only the current one, and there are three outcomes:
|
|
154
|
+
*
|
|
155
|
+
* - `erased: true` naming the generation it found the id in — the ordinary case, and the one that gives you
|
|
156
|
+
* the receipt the failed call could not;
|
|
157
|
+
* - `'not-member'` — a racing collector took that generation first. The bit is gone, but **no run reports a
|
|
158
|
+
* receipt for it**, so keep the failed call's error alongside your ledger if you need the audit trail;
|
|
159
|
+
* - `'absent'` — the segment's registry row is gone, so it is no longer a segment at all and
|
|
160
|
+
* `store.eraseSubject` will not even scan it. Anything left in the bucket is an **orphan**: list it with
|
|
161
|
+
* `listGenerations` (`store.generations`) and delete it with `dropSegment` (`store.dropSegment`), which both
|
|
162
|
+
* read the bucket as well as the row. `checkConsistency()` and the generation collection start from the row, so
|
|
163
|
+
* neither reaches a segment that has none.
|
|
164
|
+
*/
|
|
165
|
+
readonly collected: readonly number[];
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Remove `id` from `ref`: rewrite its current generation without it, and delete every other generation that holds
|
|
169
|
+
* it. See the module note for the contract.
|
|
170
|
+
*
|
|
171
|
+
* Emits one `segment.rewrite` audit event at the publish (before the superseded generation is collected), so the
|
|
172
|
+
* compliance record exists the moment the generation without the id is authoritative.
|
|
173
|
+
*/
|
|
174
|
+
export declare function eraseIdFromSegment(ref: SegmentRef, id: number, deps: EraseIdDeps, options?: {
|
|
175
|
+
audit?: IAuditSink;
|
|
176
|
+
}): Promise<EraseIdResult>;
|