@cloudbitmaps/core 0.11.2 → 0.13.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-5OMC5U7U.js → chunk-5W3XS2IU.js} +292 -22
- package/dist/chunk-5W3XS2IU.js.map +7 -0
- package/dist/core/audit.d.ts +6 -0
- package/dist/core/chunk-window.d.ts +24 -0
- package/dist/core/codec.d.ts +33 -1
- package/dist/core/consistency.d.ts +21 -2
- package/dist/core/cost.d.ts +91 -10
- package/dist/core/crbm/format.d.ts +42 -3
- package/dist/core/crbm/reader.d.ts +45 -3
- package/dist/core/crbm/writer.d.ts +32 -3
- package/dist/core/crbm-storage-source.d.ts +204 -23
- package/dist/core/crypto.d.ts +11 -7
- package/dist/core/determinism.d.ts +7 -0
- package/dist/core/engine.d.ts +41 -8
- package/dist/core/erase-id.d.ts +23 -3
- package/dist/core/erasure.d.ts +8 -4
- package/dist/core/generation-gc.d.ts +102 -4
- package/dist/core/load-input.d.ts +55 -0
- package/dist/core/load.d.ts +65 -10
- package/dist/core/metadata.d.ts +42 -0
- package/dist/core/metrics.d.ts +7 -1
- package/dist/core/pinned-storage-source.d.ts +2 -1
- package/dist/core/ports.d.ts +119 -13
- package/dist/core/registry-scan.d.ts +6 -0
- package/dist/core/retention-sweep.d.ts +47 -2
- package/dist/core/retry.d.ts +14 -0
- package/dist/core/rollback.d.ts +34 -4
- package/dist/core/summary.d.ts +61 -0
- package/dist/core/token.d.ts +36 -0
- package/dist/core/validate.d.ts +2 -0
- package/dist/driver-kit.js +39 -20
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/entropy.d.ts +5 -0
- package/dist/drivers/_shared/object-registry.d.ts +36 -6
- package/dist/drivers/_shared/registry.d.ts +47 -19
- package/dist/drivers/localfs/registry.d.ts +10 -0
- package/dist/drivers/memory.d.ts +9 -3
- package/dist/drivers/retry/retrying-chunk-source.d.ts +2 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1878 -501
- package/dist/index.js.map +4 -4
- package/dist/testing/fuzz-core.d.ts +7 -6
- package/package.json +1 -1
- package/dist/chunk-5OMC5U7U.js.map +0 -7
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
|
|
1
|
+
import type { GenKey, IStorageDriver, IRegistryDriver, RegistryRecord, SegmentRef } from './ports.js';
|
|
2
2
|
/** The two ports generation bookkeeping needs: the objects, and the pointer that says which one is current. */
|
|
3
3
|
export interface GenerationDeps {
|
|
4
4
|
readonly storage: IStorageDriver;
|
|
@@ -6,7 +6,9 @@ export interface GenerationDeps {
|
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
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.
|
|
9
|
+
* the registry points at *or* that is present in Storage — whichever is higher. The erasure rewrite numbers this
|
|
10
|
+
* way; a load numbers with {@link nextLoadGeneration}, which lists as this does when its existence check finds the
|
|
11
|
+
* number taken or cannot answer.
|
|
10
12
|
*
|
|
11
13
|
* Both are consulted because they can disagree. A load that wrote its object and crashed before publishing leaves
|
|
12
14
|
* an object *above* `currentGen`; a writer that consulted only the pointer would pick that same number and hit the
|
|
@@ -17,6 +19,46 @@ export interface GenerationDeps {
|
|
|
17
19
|
* A segment with no row and no objects starts at 0. An admin/write-path helper, never on the read path.
|
|
18
20
|
*/
|
|
19
21
|
export declare function nextGeneration(ref: SegmentRef, deps: GenerationDeps): Promise<number>;
|
|
22
|
+
/** The number a load takes, and whether one existence check proved it free (so nothing is above the pointer). */
|
|
23
|
+
export interface LoadNumber {
|
|
24
|
+
readonly generation: number;
|
|
25
|
+
/**
|
|
26
|
+
* `true` when the number is `currentGen + 1` and the check found no object holding it. Collection by name rests
|
|
27
|
+
* on it: the row named `generation - 1` as current, so every number the window evicts is below it, and nothing
|
|
28
|
+
* the listing would have kept is a name the pass takes. `false` when the listing numbered the load.
|
|
29
|
+
*/
|
|
30
|
+
readonly checked: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The generation number a load takes, from the row it has already read: `currentGen + 1` (0 with no row or no
|
|
34
|
+
* pointer yet) when no object holds that number, which one existence check proves (a zero-byte `getTail`, one
|
|
35
|
+
* metadata request on every shipped driver); otherwise one above the pointer and above everything listed, as
|
|
36
|
+
* {@link nextGeneration} numbers. The check finding the number taken (a crashed load's orphan, the object of a
|
|
37
|
+
* load still in flight, or the generations a rollback left above the pointer) and the check failing in any way
|
|
38
|
+
* other than "not found" both take the listing, so the listing stays the authority whenever the check cannot
|
|
39
|
+
* prove the number free. A failed check falls back silently: nothing records it, and what it costs is the listing,
|
|
40
|
+
* one more PUT-class request on S3, so a fault that makes every check fail shows only in the bill.
|
|
41
|
+
*
|
|
42
|
+
* A load can therefore take a number **below** an object already in the bucket: an orphan at `currentGen + 2`
|
|
43
|
+
* with `currentGen + 1` free is numbered under, not past. That is as safe as a listing's number. A load never
|
|
44
|
+
* takes a number an object holds, and a put that races onto one fails write-once, so the load reports
|
|
45
|
+
* `superseded`. The orphan above stays unpublished; the load whose check meets it numbers past it by the listing,
|
|
46
|
+
* and collection takes it once a generation above it is current. Like a listing's, the number can be one whose
|
|
47
|
+
* object was deleted (an erasure removes the generations above a rolled-back pointer that held the id), which is
|
|
48
|
+
* why nothing identifies a generation by its number alone: the reader cache, the chunk cache and a pin all key on
|
|
49
|
+
* the number and the row's token, and the token has moved on.
|
|
50
|
+
*/
|
|
51
|
+
export declare function nextLoadGeneration(ref: SegmentRef, deps: GenerationDeps, row: RegistryRecord | null): Promise<LoadNumber>;
|
|
52
|
+
/**
|
|
53
|
+
* How often a load that could collect by name lists instead: every generation divisible by this. A name-only pass
|
|
54
|
+
* takes one generation a load and never looks at the rest, so what it misses (a refused load's object that stayed
|
|
55
|
+
* because the row had changed, extras left by a `keep` that shrank, an object a rollback or an erasure left
|
|
56
|
+
* stranded below the pointer) waits for a listing. The number is a generation's, not a clock's, so the choice needs
|
|
57
|
+
* no state and no time, and a segment that loads cleanly lists once in this many loads at most. The bound is in
|
|
58
|
+
* generation numbers: a rollback moves the pointer down, and the loads that then take the numbers above it count
|
|
59
|
+
* from there.
|
|
60
|
+
*/
|
|
61
|
+
export declare const LIST_COLLECTION_CADENCE = 16;
|
|
20
62
|
/**
|
|
21
63
|
* Garbage-collect superseded Storage generations for a segment: everything strictly below `currentGen`, keeping
|
|
22
64
|
* the most recent `keep` of them as a grace window, so a read still fetching from a just-superseded
|
|
@@ -42,8 +84,8 @@ export declare function nextGeneration(ref: SegmentRef, deps: GenerationDeps): P
|
|
|
42
84
|
* an empty array is not: `eraseIdFromSegment` reads the returned list as the physical half of its erasure
|
|
43
85
|
* receipt, and would otherwise report `erased: true` over bytes still in the bucket. Re-run it.
|
|
44
86
|
*
|
|
45
|
-
* That state is reachable in practice: a `dropSegment` whose Storage sweep threw part-way, or a load that
|
|
46
|
-
*
|
|
87
|
+
* That state is reachable in practice: a `dropSegment` whose Storage sweep threw part-way, or a load that had read
|
|
88
|
+
* the row before the tombstone landed, wrote its object afterwards, and stopped before its refusal deleted it. `dropSegment` re-sweeps
|
|
47
89
|
* and reports whatever it could not reclaim in `generationsRemaining`, but a drop that was never re-run leaves a
|
|
48
90
|
* residual, and this is what eventually collects it from the retention sweep.
|
|
49
91
|
*
|
|
@@ -54,3 +96,59 @@ export declare function nextGeneration(ref: SegmentRef, deps: GenerationDeps): P
|
|
|
54
96
|
export declare function gcOrphanGenerations(ref: SegmentRef, deps: GenerationDeps, options?: {
|
|
55
97
|
keep?: number;
|
|
56
98
|
}): Promise<number[]>;
|
|
99
|
+
/**
|
|
100
|
+
* Collect after a load's publish: by name when `byName` (the load numbered its generation with one existence check,
|
|
101
|
+
* the object its row named was there when the guard looked, and its caller does not ask for a listing) and `keep` is
|
|
102
|
+
* at most one, and by listing otherwise ({@link gcOrphanGenerations}). A load lists in three cases: `keep` of 2 or
|
|
103
|
+
* more, which a name cannot serve because the window then counts generations that may be absent (a refused
|
|
104
|
+
* neighbour's number is a gap, and by name would take one the window promised to keep); a number the listing chose,
|
|
105
|
+
* so something sits above the pointer or the check could not answer, or a current object the load's check found gone; and
|
|
106
|
+
* every {@link LIST_COLLECTION_CADENCE}th generation, which collects what a name-only pass leaves. It makes no
|
|
107
|
+
* request when `keep` is at least the generation published, since no more generations than that exist below it. A
|
|
108
|
+
* load onto a `destroyed` row is refused at its publish and never reaches here; a drop that lands after the publish
|
|
109
|
+
* leaves this pass collecting the tombstone's generations, one by name or all of them by listing, and the drop's own
|
|
110
|
+
* sweep takes the rest.
|
|
111
|
+
*/
|
|
112
|
+
export declare function collectAfterLoad(ref: SegmentRef, deps: GenerationDeps, options: {
|
|
113
|
+
generation: number;
|
|
114
|
+
keep: number;
|
|
115
|
+
byName: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* The object the load's row named as current, which the caller did not open. Before it collects by name a generation
|
|
118
|
+
* the window keeps (a `keep` of 1), the pass looks for this object with one zero-byte read, and lists instead
|
|
119
|
+
* unless that finds it: a name-only pass is safe only while the object the row named is in the bucket.
|
|
120
|
+
*/
|
|
121
|
+
proveCurrent?: GenKey;
|
|
122
|
+
}): Promise<number[]>;
|
|
123
|
+
/**
|
|
124
|
+
* Collect, with no listing, the one generation a publish of `generation` pushed out of a grace window of `keep`
|
|
125
|
+
* (0 or 1): `generation - keep - 1`, when that is a number at all. Returns that name, whether or not an object was
|
|
126
|
+
* there: a delete of an absent object succeeds on every backend and says nothing, so the name returned is the one
|
|
127
|
+
* asked for, not one found, and a generation a concurrent pass, an erasure or a lifecycle rule had already taken is
|
|
128
|
+
* listed all the same. A list is not a receipt (invariant 4), and neither is this.
|
|
129
|
+
*
|
|
130
|
+
* The caller has published `generation` having numbered it with one existence check, so the row named
|
|
131
|
+
* `generation - 1` and every generation the window newly evicts is below it. With `keep` of 0 or 1 that name is
|
|
132
|
+
* the one a listing pass would also take, or one it left to a later pass: never a generation the listing keeps.
|
|
133
|
+
* That rests on the object the row named being in the bucket, and a fault can break it (a lifecycle rule or a
|
|
134
|
+
* partial restore removing it, or an erasure deleting the object of a load whose publish then landed;
|
|
135
|
+
* `checkConsistency` reports the state). The caller, which learns that from its guard's read of the current
|
|
136
|
+
* generation or, when the guard took the size from the row's summary and opened nothing, from one zero-byte read of
|
|
137
|
+
* it (see {@link collectAfterLoad}), lists instead; a load that made neither (`allowEmpty` without `minRetained`)
|
|
138
|
+
* cannot tell, and deletes by name the older generation a listing would have kept as the window. `keep` of 2 or more is refused,
|
|
139
|
+
* because there the window counts the generations that exist, which a name cannot know.
|
|
140
|
+
*
|
|
141
|
+
* Collection never touches the current generation, and the row is re-proved before the delete as every pass does.
|
|
142
|
+
* A row that is gone, or a pointer that has fallen below `generation` (a rollback, or a name purged and re-created
|
|
143
|
+
* with fewer loads), deletes nothing and returns `[]`: the publish already landed, nothing here protects it or
|
|
144
|
+
* needs the result, and a throw would hand the caller a failure for a load that took effect. A pointer that has
|
|
145
|
+
* moved above `generation` does not stop it: a publish landing in the meantime leaves routine collection working.
|
|
146
|
+
* A fault, a registry read or a delete that throws, still propagates, with the pointer at `generation`. The bound
|
|
147
|
+
* is `generation`, the pointer this load's own publish set, so the lower-of-two-pointers rule of a listing pass
|
|
148
|
+
* holds with one observation in place of two. Nothing is deleted when `generation` has nothing below its window,
|
|
149
|
+
* and no request is made.
|
|
150
|
+
*/
|
|
151
|
+
export declare function collectByName(ref: SegmentRef, deps: GenerationDeps, options: {
|
|
152
|
+
generation: number;
|
|
153
|
+
keep: number;
|
|
154
|
+
}): Promise<number[]>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a load accepts: ids, or a whole bitmap as `{ serialized }` bytes or as a `{ bitmap }` that serializes to
|
|
3
|
+
* them.
|
|
4
|
+
*
|
|
5
|
+
* Every bitmap input takes one path. The bytes are size-capped, checked structurally and decoded by the codec's
|
|
6
|
+
* safe deserializer into a bitmap of the codec's own, which then writes its chunks straight from its containers:
|
|
7
|
+
* no id passes through JavaScript. `{ bitmap }` is only a spelling of `{ serialized }`, serialized once, here, so
|
|
8
|
+
* a caller's bitmap is never trusted as a structure and a change made to it after the call is not loaded.
|
|
9
|
+
*
|
|
10
|
+
* All of it runs before the load's first request, so a malformed input costs no round trip and writes nothing.
|
|
11
|
+
*/
|
|
12
|
+
import type { CodecBitmap, CodecInterface } from './codec.js';
|
|
13
|
+
/**
|
|
14
|
+
* Anything that serializes itself to portable Roaring: `roaring`'s `RoaringBitmap32` is one. Typed by shape, so
|
|
15
|
+
* no codec's own class appears in a signature. A bitmap that can also say how large its serialization is
|
|
16
|
+
* (`getSerializationSizeInBytes`) is asked first, so one over the cap is refused before it is serialized.
|
|
17
|
+
*/
|
|
18
|
+
export interface PortableBitmap {
|
|
19
|
+
serialize(format: 'portable'): Uint8Array;
|
|
20
|
+
getSerializationSizeInBytes?(format: 'portable'): number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* What a load writes as the segment's new contents.
|
|
24
|
+
*
|
|
25
|
+
* - **ids**: an iterable or async iterable of integers in `[0, 2^32)`, consumed lazily. Not a `Uint8Array`,
|
|
26
|
+
* `Uint8ClampedArray` or `Buffer`, which a load refuses, since each byte would load as an id.
|
|
27
|
+
* - **`{ serialized }`**: one bitmap in the portable Roaring format, checked before anything is written.
|
|
28
|
+
* - **`{ bitmap }`**: an object with `serialize('portable')`, loaded as `{ serialized: bitmap.serialize('portable') }`.
|
|
29
|
+
*/
|
|
30
|
+
export type LoadInput = Iterable<number> | AsyncIterable<number> | {
|
|
31
|
+
readonly serialized: Uint8Array;
|
|
32
|
+
} | {
|
|
33
|
+
readonly bitmap: PortableBitmap;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* No canonical 32-bit Roaring bitmap serializes to more than this. Under the run cookie, the larger of the two
|
|
37
|
+
* headers, it is a 4-byte cookie, 8,192 bytes of run flags and 8 bytes of header for each of the 65,536
|
|
38
|
+
* containers, and no container's body is over 8,192 bytes: a bitset is 8,192, and a run container is kept only
|
|
39
|
+
* where it is smaller. 537,403,396 bytes, about 512.5 MiB.
|
|
40
|
+
*/
|
|
41
|
+
export declare const MAX_SERIALIZED_LOAD_BYTES: number;
|
|
42
|
+
/** A bitmap input, decoded into the codec's own bitmap: written from its chunks, never id by id. */
|
|
43
|
+
export declare class DecodedLoadInput {
|
|
44
|
+
readonly bitmap: CodecBitmap;
|
|
45
|
+
constructor(bitmap: CodecBitmap);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Check a load's input and decode a bitmap input, before the load makes any request.
|
|
49
|
+
*
|
|
50
|
+
* @returns ids unchanged, to be consumed lazily by the write, or the decoded bitmap.
|
|
51
|
+
* @throws {ValidationError} for a byte array passed as ids; bytes that are malformed, over the cap, or followed by
|
|
52
|
+
* more bytes; a `{ bitmap }` with no `serialize` method, or whose `serialize('portable')` returns no `Uint8Array`;
|
|
53
|
+
* and anything that is none of the three inputs. An error the caller's own `serialize` throws propagates as it is.
|
|
54
|
+
*/
|
|
55
|
+
export declare function prepareLoadInput(input: LoadInput, codec: CodecInterface): Iterable<number> | AsyncIterable<number> | DecodedLoadInput;
|
package/dist/core/load.d.ts
CHANGED
|
@@ -15,15 +15,17 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Refusing therefore has to clean up after itself. The object is already durable at that point, and it sits
|
|
17
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
|
|
19
|
-
*
|
|
20
|
-
* so it stays, and collection takes it like any other generation once one above it is current.
|
|
18
|
+
* pointer), so a refused load deletes its own object before returning, while the segment's row is unchanged, gone
|
|
19
|
+
* or `destroyed`. Once another write has changed the row, the object's number may name a re-created segment's live
|
|
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
23
|
import { type CodecInterface } from './codec.js';
|
|
24
|
-
import type { Clock } from './determinism.js';
|
|
24
|
+
import type { Clock, Rng } from './determinism.js';
|
|
25
25
|
import type { IKeystore } from './crypto.js';
|
|
26
|
-
import
|
|
26
|
+
import { type LoadInput } from './load-input.js';
|
|
27
|
+
import { type ReadRetry } from './retry.js';
|
|
28
|
+
import type { GenerationMetadata, IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
|
|
27
29
|
/** What {@link loadSegment} needs: the objects, the pointer, the codec, and key material if encrypted. */
|
|
28
30
|
export interface LoadDeps {
|
|
29
31
|
readonly storage: IStorageDriver;
|
|
@@ -32,6 +34,27 @@ export interface LoadDeps {
|
|
|
32
34
|
readonly keystore?: IKeystore;
|
|
33
35
|
readonly requireEncryption?: boolean;
|
|
34
36
|
readonly clock?: Clock;
|
|
37
|
+
/**
|
|
38
|
+
* The random source that spreads the waits between a publish's fresh compare-and-swaps (a random time under each
|
|
39
|
+
* bound). Absent, the read retry's source is used if it has one, and otherwise each wait is its bound.
|
|
40
|
+
*/
|
|
41
|
+
readonly rng?: Rng;
|
|
42
|
+
/**
|
|
43
|
+
* The store's read retry, for the read the guard makes of the current generation: a transient fault there is run
|
|
44
|
+
* again under it rather than failing the load. Absent, the read is made once. It does not govern the write: a
|
|
45
|
+
* registry write that gets no answer is settled by reading the row, and sent again only as a bounded fresh
|
|
46
|
+
* compare-and-swap.
|
|
47
|
+
*/
|
|
48
|
+
readonly readRetry?: ReadRetry;
|
|
49
|
+
/**
|
|
50
|
+
* Collect by listing the segment's objects after the publish, whatever `keep` is. Absent, a load that keeps at
|
|
51
|
+
* most one generation and found nothing above its pointer deletes by name the one generation its publish pushed
|
|
52
|
+
* out of the window, and lists every sixteenth generation to take what that leaves; the generations older than
|
|
53
|
+
* the window wait for that listing. Set it for a caller whose `keep` promises every generation below the new
|
|
54
|
+
* one beyond the window is gone when the call returns: the `*Into` verbs, whose `keep` is how an operator clears
|
|
55
|
+
* a destination that earlier materialisations kept in full.
|
|
56
|
+
*/
|
|
57
|
+
readonly collectByListing?: boolean;
|
|
35
58
|
}
|
|
36
59
|
/**
|
|
37
60
|
* What a plausible result looks like, so an implausible one is refused instead of published.
|
|
@@ -71,8 +94,27 @@ export interface LoadOptions {
|
|
|
71
94
|
* collected. Keep at least one generation for every one that can be written above the pinned one while your
|
|
72
95
|
* longest pinned job runs, on every writer that loads the segment — see "Generations and `keep`" in the loading
|
|
73
96
|
* guide.
|
|
97
|
+
*
|
|
98
|
+
* With `keep` of 0 or 1, a load that found nothing above the pointer collects without listing: it deletes the one
|
|
99
|
+
* generation its publish pushed out of the window, and lists the segment's objects on every sixteenth generation to
|
|
100
|
+
* take whatever that pass leaves, such as the generations an earlier, wider `keep` held. With `keep` of 2 or more
|
|
101
|
+
* it lists on every load, and so does one whose check found the current generation's object gone: the guard's read of
|
|
102
|
+
* it, or, when the guard took the size from the row's summary, one zero-byte read made before a `keep` of 1 takes a
|
|
103
|
+
* name. A `keep` at least the generation published collects nothing and asks for nothing.
|
|
74
104
|
*/
|
|
75
105
|
readonly keep?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Small, immutable metadata of your own for this generation: what it was computed from, such as a definition's
|
|
108
|
+
* version, the time its data landed, or a run id. A flat object of string keys and string or finite-number values,
|
|
109
|
+
* at most 1,024 bytes as canonical JSON (keys sorted by UTF-16 code unit, no whitespace), and no key longer than 128
|
|
110
|
+
* bytes. It is stored in the generation's object, and written to the segment's row by the write that makes the
|
|
111
|
+
* generation current, so whoever sees this generation as current sees its metadata. It never changes once written:
|
|
112
|
+
* a new generation is how it changes. `undefined` and the empty object store nothing. Anything else that breaks a
|
|
113
|
+
* rule (a boolean, `null`, an array, nesting, `NaN`, a key that is not well-formed text) throws `ValidationError`
|
|
114
|
+
* before a request is made. **Never put a subject's id in it**: an erasure rewrites the ids and carries the metadata
|
|
115
|
+
* over as it is. Sealed under the segment's key when the segment is encrypted.
|
|
116
|
+
*/
|
|
117
|
+
readonly metadata?: GenerationMetadata;
|
|
76
118
|
readonly audit?: IAuditSink;
|
|
77
119
|
}
|
|
78
120
|
/** Why a load did not become current. */
|
|
@@ -82,13 +124,14 @@ export type LoadRefusal = 'empty' | 'min-cardinality' | 'min-retained'
|
|
|
82
124
|
* put refused this one and it wrote nothing (`size: 0`); or the segment's registry row changed while the load was
|
|
83
125
|
* writing: another load published, a retention change, a rollback or an erasure wrote the row, or the row was
|
|
84
126
|
* 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
|
|
127
|
+
* may by then name another incarnation's live object; once the row is `destroyed` (dropped or crypto-shredded),
|
|
128
|
+
* the object is deleted, and once the row is gone, so is an object its footer proves this load's own.
|
|
86
129
|
*/
|
|
87
130
|
| 'superseded';
|
|
88
131
|
export interface LoadResult {
|
|
89
132
|
/**
|
|
90
133
|
* 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
|
|
134
|
+
* unchanged or `destroyed`, or gone once its footer proves it this load's own, and leaves it in the bucket once another write has changed the row. A load that lost its
|
|
92
135
|
* generation number to another wrote nothing, and reports `size: 0`.
|
|
93
136
|
*/
|
|
94
137
|
readonly generation: number;
|
|
@@ -108,14 +151,26 @@ export interface LoadResult {
|
|
|
108
151
|
* whatever happens next.
|
|
109
152
|
*/
|
|
110
153
|
readonly cardinalityBefore: number | null;
|
|
111
|
-
/**
|
|
154
|
+
/**
|
|
155
|
+
* The superseded generations collected after publishing. Empty when nothing was published.
|
|
156
|
+
*
|
|
157
|
+
* When collection deleted by name (see {@link LoadOptions.keep}) this is the name it deleted, and that generation
|
|
158
|
+
* may have been gone already: a delete of an absent object succeeds on every backend and does not say so, so the
|
|
159
|
+
* list names what the pass asked the bucket to delete, not what it found there. A listing pass names the
|
|
160
|
+
* generations it found and deleted. Neither is a receipt, since a concurrent collector may have taken a generation
|
|
161
|
+
* first.
|
|
162
|
+
*/
|
|
112
163
|
readonly collected: readonly number[];
|
|
113
164
|
}
|
|
114
165
|
/**
|
|
115
|
-
* Replace a segment's contents with `
|
|
166
|
+
* Replace a segment's contents with `input`, as one immutable generation, and make it current.
|
|
167
|
+
*
|
|
168
|
+
* `input` is ids, or a whole bitmap as `{ serialized }` portable Roaring bytes or a `{ bitmap }` with
|
|
169
|
+
* `serialize('portable')` ({@link LoadInput}). A bitmap input is checked, decoded and serialized before the first
|
|
170
|
+
* request, and is written from its own chunks; the generation is byte for byte the one its ids would write.
|
|
116
171
|
*
|
|
117
172
|
* Returns what was written and whether it became current. A refusal is **not** an exception: `published: false`
|
|
118
173
|
* with a `reason` is a normal outcome a caller branches on, in the same shape as a successful load, because the
|
|
119
174
|
* interesting cases (a guard tripped, a racing writer won) are operational facts rather than faults.
|
|
120
175
|
*/
|
|
121
|
-
export declare function loadSegment(ref: SegmentRef,
|
|
176
|
+
export declare function loadSegment(ref: SegmentRef, input: LoadInput, deps: LoadDeps, options?: LoadOptions): Promise<LoadResult>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generation metadata: a small, flat record of strings and finite numbers attached to one generation.
|
|
3
|
+
*
|
|
4
|
+
* The rules live here once, for every boundary that meets a value: a caller's input, refused with
|
|
5
|
+
* `ValidationError`, and bytes read back from a tier, refused with `IntegrityError`. The caller passes the error
|
|
6
|
+
* to raise. Pure: no I/O.
|
|
7
|
+
*/
|
|
8
|
+
import type { GenerationMetadata } from './ports.js';
|
|
9
|
+
/** Cap on the canonical JSON of one generation's metadata, in UTF-8 bytes (braces and quotes included). */
|
|
10
|
+
export declare const MAX_METADATA_BYTES = 1024;
|
|
11
|
+
/** Cap on one metadata key, in UTF-8 bytes. */
|
|
12
|
+
export declare const MAX_METADATA_KEY_BYTES = 128;
|
|
13
|
+
/**
|
|
14
|
+
* Validate `value` as generation metadata (`GenerationMetadata` in the ports) and return its canonical JSON: RFC 8785
|
|
15
|
+
* (the JSON Canonicalization Scheme) for a flat object of strings and finite numbers. Keys are sorted by UTF-16 code
|
|
16
|
+
* unit, strings are escaped as `JSON.stringify` escapes them, numbers are written as ECMAScript's
|
|
17
|
+
* `Number.prototype.toString` writes them (so `-0` is `0`, and `1e21` is `1e+21`), and there is no whitespace. Key
|
|
18
|
+
* order therefore never changes the bytes, and `tests/golden/metadata-canonical.json` holds vectors a port can check
|
|
19
|
+
* itself against. Both caps are in UTF-8 bytes. The container is an object whose prototype is
|
|
20
|
+
* `Object.prototype` or `null`, holding only enumerable data properties with string keys. `fail` raises the error the
|
|
21
|
+
* boundary calls for. The empty object passes and returns `{}`: whether it may be stored is the caller's rule.
|
|
22
|
+
*/
|
|
23
|
+
export declare function canonicalMetadataJson(value: unknown, fail: (message: string) => never): string;
|
|
24
|
+
/**
|
|
25
|
+
* The bytes a generation stores for `value`: its canonical JSON in UTF-8, or `undefined` when it has none (`undefined`
|
|
26
|
+
* or the empty object, which store nothing). Synchronous, so what is stored is what the caller passed at the call,
|
|
27
|
+
* whatever it does with its object afterwards. `fail` raises the boundary's error.
|
|
28
|
+
*/
|
|
29
|
+
export declare function metadataBytes(value: unknown, fail: (message: string) => never): Uint8Array | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* A caller's metadata, checked against the rules and copied, so what is stored is what the caller passed at the call,
|
|
32
|
+
* whatever it does with its object afterwards: the frozen record in canonical key order, or `undefined` when there is
|
|
33
|
+
* none (`undefined`, or the empty object, which store nothing). Synchronous. `fail` raises the boundary's error.
|
|
34
|
+
*/
|
|
35
|
+
export declare function copiedMetadata(value: unknown, fail: (message: string) => never): GenerationMetadata | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Read metadata back from bytes a tier holds, which are untrusted. They must be at most {@link MAX_METADATA_BYTES},
|
|
38
|
+
* valid UTF-8, and exactly the canonical JSON of a non-empty record, so a stored copy has one spelling: whitespace, a
|
|
39
|
+
* key out of order or listed twice, and a number or an escape `JSON.stringify` would write another way are all
|
|
40
|
+
* refused. Returns a frozen record of its own. `fail` raises the boundary's error.
|
|
41
|
+
*/
|
|
42
|
+
export declare function metadataFromBytes(bytes: Uint8Array, fail: (message: string) => never): GenerationMetadata;
|
package/dist/core/metrics.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type MetricEvent = {
|
|
|
26
26
|
readonly kind: 'storage.get';
|
|
27
27
|
readonly namespace?: string;
|
|
28
28
|
readonly segment: string;
|
|
29
|
-
/** Bytes returned (0 if the chunk was absent — a GET still happened). */
|
|
29
|
+
/** One event per request, however many callers were waiting on it. Bytes returned (0 if the chunk was absent — a GET still happened). */
|
|
30
30
|
readonly bytes: number;
|
|
31
31
|
/**
|
|
32
32
|
* Elapsed wall time of the read — includes any transient-retry backoff on the storage call. From the
|
|
@@ -34,6 +34,12 @@ export type MetricEvent = {
|
|
|
34
34
|
*/
|
|
35
35
|
readonly ms: number;
|
|
36
36
|
} | {
|
|
37
|
+
/**
|
|
38
|
+
* One decoded-chunk cache lookup (emitted only when a cache is configured). `hit: false` means the lookup found
|
|
39
|
+
* no cached chunk. A caller that missed while another caller's read of the same chunk was open waits on that
|
|
40
|
+
* read and adds no `storage.get`, so the misses can outnumber the `storage.get` events; the `storage.get`
|
|
41
|
+
* count is the number of requests.
|
|
42
|
+
*/
|
|
37
43
|
readonly kind: 'cache';
|
|
38
44
|
readonly hit: boolean;
|
|
39
45
|
} | {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* on some other handle. That is why the routing is by segment identity here rather than by which object the
|
|
22
22
|
* caller happened to start from.
|
|
23
23
|
*/
|
|
24
|
-
import type { ChunkRef, StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
|
|
24
|
+
import type { ChunkRef, GenerationSummary, StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
|
|
25
25
|
import type { CrbmStorageChunkSource } from './crbm-storage-source.js';
|
|
26
26
|
/**
|
|
27
27
|
* What a pin holds for one segment: the generation, the version identifying those exact bytes, and the pinned
|
|
@@ -67,6 +67,7 @@ export declare class PinnedStorageChunkSource implements StorageChunkSource {
|
|
|
67
67
|
listChunkKeys(ref: SegmentRef): Promise<number[]>;
|
|
68
68
|
sizeOf(ref: SegmentRef): Promise<SegmentSize | null>;
|
|
69
69
|
cardinalities(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
|
|
70
|
+
summary(ref: SegmentRef): Promise<GenerationSummary | null>;
|
|
70
71
|
currentGeneration(ref: SegmentRef): Promise<number | null>;
|
|
71
72
|
/**
|
|
72
73
|
* A pinned segment reports the version captured when it was pinned, marked as a pin's, so its decoded chunks
|
package/dist/core/ports.d.ts
CHANGED
|
@@ -59,10 +59,18 @@ export interface StorageChunkSource {
|
|
|
59
59
|
/**
|
|
60
60
|
* Optional: the current generation's **per-chunk cardinality** (`chunkKey → count`), read from the
|
|
61
61
|
* already-parsed `.crbm` index with **no payload reads**, or `null` if the segment has no Storage generation.
|
|
62
|
-
* Powers the free `count()`
|
|
62
|
+
* Powers the free `count()` of a source with no {@link StorageChunkSource.summary} — the engine sums the index
|
|
63
|
+
* instead of fetching a single chunk. A source with no
|
|
63
64
|
* index (e.g. the in-memory source) omits this, and `count()` falls back to fetching every chunk.
|
|
64
65
|
*/
|
|
65
66
|
cardinalities?(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
|
|
67
|
+
/**
|
|
68
|
+
* Optional: the current generation's number, id count and metadata, or `null` if the segment has no Storage
|
|
69
|
+
* generation. The `.crbm` source answers from the registry row's summary when it can use it, which is one
|
|
70
|
+
* registry read and no read of the object, and from the opened generation when it cannot. A source with no
|
|
71
|
+
* summary omits this, and `count()` keeps its other paths.
|
|
72
|
+
*/
|
|
73
|
+
summary?(ref: SegmentRef): Promise<GenerationSummary | null>;
|
|
66
74
|
/**
|
|
67
75
|
* Optional: the segment's **current generation number** as this source resolves it right now (registry
|
|
68
76
|
* `currentGen`, or the highest storage generation), or `null` if the segment has no Storage generation. The engine
|
|
@@ -105,10 +113,10 @@ export interface StorageChunkSource {
|
|
|
105
113
|
* the generation *and* the incarnation of the name it belongs to.
|
|
106
114
|
*
|
|
107
115
|
* {@link StorageChunkSource.currentGeneration} is not enough to key a decoded-chunk cache, and the gap is not
|
|
108
|
-
* theoretical: a load numbers its generation
|
|
109
|
-
* a registry row is purged and the bucket emptied. A retired, re-created name therefore
|
|
110
|
-
* data at the same `currentGen`, and a cache keyed on `(segment, chunk, generation)` hands back
|
|
111
|
-
* incarnation's ids — an erased id reappearing, with no read to intercept.
|
|
116
|
+
* theoretical: a load numbers its generation from the row's pointer and what is in the bucket, so the number
|
|
117
|
+
* **restarts at 0** once a registry row is purged and the bucket emptied. A retired, re-created name therefore
|
|
118
|
+
* serves different data at the same `currentGen`, and a cache keyed on `(segment, chunk, generation)` hands back
|
|
119
|
+
* the previous incarnation's ids — an erased id reappearing, with no read to intercept.
|
|
112
120
|
*
|
|
113
121
|
* Note that putting the incarnation in the *object key* would not fix this: the new incarnation still starts
|
|
114
122
|
* at generation 0, so the cache key collides either way. The identity has to reach the cache.
|
|
@@ -160,8 +168,9 @@ export interface StorageCaps {
|
|
|
160
168
|
* loses its response, sent again, finds its own object and would report a collision. Either send each write
|
|
161
169
|
* once, with the backend client's retry off for that request, or recognise your own write when the
|
|
162
170
|
* precondition fails by reading an id you stored with it back, and treat a match as success. The first is
|
|
163
|
-
* what the
|
|
164
|
-
* resumable GCS upload do
|
|
171
|
+
* what the registry writes of every shipped driver do; the second is what the Azure Blob driver and the
|
|
172
|
+
* resumable GCS upload do, and what the S3 driver and the single-request GCS upload do when they send an object
|
|
173
|
+
* again after a throttle, which is the one replay a driver may make on purpose.
|
|
165
174
|
* - **Raise a transient fault as {@link TransientError}** (throttling, a 5xx, a dropped connection), so the read
|
|
166
175
|
* retry can ride it out and a write's caller can tell it from a deterministic failure.
|
|
167
176
|
*/
|
|
@@ -212,6 +221,50 @@ export type RegistryStatus = 'active' | 'destroyed';
|
|
|
212
221
|
* shape-later: stored and round-tripped, no semantics attached.
|
|
213
222
|
*/
|
|
214
223
|
export type GovernanceMeta = Record<string, unknown>;
|
|
224
|
+
/**
|
|
225
|
+
* A cached description of one generation, carried on the registry row: how many ids it holds, and the metadata it
|
|
226
|
+
* was loaded with. The generation's `.crbm` object stays the truth; this is a copy, written by the same write that
|
|
227
|
+
* moves the pointer, and it names the generation it describes so a copy left behind by a writer that did not
|
|
228
|
+
* carry it can never be taken for another generation's.
|
|
229
|
+
*
|
|
230
|
+
* Two shapes. On a cleartext segment the values are in the clear. On an encrypted segment (a row with
|
|
231
|
+
* `wrappedDeks`) they are sealed under the segment's data key, as the generation's index is, so the row reveals
|
|
232
|
+
* neither the count nor the metadata. A shipped registry refuses a write whose summary disagrees with the row's
|
|
233
|
+
* keys. It still reads a stored row that disagrees, so that one such row cannot stop every listing: a reader must not
|
|
234
|
+
* use that row's summary.
|
|
235
|
+
*/
|
|
236
|
+
export type RegistrySummary = ClearRegistrySummary | SealedRegistrySummary;
|
|
237
|
+
/**
|
|
238
|
+
* A generation's metadata: string keys, string or finite-number values, no nesting, at most 1 KiB as canonical JSON.
|
|
239
|
+
* The rules are in `core/metadata.ts`.
|
|
240
|
+
*/
|
|
241
|
+
export type GenerationMetadata = Readonly<Record<string, string | number>>;
|
|
242
|
+
/** What a generation is, as one read answers it: its number, its id count and the metadata it was written with. */
|
|
243
|
+
export interface GenerationSummary {
|
|
244
|
+
readonly generation: number;
|
|
245
|
+
readonly cardinality: number;
|
|
246
|
+
/** Absent when the generation has none. */
|
|
247
|
+
readonly metadata?: GenerationMetadata;
|
|
248
|
+
}
|
|
249
|
+
/** {@link RegistrySummary} on a cleartext segment. */
|
|
250
|
+
export interface ClearRegistrySummary {
|
|
251
|
+
/** The generation this describes. A non-negative safe integer. */
|
|
252
|
+
readonly generation: number;
|
|
253
|
+
/** How many ids the generation holds: an integer from 0 to 2^32. */
|
|
254
|
+
readonly cardinality: number;
|
|
255
|
+
/** The generation's metadata, when it has any. Never the empty object. */
|
|
256
|
+
readonly metadata?: GenerationMetadata;
|
|
257
|
+
}
|
|
258
|
+
/** {@link RegistrySummary} on an encrypted segment. */
|
|
259
|
+
export interface SealedRegistrySummary {
|
|
260
|
+
/** The generation this describes. A non-negative safe integer. */
|
|
261
|
+
readonly generation: number;
|
|
262
|
+
/**
|
|
263
|
+
* Base64 of `nonce(12) ‖ ciphertext ‖ tag(16)`, sealing the cardinality as a little-endian u64 followed by the
|
|
264
|
+
* metadata's canonical JSON, if any. The count is fixed-width, so the length reveals only the metadata's size.
|
|
265
|
+
*/
|
|
266
|
+
readonly sealed: string;
|
|
267
|
+
}
|
|
215
268
|
/**
|
|
216
269
|
* One registry row — the authoritative per-segment record. Exactly one per segment.
|
|
217
270
|
*/
|
|
@@ -249,10 +302,23 @@ export interface RegistryRecord extends SegmentRef {
|
|
|
249
302
|
*/
|
|
250
303
|
readonly retention?: GovernanceMeta;
|
|
251
304
|
readonly residency?: GovernanceMeta;
|
|
305
|
+
/**
|
|
306
|
+
* A cached description of the current generation (see {@link RegistrySummary}). Optional: a row without one is
|
|
307
|
+
* correct, and a reader then opens the generation instead. Trusted only for the generation it names.
|
|
308
|
+
*/
|
|
309
|
+
readonly summary?: RegistrySummary;
|
|
252
310
|
/** Epoch-ms of creation / last mutation (from the driver's injected clock). */
|
|
253
311
|
readonly createdAt: number;
|
|
254
312
|
readonly updatedAt: number;
|
|
255
|
-
/**
|
|
313
|
+
/**
|
|
314
|
+
* Opaque OCC token — compare-by-equality, ABA-safe. A shipped registry gives no two writes under one name the same
|
|
315
|
+
* token: not two writes of this row, not a write of an earlier row under the name, and not a write after this row is
|
|
316
|
+
* restored from a backup. It holds with overwhelming probability rather than by construction, since the token carries random parts: a
|
|
317
|
+
* 128-bit incarnation id drawn when the row is created, and a 64-bit part drawn for each write. Two incarnations of one
|
|
318
|
+
* name meet with probability 2^-128 for any pair (about n^2 / 2^129 among n of them), and two writes at one counter,
|
|
319
|
+
* after a restore, with probability 2^-64. A bare decimal token, on a row no 0.12 or later registry has written,
|
|
320
|
+
* carries neither part.
|
|
321
|
+
*/
|
|
256
322
|
readonly token: Token;
|
|
257
323
|
}
|
|
258
324
|
/** The caller-settable fields at {@link IRegistryDriver.create} (audit + token are driver-managed). */
|
|
@@ -265,14 +331,47 @@ export interface NewRegistryRecord {
|
|
|
265
331
|
readonly status?: RegistryStatus;
|
|
266
332
|
readonly retention?: GovernanceMeta;
|
|
267
333
|
readonly residency?: GovernanceMeta;
|
|
334
|
+
/** Must name `currentGen` when given. */
|
|
335
|
+
readonly summary?: RegistrySummary;
|
|
268
336
|
}
|
|
269
|
-
/**
|
|
270
|
-
|
|
337
|
+
/**
|
|
338
|
+
* Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity + audit + token are off-limits).
|
|
339
|
+
*
|
|
340
|
+
* Presence-based: a field the patch does not mention is left as it was. `summary` is the one exception, because it
|
|
341
|
+
* describes the current generation: a patch that moves `currentGen` and does not mention `summary` drops the old
|
|
342
|
+
* one rather than keep a description of another generation. A `summary` the patch gives must name the
|
|
343
|
+
* `currentGen` the row will have.
|
|
344
|
+
*/
|
|
345
|
+
export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency' | 'summary'>>;
|
|
271
346
|
/** Capabilities a registry driver advertises; validated fail-fast at wiring time. */
|
|
272
347
|
export interface RegCaps {
|
|
273
348
|
/** REQUIRED — `currentGen` feeds read correctness + the publish CAS, so reads must be strongly consistent. */
|
|
274
349
|
readonly strongRead: true;
|
|
350
|
+
/**
|
|
351
|
+
* `false` when this registry cannot write a row in this runtime, though it reads: a shipped registry draws random
|
|
352
|
+
* bytes for every token it issues, and on a runtime with no Web Crypto it has none to draw. A write path that
|
|
353
|
+
* writes an object before its row (a load, an erasure rewrite) checks it first and refuses with `UnsupportedError`
|
|
354
|
+
* before it writes anything, rather than leave an object no row names. Absent means the registry can write.
|
|
355
|
+
*/
|
|
356
|
+
readonly canWrite?: false;
|
|
357
|
+
/**
|
|
358
|
+
* `true` when this registry's `delete` removes a row from its backend for good, so that no later `list` reads it, and
|
|
359
|
+
* removes it only while it is still the exact version the delete read: by a delete the backend applies under a
|
|
360
|
+
* precondition (an S3 or Azure Blob ETag, a GCS object generation), or under a lock no other writer of the backend
|
|
361
|
+
* can take. Only a row whose token carries an incarnation id is removed so. A row first written by a release before
|
|
362
|
+
* 0.12 has a bare decimal token, and a delete still tombstones it: a process on that release, re-creating the name
|
|
363
|
+
* over nothing, would issue those counters again from 0, so its row could not be told apart from the deleted one.
|
|
364
|
+
*
|
|
365
|
+
* `false` or absent: every `delete` leaves a tombstone, which every later full `list` still reads. A shipped registry
|
|
366
|
+
* says which it is.
|
|
367
|
+
*/
|
|
368
|
+
readonly conditionalDelete?: boolean;
|
|
275
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* Refuse, with `UnsupportedError`, a write that would write an object before its row when the registry reports it
|
|
372
|
+
* cannot write a row (see {@link RegCaps.canWrite}). Called before the write's first request.
|
|
373
|
+
*/
|
|
374
|
+
export declare function assertRegistryCanWrite(registry: IRegistryDriver, what: string): void;
|
|
276
375
|
/**
|
|
277
376
|
* Brand for {@link StorageBackend}.
|
|
278
377
|
*
|
|
@@ -365,8 +464,13 @@ export declare function isStorageBackend(value: unknown): value is StorageBacken
|
|
|
365
464
|
* - **`create` and `compareAndSwap` are atomic conditional writes.** A `create` over a live row, and a
|
|
366
465
|
* `compareAndSwap` whose token is not the stored one, throw {@link WriteConflictError} and change nothing.
|
|
367
466
|
* Two racing writers have exactly one winner.
|
|
368
|
-
* - **
|
|
369
|
-
*
|
|
467
|
+
* - **A token is not reused, not even across `delete` then `create`**, short of a collision of probability 2^-128 for
|
|
468
|
+
* any two incarnations of one name: a recreated row carries a token no earlier incarnation held, so a stale holder
|
|
469
|
+
* cannot swap into it. The shipped drivers draw a random 128-bit incarnation id into the token of every row they
|
|
470
|
+
* create and a random 64-bit part into the token of every write, beside a counter a tombstone (or a global counter)
|
|
471
|
+
* keeps going; where nothing of the earlier row is left, or the row is restored from a backup to an older counter, the
|
|
472
|
+
* random parts alone keep the tokens apart: with probability 1 - 2^-128 per pair of incarnations, and 1 - 2^-64 per
|
|
473
|
+
* pair of writes at one counter.
|
|
370
474
|
* - **`delete` is idempotent** without an `expected` token: deleting an absent row is a no-op, not an error.
|
|
371
475
|
* **With `expected` it is fenced**: it lands only while the row still carries that token, and otherwise throws
|
|
372
476
|
* {@link WriteConflictError} and leaves the row.
|
|
@@ -408,7 +512,9 @@ export interface IRegistryDriver {
|
|
|
408
512
|
*/
|
|
409
513
|
list(namespace?: string): AsyncIterable<RegistryRecord>;
|
|
410
514
|
/**
|
|
411
|
-
* Remove the row
|
|
515
|
+
* Remove the row. A registry whose {@link RegCaps.conditionalDelete} is `true` removes a row whose token carries an
|
|
516
|
+
* incarnation id from its backend; every other row is tombstoned. Either way a later `create` gets a token no
|
|
517
|
+
* earlier incarnation of the name held, but for a collision of probability 2^-128 per pair of incarnations.
|
|
412
518
|
*
|
|
413
519
|
* Without `expected`, idempotent: deleting an absent row is a no-op.
|
|
414
520
|
*
|
|
@@ -27,6 +27,12 @@ export declare function drainRegistry(registry: IRegistryDriver, options: {
|
|
|
27
27
|
* one namespace, so it passes false and the refusal does not tell it to narrow what it cannot.
|
|
28
28
|
*/
|
|
29
29
|
narrowable?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Told of each bookkeeping row an unscoped scan skips, which the listing has already paid to read. Only the
|
|
32
|
+
* retention sweep uses it, to find the due-index pointers whose segment has no row. It is not counted against
|
|
33
|
+
* `maxScanSegments`, and a caller that holds the rows it is given bounds them itself.
|
|
34
|
+
*/
|
|
35
|
+
onReserved?: (record: RegistryRecord) => void;
|
|
30
36
|
}): Promise<RegistryRecord[]>;
|
|
31
37
|
/**
|
|
32
38
|
* Is this record **bookkeeping** rather than a segment? Covers every reserved family — today the due-index
|