@cloudbitmaps/core 0.20.0 → 0.21.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/README.md +85 -0
- package/dist/{chunk-YSC7NJWV.js → chunk-IGLIZBR7.js} +740 -137
- package/dist/chunk-IGLIZBR7.js.map +7 -0
- package/dist/core/absent-operands.d.ts +8 -0
- package/dist/core/audit.d.ts +12 -2
- package/dist/core/bit-route.d.ts +2 -1
- package/dist/core/blob.d.ts +12 -1
- package/dist/core/bulk-load.d.ts +148 -0
- package/dist/core/cas-retry.d.ts +27 -0
- package/dist/core/check.d.ts +22 -0
- package/dist/core/chunk-checks.d.ts +6 -1
- package/dist/core/chunk-window.d.ts +11 -4
- package/dist/core/combine-expr.d.ts +1 -3
- package/dist/core/combine-feed.d.ts +2 -16
- package/dist/core/combine-many.d.ts +12 -9
- package/dist/core/concurrency.d.ts +8 -0
- package/dist/core/consistency.d.ts +69 -17
- package/dist/core/crbm/fingerprint.d.ts +5 -0
- package/dist/core/crbm/format.d.ts +0 -1
- package/dist/core/crbm/plan-reads.d.ts +9 -3
- package/dist/core/crbm/reader.d.ts +52 -6
- package/dist/core/crbm/varint.d.ts +2 -1
- package/dist/core/crbm/writer.d.ts +5 -1
- package/dist/core/crbm-open.d.ts +154 -0
- package/dist/core/crbm-storage-source.d.ts +65 -452
- package/dist/core/crbm-write.d.ts +75 -0
- package/dist/core/crypto.d.ts +6 -1
- package/dist/core/engine.d.ts +222 -33
- package/dist/core/erase-id.d.ts +42 -7
- package/dist/core/erasure.d.ts +4 -3
- package/dist/core/errors.d.ts +8 -2
- package/dist/core/generation-gc.d.ts +129 -15
- package/dist/core/generation-list.d.ts +11 -0
- package/dist/core/id-window.d.ts +46 -0
- package/dist/core/invalidation-scope.d.ts +77 -0
- package/dist/core/known-generations-shape.d.ts +13 -0
- package/dist/core/known-generations.d.ts +147 -0
- package/dist/core/leases.d.ts +4 -3
- package/dist/core/ledger-seed.d.ts +17 -0
- package/dist/core/load.d.ts +17 -38
- package/dist/core/lru.d.ts +2 -15
- package/dist/core/metrics.d.ts +4 -2
- package/dist/core/order.d.ts +2 -0
- package/dist/core/pinned-storage-source.d.ts +4 -4
- package/dist/core/ports.d.ts +89 -16
- package/dist/core/publish.d.ts +207 -0
- package/dist/core/registry-scan.d.ts +0 -2
- package/dist/core/retention-sweep.d.ts +69 -65
- package/dist/core/retention.d.ts +1 -1
- package/dist/core/retry.d.ts +12 -19
- package/dist/core/rollback.d.ts +60 -31
- package/dist/core/summary.d.ts +50 -18
- package/dist/core/token.d.ts +2 -2
- package/dist/core/validate.d.ts +2 -0
- package/dist/driver-kit.d.ts +4 -0
- package/dist/driver-kit.js +80 -7
- package/dist/driver-kit.js.map +3 -3
- package/dist/drivers/_shared/bytes.d.ts +6 -0
- package/dist/drivers/_shared/faults.d.ts +15 -0
- package/dist/drivers/_shared/object-keys.d.ts +18 -0
- package/dist/drivers/_shared/object-registry-keys.d.ts +0 -2
- package/dist/drivers/_shared/object-registry.d.ts +2 -0
- package/dist/drivers/_shared/options.d.ts +19 -0
- package/dist/drivers/_shared/registry.d.ts +13 -10
- package/dist/drivers/localfs/exact-case.d.ts +0 -3
- package/dist/drivers/localfs/fs-util.d.ts +22 -0
- package/dist/drivers/localfs/registry.d.ts +10 -4
- package/dist/drivers/localfs/storage.d.ts +9 -2
- package/dist/drivers/memory.d.ts +7 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3647 -2410
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/chunk-YSC7NJWV.js.map +0 -7
- package/dist/core/kept-generations.d.ts +0 -40
package/dist/core/load.d.ts
CHANGED
|
@@ -1,26 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `loadSegment` — the loaded store's primary write path, as one call.
|
|
3
|
-
*
|
|
4
|
-
* A load is always the same four steps: take the next generation number, write one immutable object, move the
|
|
5
|
-
* pointer, collect what the move superseded. Inside the library each is its own internal function, and the
|
|
6
|
-
* step a hand-composed load leaves out is the last one, so a store that composed it would accumulate superseded
|
|
7
|
-
* generations it keeps paying for.
|
|
8
|
-
* Composing it here, once, is the point.
|
|
9
|
-
*
|
|
10
|
-
* The other reason it is one call is the **guard**. A load REPLACES a segment: whatever the stream contains is
|
|
11
|
-
* what the segment contains afterwards. That makes an upstream query returning fewer rows than usual — or none —
|
|
12
|
-
* a silent, successful-looking wipe, and nothing downstream can tell it from a legitimate shrink. The guard is
|
|
13
|
-
* the place to say what a plausible result looks like, and it has to sit **between the write and the publish**,
|
|
14
|
-
* because that is the only moment where the new content is known and the old one is still authoritative.
|
|
15
|
-
*
|
|
16
|
-
* Refusing therefore has to clean up after itself. The object is already durable at that point, and it sits
|
|
17
|
-
* ABOVE `currentGen`, where generation collection deliberately never looks (it deletes strictly below the
|
|
18
|
-
* pointer), so a refused load deletes its own object before returning, while the segment's row is unchanged (or
|
|
19
|
-
* 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.
|
|
23
|
-
*/
|
|
24
1
|
import { type IAuditSink } from './audit.js';
|
|
25
2
|
import { type CodecBitmap, type CodecInterface } from './codec.js';
|
|
26
3
|
import type { Clock, Rng } from './determinism.js';
|
|
@@ -116,11 +93,15 @@ export interface LoadOptions {
|
|
|
116
93
|
* longest pinned job runs, on every writer that loads the segment — see "Generations and `keep`" in the loading
|
|
117
94
|
* guide.
|
|
118
95
|
*
|
|
119
|
-
* The
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* row
|
|
96
|
+
* The window is the `keep` most recently current generations below the one the load publishes, so the generation the
|
|
97
|
+
* load superseded comes first. After a rollback that is the one the rollback restored, and it stays in the window of
|
|
98
|
+
* every load after it, ahead of the generations the rollback moved away from, for as many loads as `keep` says.
|
|
99
|
+
*
|
|
100
|
+
* The segment's row records the generations a load keeps, up to 64, so a load whose number one existence check found
|
|
101
|
+
* free collects without listing: it deletes the generations its publish pushed out of the window, and lists the
|
|
102
|
+
* segment's objects on every sixteenth generation to take whatever that pass leaves, such as an object a crashed load
|
|
103
|
+
* left below the pointer. A `keep` above 64 collects by listing on every load, and so does the first load of a row with
|
|
104
|
+
* no usable ledger, which starts one from the bucket; so does a load whose check found the current generation's object
|
|
124
105
|
* gone: the guard's read of it, or, when the guard took the size from the row's summary, one zero-byte read made
|
|
125
106
|
* before a `keep` of 1 or more takes a name. A `keep` at least the generation published collects nothing and asks
|
|
126
107
|
* for nothing.
|
|
@@ -142,11 +123,6 @@ export interface LoadOptions {
|
|
|
142
123
|
}
|
|
143
124
|
/** The bounds a guard can refuse a generation for: every {@link LoadRefusal} but a lost race. */
|
|
144
125
|
export type GuardRefusal = Exclude<LoadRefusal, 'superseded'>;
|
|
145
|
-
/**
|
|
146
|
-
* The bound a generation of `cardinality` ids breaks over a current one of `before` (`null`: none, or not read), in the
|
|
147
|
-
* order a load checks them, or `undefined`. Pure: the one judgement a load and a dry run share.
|
|
148
|
-
*/
|
|
149
|
-
export declare function guardRefusal(cardinality: number, before: number | null, guard: LoadGuard | undefined, allowEmpty: boolean | undefined): GuardRefusal | undefined;
|
|
150
126
|
/** What a dry run learns about one write: the size it would replace, and the bound that would refuse it. */
|
|
151
127
|
export interface LoadJudgement {
|
|
152
128
|
/**
|
|
@@ -173,16 +149,19 @@ export type LoadRefusal = 'empty' | 'min-cardinality' | 'min-retained' | 'max-gr
|
|
|
173
149
|
* put refused this one and it wrote nothing (`size: 0`); or the segment's registry row changed while the load was
|
|
174
150
|
* writing: another load published, a retention change, a rollback or an erasure wrote the row, the row was
|
|
175
151
|
* deleted, or a row appeared where the load found none. Once another write has changed the row, the object stays in
|
|
176
|
-
* the bucket, still billed, until a later load's collection deletes it
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
152
|
+
* the bucket, still billed, until a later load's collection deletes it or the segment is dropped, because its
|
|
153
|
+
* generation number may by then name another writer's object; until it goes, a rollback onto it needs `allowUnknown`, as onto any generation the row does not know. Once the row is
|
|
154
|
+
* `destroyed` (dropped or crypto-shredded), the object is deleted; once the row is gone, so is an object whose footer
|
|
155
|
+
* matches what this load wrote (a re-created segment's load of the same cleartext ids writes a match), and so is a
|
|
156
|
+
* cleartext one under a row with key material.
|
|
180
157
|
*/
|
|
181
158
|
| 'superseded';
|
|
182
159
|
export interface LoadResult {
|
|
183
160
|
/**
|
|
184
161
|
* The generation written. Present even when refused: a refusal deletes that object while the segment's row is
|
|
185
|
-
* unchanged or `destroyed`,
|
|
162
|
+
* unchanged or `destroyed`, and, once one read of its footer matches what the load wrote, when the row is gone or,
|
|
163
|
+
* for a cleartext object, has key material. Under any other row another write changed, the object stays in the
|
|
164
|
+
* bucket, an orphan a later load's collection takes, and until then a rollback onto it needs `allowUnknown`. A load that lost its
|
|
186
165
|
* generation number to another wrote nothing, and reports `size: 0`.
|
|
187
166
|
*/
|
|
188
167
|
readonly generation: number;
|
package/dist/core/lru.d.ts
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bounded LRU cache with optional TTL — the cache tier's memory ceiling.
|
|
3
|
-
*
|
|
4
|
-
* Time comes from an injected `Clock` (never `Date.now()`) so the cache is deterministic
|
|
5
|
-
* under simulation.
|
|
6
|
-
*
|
|
7
|
-
* The cache can be bounded by entry **count** (`maxEntries`, always) and, optionally, by an aggregate
|
|
8
|
-
* **byte weight** (`maxBytes`). The byte bound exists for values whose footprint is large and uneven — the
|
|
9
|
-
* storage reader cache, where one entry pins a fully-parsed `.crbm` index that can be ~6 MB for a wide segment
|
|
10
|
-
* yet a few hundred bytes for a small one (a count-only bound lets 1024 wide indices blow a Lambda's
|
|
11
|
-
* memory even though the count is "in bounds"). Weights arrive **after** insertion (a cached reader is a
|
|
12
|
-
* pending promise; its size is known only once it resolves), so weight is reported via {@link setWeight} and
|
|
13
|
-
* eviction re-runs then.
|
|
14
|
-
*/
|
|
15
1
|
import type { Clock } from './determinism.js';
|
|
16
|
-
|
|
2
|
+
interface LruOptions {
|
|
17
3
|
/** Hard ceiling on entries; the least-recently-used is evicted past it. */
|
|
18
4
|
readonly maxEntries: number;
|
|
19
5
|
/**
|
|
@@ -67,3 +53,4 @@ export declare class BoundedLru<K, V> {
|
|
|
67
53
|
/** Current aggregate reported weight (bytes) across all entries. */
|
|
68
54
|
get weightBytes(): number;
|
|
69
55
|
}
|
|
56
|
+
export {};
|
package/dist/core/metrics.d.ts
CHANGED
|
@@ -49,8 +49,10 @@ export type MetricEvent = {
|
|
|
49
49
|
readonly ms: number;
|
|
50
50
|
} | {
|
|
51
51
|
/**
|
|
52
|
-
* One decoded-chunk cache lookup
|
|
53
|
-
*
|
|
52
|
+
* One decoded-chunk cache lookup, or one chunk counted a miss without a lookup (emitted only when a cache is
|
|
53
|
+
* configured). `hit: false` means the cache did not hold the chunk. A combine or `iterate` counts every chunk it
|
|
54
|
+
* needs when it opens its streams: a lookup each, except that on a cache that holds nothing each chunk after the
|
|
55
|
+
* one a stream starts at is counted a miss without one. A caller that
|
|
54
56
|
* missed while another caller's read of the same chunk was open waits on that read and adds no `storage.get`, and a
|
|
55
57
|
* range carries several chunks, so the misses can outnumber the `storage.get` events; the `storage.get` count is
|
|
56
58
|
* the number of requests for chunks. `materializeMany` never looks up the cache, so it emits none.
|
|
@@ -27,14 +27,14 @@ import type { CrbmStorageChunkSource } from './crbm-storage-source.js';
|
|
|
27
27
|
* What a pin holds for one segment: the generation, the version identifying those exact bytes, and the pinned
|
|
28
28
|
* object's fingerprint, so a read can tell that the generation it opens is still that object.
|
|
29
29
|
*
|
|
30
|
-
* `fingerprint` is
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* `fingerprint` is required for a pin of a generation (`null` only when `generation` is `null`): without it a read
|
|
31
|
+
* could not tell a name purged and loaded again from the segment it pinned. `seg.pin()` always records it, and a
|
|
32
|
+
* pin of a generation without a string `version` and `fingerprint` is refused with a `ValidationError`.
|
|
33
33
|
*/
|
|
34
34
|
export interface PinnedAt {
|
|
35
35
|
readonly generation: number | null;
|
|
36
36
|
readonly version: string | null;
|
|
37
|
-
readonly fingerprint
|
|
37
|
+
readonly fingerprint: string | null;
|
|
38
38
|
}
|
|
39
39
|
export declare class PinnedStorageChunkSource implements StorageChunkSource {
|
|
40
40
|
private readonly inner;
|
package/dist/core/ports.d.ts
CHANGED
|
@@ -281,12 +281,18 @@ export interface StorageDeleteOptions {
|
|
|
281
281
|
* `getRange` may answer empty without reaching the backend, so it proves neither that the object exists nor
|
|
282
282
|
* that the offset lies inside it: the cloud drivers answer it empty, the memory and local-filesystem drivers
|
|
283
283
|
* throw for a missing object or an offset past the end.)
|
|
284
|
+
* - **A deleted object is missing to the next read**: once `delete(key)` has resolved, `getTail` and a non-empty `getRange`
|
|
285
|
+
* of that key throw {@link NotFoundError} (a zero-length `getRange` is the exemption above), with no stale answer from a
|
|
286
|
+
* cache or a replica. A rollback reads its target's footer once
|
|
287
|
+
* after moving the pointer, in place of listing the bucket, and takes a read that succeeds as proof that the object is
|
|
288
|
+
* there.
|
|
284
289
|
* - **An out-of-range read throws {@link ValidationError}**: a range that runs past the end of the object, or a
|
|
285
290
|
* negative or non-integer offset or length. Never a clamped, short or adjacent read. A pin whose object
|
|
286
291
|
* was purged and loaded again as a smaller one asks for a range past its end, and this error is what sends it
|
|
287
292
|
* to check that the object is still the one it pinned.
|
|
288
293
|
* - **`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.
|
|
294
|
+
* one, the object's `version` ({@link TailRead.version}) from the same response as the bytes. Its length is a safe
|
|
295
|
+
* integer, zero or less for an empty tail; any other length throws {@link ValidationError} before anything is read.
|
|
290
296
|
* - **`delete` is idempotent.** Deleting an absent key is a no-op, not an error: collection passes race each
|
|
291
297
|
* other and retry. That holds with `ifVersion` too: an absent object is a no-op whatever version is given.
|
|
292
298
|
* - **`delete` with `ifVersion` does what {@link StorageCaps.conditionalDelete} says.** A driver that reports `true`
|
|
@@ -299,7 +305,7 @@ export interface StorageDeleteOptions {
|
|
|
299
305
|
* under the key. A number can be taken again once its object is deleted, so this is what keeps a delete decided from
|
|
300
306
|
* one read, and delayed, from removing an object stored under the number since: the erasure's delete of a holder
|
|
301
307
|
* above the pointer passes the version it read when it searched that object, and a refused load's delete of an
|
|
302
|
-
* object
|
|
308
|
+
* object whose footer, read once, matched what it wrote passes the version of that read.
|
|
303
309
|
* - **A driver that wraps another forwards `ifVersion` and the `version` of a tail read, or reports
|
|
304
310
|
* `conditionalDelete: false`.** A wrapper that hands `delete` the key alone, and reports the inner driver's
|
|
305
311
|
* capability, makes every conditional delete through it unconditional without a word.
|
|
@@ -341,7 +347,10 @@ export interface IStorageDriver {
|
|
|
341
347
|
getRange(key: GenKey, offset: number, length: number): Promise<Uint8Array>;
|
|
342
348
|
/**
|
|
343
349
|
* 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}).
|
|
350
|
+
* where the driver has one ({@link TailRead}). `maxBytes` must be a safe integer: zero or less answers an empty tail
|
|
351
|
+
* with the size, and one past the size the whole object. Any other `maxBytes` (`NaN`, a fraction, a negative one
|
|
352
|
+
* included, an infinity of either sign, a whole number past the safe range (2^53 or more, or -2^53 or less)) throws
|
|
353
|
+
* {@link ValidationError} before anything is read. A missing object throws {@link NotFoundError}.
|
|
345
354
|
*/
|
|
346
355
|
getTail(key: GenKey, maxBytes: number): Promise<TailRead>;
|
|
347
356
|
/**
|
|
@@ -410,6 +419,12 @@ export interface ClearRegistrySummary {
|
|
|
410
419
|
* footer stores.
|
|
411
420
|
*/
|
|
412
421
|
readonly fingerprint: string;
|
|
422
|
+
/**
|
|
423
|
+
* The bytes from the end of the last chunk payload to the end of the object: the extension block, if any, the index and
|
|
424
|
+
* the footer. A cold open reads this many bytes from the object's end to open it in one request. An integer from 104,
|
|
425
|
+
* the footer's size, to the object's size less its 8-byte preamble, and at most 8,392,820.
|
|
426
|
+
*/
|
|
427
|
+
readonly tailBytes: number;
|
|
413
428
|
/** The generation's metadata, when it has any. Never the empty object. */
|
|
414
429
|
readonly metadata?: GenerationMetadata;
|
|
415
430
|
}
|
|
@@ -419,11 +434,24 @@ export interface SealedRegistrySummary {
|
|
|
419
434
|
readonly generation: number;
|
|
420
435
|
/**
|
|
421
436
|
* 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),
|
|
423
|
-
* JSON, if any. The numbers are fixed-width, so the length reveals only
|
|
437
|
+
* little-endian u64 and its footer checksum as a little-endian u32 (its fingerprint), its `tailBytes` as a
|
|
438
|
+
* little-endian u32, then the metadata's canonical JSON, if any. The numbers are fixed-width, so the length reveals only
|
|
439
|
+
* the metadata's size.
|
|
424
440
|
*/
|
|
425
441
|
readonly sealed: string;
|
|
426
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* A generation a row knows it published: its number, and the checksum its object's footer stores. A rollback moves only
|
|
445
|
+
* onto a generation the row knows, and an object whose footer carries another checksum is another object under that
|
|
446
|
+
* number. The footer's checksum covers the index's checksum, which covers each chunk's, so it commits to the object's ids
|
|
447
|
+
* and shape; for a cleartext object it does not cover the extension block's bytes.
|
|
448
|
+
*/
|
|
449
|
+
export interface KnownGeneration {
|
|
450
|
+
/** A non-negative safe integer. */
|
|
451
|
+
readonly generation: number;
|
|
452
|
+
/** The object's footer CRC32C (the second half of its fingerprint `<size>:<crc>`), an integer from 0 to 2^32 - 1. */
|
|
453
|
+
readonly footerCrc: number;
|
|
454
|
+
}
|
|
427
455
|
/**
|
|
428
456
|
* One lease on a generation. It names no fingerprint: a collector spares a generation by number and cannot compare an
|
|
429
457
|
* object without reading it, and a purge and re-create drops the whole list with the row. A pin's own fingerprint,
|
|
@@ -481,15 +509,28 @@ export interface RegistryRecord extends SegmentRef {
|
|
|
481
509
|
*/
|
|
482
510
|
readonly summary?: RegistrySummary;
|
|
483
511
|
/**
|
|
484
|
-
* The generations
|
|
485
|
-
*
|
|
486
|
-
*
|
|
512
|
+
* The generations this row knows and has not decided to delete, the current one included, with each object's footer
|
|
513
|
+
* checksum: the ones it made current, in the order they were made current, oldest first and the current one last, and,
|
|
514
|
+
* when a publish over a row with no usable ledger seeds one, the ones a count by `keep` keeps in the bucket, which can
|
|
515
|
+
* include an object no load published (an orphan or a stray). An entry says the library knows the generation and its
|
|
516
|
+
* footer checksum, not that a load published it. A generation appears once,
|
|
517
|
+
* at its most recent time current (a rollback onto it moves it to the end). The entries before the last are the kept
|
|
518
|
+
* window, the generations a live lease holds, and what a rollback left; those numbered above `currentGen` are what a
|
|
519
|
+
* rollback left. In the clear on every
|
|
520
|
+
* row, an encrypted one included: a footer checksum of an encrypted object covers a random nonce and says nothing about
|
|
521
|
+
* its count, size or content, but the entries above the pointer show anyone who reads the registry that a rollback
|
|
522
|
+
* happened, and from which generation.
|
|
523
|
+
*
|
|
524
|
+
* Optional: absent means the row does not know, and a load then lists the bucket to collect. A shipped registry stores
|
|
525
|
+
* the list in the order given, at most 192 entries written and 384 read, and refuses a list that repeats a
|
|
526
|
+
* generation, one whose last entry is not the row's `currentGen`, and one whose current entry's checksum disagrees with
|
|
527
|
+
* a clear `summary`'s fingerprint. A stored list that disagrees with its row is read and not used. A patch that moves
|
|
487
528
|
* `currentGen` without naming it drops it.
|
|
488
529
|
*/
|
|
489
|
-
readonly
|
|
530
|
+
readonly knownGens?: readonly KnownGeneration[];
|
|
490
531
|
/**
|
|
491
532
|
* The leases on this segment's generations: each keeps one generation out of a load's collection until it ends.
|
|
492
|
-
* Optional; absent means none. Unlike {@link
|
|
533
|
+
* Optional; absent means none. Unlike {@link knownGens} it does not follow the pointer: a patch that moves
|
|
493
534
|
* `currentGen` leaves it as it was, and only a patch that names it, or the row going away, changes it. Entries
|
|
494
535
|
* past their end are ignored by every collector and pruned by the next write that names the list. A row's identity
|
|
495
536
|
* is its token, so a purged and re-created segment starts with none.
|
|
@@ -510,7 +551,7 @@ export interface RegistryRecord extends SegmentRef {
|
|
|
510
551
|
/**
|
|
511
552
|
* 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
553
|
* 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 `
|
|
554
|
+
* the one the field already had included. A write that names only `leases`, `retention`, `residency` or `knownGens`
|
|
514
555
|
* leaves it. The registry sets it; a caller never does. Compared by equality only, and never reused under one name,
|
|
515
556
|
* 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
557
|
* warm reader's cache standing, and a write that changes the resolution never does.
|
|
@@ -532,8 +573,8 @@ export interface NewRegistryRecord {
|
|
|
532
573
|
readonly residency?: GovernanceMeta;
|
|
533
574
|
/** Must name `currentGen` when given. */
|
|
534
575
|
readonly summary?: RegistrySummary;
|
|
535
|
-
/**
|
|
536
|
-
readonly
|
|
576
|
+
/** In the order made current, the last entry the new row's `currentGen`; see {@link RegistryRecord.knownGens}. */
|
|
577
|
+
readonly knownGens?: readonly KnownGeneration[];
|
|
537
578
|
}
|
|
538
579
|
/**
|
|
539
580
|
* Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity, audit, `token` and `pointerId` are
|
|
@@ -542,14 +583,14 @@ export interface NewRegistryRecord {
|
|
|
542
583
|
* Presence-based: a field the patch does not mention is left as it was. `summary` is the one exception, because it
|
|
543
584
|
* describes the current generation: a patch that moves `currentGen` and does not mention `summary` drops the old
|
|
544
585
|
* one rather than keep a description of another generation. A `summary` the patch gives must name the
|
|
545
|
-
* `currentGen` the row will have. `
|
|
546
|
-
* not mention it drops it, and one that gives it must
|
|
586
|
+
* `currentGen` the row will have. `knownGens` follows the pointer the same way: a patch that moves `currentGen` and does
|
|
587
|
+
* not mention it drops it, and one that gives it must end with the `currentGen` the row will have.
|
|
547
588
|
* "Moves" means changes value: a patch that names `currentGen` at the value it has keeps both.
|
|
548
589
|
*
|
|
549
590
|
* A patch that names a resolved field (`currentGen`, `status`, `wrappedDeks`, `keyId`, `summary`), by presence and at any
|
|
550
591
|
* value, sets the row's {@link RegistryRecord.pointerId} to the token the write returns; any other patch leaves it.
|
|
551
592
|
*/
|
|
552
|
-
export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency' | 'summary' | '
|
|
593
|
+
export type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'status' | 'retention' | 'residency' | 'summary' | 'knownGens' | 'leases'>>;
|
|
553
594
|
/**
|
|
554
595
|
* What a caller passes {@link IRegistryDriver.create} or {@link IRegistryDriver.compareAndSwap} besides the write itself.
|
|
555
596
|
*/
|
|
@@ -566,6 +607,25 @@ export interface RegistryWriteOptions {
|
|
|
566
607
|
* settled by reading the row, never by this record. A driver that ignores the option reads the row.
|
|
567
608
|
*/
|
|
568
609
|
readonly held?: RegistryRecord | null;
|
|
610
|
+
/**
|
|
611
|
+
* Called with the token this write carries, before the write is sent. A write sent again with a new token reports
|
|
612
|
+
* it; one that repeats the same body repeats the token already reported. The token is the one the write records:
|
|
613
|
+
* when the write lands, the row's {@link RegistryRecord.token} is it, and so is its {@link RegistryRecord.pointerId}
|
|
614
|
+
* when the write names a resolved field. A write that does not land leaves no row carrying it. A call refused before
|
|
615
|
+
* it has a token to send, on a conflict its own read finds, reports none.
|
|
616
|
+
*
|
|
617
|
+
* The token is unique to that write attempt, also against another writer racing the same version: two writers that
|
|
618
|
+
* send a write against the same row version never carry the same token, so a version counter alone is not enough.
|
|
619
|
+
* Otherwise a loser's write would pass for the winner's.
|
|
620
|
+
*
|
|
621
|
+
* It tells a writer which row writes are its own, whatever became of its request: a write that failed, or got no
|
|
622
|
+
* answer, may still have landed, and the row's `pointerId` names its writer when no later write has renewed it: it is
|
|
623
|
+
* then one of the tokens the writer was told. Only a registry that reports {@link IRegistryDriver.reportsWriteTokens}
|
|
624
|
+
* calls it; one that does not ignores it, and a writer then settles such a write by reading the object it published.
|
|
625
|
+
* A registry that wraps or extends another and overrides `create` or `compareAndSwap` passes this option through to
|
|
626
|
+
* the write it forwards, or sets the flag to `false`.
|
|
627
|
+
*/
|
|
628
|
+
readonly onToken?: (token: Token) => void;
|
|
569
629
|
}
|
|
570
630
|
/** Capabilities a registry driver advertises; validated fail-fast at wiring time. */
|
|
571
631
|
export interface RegCaps {
|
|
@@ -723,6 +783,19 @@ export declare function isStorageBackend(value: unknown): value is StorageBacken
|
|
|
723
783
|
*/
|
|
724
784
|
export interface IRegistryDriver {
|
|
725
785
|
capabilities(): RegCaps;
|
|
786
|
+
/**
|
|
787
|
+
* `true` when every `create` and `compareAndSwap` of this registry reports the token its write carries to
|
|
788
|
+
* {@link RegistryWriteOptions.onToken} before sending it, a resend included. A load, an erasure's rewrite and every
|
|
789
|
+
* other publish then knows whether a write that conflicted or got no answer landed by whether the row's `pointerId`
|
|
790
|
+
* is a token it was told, which names the writer and not only the bytes. Absent or `false`: the registry reports
|
|
791
|
+
* none, and such a write is settled by reading the object the publish wrote. A shipped registry says which it is.
|
|
792
|
+
*
|
|
793
|
+
* The flag may be a getter or a field. A registry that wraps or extends another and overrides `create` or
|
|
794
|
+
* `compareAndSwap` either passes `options` through to the write it forwards, or sets `reportsWriteTokens` to
|
|
795
|
+
* `false`. One that does neither reads every write of its own that landed after an error or a lost answer as not its
|
|
796
|
+
* own (`superseded`), and deletes nothing.
|
|
797
|
+
*/
|
|
798
|
+
readonly reportsWriteTokens?: boolean;
|
|
726
799
|
/**
|
|
727
800
|
* The segment's record, or `null` if it doesn't exist (or was deleted). Throw {@link TransientError} for a fault
|
|
728
801
|
* worth riding out (throttling, a 5xx, a dropped connection): a reader whose pointer refresh meets one keeps
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { Yielder } from './cooperative.js';
|
|
2
|
+
import type { Rng } from './determinism.js';
|
|
3
|
+
import type { PublishedKept } from './generation-gc.js';
|
|
4
|
+
import type { WrappedDek } from './crypto.js';
|
|
5
|
+
import type { GenKey, IRegistryDriver, KnownGeneration, RegistryRecord, RegistrySummary, Token } from './ports.js';
|
|
6
|
+
/** What a publish is told, beyond the registry and the generation key. */
|
|
7
|
+
interface PublishOptions {
|
|
8
|
+
wrappedDeks?: readonly WrappedDek[];
|
|
9
|
+
expectFrom?: number;
|
|
10
|
+
expectToken?: Token;
|
|
11
|
+
/**
|
|
12
|
+
* The row `expectToken` names, as the caller read it. With it, a row whose token has moved but which differs from
|
|
13
|
+
* this one only in its leases (a reader took or released one) does not refuse the publish: it goes on against the
|
|
14
|
+
* fresh row, after a jittered wait and without the caller redoing its work, up to a bound. Any other difference
|
|
15
|
+
* refuses as ever. Without it, a moved token refuses.
|
|
16
|
+
*/
|
|
17
|
+
expectRow?: RegistryRecord | null;
|
|
18
|
+
/**
|
|
19
|
+
* Fields of `expectRow` that a row may also differ in, beside its leases, without the fence refusing: a write that
|
|
20
|
+
* changed only those did not touch what the caller derived its content from. An erasure's rewrite passes `knownGens`,
|
|
21
|
+
* since another erasure's removal of generations from the ledger leaves the generation it streamed alone. A write that
|
|
22
|
+
* also renewed the row's `pointerId` still refuses.
|
|
23
|
+
*/
|
|
24
|
+
expectRowIgnores?: readonly (keyof RegistryRecord)[];
|
|
25
|
+
/**
|
|
26
|
+
* Publish only while the segment still has **no registry row**.
|
|
27
|
+
*
|
|
28
|
+
* The fence for a caller whose decision rests on the row being ABSENT: every load that found no row sets it,
|
|
29
|
+
* guarded or not. `expectFrom` and `expectToken` cannot express it: both compare against a value read from a
|
|
30
|
+
* row, so when there was no row there is nothing to compare and both are simply omitted — leaving the publish a
|
|
31
|
+
* bare forward-only advance that happily lands over whatever appeared in the meantime.
|
|
32
|
+
*
|
|
33
|
+
* Without it, a guarded load into a segment that did not exist yet would judge "no row", pass its empty and
|
|
34
|
+
* `minRetained` bounds vacuously, and land an EMPTY generation over the ids a concurrent writer had just loaded,
|
|
35
|
+
* reporting `published: true`. An unguarded load would move the pointer over another first load's generation or
|
|
36
|
+
* onto a row `setRetention` made, and, where an erasure had deleted its object above that row's pointer, onto an
|
|
37
|
+
* object that is gone. Materialising into a destination that does not exist yet is the ordinary first run of a
|
|
38
|
+
* pipeline, so the `*Into` verbs reach this as often as `loadSegment` does.
|
|
39
|
+
*/
|
|
40
|
+
expectAbsent?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The generation's summary (see {@link RegistrySummary}), written by the write that moves the pointer to it. It
|
|
43
|
+
* names `key.generation`. Absent, the row is left with none for this generation.
|
|
44
|
+
*/
|
|
45
|
+
summary?: RegistrySummary;
|
|
46
|
+
/**
|
|
47
|
+
* The segment's row as the caller already read it (`null`: it found none). The first attempt acts on it
|
|
48
|
+
* instead of reading the row again, which is sound because that attempt writes only under the row's own
|
|
49
|
+
* fence: its compare-and-swap carries this row's token, and with no row it creates create-only, so a row
|
|
50
|
+
* that changed or appeared since makes the write lose and the next attempt reads the row afresh. Only the
|
|
51
|
+
* idempotent "already current" answer writes nothing, so a reused row never gives it.
|
|
52
|
+
*/
|
|
53
|
+
row?: RegistryRecord | null;
|
|
54
|
+
/**
|
|
55
|
+
* The object was written without encryption. Its publish is refused, with {@link KeyUnavailableError}, onto a row
|
|
56
|
+
* that carries key material: a reader of that segment opens every generation with its key, and a later
|
|
57
|
+
* `destroySegment` would attest that shredding it made a readable object unreadable. A caller decides its key
|
|
58
|
+
* from the row it read before the write, so this closes the window in which another writer creates the
|
|
59
|
+
* segment encrypted meanwhile.
|
|
60
|
+
*/
|
|
61
|
+
cleartext?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Whether the object under `key` is the one the caller wrote, and throws when it cannot tell. Asked only when a
|
|
64
|
+
* registry write failed and the row then names `key.generation`, on a registry that does not report its writes'
|
|
65
|
+
* tokens ({@link IRegistryDriver.reportsWriteTokens}; one that does settles it by the row's `pointerId` and never
|
|
66
|
+
* asks), to tell this publish's write from another writer's at the same number. Every caller that writes the object
|
|
67
|
+
* passes it. Without it, such a write that ended without an answer over a row that names `key.generation` cannot be
|
|
68
|
+
* settled: the publish throws {@link TransientError} and the caller keeps its object.
|
|
69
|
+
*/
|
|
70
|
+
holdsOwnObject?: () => Promise<boolean>;
|
|
71
|
+
/**
|
|
72
|
+
* What a publish waits on before it sends a fresh compare-and-swap, after a write that ended without an answer and
|
|
73
|
+
* left the row as it was. Without a `sleep`, such a write throws at once instead of being sent again.
|
|
74
|
+
*/
|
|
75
|
+
clock?: Yielder;
|
|
76
|
+
/** Spreads that wait (full jitter) so concurrent publishers do not retry together. Without it the wait is the bound. */
|
|
77
|
+
rng?: Rng;
|
|
78
|
+
/**
|
|
79
|
+
* Called once for each registry write that ended without an answer, before the row is read back. A caller whose
|
|
80
|
+
* publish then answers `false` can say that it cannot tell whether its own write landed first: the row may have
|
|
81
|
+
* held its generation for a while before another writer moved on.
|
|
82
|
+
*/
|
|
83
|
+
onUnanswered?: () => void;
|
|
84
|
+
/**
|
|
85
|
+
* The clock that judges whether a lease has ended. With it, a publish over a row that holds ended leases drops
|
|
86
|
+
* them in the write that moves the pointer, which rewrites the row anyway. Without it, leases are left as they are.
|
|
87
|
+
*/
|
|
88
|
+
leasesNow?: () => number;
|
|
89
|
+
/**
|
|
90
|
+
* Clear the row's leases in the write that moves the pointer: the caller's collection deletes every generation
|
|
91
|
+
* below the new pointer whatever a lease says, as a subject erasure's does.
|
|
92
|
+
*/
|
|
93
|
+
clearLeases?: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* How many generations below the new pointer the caller keeps. With it, the write that moves the pointer also
|
|
96
|
+
* records the window in the row (the entries of `knownGens`: the `keep` most recently current of the generations
|
|
97
|
+
* below the new one, and the ones a live lease holds), derived from the very row the write is made against, and the
|
|
98
|
+
* result names the generations the window dropped. Without it, or above the most a row collects by name, the result
|
|
99
|
+
* names no window, and a collection lists.
|
|
100
|
+
*/
|
|
101
|
+
keep?: number;
|
|
102
|
+
/**
|
|
103
|
+
* The footer checksum of the object under `key` (the second half of its fingerprint), which the write that moves the
|
|
104
|
+
* pointer records in the row's `knownGens` beside the generation. It comes from the object the caller wrote or opened,
|
|
105
|
+
* the same one its `summary` describes.
|
|
106
|
+
*/
|
|
107
|
+
footerCrc: number;
|
|
108
|
+
/**
|
|
109
|
+
* The window a ledger starts with, over a row that has no usable one ({@link readSeed}): the generations a count by
|
|
110
|
+
* `keep` keeps, which the write records as the ledger's entries before the new generation.
|
|
111
|
+
*/
|
|
112
|
+
seed?: readonly KnownGeneration[];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Point a segment's registry `currentGen` at `key.generation` — the publish step that makes a freshly-written
|
|
116
|
+
* generation the authoritative latest (so registry-aware readers see it). **Forward-only and idempotent:**
|
|
117
|
+
* if the registry has no row it creates one; if it's already at/ahead of `key.generation` it's a no-op (an
|
|
118
|
+
* out-of-order/duplicate publish never regresses the pointer); otherwise it advances via compare-and-swap,
|
|
119
|
+
* retrying a few times under contention. Separated from the Storage write so callers can publish atomically
|
|
120
|
+
* after the immutable object is durable (write-then-publish).
|
|
121
|
+
*
|
|
122
|
+
* **Throws** rather than returning, in two cases, because each would otherwise create a state no reader can
|
|
123
|
+
* recover from: a `destroyed` (crypto-shredded) row, where advancing the pointer would name an object encrypted
|
|
124
|
+
* under a shredded key; and an advance carrying **new** `wrappedDeks`, which would either strand that key or make
|
|
125
|
+
* the row advertise encryption over cleartext generations. Contention that never clears throws
|
|
126
|
+
* {@link WriteConflictError} after the retries.
|
|
127
|
+
*
|
|
128
|
+
* **`expectFrom` turns forward-only into read-modify-write.** Forward-only is the right rule for a writer whose
|
|
129
|
+
* content does not depend on what was current — a load computes its ids upstream, so publishing over a newer
|
|
130
|
+
* generation loses nothing the loader knew about. It is the WRONG rule for a writer that *derived* its content
|
|
131
|
+
* from a particular generation: the erasure rewrite streams generation `from` and clears one bit, so publishing
|
|
132
|
+
* it over a newer generation silently discards whatever that generation added. Passing `expectFrom` makes the
|
|
133
|
+
* publish land only while the pointer is still exactly there, and return `false` otherwise, so the caller can
|
|
134
|
+
* report `superseded` and re-derive. The compare-and-swap itself carries the token read in the same iteration,
|
|
135
|
+
* so the check and the write see the same row.
|
|
136
|
+
*
|
|
137
|
+
* **`expectToken` fences the LINEAGE, and a derived writer needs both.** A generation number identifies a
|
|
138
|
+
* generation only within one incarnation of a name: the numbering restarts at `0` once the row is purged and
|
|
139
|
+
* the bucket empty, so a name that is retired and re-created has a *different* segment wearing the *same*
|
|
140
|
+
* `currentGen`. `expectFrom` alone matched it — and an erasure rewrite then published one incarnation's content
|
|
141
|
+
* over another's, deleted the live objects with its `keep: 0` collection, and returned `erased: true`. The row's
|
|
142
|
+
* OCC token is the identity that survives this: a shipped registry's later `create` gets a token never issued before
|
|
143
|
+
* under that name, but for a collision of probability 2^-128 per pair of incarnations, so no token is reused across them.
|
|
144
|
+
*
|
|
145
|
+
* Pass the token read alongside `expectFrom` and the publish lands only on the same row it was derived from.
|
|
146
|
+
* The check is deliberately **conservative**: the token also changes on writes that are not supersessions at
|
|
147
|
+
* all (a `setRetention`, a due-index reindex), so such a write makes a derived publish report `superseded` and
|
|
148
|
+
* the caller re-derive. That costs a re-run on a rare, unrelated write; the alternative costs a segment.
|
|
149
|
+
*
|
|
150
|
+
* **The driver sends a registry write once, and an unanswered one is reconciled by its effect.** A `create` or
|
|
151
|
+
* `compareAndSwap` that fails with {@link TransientError} (a throttle, a lost response, a timeout) may or may not
|
|
152
|
+
* have landed, and may still be in flight. The publish reads the row once more and decides from what it holds:
|
|
153
|
+
*
|
|
154
|
+
* - the pointer names `key.generation`, on the incarnation the write was made against (the row's `createdAt`), and the
|
|
155
|
+
* write is the caller's own: the write is taken to have landed, and the publish returns `true`. A registry that
|
|
156
|
+
* reports each write's token ({@link IRegistryDriver.reportsWriteTokens}) settles it by the row's `pointerId`, which
|
|
157
|
+
* names the writer of the write that last set the pointer: the write is the call's own when that is the token of a
|
|
158
|
+
* write this call sent. One that does not is settled by `holdsOwnObject` finding the caller's bytes under the number:
|
|
159
|
+
* only the writer of a write-once object publishes its number, but a number is taken again once its object is
|
|
160
|
+
* deleted, and a load of the same cleartext ids writes the same bytes, so another writer's identical object under
|
|
161
|
+
* the number passes for the caller's;
|
|
162
|
+
* - the row is still the one the write was made against (the same token, or still no row): the write may land yet. The
|
|
163
|
+
* publish then waits, on the injected clock, and sends a **new** write from the row it just read: a request of its
|
|
164
|
+
* own, not a replay of the driver's, carrying the same version, so under the registry's fence at most one of the two
|
|
165
|
+
* lands and the other meets a conflict, which the next read settles. At most {@link UNANSWERED_RESENDS} follow, after
|
|
166
|
+
* a wait under 500 ms, then 1 s, then 2 s (full jitter when an `rng` is given); each is settled the same way. Still
|
|
167
|
+
* unanswered, or with no clock to wait on, this throws the registry's own `TransientError` and writes nothing more.
|
|
168
|
+
* A caller must not delete its object on it;
|
|
169
|
+
* - anything else: the row has moved past the state the write was conditioned on, so that write can never land, and
|
|
170
|
+
* the publish goes on as after a lost race, from the row it just read. A pointer at `key.generation` that is not over
|
|
171
|
+
* the caller's own object, or is on another incarnation, is a lost race too and answers `false`, never an already
|
|
172
|
+
* current re-publish.
|
|
173
|
+
*
|
|
174
|
+
* A failed read throws `TransientError` too. The same check of the pointer runs after a {@link WriteConflictError},
|
|
175
|
+
* so a write that landed and then met itself (a registry whose client re-sent it) is not taken for a lost race.
|
|
176
|
+
* The check runs before the next attempt's `expectAbsent`, `expectToken` and `expectFrom` fences, which a write of the
|
|
177
|
+
* caller's own that landed would fail. With a registry that reports tokens it names the writer of the write that last
|
|
178
|
+
* set the pointer, so another writer's identical object under the number does not pass it; with one that does not,
|
|
179
|
+
* that object can.
|
|
180
|
+
*
|
|
181
|
+
* **A `summary` goes in the same write that moves the pointer**, so a reader that sees this generation as current
|
|
182
|
+
* sees the count and metadata that describe it, and every attempt sends the same one. It must be the shape the row's
|
|
183
|
+
* keys call for: sealed for an object written with a key, clear for one written without.
|
|
184
|
+
*/
|
|
185
|
+
export declare function publishGenerationKept(registry: IRegistryDriver, key: GenKey, options: PublishOptions): Promise<PublishResult>;
|
|
186
|
+
/** What a publish did: whether the pointer is at the generation, and what the write recorded in the row. */
|
|
187
|
+
export interface PublishResult {
|
|
188
|
+
readonly published: boolean;
|
|
189
|
+
/**
|
|
190
|
+
* What the write that moved the pointer recorded: the list, the generations the window dropped, and the new row's
|
|
191
|
+
* token. Absent when no write of this call is known to have landed (the pointer was already there, or the outcome
|
|
192
|
+
* was settled without knowing which write it was), and a collection then lists.
|
|
193
|
+
*/
|
|
194
|
+
readonly kept?: PublishedKept;
|
|
195
|
+
/**
|
|
196
|
+
* A token of the row whose pointer is at the generation, when it is published: the one the write was given, or the
|
|
197
|
+
* row read that showed the pointer there. Every write of a row keeps its incarnation, so this names the incarnation
|
|
198
|
+
* the publish landed in, for the audit event.
|
|
199
|
+
*/
|
|
200
|
+
readonly token?: Token;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* {@link publishGenerationKept}'s answer to "did the pointer move", for a caller with no use for what it recorded:
|
|
204
|
+
* a publish with no `keep` leaves the row recording no list.
|
|
205
|
+
*/
|
|
206
|
+
export declare function publishGeneration(registry: IRegistryDriver, key: GenKey, options: PublishOptions): Promise<boolean>;
|
|
207
|
+
export {};
|
|
@@ -4,8 +4,6 @@ import type { IRegistryDriver, RegistryRecord } from './ports.js';
|
|
|
4
4
|
* the fleet really is that large *and* the memory is there; narrow the scan with a `namespace` otherwise.
|
|
5
5
|
*/
|
|
6
6
|
export declare const DEFAULT_MAX_SCAN_SEGMENTS = 250000;
|
|
7
|
-
/** Fail fast on a bad ceiling BEFORE the (possibly huge) scan, not after. */
|
|
8
|
-
export declare function validateMaxScanSegments(value: number, op: string): void;
|
|
9
7
|
/**
|
|
10
8
|
* Drain every registry record (optionally one namespace) into an array, refusing past `maxScanSegments`.
|
|
11
9
|
*
|