@cloudbitmaps/core 0.15.0 → 0.17.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/dist/{chunk-B7IGS3W6.js → chunk-CV7GJO3B.js} +363 -23
- package/dist/chunk-CV7GJO3B.js.map +7 -0
- package/dist/core/codec.d.ts +10 -0
- package/dist/core/cost.d.ts +4 -4
- package/dist/core/crbm-storage-source.d.ts +76 -68
- package/dist/core/engine.d.ts +59 -6
- package/dist/core/erasure.d.ts +2 -1
- package/dist/core/errors.d.ts +23 -0
- package/dist/core/generation-gc.d.ts +69 -45
- package/dist/core/kept-generations.d.ts +40 -0
- package/dist/core/leases.d.ts +119 -0
- package/dist/core/load-input.d.ts +21 -0
- package/dist/core/load.d.ts +33 -12
- package/dist/core/metrics.d.ts +24 -0
- package/dist/core/ports.d.ts +128 -6
- package/dist/core/registry-reap.d.ts +22 -0
- package/dist/core/retention.d.ts +2 -1
- package/dist/core/rollback.d.ts +2 -1
- package/dist/driver-kit.d.ts +1 -0
- package/dist/driver-kit.js +142 -10
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/object-registry.d.ts +34 -3
- package/dist/drivers/_shared/registry.d.ts +7 -9
- package/dist/drivers/localfs/registry.d.ts +4 -0
- package/dist/drivers/memory.d.ts +4 -0
- package/dist/export/index.d.ts +20 -8
- package/dist/index.d.ts +10 -4
- package/dist/index.js +745 -123
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/chunk-B7IGS3W6.js.map +0 -7
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Clock, Rng } from './determinism.js';
|
|
2
|
+
import type { IRegistryDriver, LeaseEntry, RegistryRecord, SegmentRef, Token } from './ports.js';
|
|
3
|
+
/** The longest lease: a `leaseUntil` further than this from the clock is refused. */
|
|
4
|
+
export declare const MAX_LEASE_MS: number;
|
|
5
|
+
/**
|
|
6
|
+
* How much longer than `until` a collector holds a lease, and the most a collector will believe a lease runs past
|
|
7
|
+
* its own clock. It is the tolerated difference between the holder's clock and the collector's, in either direction.
|
|
8
|
+
*/
|
|
9
|
+
export declare const LEASE_SKEW_MS = 60000;
|
|
10
|
+
/** The most live leases a writer records on one segment; the next one is refused with {@link LeaseLimitError}. */
|
|
11
|
+
export declare const MAX_LEASES_PER_SEGMENT = 64;
|
|
12
|
+
/** The most entries a stored row may hold: four times what a writer writes, so a release can raise the cap. */
|
|
13
|
+
export declare const MAX_STORED_LEASES = 256;
|
|
14
|
+
/**
|
|
15
|
+
* Read-modify-write attempts of one take or release before it reports contention. Takers of one row serialise: each
|
|
16
|
+
* round exactly one write lands, so the slowest of `k` simultaneous takers needs `k` attempts. The bound is the cap
|
|
17
|
+
* plus a few, so that the most a segment can hold, all arriving at once, can all land.
|
|
18
|
+
*/
|
|
19
|
+
export declare const LEASE_CAS_ATTEMPTS: number;
|
|
20
|
+
/**
|
|
21
|
+
* Whether an entry holds its generation against a collector reading `now`: it has not ended (`until` plus the margin
|
|
22
|
+
* is ahead), and it does not run further past `now` than any honest holder with a clock inside the margin could have
|
|
23
|
+
* written (`until` at most the longest lease plus the margin ahead). The second clause bounds a corrupt or forged
|
|
24
|
+
* `until` without refusing the row: an entry past it is read and not honoured.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isLive(entry: LeaseEntry, now: number): boolean;
|
|
27
|
+
/** The generations a collector reading `now` must spare, from the row it holds. */
|
|
28
|
+
export declare function heldGenerations(record: RegistryRecord | null, now: number): ReadonlySet<number>;
|
|
29
|
+
/** The entries of a row that are live at `now`, in the row's order. */
|
|
30
|
+
export declare function liveLeases(record: RegistryRecord | null, now: number): readonly LeaseEntry[];
|
|
31
|
+
/**
|
|
32
|
+
* What a writer that fences on the row's token needs to wait out lease churn: a sleep, and the rng that spreads it.
|
|
33
|
+
* Both are optional; without a sleep a retry follows at once.
|
|
34
|
+
*/
|
|
35
|
+
export interface ChurnDeps {
|
|
36
|
+
readonly clock?: {
|
|
37
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
38
|
+
} | undefined;
|
|
39
|
+
readonly rng?: Rng | undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* How many times one writer retries after meeting a row that differs from the one it read only in its leases. Takers of
|
|
43
|
+
* one row serialise: each round exactly one write lands, so a writer racing `k` holders needs up to `k + 1` rounds, and a
|
|
44
|
+
* holder can take and release inside one job, so the bound covers a take and a release by every holder the row can
|
|
45
|
+
* hold, with a few to spare: 2 * {@link MAX_LEASES_PER_SEGMENT} + 8. At waits of up to 25 ms growing to up to 400 ms it is a
|
|
46
|
+
* minute at the very most before the writer reports the conflict it had, and a lease writer that goes on beyond
|
|
47
|
+
* that has stopped being a lease writer and become a flood.
|
|
48
|
+
*/
|
|
49
|
+
export declare const LEASE_ONLY_RETRIES: number;
|
|
50
|
+
/**
|
|
51
|
+
* Whether `now` is `held` with nothing changed but its leases: the same incarnation, and every other field equal,
|
|
52
|
+
* `updatedAt` and the token (which every write moves) aside. `alsoIgnore` names fields the caller's own earlier write
|
|
53
|
+
* changed, so a writer comparing against a row it has itself written does not take its own change for another's.
|
|
54
|
+
*
|
|
55
|
+
* This is the one test that lets a writer fenced on the row's token go on after a lease write: a lease is written by
|
|
56
|
+
* readers, in numbers no operator controls, and it says nothing about the content the writer derived, the pointer it
|
|
57
|
+
* judged or the key it holds. Any other difference is another writer's, and the fence refuses as it always has.
|
|
58
|
+
*/
|
|
59
|
+
export declare function onlyLeasesDiffer(held: RegistryRecord, now: RegistryRecord, alsoIgnore?: readonly (keyof RegistryRecord)[]): boolean;
|
|
60
|
+
/** A writer's count of the lease-only changes it has waited out, and the wait. One per call of the writer. */
|
|
61
|
+
export interface LeaseChurn {
|
|
62
|
+
/**
|
|
63
|
+
* When `now` is `held` changed only in its leases and the bound is not spent: wait, then read the row again with
|
|
64
|
+
* `reread`, and return what it found. The wait comes before the read, so the writer's next write is made against a row
|
|
65
|
+
* as fresh as one round trip, not one as stale as the wait. The caller checks that row against `held` once more (a
|
|
66
|
+
* write may have landed during the wait) and goes on against it, without redoing its work. `undefined` otherwise, and
|
|
67
|
+
* the caller does what it always did.
|
|
68
|
+
*
|
|
69
|
+
* The wait is uniform in `[0, bound)`, the bound starting at 25 ms and doubling to 400 ms, taken on the injected
|
|
70
|
+
* clock's `sleep` and spread by its `rng`. With no `sleep` there is no wait, and the retries follow one another at
|
|
71
|
+
* once; with no `rng` the wait is exactly the bound, so contenders stay in step. The store always supplies both.
|
|
72
|
+
*/
|
|
73
|
+
settle(held: RegistryRecord | null | undefined, now: RegistryRecord | null, reread: () => Promise<RegistryRecord | null>, alsoIgnore?: readonly (keyof RegistryRecord)[]): Promise<{
|
|
74
|
+
readonly row: RegistryRecord | null;
|
|
75
|
+
} | undefined>;
|
|
76
|
+
}
|
|
77
|
+
export declare function leaseChurn(deps: ChurnDeps): LeaseChurn;
|
|
78
|
+
/** What the lease writers need: the registry, the clock that judges liveness and waits, and the rng that spreads waits. */
|
|
79
|
+
export interface LeaseDeps {
|
|
80
|
+
readonly registry: IRegistryDriver;
|
|
81
|
+
readonly clock: Clock;
|
|
82
|
+
readonly rng?: Rng;
|
|
83
|
+
}
|
|
84
|
+
/** What a take asks for. */
|
|
85
|
+
export interface LeaseTake {
|
|
86
|
+
/** Drawn once per pin call, outside any retry, so a write that landed unseen is found again and never duplicated. */
|
|
87
|
+
readonly holder: string;
|
|
88
|
+
/** The generation to hold. */
|
|
89
|
+
readonly generation: number;
|
|
90
|
+
/** Epoch-ms, already checked against the caller's clock. */
|
|
91
|
+
readonly until: number;
|
|
92
|
+
/** The row the caller read; the first write is made against it, with no row read of its own. */
|
|
93
|
+
readonly row: RegistryRecord;
|
|
94
|
+
/** The generation must still be the row's pointer on every attempt: a pin of the current generation. */
|
|
95
|
+
readonly current: boolean;
|
|
96
|
+
}
|
|
97
|
+
/** A lease that landed: the token its write returned, and the row that write was made against. */
|
|
98
|
+
export interface TakenLease {
|
|
99
|
+
readonly token: Token;
|
|
100
|
+
readonly row: RegistryRecord;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Write a lease entry for `take.holder`, fenced on the row it was made against, so a lease that lands proves the row
|
|
104
|
+
* was unchanged since the caller read it. Returns `'moved'` when `take.current` and the pointer is no longer at the
|
|
105
|
+
* generation (the caller starts over from a fresh read). Throws {@link NotFoundError} for a row that is gone,
|
|
106
|
+
* destroyed or has no such published generation, {@link LeaseLimitError} when the segment already has
|
|
107
|
+
* {@link MAX_LEASES_PER_SEGMENT} live leases (nothing is written), `WriteConflictError` after
|
|
108
|
+
* {@link LEASE_CAS_ATTEMPTS} lost races, and the registry's own `TransientError` for a write that stayed unanswered.
|
|
109
|
+
*
|
|
110
|
+
* Idempotent by holder: an entry for the holder is replaced, so a retry of a whole pin re-takes without duplicating,
|
|
111
|
+
* and a write that got no answer is settled by reading the row for the holder's entry.
|
|
112
|
+
*/
|
|
113
|
+
export declare function takeLease(ref: SegmentRef, deps: LeaseDeps, take: LeaseTake): Promise<TakenLease | 'moved'>;
|
|
114
|
+
/**
|
|
115
|
+
* Remove `holder`'s entry, and every entry that has ended, in one write. Idempotent: a row that is gone, destroyed or
|
|
116
|
+
* has no entry for the holder is already released. Throws `WriteConflictError` after {@link LEASE_CAS_ATTEMPTS} lost
|
|
117
|
+
* races and the registry's `TransientError` for a write that stayed unanswered over an unchanged row.
|
|
118
|
+
*/
|
|
119
|
+
export declare function releaseLease(ref: SegmentRef, deps: LeaseDeps, holder: string): Promise<void>;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* All of it runs before the load's first request, so a malformed input costs no round trip and writes nothing.
|
|
11
11
|
*/
|
|
12
12
|
import type { CodecBitmap, CodecInterface } from './codec.js';
|
|
13
|
+
import type { CombinedChunk } from './engine.js';
|
|
13
14
|
/**
|
|
14
15
|
* Anything that serializes itself to portable Roaring: `roaring`'s `RoaringBitmap32` is one. Typed by shape, so
|
|
15
16
|
* no codec's own class appears in a signature. A bitmap that can also say how large its serialization is
|
|
@@ -44,6 +45,14 @@ export declare class DecodedLoadInput {
|
|
|
44
45
|
readonly bitmap: CodecBitmap;
|
|
45
46
|
constructor(bitmap: CodecBitmap);
|
|
46
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* A combine's result as the bitmaps of its chunks, ascending by key: written as they are, never id by id. Made only by
|
|
50
|
+
* {@link loadSegmentChunks}, never by {@link prepareLoadInput}, so no input a load is handed can be one.
|
|
51
|
+
*/
|
|
52
|
+
export declare class ChunkLoadInput {
|
|
53
|
+
readonly chunks: AsyncIterable<CombinedChunk>;
|
|
54
|
+
constructor(chunks: AsyncIterable<CombinedChunk>);
|
|
55
|
+
}
|
|
47
56
|
/**
|
|
48
57
|
* Check a load's input and decode a bitmap input, before the load makes any request.
|
|
49
58
|
*
|
|
@@ -53,3 +62,15 @@ export declare class DecodedLoadInput {
|
|
|
53
62
|
* and anything that is none of the three inputs. An error the caller's own `serialize` throws propagates as it is.
|
|
54
63
|
*/
|
|
55
64
|
export declare function prepareLoadInput(input: LoadInput, codec: CodecInterface): Iterable<number> | AsyncIterable<number> | DecodedLoadInput;
|
|
65
|
+
/**
|
|
66
|
+
* Portable bytes a caller holds, checked and decoded by the codec's safe deserializer: the one check every bitmap
|
|
67
|
+
* input of a load goes through, and what a flavor offers its users to decode bytes they hold themselves.
|
|
68
|
+
*
|
|
69
|
+
* The bytes must be a `Uint8Array` (a Node `Buffer` is one), at most {@link MAX_SERIALIZED_LOAD_BYTES}, one whole
|
|
70
|
+
* bitmap and structurally sound. A view is read over the bytes it really holds, and bytes in a `SharedArrayBuffer`
|
|
71
|
+
* are copied first. An empty or detached buffer is the empty bitmap.
|
|
72
|
+
*
|
|
73
|
+
* @param what names the input in a refusal's message.
|
|
74
|
+
* @throws {ValidationError} for anything that is not such bytes; nothing reaches the codec's native decoder then.
|
|
75
|
+
*/
|
|
76
|
+
export declare function decodeSerialized(bytes: unknown, codec: CodecInterface, what?: string): CodecBitmap;
|
package/dist/core/load.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* object, so it stays, and collection takes it like any other generation once one above it is current.
|
|
21
21
|
*/
|
|
22
22
|
import { type IAuditSink } from './audit.js';
|
|
23
|
-
import { type CodecInterface } from './codec.js';
|
|
23
|
+
import { type CodecBitmap, type CodecInterface } from './codec.js';
|
|
24
24
|
import type { Clock, Rng } from './determinism.js';
|
|
25
25
|
import type { IKeystore } from './crypto.js';
|
|
26
26
|
import { type LoadInput } from './load-input.js';
|
|
@@ -47,10 +47,10 @@ export interface LoadDeps {
|
|
|
47
47
|
*/
|
|
48
48
|
readonly readRetry?: ReadRetry;
|
|
49
49
|
/**
|
|
50
|
-
* Collect by listing the segment's objects after the publish, whatever `keep` is. Absent, a load
|
|
51
|
-
*
|
|
52
|
-
* out of the window, and lists every sixteenth generation to take what that leaves; the generations
|
|
53
|
-
*
|
|
50
|
+
* Collect by listing the segment's objects after the publish, whatever `keep` is. Absent, a load whose row records
|
|
51
|
+
* the generations it keeps and that found nothing above its pointer deletes by name the generations its publish
|
|
52
|
+
* pushed out of the window, and lists every sixteenth generation to take what that leaves; the generations no list
|
|
53
|
+
* names wait for that listing. Set it for a caller whose `keep` promises every generation below the new
|
|
54
54
|
* one beyond the window is gone when the call returns: the `*Into` verbs, whose `keep` is how an operator clears
|
|
55
55
|
* a destination that earlier materialisations kept in full.
|
|
56
56
|
*/
|
|
@@ -95,12 +95,14 @@ export interface LoadOptions {
|
|
|
95
95
|
* longest pinned job runs, on every writer that loads the segment — see "Generations and `keep`" in the loading
|
|
96
96
|
* guide.
|
|
97
97
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* take whatever that pass leaves, such as
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
98
|
+
* The segment's row records the generations a load keeps, up to 64, so a load that found nothing above the pointer
|
|
99
|
+
* collects without listing: it deletes the generations its publish pushed out of the window, and lists the segment's
|
|
100
|
+
* objects on every sixteenth generation to take whatever that pass leaves, such as an object a crashed load left
|
|
101
|
+
* below the pointer. A `keep` above 64 records no list and lists on every load, and so does the first load of a
|
|
102
|
+
* row that records none, which then records it; so does a load whose check found the current generation's object
|
|
103
|
+
* gone: the guard's read of it, or, when the guard took the size from the row's summary, one zero-byte read made
|
|
104
|
+
* before a `keep` of 1 or more takes a name. A `keep` at least the generation published collects nothing and asks
|
|
105
|
+
* for nothing.
|
|
104
106
|
*/
|
|
105
107
|
readonly keep?: number;
|
|
106
108
|
/**
|
|
@@ -154,7 +156,7 @@ export interface LoadResult {
|
|
|
154
156
|
/**
|
|
155
157
|
* The superseded generations collected after publishing. Empty when nothing was published.
|
|
156
158
|
*
|
|
157
|
-
* When collection deleted by name (see {@link LoadOptions.keep})
|
|
159
|
+
* When collection deleted by name (see {@link LoadOptions.keep}) these are the names it deleted, and one
|
|
158
160
|
* may have been gone already: a delete of an absent object succeeds on every backend and does not say so, so the
|
|
159
161
|
* list names what the pass asked the bucket to delete, not what it found there. A listing pass names the
|
|
160
162
|
* generations it found and deleted. Neither is a receipt, since a concurrent collector may have taken a generation
|
|
@@ -174,3 +176,22 @@ export interface LoadResult {
|
|
|
174
176
|
* interesting cases (a guard tripped, a racing writer won) are operational facts rather than faults.
|
|
175
177
|
*/
|
|
176
178
|
export declare function loadSegment(ref: SegmentRef, input: LoadInput, deps: LoadDeps, options?: LoadOptions): Promise<LoadResult>;
|
|
179
|
+
/**
|
|
180
|
+
* {@link loadSegment} for the result of a combine, as the chunks it is made of: each `{ chunkKey, bitmap }` is the
|
|
181
|
+
* bitmap of one 16-bit chunk's remainders, and they ascend by key. The chunks are written as they are, so no id is built
|
|
182
|
+
* for a value. What the `*Into` verbs call; the unwired form of them, as {@link loadSegment} is of `store.load`.
|
|
183
|
+
*
|
|
184
|
+
* Every chunk is checked before anything is written. Its bitmap must be one the `codec` made (`codec.owns`, so a codec
|
|
185
|
+
* without it refuses every chunk), its key an integer in `[0, 65535]` above the one before, and its values 16-bit;
|
|
186
|
+
* anything else is a {@link ValidationError}. An empty bitmap is left out, as no empty chunk is stored.
|
|
187
|
+
*
|
|
188
|
+
* **The load consumes the bitmaps it is given.** Writing a chunk may re-encode its bitmap in place for size, which
|
|
189
|
+
* changes its representation and never its members, so pass bitmaps you made for this call and do not reuse them. A
|
|
190
|
+
* bitmap the chunk cache holds, or one a caller still reads, is not one to pass.
|
|
191
|
+
*
|
|
192
|
+
* Everything else is `loadSegment`'s, the options, the result and the refusals included.
|
|
193
|
+
*/
|
|
194
|
+
export declare function loadSegmentChunks(ref: SegmentRef, chunks: AsyncIterable<{
|
|
195
|
+
readonly chunkKey: number;
|
|
196
|
+
readonly bitmap: CodecBitmap;
|
|
197
|
+
}>, deps: LoadDeps, options?: LoadOptions): Promise<LoadResult>;
|
package/dist/core/metrics.d.ts
CHANGED
|
@@ -92,6 +92,30 @@ export type MetricEvent = {
|
|
|
92
92
|
readonly kind: 'op';
|
|
93
93
|
readonly name: MetricOpName;
|
|
94
94
|
readonly ms: number;
|
|
95
|
+
} | {
|
|
96
|
+
/**
|
|
97
|
+
* A once-only note that a setting looks too small for how the store is used. It is not a fault: nothing failed.
|
|
98
|
+
* Emitted by a backend that can read the setting, never on a request's path after the first, and silent for
|
|
99
|
+
* anything the backend cannot read, so no event does not mean the setting is large enough.
|
|
100
|
+
*
|
|
101
|
+
* `'socket-pool-below-window'`: the S3 client's pool is smaller than `threshold` sockets, twice `concurrency`.
|
|
102
|
+
* `concurrency` is the default window of a combine (32), since a combine's own `concurrency` is chosen per call
|
|
103
|
+
* and the backend cannot see it: a store that always passes a lower one can ignore the event. `maxSockets` is
|
|
104
|
+
* the `maxSockets` of the agent for the scheme of the client's endpoint, or, when the client does not expose an
|
|
105
|
+
* endpoint, the larger of the agents its handler has made (the event fires only when all are below `threshold`).
|
|
106
|
+
* `driver` and `bucket` say which backend the pool
|
|
107
|
+
* belongs to; they never carry a credential, an endpoint or a key.
|
|
108
|
+
*
|
|
109
|
+
* A sink that switches on `kind` with an exhaustive `never` check stops compiling at this variant: add a case
|
|
110
|
+
* for `'advisory'` (ignoring it is fine). A sink with a `default` branch needs no change.
|
|
111
|
+
*/
|
|
112
|
+
readonly kind: 'advisory';
|
|
113
|
+
readonly code: 'socket-pool-below-window';
|
|
114
|
+
readonly driver: string;
|
|
115
|
+
readonly bucket: string;
|
|
116
|
+
readonly maxSockets: number;
|
|
117
|
+
readonly threshold: number;
|
|
118
|
+
readonly concurrency: number;
|
|
95
119
|
};
|
|
96
120
|
/**
|
|
97
121
|
* The sink you plug in. One method, synchronous, fire-and-forget. It must never throw back into the
|
package/dist/core/ports.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { BlobSink } from './blob.js';
|
|
11
11
|
import type { WrappedDek } from './crypto.js';
|
|
12
|
+
import type { IMetricsSink } from './metrics.js';
|
|
12
13
|
/** Opaque optimistic-concurrency token — unique per write, compared by equality only. */
|
|
13
14
|
export type Token = string;
|
|
14
15
|
/**
|
|
@@ -334,6 +335,20 @@ export interface SealedRegistrySummary {
|
|
|
334
335
|
*/
|
|
335
336
|
readonly sealed: string;
|
|
336
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* One lease on a generation. It names no fingerprint: a collector spares a generation by number and cannot compare an
|
|
340
|
+
* object without reading it, and a purge and re-create drops the whole list with the row. A pin's own fingerprint,
|
|
341
|
+
* which its reads check, stops a number retaken after a rollback being read as the leased object; the entry then spares a
|
|
342
|
+
* different object from collection until it ends, at most the longest lease.
|
|
343
|
+
*/
|
|
344
|
+
export interface LeaseEntry {
|
|
345
|
+
/** 16 lowercase hex digits drawn once per pin call; names the holder, so a retried write is idempotent. */
|
|
346
|
+
readonly holder: string;
|
|
347
|
+
/** The generation held. */
|
|
348
|
+
readonly generation: number;
|
|
349
|
+
/** Epoch-ms the lease runs to on the holder's clock; a collector holds it for the skew margin longer. */
|
|
350
|
+
readonly until: number;
|
|
351
|
+
}
|
|
337
352
|
/**
|
|
338
353
|
* One registry row — the authoritative per-segment record. Exactly one per segment.
|
|
339
354
|
*/
|
|
@@ -376,6 +391,21 @@ export interface RegistryRecord extends SegmentRef {
|
|
|
376
391
|
* correct, and a reader then opens the generation instead. Trusted only for the generation it names.
|
|
377
392
|
*/
|
|
378
393
|
readonly summary?: RegistrySummary;
|
|
394
|
+
/**
|
|
395
|
+
* The generations below `currentGen` that the last publish kept as its grace window, ascending. Optional: absent
|
|
396
|
+
* means the row does not know which generations are kept, and a load then lists the bucket to collect and records
|
|
397
|
+
* them; an empty list means none are. Trusted only while every entry is below `currentGen`. A patch that moves
|
|
398
|
+
* `currentGen` without naming it drops it.
|
|
399
|
+
*/
|
|
400
|
+
readonly keptGens?: readonly number[];
|
|
401
|
+
/**
|
|
402
|
+
* The leases on this segment's generations: each keeps one generation out of a load's collection until it ends.
|
|
403
|
+
* Optional; absent means none. Unlike {@link keptGens} it does not follow the pointer: a patch that moves
|
|
404
|
+
* `currentGen` leaves it as it was, and only a patch that names it, or the row going away, changes it. Entries
|
|
405
|
+
* past their end are ignored by every collector and pruned by the next write that names the list. A row's identity
|
|
406
|
+
* is its token, so a purged and re-created segment starts with none.
|
|
407
|
+
*/
|
|
408
|
+
readonly leases?: readonly LeaseEntry[];
|
|
379
409
|
/** Epoch-ms of creation / last mutation (from the driver's injected clock). */
|
|
380
410
|
readonly createdAt: number;
|
|
381
411
|
readonly updatedAt: number;
|
|
@@ -402,6 +432,8 @@ export interface NewRegistryRecord {
|
|
|
402
432
|
readonly residency?: GovernanceMeta;
|
|
403
433
|
/** Must name `currentGen` when given. */
|
|
404
434
|
readonly summary?: RegistrySummary;
|
|
435
|
+
/** Every entry below `currentGen`, ascending; see {@link RegistryRecord.keptGens}. */
|
|
436
|
+
readonly keptGens?: readonly number[];
|
|
405
437
|
}
|
|
406
438
|
/**
|
|
407
439
|
* Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity + audit + token are off-limits).
|
|
@@ -409,9 +441,27 @@ export interface NewRegistryRecord {
|
|
|
409
441
|
* Presence-based: a field the patch does not mention is left as it was. `summary` is the one exception, because it
|
|
410
442
|
* describes the current generation: a patch that moves `currentGen` and does not mention `summary` drops the old
|
|
411
443
|
* one rather than keep a description of another generation. A `summary` the patch gives must name the
|
|
412
|
-
* `currentGen` the row will have.
|
|
444
|
+
* `currentGen` the row will have. `keptGens` follows the pointer the same way: a patch that moves `currentGen` and does
|
|
445
|
+
* not mention it drops it, and one that gives it must name only generations below the `currentGen` the row will have.
|
|
413
446
|
*/
|
|
414
|
-
export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency' | 'summary'>>;
|
|
447
|
+
export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency' | 'summary' | 'keptGens' | 'leases'>>;
|
|
448
|
+
/**
|
|
449
|
+
* What a caller passes {@link IRegistryDriver.create} or {@link IRegistryDriver.compareAndSwap} besides the write itself.
|
|
450
|
+
*/
|
|
451
|
+
export interface RegistryWriteOptions {
|
|
452
|
+
/**
|
|
453
|
+
* The row the caller read, and is writing against: the record a {@link IRegistryDriver.get} of this registry returned
|
|
454
|
+
* to it, or `null` when that read found no row. A driver that would read the row to learn the version its conditional
|
|
455
|
+
* write is fenced on can use the one it observed then, and send the write at once.
|
|
456
|
+
*
|
|
457
|
+
* It is a hint, and never the fence: the write is still conditioned on the store's own version of the row, so a row
|
|
458
|
+
* that has changed since makes it fail as a lost race does, with {@link WriteConflictError}. A driver reads the row
|
|
459
|
+
* as it does without a hint when it does not recognise the record (one it did not return, or one that is not of
|
|
460
|
+
* this row), or when `held` and the `expected` token of a `compareAndSwap` disagree. A write that gets no answer is
|
|
461
|
+
* settled by reading the row, never by this record. A driver that ignores the option reads the row.
|
|
462
|
+
*/
|
|
463
|
+
readonly held?: RegistryRecord | null;
|
|
464
|
+
}
|
|
415
465
|
/** Capabilities a registry driver advertises; validated fail-fast at wiring time. */
|
|
416
466
|
export interface RegCaps {
|
|
417
467
|
/** REQUIRED — `currentGen` feeds read correctness + the publish CAS, so reads must be strongly consistent. */
|
|
@@ -436,6 +486,47 @@ export interface RegCaps {
|
|
|
436
486
|
*/
|
|
437
487
|
readonly conditionalDelete?: boolean;
|
|
438
488
|
}
|
|
489
|
+
/** What {@link IRegistryDriver.reapLegacyTombstones} is asked to do. */
|
|
490
|
+
export interface ReapLegacyTombstonesOptions {
|
|
491
|
+
/** Only rows in this namespace; every namespace, the library's bookkeeping namespaces included, when absent. */
|
|
492
|
+
readonly namespace?: string;
|
|
493
|
+
/** Count what would be removed and remove nothing. */
|
|
494
|
+
readonly dryRun: boolean;
|
|
495
|
+
/** The most rows to remove (or, in a dry run, to count as removable) in this call. */
|
|
496
|
+
readonly limit: number;
|
|
497
|
+
}
|
|
498
|
+
/** What a reap of the tombstones a registry holds found. Every count is of rows, not requests. */
|
|
499
|
+
export interface ReapLegacyTombstonesResult {
|
|
500
|
+
/** The call was a dry run: nothing was removed, and `wouldReap` says what a real run removes. */
|
|
501
|
+
readonly dryRun: boolean;
|
|
502
|
+
/** Rows read. Rows the `limit` stopped the run before reading are not counted. */
|
|
503
|
+
readonly examined: number;
|
|
504
|
+
/** Rows removed. Always 0 in a dry run. */
|
|
505
|
+
readonly reaped: number;
|
|
506
|
+
/** Rows a dry run found removable. Always 0 in a real run. */
|
|
507
|
+
readonly wouldReap: number;
|
|
508
|
+
/**
|
|
509
|
+
* The `limit` was spent and the listing had more keys: rows were left unread, and they may or may not be removable,
|
|
510
|
+
* so run again. A run is not resumable: each lists and reads from the start, so a dry run with a `limit` reports the
|
|
511
|
+
* same first rows every time, and `wouldReap` is a total only when this is `false`.
|
|
512
|
+
*/
|
|
513
|
+
readonly limited: boolean;
|
|
514
|
+
/** Rows read and left, by why. */
|
|
515
|
+
readonly skipped: {
|
|
516
|
+
/** A live (`active`, not deleted) row. */
|
|
517
|
+
readonly live: number;
|
|
518
|
+
/** A `destroyed` row that is not `deleted`: a crypto-shred's or a drop's tombstone, kept as the attestation of an erasure. */
|
|
519
|
+
readonly destroyed: number;
|
|
520
|
+
/**
|
|
521
|
+
* A deleted row that carries an incarnation id, written by a 0.12 or later release. On a registry that removes rows,
|
|
522
|
+
* `delete` and the sweep remove such a row themselves; one left while the registry could not remove rows (GCS by
|
|
523
|
+
* default, S3 on a custom endpoint) stays, and this call, which refuses on such a registry, does not remove it.
|
|
524
|
+
*/
|
|
525
|
+
readonly incarnated: number;
|
|
526
|
+
/** A row that changed, or was already gone (a 404), between its read and its delete, so the delete (fenced on the version read) refused. */
|
|
527
|
+
readonly raced: number;
|
|
528
|
+
};
|
|
529
|
+
}
|
|
439
530
|
/**
|
|
440
531
|
* Refuse, with `UnsupportedError`, a write that would write an object before its row when the registry reports it
|
|
441
532
|
* cannot write a row (see {@link RegCaps.canWrite}). Called before the write's first request.
|
|
@@ -479,6 +570,14 @@ export interface StorageBackend {
|
|
|
479
570
|
readonly storage: IStorageDriver;
|
|
480
571
|
/** Where the `currentGen` pointer, the discovery index and the wrapped DEKs live. */
|
|
481
572
|
readonly registry: IRegistryDriver;
|
|
573
|
+
/**
|
|
574
|
+
* Optional. A store with a metrics sink calls this once, as it is built, with that sink (already safe to call: it
|
|
575
|
+
* never throws back). A backend that has something to say about its own setup, such as a pool it can read and finds
|
|
576
|
+
* small, keeps the sink and emits an `advisory` event to it later. A backend without it is simply never told. One backend
|
|
577
|
+
* may be handed to several stores, and is called once by each, with each store's own sink. It must not throw: an
|
|
578
|
+
* exception propagates from the store's constructor, so a backend's bug fails fast rather than going unseen.
|
|
579
|
+
*/
|
|
580
|
+
attachMetrics?(sink: IMetricsSink): void;
|
|
482
581
|
}
|
|
483
582
|
/**
|
|
484
583
|
* The brand key, for the type-only `declare` field a backend class carries.
|
|
@@ -560,12 +659,20 @@ export interface IRegistryDriver {
|
|
|
560
659
|
* serving what it holds and asks again shortly, while any other error reaches the read that met it.
|
|
561
660
|
*/
|
|
562
661
|
get(ref: SegmentRef): Promise<RegistryRecord | null>;
|
|
563
|
-
/**
|
|
564
|
-
|
|
662
|
+
/**
|
|
663
|
+
* Create the row; throws {@link WriteConflictError} if it already exists (use CAS to mutate). With
|
|
664
|
+
* `options.held` of `null`, the caller found no row: a driver may send the create-only write without reading first,
|
|
665
|
+
* and one that then loses it reads the row, as it does without the hint.
|
|
666
|
+
*/
|
|
667
|
+
create(ref: SegmentRef, record: NewRegistryRecord, options?: RegistryWriteOptions): Promise<{
|
|
565
668
|
token: Token;
|
|
566
669
|
}>;
|
|
567
|
-
/**
|
|
568
|
-
|
|
670
|
+
/**
|
|
671
|
+
* Server-side compare-and-set: apply `patch` iff the stored token equals `expected`, else `WriteConflictError`.
|
|
672
|
+
* `options.held` is the row the caller read, which a driver may use to skip its own read; see
|
|
673
|
+
* {@link RegistryWriteOptions}.
|
|
674
|
+
*/
|
|
675
|
+
compareAndSwap(ref: SegmentRef, expected: Token, patch: RegistryPatch, options?: RegistryWriteOptions): Promise<{
|
|
569
676
|
token: Token;
|
|
570
677
|
}>;
|
|
571
678
|
/**
|
|
@@ -594,5 +701,20 @@ export interface IRegistryDriver {
|
|
|
594
701
|
* created after the decision. A third-party driver that ignores `expected` keeps the unfenced behaviour.
|
|
595
702
|
*/
|
|
596
703
|
delete(ref: SegmentRef, expected?: Token): Promise<void>;
|
|
704
|
+
/**
|
|
705
|
+
* Optional: remove the `deleted: true` rows with no incarnation id, which a release before 0.12 left, or a 0.12 or
|
|
706
|
+
* later release left by deleting a row born before 0.12, so that no later full `list` reads them. Only a registry that
|
|
707
|
+
* keeps such rows in its backend implements it; the object registries do.
|
|
708
|
+
*
|
|
709
|
+
* It removes a row only if it is `deleted: true` with no incarnation id (whatever its `status`), and only by a delete
|
|
710
|
+
* the backend applies under the version it read (see {@link RegCaps.conditionalDelete}), so a `create` that lands over
|
|
711
|
+
* the row first wins. A registry that cannot delete under that precondition throws `CapabilityError` before its first
|
|
712
|
+
* request. It never touches a live row, a `destroyed` row that is not `deleted`, or a deleted row with an incarnation
|
|
713
|
+
* id, so it does not remove every row a full `list` reads. If `conditionalDelete` was set on a backend that ignores the
|
|
714
|
+
* precondition, the delete is unfenced.
|
|
715
|
+
*
|
|
716
|
+
* Not resumable: every call lists and reads from the start (R reads for R rows, whatever the `limit`).
|
|
717
|
+
*/
|
|
718
|
+
reapLegacyTombstones?(options: ReapLegacyTombstonesOptions): Promise<ReapLegacyTombstonesResult>;
|
|
597
719
|
}
|
|
598
720
|
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IRegistryDriver, ReapLegacyTombstonesResult } from './ports.js';
|
|
2
|
+
/** Rows one call removes when no `limit` says otherwise. */
|
|
3
|
+
export declare const DEFAULT_REAP_LIMIT = 1000;
|
|
4
|
+
/** The keys `reapRegistryTombstones` takes. */
|
|
5
|
+
export interface ReapRegistryTombstonesOptions {
|
|
6
|
+
/** Only rows in this namespace. Omit to cover every namespace, the library's own bookkeeping rows included. */
|
|
7
|
+
readonly namespace?: string;
|
|
8
|
+
/** Count what a real run would remove, and remove nothing. Needs no `confirmNoLegacyWriters`. */
|
|
9
|
+
readonly dryRun?: boolean;
|
|
10
|
+
/** Required `true` for a real run: no process on a release before 0.12 writes this registry. */
|
|
11
|
+
readonly confirmNoLegacyWriters?: boolean;
|
|
12
|
+
/** The most rows to remove in this call (default {@link DEFAULT_REAP_LIMIT}); a run that stops on it says `limited`. */
|
|
13
|
+
readonly limit?: number;
|
|
14
|
+
}
|
|
15
|
+
/** What a reap found and did: see {@link ReapLegacyTombstonesResult}. */
|
|
16
|
+
export type ReapRegistryTombstonesResult = ReapLegacyTombstonesResult;
|
|
17
|
+
/**
|
|
18
|
+
* Remove the `deleted: true` rows with no incarnation id from `registry`, or count them (`dryRun`).
|
|
19
|
+
*
|
|
20
|
+
* Cost over R rows read and E removed: `ceil(R / 1000)` LIST requests, R GETs and E DELETEs.
|
|
21
|
+
*/
|
|
22
|
+
export declare function reapRegistryTombstones(registry: IRegistryDriver, options?: ReapRegistryTombstonesOptions): Promise<ReapRegistryTombstonesResult>;
|
package/dist/core/retention.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ChurnDeps } from './leases.js';
|
|
1
2
|
import type { GovernanceMeta, IRegistryDriver, SegmentRef } from './ports.js';
|
|
2
3
|
/**
|
|
3
4
|
* The smallest accepted `expiresAt`: 2001-09-09, the point below which an epoch-**seconds** value would land.
|
|
@@ -18,7 +19,7 @@ export interface RetentionPolicy {
|
|
|
18
19
|
*/
|
|
19
20
|
readonly expiresAt: number;
|
|
20
21
|
}
|
|
21
|
-
export interface RetentionDeps {
|
|
22
|
+
export interface RetentionDeps extends ChurnDeps {
|
|
22
23
|
readonly registry: IRegistryDriver;
|
|
23
24
|
}
|
|
24
25
|
export interface SetRetentionResult {
|
package/dist/core/rollback.d.ts
CHANGED
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { type IAuditSink } from './audit.js';
|
|
20
20
|
import type { IKeystore } from './crypto.js';
|
|
21
|
+
import { type ChurnDeps } from './leases.js';
|
|
21
22
|
import type { IStorageDriver, GenerationMetadata, IRegistryDriver, SegmentRef } from './ports.js';
|
|
22
23
|
/** What the generation helpers need: the objects, and the pointer that says which one is current. */
|
|
23
|
-
export interface GenerationListDeps {
|
|
24
|
+
export interface GenerationListDeps extends ChurnDeps {
|
|
24
25
|
readonly storage: IStorageDriver;
|
|
25
26
|
readonly registry: IRegistryDriver;
|
|
26
27
|
/**
|
package/dist/driver-kit.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { GenKey, IRegistryDriver, IStorageDriver, SegmentRef, StorageBackend, StorageCaps, } from './core/ports.js';
|
|
2
2
|
export { brandAsBackend, STORAGE_BACKEND } from './core/ports.js';
|
|
3
|
+
export type { IMetricsSink, MetricEvent } from './core/metrics.js';
|
|
3
4
|
export { IntegrityError, isNotFoundError, isValidationError, isWriteConflictError, NotFoundError, TransientError, ValidationError, WriteConflictError, } from './core/errors.js';
|
|
4
5
|
export { validateSegmentRef } from './core/validate.js';
|
|
5
6
|
export type { BlobSink } from './core/blob.js';
|