@cloudbitmaps/core 0.19.1 → 0.20.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-RSLVTRJY.js → chunk-YSC7NJWV.js} +314 -216
- package/dist/chunk-YSC7NJWV.js.map +7 -0
- package/dist/core/audit.d.ts +56 -3
- package/dist/core/combine-many.d.ts +9 -6
- package/dist/core/consistency.d.ts +10 -4
- package/dist/core/crbm/fingerprint.d.ts +18 -0
- package/dist/core/crbm/format.d.ts +2 -4
- package/dist/core/crbm/frame.d.ts +10 -0
- package/dist/core/crbm/reader.d.ts +1 -8
- package/dist/core/crbm-storage-source.d.ts +229 -89
- package/dist/core/engine.d.ts +17 -14
- package/dist/core/erase-id.d.ts +62 -27
- package/dist/core/generation-gc.d.ts +2 -1
- package/dist/core/leases.d.ts +7 -0
- package/dist/core/load.d.ts +5 -3
- package/dist/core/metrics.d.ts +5 -5
- package/dist/core/pinned-storage-source.d.ts +3 -3
- package/dist/core/pointer-id.d.ts +19 -0
- package/dist/core/ports.d.ts +127 -35
- package/dist/core/reader-defaults.d.ts +14 -3
- package/dist/core/rollback.d.ts +2 -1
- package/dist/core/summary.d.ts +38 -21
- package/dist/core/token.d.ts +13 -8
- package/dist/driver-kit.d.ts +2 -1
- package/dist/driver-kit.js +11 -4
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/object-registry.d.ts +1 -1
- package/dist/drivers/_shared/registry.d.ts +26 -17
- package/dist/drivers/localfs/storage.d.ts +7 -6
- package/dist/drivers/memory.d.ts +8 -6
- package/dist/drivers/retry/retrying-chunk-source.d.ts +1 -5
- package/dist/export/index.d.ts +16 -0
- package/dist/index.d.ts +2 -5
- package/dist/index.js +1169 -1041
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/chunk-RSLVTRJY.js.map +0 -7
- package/dist/core/cost.d.ts +0 -351
package/dist/core/erase-id.d.ts
CHANGED
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
*
|
|
12
12
|
* The rewrite is the same generation without one id, so it keeps everything else: the new object carries the source's
|
|
13
13
|
* metadata as it is, and the row's summary of it, built from what was written, counts one id fewer and holds the same
|
|
14
|
-
* metadata. Metadata is not scanned for the id.
|
|
15
|
-
* row's
|
|
16
|
-
* the
|
|
14
|
+
* metadata. Metadata is not scanned for the id. A source object whose metadata block was stripped is another object
|
|
15
|
+
* than the row's summary names: its size is not the one the summary records, so the fingerprint check refuses it
|
|
16
|
+
* before the erasure reads any of its ids. Only one forged to match that fingerprint gets past, and on an encrypted
|
|
17
|
+
* segment it is rewritten with the metadata of the row's sealed summary: the block's presence is not authenticated, so
|
|
18
|
+
* the authenticated copy decides.
|
|
17
19
|
*
|
|
18
20
|
* **`erased: true` is a claim about every generation of the segment, not only the one it replaced.** A rollback
|
|
19
21
|
* leaves generations above the pointer that were once current and can be made current again, so a holder can sit
|
|
@@ -48,16 +50,17 @@
|
|
|
48
50
|
* Which reason comes from **re-reading the row**, not from assuming a supersession: a moved pointer is
|
|
49
51
|
* `'superseded'`, a tombstoned row `'destroyed'`, a purged row `'absent'`, a row with no pointer
|
|
50
52
|
* `'no-generation'`. A pointer still on `from` means the object it names is genuinely gone — the forbidden
|
|
51
|
-
* `missing-storage-generation` state —
|
|
52
|
-
*
|
|
53
|
-
* registry error.
|
|
53
|
+
* `missing-storage-generation` state — or is another object than the row's summary names by its fingerprint, and that
|
|
54
|
+
* throws, because no re-run fixes either. That `NotFoundError` is the only signal of those states, so a faulting
|
|
55
|
+
* re-read rethrows it rather than replacing it with a transient-looking registry error.
|
|
54
56
|
*
|
|
55
57
|
* **A refused rewrite deletes its own object when it would outlive the winner above the pointer.** `putImmutable`
|
|
56
58
|
* commits atomically, so a rewrite whose stream throws leaves no object at all. One that completed is either
|
|
57
59
|
* below the winner's pointer, where the winner's collection takes it, or above it: the case where this call took
|
|
58
60
|
* its number after the winner's object was already in the bucket. Up there nothing collects it, and it was derived
|
|
59
61
|
* from a generation the winner has replaced, so when the winner was another erasure it still holds the id that
|
|
60
|
-
* erasure has just reported gone. That object is deleted before this call returns
|
|
62
|
+
* erasure has just reported gone. That object is deleted before this call returns, once its footer proves it the
|
|
63
|
+
* rewrite's own, under the version that read reported — see `discardRefused`.
|
|
61
64
|
*
|
|
62
65
|
* Without that fence the failure was silent and severe, and both halves were reproduced: `nextGeneration` picks a
|
|
63
66
|
* number above everything in the bucket, so a forward-only publish here always won — discarding a concurrent
|
|
@@ -68,11 +71,21 @@
|
|
|
68
71
|
* A live read still fetching from the collected generation heals forward to the rewrite, and a pin of it fails with
|
|
69
72
|
* `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
|
|
70
73
|
* carries whatever its source held, and the library cannot know that source was meant to exclude the id.
|
|
71
|
-
* A load already in flight writes its object above the pointer before it publishes
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
74
|
+
* A load already in flight writes its object above the pointer before it publishes, or, as a first load onto a row with
|
|
75
|
+
* no pointer, before there is a pointer at all. An erasure that finds the id in that object renews the row's `pointerId`
|
|
76
|
+
* before deleting it, so the load's publish, fenced on the row it read, is refused (`published: false`) and the pointer
|
|
77
|
+
* never names a missing object (see `fenceInFlight`). An erasure that finds no holder writes nothing.
|
|
78
|
+
*
|
|
79
|
+
* An object sealed under a key its row does not hold cannot be searched, and no read of the segment can open it: a
|
|
80
|
+
* first load's on a row with no pointer, whose key that load has not published; on a segment with a key, a first
|
|
81
|
+
* load's that lost the race to the one that published, or crashed, under a key it made and never stored; and on a
|
|
82
|
+
* cleartext segment with a pointer, any encrypted object, since a key is made only for a segment's first generation
|
|
83
|
+
* and a publish never adds one to a segment that has generations: a first load's that minted a key and crashed, or
|
|
84
|
+
* lost the race to a cleartext first load. It counts as
|
|
85
|
+
* a holder whatever the id: an erasure of any id that finds one deletes it, under the same renewal of the row and read
|
|
86
|
+
* before the delete, and lists it in `collected`, and a first load still in flight that wrote it is refused. Only a
|
|
87
|
+
* searched object that held the id makes the answer `erased: true`. The current generation is never one of them: one
|
|
88
|
+
* the row's key does not open throws, as every read of the segment does.
|
|
76
89
|
*/
|
|
77
90
|
import { type IAuditSink } from './audit.js';
|
|
78
91
|
import type { CodecInterface } from './codec.js';
|
|
@@ -98,11 +111,15 @@ export interface EraseIdDeps {
|
|
|
98
111
|
readonly keystore?: IKeystore;
|
|
99
112
|
/** When true, refuse to rewrite a **cleartext** segment (the same guard the read path and a load offer). */
|
|
100
113
|
readonly requireEncryption?: boolean;
|
|
101
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* Supplying a clock that can yield makes the rewrite cooperative, as it makes a load. Its `sleep` is what a registry
|
|
116
|
+
* write that got no answer waits on before a fresh one is sent; with no `sleep`, such a write is not sent again and
|
|
117
|
+
* its `TransientError` is thrown.
|
|
118
|
+
*/
|
|
102
119
|
readonly clock?: Yielder;
|
|
103
120
|
/**
|
|
104
|
-
* The random source that spreads the waits between the
|
|
105
|
-
* source is used if it has one, and otherwise each wait is its bound.
|
|
121
|
+
* The random source that spreads the waits between the fresh compare-and-swaps of the rewrite and of the row's
|
|
122
|
+
* renewal. Absent, the read retry's source is used if it has one, and otherwise each wait is its bound.
|
|
106
123
|
*/
|
|
107
124
|
readonly rng?: Rng;
|
|
108
125
|
/** Per-chunk decode ceiling (invariant 5); defaults to 1 MiB. The `.crbm` reader refuses an entry above its own `maxPayloadBytes` (1 MiB, plus 28 bytes when encrypted) at open, so raise that on the chunk source too. */
|
|
@@ -111,7 +128,7 @@ export interface EraseIdDeps {
|
|
|
111
128
|
* The store's read retry, for the reads the rewrite makes along the way: the generation it rewrites and each of its
|
|
112
129
|
* chunks, the read-back that verifies the generation it wrote, and any other generation it checks for the id. A
|
|
113
130
|
* transient fault on one is run again under it rather than failing the erasure. Absent, each read is made once.
|
|
114
|
-
* The deletes are not retried, and the rewrite's registry write
|
|
131
|
+
* The deletes are not retried, and the rewrite's registry write and the row's renewal are settled as a load's is.
|
|
115
132
|
*/
|
|
116
133
|
readonly readRetry?: ReadRetry;
|
|
117
134
|
}
|
|
@@ -127,13 +144,20 @@ export interface EraseIdResult {
|
|
|
127
144
|
readonly erased: boolean;
|
|
128
145
|
/**
|
|
129
146
|
* Why the id was not erased, when `erased` is false. `'absent'` (no registry row), `'destroyed'` (a crypto-shred
|
|
130
|
-
* tombstone — already unreadable), `'no-generation'` (a row with no
|
|
131
|
-
*
|
|
147
|
+
* tombstone — already unreadable), `'no-generation'` (a row with no generation yet: its bucket can hold first loads'
|
|
148
|
+
* objects, none of them found to hold the id, and `collected` names any this call deleted unsearched, see below),
|
|
149
|
+
* `'not-member'` (no
|
|
150
|
+
* generation in the bucket holds the id — the common case across a fleet scan — with the same note on `collected`),
|
|
151
|
+
* or `'superseded'`.
|
|
132
152
|
*
|
|
133
153
|
* **`'superseded'` means this call did not erase the id, not that the id is still there.** Another writer — a
|
|
134
|
-
* load, another erasure, or a rollback — moved the pointer off the generation this call read
|
|
135
|
-
*
|
|
136
|
-
*
|
|
154
|
+
* load, another erasure, or a rollback — moved the pointer off the generation this call read (on a row with no
|
|
155
|
+
* pointer, any other write of the row before this call renewed it, a `setRetention` included), or replaced an
|
|
156
|
+
* object it meant to delete (another erasure deleted a holder above the pointer, or on a row with no pointer, and a
|
|
157
|
+
* load stored another object under its number, so the storage driver's conditional delete refused this call's,
|
|
158
|
+
* whether or not the pointer moved). What it was doing no longer follows from what is current: a rewrite derived
|
|
159
|
+
* from that generation is not a valid successor to the new one, and a generation it meant to delete may be the one
|
|
160
|
+
* the pointer now names, or another object than the one it read.
|
|
137
161
|
* Re-run against the new generation: if the id is still present it is erased then; if the racing writer was
|
|
138
162
|
* another erasure of the *same* id, the re-run reports `'not-member'` because it is already gone. Either way the
|
|
139
163
|
* re-run settles it, which is why it is the documented action for this reason and for no other.
|
|
@@ -151,14 +175,20 @@ export interface EraseIdResult {
|
|
|
151
175
|
readonly fromGeneration?: number;
|
|
152
176
|
/**
|
|
153
177
|
* The generation written without the id (present whenever one was written, even if `superseded`). A refused
|
|
154
|
-
* rewrite whose object sits above the winner's pointer deletes that object again before it returns
|
|
178
|
+
* rewrite whose object sits above the winner's pointer deletes that object again before it returns, when its footer
|
|
179
|
+
* proves it the rewrite's own.
|
|
155
180
|
*/
|
|
156
181
|
readonly generation?: number;
|
|
157
182
|
/**
|
|
158
183
|
* Generations this call deleted. On a rewrite that is normally `[fromGeneration]`, plus any older orphans. When
|
|
159
|
-
* the current generation did not hold the id, it is every generation below
|
|
160
|
-
* together) and each one above the pointer that held the id; the ones above it
|
|
161
|
-
* because they are an operator's rollback targets.
|
|
184
|
+
* the current generation did not hold the id and a generation this call searched did, it is every generation below
|
|
185
|
+
* the pointer (`keep: 0` takes them together) and each one above the pointer that held the id; the ones above it
|
|
186
|
+
* that did not hold the id stay, because they are an operator's rollback targets. On a row with no pointer it is
|
|
187
|
+
* each first load's object that held the id. Wherever it found them, it is also each object sealed under a key the
|
|
188
|
+
* row does not hold, which this call cannot search and deletes whatever the id. When those are the only holders, it
|
|
189
|
+
* is only those, deleted one by one, and every generation this call searched stays: `reason` is then
|
|
190
|
+
* `'no-generation'` on a row with no pointer and `'not-member'` on one with a pointer. Empty when nothing was
|
|
191
|
+
* deleted.
|
|
162
192
|
*
|
|
163
193
|
* **It is what THIS call deleted, not the proof that the id is gone** — those differ: a concurrent collector can
|
|
164
194
|
* take a generation holding the id first, and then `erased: true` is returned without it in `collected`, because
|
|
@@ -170,7 +200,10 @@ export interface EraseIdResult {
|
|
|
170
200
|
* collection pass that could not prove the segment was still the same one (which an ordinary retirement landing
|
|
171
201
|
* mid-call is enough to cause); the same refusal on the path where the current generation did not hold the id
|
|
172
202
|
* and nothing was published at all; and a generation still holding the id when the bucket is listed at the end
|
|
173
|
-
* — one an operator rolled the pointer onto while a rewrite was collecting, say
|
|
203
|
+
* — one an operator rolled the pointer onto while a rewrite was collecting, say, or, on a row with no pointer, the
|
|
204
|
+
* object of a first load that read the row before this call renewed it and wrote it after this call listed the
|
|
205
|
+
* bucket. The row write an erasure makes before deleting a holder a load may still publish, when it gets no answer
|
|
206
|
+
* that reading the row can settle, throws the registry's `TransientError`, and that holder is not deleted.
|
|
174
207
|
*
|
|
175
208
|
* **Re-run it**, and read what the re-run says rather than assuming it finished the job. The re-run looks for
|
|
176
209
|
* the id in every generation in the bucket, not only the current one, and there are three outcomes:
|
|
@@ -192,7 +225,9 @@ export interface EraseIdResult {
|
|
|
192
225
|
* it. See the module note for the contract.
|
|
193
226
|
*
|
|
194
227
|
* Emits one `segment.rewrite` audit event at the publish (before the superseded generation is collected), so the
|
|
195
|
-
* compliance record exists the moment the generation without the id is authoritative.
|
|
228
|
+
* compliance record exists the moment the generation without the id is authoritative. An erasure that rewrites nothing,
|
|
229
|
+
* because only other generations held the id, emits one `segment.collect` once no generation holds it, and so does one
|
|
230
|
+
* that deleted only objects no read of the segment can open, which names no generation the id was found in.
|
|
196
231
|
*/
|
|
197
232
|
export declare function eraseIdFromSegment(ref: SegmentRef, id: number, deps: EraseIdDeps, options?: {
|
|
198
233
|
audit?: IAuditSink;
|
|
@@ -46,7 +46,8 @@ export interface LoadNumber {
|
|
|
46
46
|
* and collection takes it once a generation above it is current. Like a listing's, the number can be one whose
|
|
47
47
|
* object was deleted (an erasure removes the generations above a rolled-back pointer that held the id), which is
|
|
48
48
|
* why nothing identifies a generation by its number alone: the reader cache and the chunk cache key on the number and
|
|
49
|
-
* the row's
|
|
49
|
+
* the row's `pointerId`, which every write that takes a number again renews, and an open of a row with a usable
|
|
50
|
+
* summary holds the object to the fingerprint the summary records; a pin keys on its object's fingerprint.
|
|
50
51
|
*/
|
|
51
52
|
export declare function nextLoadGeneration(ref: SegmentRef, deps: GenerationDeps, row: RegistryRecord | null): Promise<LoadNumber>;
|
|
52
53
|
/**
|
package/dist/core/leases.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export declare const MAX_STORED_LEASES = 256;
|
|
|
17
17
|
* plus a few, so that the most a segment can hold, all arriving at once, can all land.
|
|
18
18
|
*/
|
|
19
19
|
export declare const LEASE_CAS_ATTEMPTS: number;
|
|
20
|
+
/**
|
|
21
|
+
* Fresh writes sent after one that got no answer and left the row as it was, by the lease writers and by an erasure's
|
|
22
|
+
* renewal of the row.
|
|
23
|
+
*/
|
|
24
|
+
export declare const UNANSWERED_RESENDS = 3;
|
|
25
|
+
/** The upper bound of the first of those waits, in ms; each next one doubles it. */
|
|
26
|
+
export declare const UNANSWERED_BASE_MS = 500;
|
|
20
27
|
/**
|
|
21
28
|
* Whether an entry holds its generation against a collector reading `now`: it has not ended (`until` plus the margin
|
|
22
29
|
* is ahead), and it does not run further past `now` than any honest holder with a clock inside the margin could have
|
package/dist/core/load.d.ts
CHANGED
|
@@ -15,9 +15,11 @@
|
|
|
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
|
-
* or `destroyed
|
|
20
|
-
*
|
|
18
|
+
* pointer), so a refused load deletes its own object before returning, while the segment's row is unchanged (or
|
|
19
|
+
* differs only in its leases), gone or `destroyed`, or has key material when the load wrote cleartext. Under a row
|
|
20
|
+
* that is gone or keyed, a read of the object's footer must prove it the load's own first. Once another write has
|
|
21
|
+
* otherwise changed the row, the object's number may name a re-created segment's live object, so it stays, and
|
|
22
|
+
* collection takes it like any other generation once one above it is current.
|
|
21
23
|
*/
|
|
22
24
|
import { type IAuditSink } from './audit.js';
|
|
23
25
|
import { type CodecBitmap, type CodecInterface } from './codec.js';
|
package/dist/core/metrics.d.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Design: the library emits **neutral domain events** and stays vendor-agnostic — you (or a ~12-line
|
|
6
6
|
* adapter) map them to OpenTelemetry / Datadog / a log line. The default is a **no-op**, and emission is
|
|
7
7
|
* skipped entirely when no sink is wired (near-zero overhead, no telemetry dependency). Events carry **raw
|
|
8
|
-
* observations** (bytes, counts, ms); turning those into billable units / dollars is
|
|
9
|
-
* which keeps rates
|
|
8
|
+
* observations** (bytes, counts, ms); turning those into billable units / dollars is left to whoever reads them,
|
|
9
|
+
* which keeps rates out of the library (`@cloudbitmaps/tools` prices a workload you give it).
|
|
10
10
|
*
|
|
11
11
|
* Two caveats for sink authors: `onEvent` runs **synchronously on the I/O path**, so keep it cheap and
|
|
12
12
|
* non-blocking (offload batching/network to your own async queue); and `segment`/`namespace` are
|
|
@@ -168,9 +168,9 @@ export interface MetricsSnapshot {
|
|
|
168
168
|
}>>;
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
171
|
-
* A ready-made sink that tallies events into a {@link MetricsSnapshot} — handy for tests, quick scripts,
|
|
172
|
-
*
|
|
173
|
-
*
|
|
171
|
+
* A ready-made sink that tallies events into a {@link MetricsSnapshot} — handy for tests, quick scripts, and for
|
|
172
|
+
* reading the request rates a cost model is given. `snapshot()` returns an independent copy; `reset()` zeroes the
|
|
173
|
+
* counters.
|
|
174
174
|
*/
|
|
175
175
|
export declare class CountingMetricsSink implements IMetricsSink {
|
|
176
176
|
private storage;
|
|
@@ -53,8 +53,6 @@ export declare class PinnedStorageChunkSource implements StorageChunkSource {
|
|
|
53
53
|
* wrong answer this exists to prevent.
|
|
54
54
|
*/
|
|
55
55
|
pins: ReadonlyMap<string, PinnedAt>);
|
|
56
|
-
/** The unpinned segments refresh as the store's own source does; a pinned one never does. */
|
|
57
|
-
get pointerRefreshMs(): number;
|
|
58
56
|
/**
|
|
59
57
|
* The pin for this segment, or `undefined` if it is not pinned here.
|
|
60
58
|
*
|
|
@@ -74,12 +72,14 @@ export declare class PinnedStorageChunkSource implements StorageChunkSource {
|
|
|
74
72
|
sizeOf(ref: SegmentRef): Promise<SegmentSize | null>;
|
|
75
73
|
cardinalities(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
|
|
76
74
|
summary(ref: SegmentRef): Promise<GenerationSummary | null>;
|
|
75
|
+
stat(ref: SegmentRef): Promise<(GenerationSummary & SegmentSize) | null>;
|
|
77
76
|
currentGeneration(ref: SegmentRef): Promise<number | null>;
|
|
78
77
|
/**
|
|
79
78
|
* A pinned segment reports the version captured when it was pinned, marked as a pin's, so its decoded chunks
|
|
80
79
|
* are cached under keys that no live read writes. A live read keys its fetches by the version it resolved when
|
|
81
80
|
* it began, but is served whatever generation the live source holds when each fetch lands — after a publish
|
|
82
|
-
* and a lapsed `cache.genTtlMs`, a reader-cache eviction
|
|
81
|
+
* and a lapsed `cache.genTtlMs`, a reader-cache eviction on a store with no timed refresh, the resolution
|
|
82
|
+
* cache letting the segment's resolution go on a store with one, a sweep that heals the read forward, or an
|
|
83
83
|
* invalidation, a different one — so an entry under a live version can hold another generation's chunk.
|
|
84
84
|
* Invariant 3 lets that live call see it, and later live reads resolve the current version and never look the
|
|
85
85
|
* entry up; a pin sharing the key would be handed it, and return a read that mixed two generations. A pinned
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RegistryPatch, RegistryRecord, Token } from './ports.js';
|
|
2
|
+
/** The fields a read resolves through: a patch that names any of them renews the row's `pointerId`. */
|
|
3
|
+
export declare const RESOLVED_FIELDS: readonly ["currentGen", "status", "wrappedDeks", "keyId", "summary"];
|
|
4
|
+
/**
|
|
5
|
+
* Whether a compare-and-swap of `patch` renews the row's `pointerId`: whether it names a resolved field, by presence
|
|
6
|
+
* (`'field' in patch`), whatever the value, as every patch rule reads a patch.
|
|
7
|
+
*/
|
|
8
|
+
export declare function renewsPointer(patch: RegistryPatch): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The patch that renews a row's `pointerId` and changes nothing a read resolves: the pointer, named at the value it
|
|
11
|
+
* has. A same-value pointer keeps the summary and the kept window, which follow the pointer only when it changes value.
|
|
12
|
+
*/
|
|
13
|
+
export declare function renewPointer(row: Pick<RegistryRecord, 'currentGen'>): RegistryPatch;
|
|
14
|
+
/**
|
|
15
|
+
* The row's `pointerId`, refusing a row that has none with {@link UnsupportedError}: a registry written against an
|
|
16
|
+
* earlier port contract returns rows without it, and a reader that cannot tell which write set the row's resolution
|
|
17
|
+
* cannot key a cache on it. A TypeScript registry fails to compile instead; this catches one in JavaScript.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pointerIdOf(row: RegistryRecord): Token;
|
package/dist/core/ports.d.ts
CHANGED
|
@@ -38,8 +38,9 @@ export interface GenKey extends SegmentRef {
|
|
|
38
38
|
readonly generation: number;
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
-
* A segment's
|
|
42
|
-
*
|
|
41
|
+
* A segment's on-disk footprint — the current generation's Storage object bytes, read cheaply from the `.crbm`
|
|
42
|
+
* footer/index (no payload reads). It is the `sizeBytes` a segment's `stat()` reports, and what bounds the bytes a combine
|
|
43
|
+
* holds for an operand's stream.
|
|
43
44
|
*/
|
|
44
45
|
export interface SegmentSize {
|
|
45
46
|
readonly sizeBytes: number;
|
|
@@ -119,16 +120,10 @@ export interface StorageChunkSource {
|
|
|
119
120
|
*/
|
|
120
121
|
getChunks?(ref: SegmentRef, keys: readonly number[], options?: ReadChunksOptions): AsyncIterable<ChunkRead>;
|
|
121
122
|
/**
|
|
122
|
-
* Optional: the current generation's
|
|
123
|
-
*
|
|
123
|
+
* Optional: the current generation's size, cheaply (from the already-parsed `.crbm` index — no payload reads), or
|
|
124
|
+
* `null` if the segment has no Storage generation.
|
|
124
125
|
*/
|
|
125
126
|
sizeOf?(ref: SegmentRef): Promise<SegmentSize | null>;
|
|
126
|
-
/**
|
|
127
|
-
* Optional: how often this source re-reads a segment's pointer while the segment is being read, in ms, or 0 if
|
|
128
|
-
* it never does. The grounded `costReport()` prices the pointer refresh at this interval; a source that omits it
|
|
129
|
-
* is priced at the store's default.
|
|
130
|
-
*/
|
|
131
|
-
readonly pointerRefreshMs?: number;
|
|
132
127
|
/**
|
|
133
128
|
* Optional: the current generation's **per-chunk cardinality** (`chunkKey → count`), read from the
|
|
134
129
|
* already-parsed `.crbm` index with **no payload reads**, or `null` if the segment has no Storage generation.
|
|
@@ -144,6 +139,17 @@ export interface StorageChunkSource {
|
|
|
144
139
|
* summary omits this, and `count()` keeps its other paths.
|
|
145
140
|
*/
|
|
146
141
|
summary?(ref: SegmentRef): Promise<GenerationSummary | null>;
|
|
142
|
+
/**
|
|
143
|
+
* Optional: what {@link summary} answers, with the size of the generation's object, both from **one** resolution of
|
|
144
|
+
* the segment, so the size is the size of the generation described; `null` if the segment has no Storage
|
|
145
|
+
* generation. The `.crbm` source answers from the registry row's summary when it can use it, whose fingerprint
|
|
146
|
+
* carries the object's size: one registry read and no read of the object. When it cannot, it opens the generation
|
|
147
|
+
* (one tail read of the object when it is not open already, and a range read for an index longer than the tail) and
|
|
148
|
+
* answers from its footer and index, with no payload read. For a source that omits it, `stat()` asks
|
|
149
|
+
* {@link summary} and {@link sizeOf}, two resolutions that can straddle a publish, and reports no size when
|
|
150
|
+
* there is no `sizeOf` either.
|
|
151
|
+
*/
|
|
152
|
+
stat?(ref: SegmentRef): Promise<(GenerationSummary & SegmentSize) | null>;
|
|
147
153
|
/**
|
|
148
154
|
* Optional: the segment's **current generation number** as this source resolves it right now (registry
|
|
149
155
|
* `currentGen`, or the highest storage generation), or `null` if the segment has no Storage generation. The engine
|
|
@@ -211,6 +217,46 @@ export interface StorageCaps {
|
|
|
211
217
|
readonly maxObjectBytes: number;
|
|
212
218
|
/** Optional: enables the pure-object `LATEST`-pointer registry variant (S3 conditional put). */
|
|
213
219
|
readonly conditionalPut?: boolean;
|
|
220
|
+
/**
|
|
221
|
+
* Optional: `true` when `delete` applies {@link StorageDeleteOptions.ifVersion}. The driver then removes an object
|
|
222
|
+
* only while it is still the one that version names, by a check the backend applies in the same step as the removal
|
|
223
|
+
* (a precondition on the request, or a check and a removal with nothing between them), refuses another object under
|
|
224
|
+
* the key with {@link WriteConflictError}, and reports a `version` on every {@link IStorageDriver.getTail} of an
|
|
225
|
+
* object. `false` or absent: `delete` ignores `ifVersion` and removes whatever is under the key, and a caller that
|
|
226
|
+
* decided to delete one object can remove another stored under the key since.
|
|
227
|
+
*
|
|
228
|
+
* A driver that learns this from its client may report `false` until its first delete given `ifVersion`; what the
|
|
229
|
+
* delete does is what it states. The S3 driver does: it reads where the client sends its requests, and whether the
|
|
230
|
+
* SDK sends the header, on its first such delete, and reports `false` for good when that cannot be found out.
|
|
231
|
+
*/
|
|
232
|
+
readonly conditionalDelete?: boolean;
|
|
233
|
+
}
|
|
234
|
+
/** What {@link IStorageDriver.getTail} read. */
|
|
235
|
+
export interface TailRead {
|
|
236
|
+
/** The last `min(maxBytes, size)` bytes of the object. */
|
|
237
|
+
readonly bytes: Uint8Array;
|
|
238
|
+
/** The object's true total size, not the number of bytes returned. */
|
|
239
|
+
readonly size: number;
|
|
240
|
+
/**
|
|
241
|
+
* Optional: which object the read saw, as the backend names it (an S3 or Azure Blob ETag, a GCS object generation),
|
|
242
|
+
* taken from the response that carried `bytes`, or from the metadata read when no bytes were asked for. Opaque, and
|
|
243
|
+
* compared by equality only, by the driver that reported it. It names the object, not the key: two objects stored
|
|
244
|
+
* one after the other under one key carry different versions, except where the backend's version is computed from
|
|
245
|
+
* the bytes (an S3 ETag of an object stored without SSE-KMS or SSE-C is), where two objects with the same bytes, each
|
|
246
|
+
* stored whole in one request, or each in parts of the same sizes, share one. A driver that reports
|
|
247
|
+
* `conditionalDelete: true` reports it on every read; another may omit it.
|
|
248
|
+
*/
|
|
249
|
+
readonly version?: string;
|
|
250
|
+
}
|
|
251
|
+
/** Options of {@link IStorageDriver.delete}. */
|
|
252
|
+
export interface StorageDeleteOptions {
|
|
253
|
+
/**
|
|
254
|
+
* Delete the object only while it is the one this version names: the `version` a {@link IStorageDriver.getTail} of
|
|
255
|
+
* it reported. Pass the one read when the decision to delete was made, never one read after it, or the condition
|
|
256
|
+
* names whatever is under the key by then. Applied only by a driver that reports
|
|
257
|
+
* {@link StorageCaps.conditionalDelete}; ignored by any other, and absent, the delete is unconditional.
|
|
258
|
+
*/
|
|
259
|
+
readonly ifVersion?: string;
|
|
214
260
|
}
|
|
215
261
|
/**
|
|
216
262
|
* Immutable object storage for `.crbm` generations. A "dumb byte mover": it
|
|
@@ -239,9 +285,27 @@ export interface StorageCaps {
|
|
|
239
285
|
* negative or non-integer offset or length. Never a clamped, short or adjacent read. A pin whose object
|
|
240
286
|
* was purged and loaded again as a smaller one asks for a range past its end, and this error is what sends it
|
|
241
287
|
* to check that the object is still the one it pinned.
|
|
242
|
-
* - **`getTail` reports the object's true total size**, not the number of bytes it returned
|
|
288
|
+
* - **`getTail` reports the object's true total size**, not the number of bytes it returned, and, where the driver has
|
|
289
|
+
* one, the object's `version` ({@link TailRead.version}) from the same response as the bytes.
|
|
243
290
|
* - **`delete` is idempotent.** Deleting an absent key is a no-op, not an error: collection passes race each
|
|
244
|
-
* other and retry.
|
|
291
|
+
* other and retry. That holds with `ifVersion` too: an absent object is a no-op whatever version is given.
|
|
292
|
+
* - **`delete` with `ifVersion` does what {@link StorageCaps.conditionalDelete} says.** A driver that reports `true`
|
|
293
|
+
* removes the object only while it is the one the version names, by a check the backend applies in the same step as
|
|
294
|
+
* the removal (S3 `If-Match`, GCS `ifGenerationMatch`, Azure Blob `ifMatch`; in memory, the check and the removal with
|
|
295
|
+
* no await between them), and for another object under the key throws {@link WriteConflictError} and leaves it. A
|
|
296
|
+
* check made by reading the object first and deleting after is not one step, and does not qualify. Where the service
|
|
297
|
+
* answers a failed precondition for a key with no object, the driver looks again before it reports a conflict, so an
|
|
298
|
+
* absent object stays a no-op. A driver that reports `false`, or omits it, ignores `ifVersion` and deletes whatever is
|
|
299
|
+
* under the key. A number can be taken again once its object is deleted, so this is what keeps a delete decided from
|
|
300
|
+
* one read, and delayed, from removing an object stored under the number since: the erasure's delete of a holder
|
|
301
|
+
* above the pointer passes the version it read when it searched that object, and a refused load's delete of an
|
|
302
|
+
* object it proved its own by a footer read passes the version of that read.
|
|
303
|
+
* - **A driver that wraps another forwards `ifVersion` and the `version` of a tail read, or reports
|
|
304
|
+
* `conditionalDelete: false`.** A wrapper that hands `delete` the key alone, and reports the inner driver's
|
|
305
|
+
* capability, makes every conditional delete through it unconditional without a word.
|
|
306
|
+
* - **A conditional delete may be sent again**, by the backend client's retry, after a lost response: a copy that meets
|
|
307
|
+
* its own landed delete finds the object absent, which is its success, and one that meets an object stored since is
|
|
308
|
+
* refused. It can remove nothing the first copy could not.
|
|
245
309
|
* - **`list` is strongly consistent, read-after-delete.** Once `delete` has resolved, `list` no longer yields
|
|
246
310
|
* that generation, and once `putImmutable` has resolved it does. The erasure's re-check for a generation still
|
|
247
311
|
* holding the id, `dropSegment`'s `generationsRemaining`, the retention sweep's check that a tombstone's storage
|
|
@@ -276,15 +340,16 @@ export interface IStorageDriver {
|
|
|
276
340
|
*/
|
|
277
341
|
getRange(key: GenKey, offset: number, length: number): Promise<Uint8Array>;
|
|
278
342
|
/**
|
|
279
|
-
* Speculative tail read: the last `min(maxBytes, size)` bytes + the object's **true total size
|
|
280
|
-
* object throws {@link NotFoundError}.
|
|
343
|
+
* Speculative tail read: the last `min(maxBytes, size)` bytes + the object's **true total size**, and its `version`
|
|
344
|
+
* where the driver has one ({@link TailRead}). A missing object throws {@link NotFoundError}.
|
|
281
345
|
*/
|
|
282
|
-
getTail(key: GenKey, maxBytes: number): Promise<
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
346
|
+
getTail(key: GenKey, maxBytes: number): Promise<TailRead>;
|
|
347
|
+
/**
|
|
348
|
+
* Idempotent: deleting an absent key is a no-op, not an error. With `options.ifVersion`, on a driver that reports
|
|
349
|
+
* {@link StorageCaps.conditionalDelete}, the object is removed only while it is the one that version names, and
|
|
350
|
+
* another object under the key is left and the delete throws {@link WriteConflictError}; any other driver ignores it.
|
|
351
|
+
*/
|
|
352
|
+
delete(key: GenKey, options?: StorageDeleteOptions): Promise<void>;
|
|
288
353
|
/**
|
|
289
354
|
* Enumerate the generations present for a segment (orphan sweep / latest-gen resolution). Strongly
|
|
290
355
|
* consistent, read-after-delete and read-after-put.
|
|
@@ -307,10 +372,12 @@ export type RegistryStatus = 'active' | 'destroyed';
|
|
|
307
372
|
*/
|
|
308
373
|
export type GovernanceMeta = Record<string, unknown>;
|
|
309
374
|
/**
|
|
310
|
-
* A cached description of one generation, carried on the registry row: how many ids it holds,
|
|
311
|
-
*
|
|
312
|
-
* moves the pointer, and it names the generation it describes
|
|
313
|
-
* carry it can never be taken for another generation's.
|
|
375
|
+
* A cached description of one generation, carried on the registry row: how many ids it holds, the metadata it was
|
|
376
|
+
* loaded with, and the fingerprint of its object (its size and footer checksum). The generation's `.crbm` object stays
|
|
377
|
+
* the truth; this is a copy, written by the same write that moves the pointer, and it names the generation it describes
|
|
378
|
+
* so a copy left behind by a writer that did not carry it can never be taken for another generation's. The fingerprint
|
|
379
|
+
* names the object too: a reader checks every object it opens for the row's generation against it, and refuses one
|
|
380
|
+
* that is another object under that number.
|
|
314
381
|
*
|
|
315
382
|
* Two shapes. On a cleartext segment the values are in the clear. On an encrypted segment (a row with
|
|
316
383
|
* `wrappedDeks`) they are sealed under the segment's data key, as the generation's index is, so the row reveals
|
|
@@ -337,6 +404,12 @@ export interface ClearRegistrySummary {
|
|
|
337
404
|
readonly generation: number;
|
|
338
405
|
/** How many ids the generation holds: an integer from 0 to 2^32. */
|
|
339
406
|
readonly cardinality: number;
|
|
407
|
+
/**
|
|
408
|
+
* The fingerprint of the generation's object, `<size>:<footer checksum>` in decimal, as `CrbmReader.fingerprint` and
|
|
409
|
+
* a pin's `pinnedAt.fingerprint` spell it: the object's size in bytes, which `stat()` reports, and the CRC32C its
|
|
410
|
+
* footer stores.
|
|
411
|
+
*/
|
|
412
|
+
readonly fingerprint: string;
|
|
340
413
|
/** The generation's metadata, when it has any. Never the empty object. */
|
|
341
414
|
readonly metadata?: GenerationMetadata;
|
|
342
415
|
}
|
|
@@ -345,8 +418,9 @@ export interface SealedRegistrySummary {
|
|
|
345
418
|
/** The generation this describes. A non-negative safe integer. */
|
|
346
419
|
readonly generation: number;
|
|
347
420
|
/**
|
|
348
|
-
* Base64 of `nonce(12) ‖ ciphertext ‖ tag(16)`, sealing the cardinality as a little-endian u64
|
|
349
|
-
*
|
|
421
|
+
* Base64 of `nonce(12) ‖ ciphertext ‖ tag(16)`, sealing the cardinality as a little-endian u64, the object's size as a
|
|
422
|
+
* little-endian u64 and its footer checksum as a little-endian u32 (its fingerprint), then the metadata's canonical
|
|
423
|
+
* JSON, if any. The numbers are fixed-width, so the length reveals only the metadata's size.
|
|
350
424
|
*/
|
|
351
425
|
readonly sealed: string;
|
|
352
426
|
}
|
|
@@ -430,12 +504,23 @@ export interface RegistryRecord extends SegmentRef {
|
|
|
430
504
|
* restored from a backup. It holds with overwhelming probability rather than by construction, since the token carries random parts: a
|
|
431
505
|
* 128-bit incarnation id drawn when the row is created, and a 64-bit part drawn for each write. Two incarnations of one
|
|
432
506
|
* name meet with probability 2^-128 for any pair (about n^2 / 2^129 among n of them), and two writes at one counter,
|
|
433
|
-
* after a restore, with probability 2^-64.
|
|
434
|
-
* carries neither part.
|
|
507
|
+
* after a restore, with probability 2^-64.
|
|
435
508
|
*/
|
|
436
509
|
readonly token: Token;
|
|
510
|
+
/**
|
|
511
|
+
* The token of the most recent write that named a field a read resolves through: the row's create, or a compare-and-swap
|
|
512
|
+
* whose patch named `currentGen`, `status`, `wrappedDeks`, `keyId` or `summary` (the resolved fields), at any value,
|
|
513
|
+
* the one the field already had included. A write that names only `leases`, `retention`, `residency` or `keptGens`
|
|
514
|
+
* leaves it. The registry sets it; a caller never does. Compared by equality only, and never reused under one name,
|
|
515
|
+
* since a token is not: a reader keys what it caches on a generation with it, so a lease or a policy write leaves a
|
|
516
|
+
* warm reader's cache standing, and a write that changes the resolution never does.
|
|
517
|
+
*/
|
|
518
|
+
readonly pointerId: Token;
|
|
437
519
|
}
|
|
438
|
-
/**
|
|
520
|
+
/**
|
|
521
|
+
* The caller-settable fields at {@link IRegistryDriver.create} (audit, `token` and `pointerId` are driver-managed: the new
|
|
522
|
+
* row's `pointerId` is the token the create returns).
|
|
523
|
+
*/
|
|
439
524
|
export interface NewRegistryRecord {
|
|
440
525
|
/** `null` ⇒ the segment has no Storage generation yet — see {@link RegistryRecord.currentGen}. */
|
|
441
526
|
readonly currentGen: number | null;
|
|
@@ -451,13 +536,18 @@ export interface NewRegistryRecord {
|
|
|
451
536
|
readonly keptGens?: readonly number[];
|
|
452
537
|
}
|
|
453
538
|
/**
|
|
454
|
-
* Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity
|
|
539
|
+
* Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity, audit, `token` and `pointerId` are
|
|
540
|
+
* off-limits).
|
|
455
541
|
*
|
|
456
542
|
* Presence-based: a field the patch does not mention is left as it was. `summary` is the one exception, because it
|
|
457
543
|
* describes the current generation: a patch that moves `currentGen` and does not mention `summary` drops the old
|
|
458
544
|
* one rather than keep a description of another generation. A `summary` the patch gives must name the
|
|
459
545
|
* `currentGen` the row will have. `keptGens` follows the pointer the same way: a patch that moves `currentGen` and does
|
|
460
546
|
* not mention it drops it, and one that gives it must name only generations below the `currentGen` the row will have.
|
|
547
|
+
* "Moves" means changes value: a patch that names `currentGen` at the value it has keeps both.
|
|
548
|
+
*
|
|
549
|
+
* A patch that names a resolved field (`currentGen`, `status`, `wrappedDeks`, `keyId`, `summary`), by presence and at any
|
|
550
|
+
* value, sets the row's {@link RegistryRecord.pointerId} to the token the write returns; any other patch leaves it.
|
|
461
551
|
*/
|
|
462
552
|
export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency' | 'summary' | 'keptGens' | 'leases'>>;
|
|
463
553
|
/**
|
|
@@ -492,9 +582,7 @@ export interface RegCaps {
|
|
|
492
582
|
* `true` when this registry's `delete` removes a row from its backend for good, so that no later `list` reads it, and
|
|
493
583
|
* removes it only while it is still the exact version the delete read: by a delete the backend applies under a
|
|
494
584
|
* precondition (an S3 or Azure Blob ETag, a GCS object generation), or under a lock no other writer of the backend
|
|
495
|
-
* can take.
|
|
496
|
-
* 0.12 has a bare decimal token, and a delete still tombstones it: a process on that release, re-creating the name
|
|
497
|
-
* over nothing, would issue those counters again from 0, so its row could not be told apart from the deleted one.
|
|
585
|
+
* can take.
|
|
498
586
|
*
|
|
499
587
|
* `false` or absent: every `delete` leaves a tombstone, which every later full `list` still reads. A shipped registry
|
|
500
588
|
* says which it is.
|
|
@@ -613,6 +701,10 @@ export declare function isStorageBackend(value: unknown): value is StorageBacken
|
|
|
613
701
|
* keeps going; where nothing of the earlier row is left, or the row is restored from a backup to an older counter, the
|
|
614
702
|
* random parts alone keep the tokens apart: with probability 1 - 2^-128 per pair of incarnations, and 1 - 2^-64 per
|
|
615
703
|
* pair of writes at one counter.
|
|
704
|
+
* - **Every row carries its `pointerId`** ({@link RegistryRecord.pointerId}): `create` sets it to the token it returns,
|
|
705
|
+
* a `compareAndSwap` whose patch names a resolved field (`renewsPointer` in `@cloudbitmaps/core/driver-kit`) sets it
|
|
706
|
+
* to the token that write returns, and every other write leaves it. `get` and `list` return it. A caller's write
|
|
707
|
+
* never sets it. A row returned without one fails every read of the segment with {@link UnsupportedError}.
|
|
616
708
|
* - **`delete` is idempotent** without an `expected` token: deleting an absent row is a no-op, not an error.
|
|
617
709
|
* **With `expected` it is fenced**: it lands only while the row still carries that token, and otherwise throws
|
|
618
710
|
* {@link WriteConflictError} and leaves the row.
|
|
@@ -666,8 +758,8 @@ export interface IRegistryDriver {
|
|
|
666
758
|
*/
|
|
667
759
|
list(namespace?: string): AsyncIterable<RegistryRecord>;
|
|
668
760
|
/**
|
|
669
|
-
* Remove the row. A registry whose {@link RegCaps.conditionalDelete} is `true` removes
|
|
670
|
-
*
|
|
761
|
+
* Remove the row. A registry whose {@link RegCaps.conditionalDelete} is `true` removes it from its backend; any other
|
|
762
|
+
* tombstones it. Either way a later `create` gets a token no
|
|
671
763
|
* earlier incarnation of the name held, but for a collision of probability 2^-128 per pair of incarnations.
|
|
672
764
|
*
|
|
673
765
|
* Without `expected`, idempotent: deleting an absent row is a no-op.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The storage reader's defaults
|
|
3
|
-
*
|
|
4
|
-
* which pulled the whole read path into a module that is otherwise arithmetic.
|
|
2
|
+
* The storage reader's defaults. `@cloudbitmaps/tools` prices what two of them cause and keeps its own copies, which
|
|
3
|
+
* `tests/tools/store-defaults.test.ts` holds equal to these.
|
|
5
4
|
*/
|
|
6
5
|
/** Default TTL (ms) for re-resolving a segment's `currentGen` — the bound on post-publish read staleness. */
|
|
7
6
|
export declare const DEFAULT_CURRENT_GEN_TTL_MS = 2000;
|
|
@@ -16,3 +15,15 @@ export declare const REFRESH_RETRY_MS = 500;
|
|
|
16
15
|
export declare const DEFAULT_MAX_OPEN_SEGMENTS = 1024;
|
|
17
16
|
/** Default aggregate ceiling (bytes) on resident parsed indices in the reader cache — the steady-state byte bound. */
|
|
18
17
|
export declare const DEFAULT_MAX_OPEN_INDEX_BYTES: number;
|
|
18
|
+
/**
|
|
19
|
+
* How many segments' resolutions a store keeps per reader it keeps open: the resolution cache's count bound is this
|
|
20
|
+
* times `cache.readerMax` (8,192 by default). A resolution is the few hundred bytes of a row a read resolves through,
|
|
21
|
+
* kept for `cache.genTtlMs` whether or not the segment's reader is still open, so a store reading more segments at once
|
|
22
|
+
* than its readers hold keeps more of them than it keeps readers.
|
|
23
|
+
*/
|
|
24
|
+
export declare const RESOLUTIONS_PER_OPEN_SEGMENT = 8;
|
|
25
|
+
/**
|
|
26
|
+
* The resolution cache's byte bound is `cache.readerMaxBytes` divided by this (4 MiB by default): what it holds is a
|
|
27
|
+
* sixteenth of what the readers may.
|
|
28
|
+
*/
|
|
29
|
+
export declare const RESOLUTION_BYTES_DIVISOR = 16;
|
package/dist/core/rollback.d.ts
CHANGED
|
@@ -95,7 +95,8 @@ export interface RollbackResult {
|
|
|
95
95
|
* `currentGen`, where generation collection never looks. They stay until loads pass them (the first load whose
|
|
96
96
|
* number one of them holds numbers above them all, and collection then keeps the newest `keep` of what is below its
|
|
97
97
|
* pointer), `dropSegment` deletes them, or a subject erasure
|
|
98
|
-
* deletes them: all of those present when it rewrites, and
|
|
98
|
+
* deletes them: all of those present when it rewrites, and, when the current generation does not hold the id, only those
|
|
99
|
+
* holding it and any no read of the segment can open, whatever the id.
|
|
99
100
|
* Not collecting them is a deliberate trade: an operator who has just undone a bad load should not have the
|
|
100
101
|
* evidence collected out from under them, while a rollback target that still holds erased data would make the
|
|
101
102
|
* erasure undoable by an ordinary operator action.
|