@cloudbitmaps/core 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-5OMC5U7U.js → chunk-5W3XS2IU.js} +292 -22
- package/dist/chunk-5W3XS2IU.js.map +7 -0
- package/dist/core/audit.d.ts +6 -0
- package/dist/core/codec.d.ts +33 -1
- package/dist/core/consistency.d.ts +21 -2
- package/dist/core/cost.d.ts +91 -10
- package/dist/core/crbm/format.d.ts +31 -1
- package/dist/core/crbm/reader.d.ts +77 -18
- package/dist/core/crbm/writer.d.ts +32 -3
- package/dist/core/crbm-storage-source.d.ts +204 -23
- package/dist/core/crypto.d.ts +11 -7
- package/dist/core/determinism.d.ts +7 -0
- package/dist/core/engine.d.ts +22 -7
- package/dist/core/erase-id.d.ts +20 -0
- package/dist/core/erasure.d.ts +8 -4
- package/dist/core/generation-gc.d.ts +102 -4
- package/dist/core/load-input.d.ts +55 -0
- package/dist/core/load.d.ts +65 -10
- package/dist/core/metadata.d.ts +42 -0
- package/dist/core/pinned-storage-source.d.ts +2 -1
- package/dist/core/ports.d.ts +119 -13
- package/dist/core/registry-scan.d.ts +6 -0
- package/dist/core/retention-sweep.d.ts +47 -2
- package/dist/core/retry.d.ts +14 -0
- package/dist/core/rollback.d.ts +34 -4
- package/dist/core/summary.d.ts +61 -0
- package/dist/core/token.d.ts +36 -0
- package/dist/core/validate.d.ts +2 -0
- package/dist/driver-kit.js +39 -20
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/entropy.d.ts +5 -0
- package/dist/drivers/_shared/object-registry.d.ts +36 -6
- package/dist/drivers/_shared/registry.d.ts +47 -19
- package/dist/drivers/localfs/registry.d.ts +10 -0
- package/dist/drivers/memory.d.ts +9 -3
- package/dist/drivers/retry/retrying-chunk-source.d.ts +2 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1922 -512
- package/dist/index.js.map +4 -4
- package/dist/testing/fuzz-core.d.ts +6 -5
- package/package.json +1 -1
- package/dist/chunk-5OMC5U7U.js.map +0 -7
package/dist/core/crypto.d.ts
CHANGED
|
@@ -62,19 +62,23 @@ export interface IKeystore {
|
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
64
|
* What the encrypting codec needs: the {@link Aead} for this segment's DEK plus a way to build the AAD for a
|
|
65
|
-
* given scope (`'index'
|
|
66
|
-
* codec itself stays segment-agnostic.
|
|
65
|
+
* given scope (a chunkKey, `'index'`, or `'metadata'` for the generation's metadata section). Built per
|
|
66
|
+
* (segment, generation) by the storage-source bridge so the codec itself stays segment-agnostic.
|
|
67
67
|
*/
|
|
68
68
|
export interface CrbmCrypto {
|
|
69
69
|
readonly aead: Aead;
|
|
70
|
-
aadFor(scope: number | 'index'): Uint8Array;
|
|
70
|
+
aadFor(scope: number | 'index' | 'metadata'): Uint8Array;
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
* Build the AEAD associated-data binding a chunk
|
|
74
|
-
*
|
|
75
|
-
*
|
|
73
|
+
* Build the AEAD associated-data binding a chunk, the index, the metadata section or a registry row's sealed summary
|
|
74
|
+
* to its exact location:
|
|
75
|
+
* `v1 ‖ len(namespace) ‖ namespace ‖ len(segment) ‖ segment ‖ generation ‖ scope ‖ chunkKey`, where scope is
|
|
76
|
+
* `0` for a chunk, `1` for the index, `2` for the metadata and `3` for the row's summary, and chunkKey is `0` for
|
|
77
|
+
* the last three.
|
|
78
|
+
* Length-prefixed so distinct `(namespace, segment)` pairs can never collide. Pure (no crypto) — just the
|
|
79
|
+
* authenticated label.
|
|
76
80
|
*/
|
|
77
81
|
export declare function aadFor(ref: {
|
|
78
82
|
readonly namespace?: string;
|
|
79
83
|
readonly segment: string;
|
|
80
|
-
}, generation: number, scope: number | 'index'): Uint8Array;
|
|
84
|
+
}, generation: number, scope: number | 'index' | 'metadata' | 'summary'): Uint8Array;
|
|
@@ -40,3 +40,10 @@ export interface Rng {
|
|
|
40
40
|
/** A float in the half-open interval [0, 1). */
|
|
41
41
|
next(): number;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Injected entropy: `length` cryptographically strong random bytes, for an identity that must never repeat (a
|
|
45
|
+
* registry row's incarnation id, and the random part every registry write adds to its token). Deliberately not an
|
|
46
|
+
* {@link Rng}: an `Rng` is seedable for simulation, and a seeded source hands every process the same "random"
|
|
47
|
+
* identity. Production wiring uses the platform's Web Crypto; a test injects a deterministic source to replay a run.
|
|
48
|
+
*/
|
|
49
|
+
export type Entropy = (length: number) => Uint8Array;
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Budget, BudgetOption } from './budget.js';
|
|
|
3
3
|
import type { Clock } from './determinism.js';
|
|
4
4
|
import type { BoundedLru } from './lru.js';
|
|
5
5
|
import type { IMetricsSink } from './metrics.js';
|
|
6
|
-
import type { StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
|
|
6
|
+
import type { GenerationMetadata, StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
|
|
7
7
|
export interface EngineDeps {
|
|
8
8
|
readonly storage: StorageChunkSource;
|
|
9
9
|
/** Optional cache of decoded (immutable) Storage chunks. */
|
|
@@ -74,23 +74,38 @@ export declare class SegmentEngine {
|
|
|
74
74
|
/** Membership: one chunk lookup — the cache, else one Storage fetch of that chunk. */
|
|
75
75
|
has(seg: SegmentRef, id: number): Promise<boolean>;
|
|
76
76
|
/**
|
|
77
|
-
* Cardinality. When the Storage source can
|
|
78
|
-
*
|
|
79
|
-
*
|
|
77
|
+
* Cardinality. When the Storage source can describe the current generation (the `.crbm` source can), the count is
|
|
78
|
+
* that description's: the registry row's summary when the row has one it can use, which is one request when cold
|
|
79
|
+
* and no read of the object, and otherwise the sum of the object's index. Either way, **zero payload reads**. A
|
|
80
|
+
* source that can only serve per-chunk cardinality sums those, and one with neither (the in-memory source) falls back
|
|
81
|
+
* to fetching every chunk.
|
|
80
82
|
*
|
|
81
|
-
* The index sum
|
|
83
|
+
* The row's summary and the index sum are trusted, not confirmed: the source holds the summary against the object
|
|
84
|
+
* whenever it opens it, and the reader checked the index for internal consistency when it opened
|
|
82
85
|
* the object, and a corrupt index that is still internally consistent yields a wrong count. `iterate()` and the
|
|
83
86
|
* combines decode the payloads.
|
|
84
87
|
*/
|
|
85
88
|
count(seg: SegmentRef): Promise<number>;
|
|
89
|
+
/**
|
|
90
|
+
* What the segment's current generation is: its number, its id count and its metadata, from one resolution, so the
|
|
91
|
+
* three cannot straddle a publish. A source with no summary answers from `count()` and the generation it resolves.
|
|
92
|
+
*/
|
|
93
|
+
stat(seg: SegmentRef): Promise<{
|
|
94
|
+
generation: number | null;
|
|
95
|
+
cardinality: number;
|
|
96
|
+
metadata?: GenerationMetadata;
|
|
97
|
+
}>;
|
|
86
98
|
/** Whether the Storage source can measure segment size (for grounded cost); false ⇒ storage isn't grounded. */
|
|
87
99
|
get supportsStorageSize(): boolean;
|
|
88
100
|
/** How often the Storage source re-reads a pointer while reading (cost reporting); undefined if it does not say. */
|
|
89
101
|
get pointerRefreshMs(): number | undefined;
|
|
90
102
|
/** Grounded Storage size of a segment's current generation (cost reporting), or null if it has no generation. */
|
|
91
103
|
segmentSize(seg: SegmentRef): Promise<SegmentSize | null>;
|
|
104
|
+
/** The chunks of `seg` at `chunkKeys`, read through a window of {@link DEFAULT_INTERSECT_CONCURRENCY}. */
|
|
105
|
+
private chunkWindow;
|
|
92
106
|
/**
|
|
93
|
-
* Every id, ascending,
|
|
107
|
+
* Every id, ascending, reading ahead through a window of up to 8 chunk fetches that opens 1, 2, 4, 8 wide, so a
|
|
108
|
+
* read that stops early has fetched only a handful of chunks past the one it stopped in; with `range`, only the ids in `(after, through]`, fetching only the
|
|
94
109
|
* chunks the range overlaps (see {@link IdRange}).
|
|
95
110
|
*
|
|
96
111
|
* Two generators, not one with a branch: a second `yield` site in the full read's generator grows its frame, and
|
|
@@ -124,7 +139,7 @@ export declare class SegmentEngine {
|
|
|
124
139
|
* up front (before the fan-out) and threaded into every chunk read, so a concurrent load can't corrupt or tear
|
|
125
140
|
* the result — every chunk read is a whole, checksum-verified, immutable generation. The edge a *long* call can
|
|
126
141
|
* hit is invariant 3's: if it straddles a mid-call `cache.genTtlMs` boundary and a load has published, an
|
|
127
|
-
* operand's not-yet-
|
|
142
|
+
* operand's not-yet-requested chunks may re-resolve forward to the newer generation (a generation hop within one long
|
|
128
143
|
* call) — the call never crashes or returns a torn object, but may mix generations. Three things hop it without
|
|
129
144
|
* waiting for the TTL, so a shorter call can meet them too: **the reader cache evicting an operand mid-call**
|
|
130
145
|
* (`maxOpenSegments`), whose re-read re-resolves fresh; a sweep deleting the generation it was reading, which
|
package/dist/core/erase-id.d.ts
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* (`keep: 0`), so the bit is **physically gone from the bucket when this returns**. Constant memory: one chunk in
|
|
10
10
|
* flight, never the whole segment.
|
|
11
11
|
*
|
|
12
|
+
* The rewrite is the same generation without one id, so it keeps everything else: the new object carries the source's
|
|
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. On an encrypted segment, a source object with no metadata block whose
|
|
15
|
+
* row's sealed summary of it has metadata is rewritten with the row's: the block's presence is not authenticated, so
|
|
16
|
+
* the authenticated copy decides, and the erasure still goes through.
|
|
17
|
+
*
|
|
12
18
|
* **`erased: true` is a claim about every generation of the segment, not only the one it replaced.** A rollback
|
|
13
19
|
* leaves generations above the pointer that were once current and can be made current again, so a holder can sit
|
|
14
20
|
* above the pointer as well as below it, and there can be several. Before it reports `erased: true` the call
|
|
@@ -69,8 +75,10 @@
|
|
|
69
75
|
import { type IAuditSink } from './audit.js';
|
|
70
76
|
import type { CodecInterface } from './codec.js';
|
|
71
77
|
import type { Yielder } from './cooperative.js';
|
|
78
|
+
import type { Rng } from './determinism.js';
|
|
72
79
|
import type { IKeystore } from './crypto.js';
|
|
73
80
|
import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
|
|
81
|
+
import { type ReadRetry } from './retry.js';
|
|
74
82
|
/** What {@link eraseIdFromSegment} needs: the objects, the pointer, the codec, and the key material if encrypted. */
|
|
75
83
|
export interface EraseIdDeps {
|
|
76
84
|
readonly storage: IStorageDriver;
|
|
@@ -90,8 +98,20 @@ export interface EraseIdDeps {
|
|
|
90
98
|
readonly requireEncryption?: boolean;
|
|
91
99
|
/** Supplying a clock that can yield makes the rewrite cooperative, as it makes a load. */
|
|
92
100
|
readonly clock?: Yielder;
|
|
101
|
+
/**
|
|
102
|
+
* The random source that spreads the waits between the rewrite's fresh compare-and-swaps. Absent, the read retry's
|
|
103
|
+
* source is used if it has one, and otherwise each wait is its bound.
|
|
104
|
+
*/
|
|
105
|
+
readonly rng?: Rng;
|
|
93
106
|
/** Per-chunk decode ceiling (invariant 5); defaults to 1 MiB. */
|
|
94
107
|
readonly maxBitmapBytes?: number;
|
|
108
|
+
/**
|
|
109
|
+
* The store's read retry, for the reads the rewrite makes along the way: the generation it rewrites and each of its
|
|
110
|
+
* chunks, the read-back that verifies the generation it wrote, and any other generation it checks for the id. A
|
|
111
|
+
* transient fault on one is run again under it rather than failing the erasure. Absent, each read is made once.
|
|
112
|
+
* The deletes are not retried, and the rewrite's registry write is settled as a load's is.
|
|
113
|
+
*/
|
|
114
|
+
readonly readRetry?: ReadRetry;
|
|
95
115
|
}
|
|
96
116
|
export interface EraseIdResult {
|
|
97
117
|
readonly segment: string;
|
package/dist/core/erasure.d.ts
CHANGED
|
@@ -159,10 +159,14 @@ export interface DropResult {
|
|
|
159
159
|
* cleaned up by re-running; a torn pointer costs correctness and is not self-healing. Given the choice, leak
|
|
160
160
|
* bytes — but say so: whatever survives the sweep is reported in {@link DropResult.generationsRemaining}.
|
|
161
161
|
*
|
|
162
|
-
* **Why step 2 sweeps more than once.** A load that
|
|
163
|
-
* still finishes the write — its publish is then refused,
|
|
164
|
-
* set. A single list-then-delete misses it entirely. The re-sweep
|
|
165
|
-
* fence on **publishing**, so only already
|
|
162
|
+
* **Why step 2 sweeps more than once.** A load that had read the row before the tombstone landed, whether it was
|
|
163
|
+
* already writing its object or still consuming its ids, still finishes the write — its publish is then refused,
|
|
164
|
+
* but the object exists, and it holds the complete set. A single list-then-delete misses it entirely. The re-sweep
|
|
165
|
+
* converges because the tombstone *is* a hard fence on **publishing**, so only loads already under way can write
|
|
166
|
+
* and they are finite. One that writes after the last pass deletes its own object when its publish is refused, as a
|
|
167
|
+
* refused load does under a `destroyed` row; only one whose process stops in between, or whose publish fails without
|
|
168
|
+
* a definite answer (a lost response, a timeout), leaves it behind, for a re-run of the drop, and
|
|
169
|
+
* `generationsRemaining` cannot report an object written after this call returned.
|
|
166
170
|
*
|
|
167
171
|
* **When "reads as empty" starts being true.** Not instantly, for a store that has already read this segment: a
|
|
168
172
|
* resolved generation is cached and decoded chunks sit in the cache, so an in-flight reader can answer from
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
|
|
1
|
+
import type { GenKey, IStorageDriver, IRegistryDriver, RegistryRecord, SegmentRef } from './ports.js';
|
|
2
2
|
/** The two ports generation bookkeeping needs: the objects, and the pointer that says which one is current. */
|
|
3
3
|
export interface GenerationDeps {
|
|
4
4
|
readonly storage: IStorageDriver;
|
|
@@ -6,7 +6,9 @@ export interface GenerationDeps {
|
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* The generation number a writer should use for the segment's **next** object: one above the highest generation
|
|
9
|
-
* the registry points at *or* that is present in Storage — whichever is higher.
|
|
9
|
+
* the registry points at *or* that is present in Storage — whichever is higher. The erasure rewrite numbers this
|
|
10
|
+
* way; a load numbers with {@link nextLoadGeneration}, which lists as this does when its existence check finds the
|
|
11
|
+
* number taken or cannot answer.
|
|
10
12
|
*
|
|
11
13
|
* Both are consulted because they can disagree. A load that wrote its object and crashed before publishing leaves
|
|
12
14
|
* an object *above* `currentGen`; a writer that consulted only the pointer would pick that same number and hit the
|
|
@@ -17,6 +19,46 @@ export interface GenerationDeps {
|
|
|
17
19
|
* A segment with no row and no objects starts at 0. An admin/write-path helper, never on the read path.
|
|
18
20
|
*/
|
|
19
21
|
export declare function nextGeneration(ref: SegmentRef, deps: GenerationDeps): Promise<number>;
|
|
22
|
+
/** The number a load takes, and whether one existence check proved it free (so nothing is above the pointer). */
|
|
23
|
+
export interface LoadNumber {
|
|
24
|
+
readonly generation: number;
|
|
25
|
+
/**
|
|
26
|
+
* `true` when the number is `currentGen + 1` and the check found no object holding it. Collection by name rests
|
|
27
|
+
* on it: the row named `generation - 1` as current, so every number the window evicts is below it, and nothing
|
|
28
|
+
* the listing would have kept is a name the pass takes. `false` when the listing numbered the load.
|
|
29
|
+
*/
|
|
30
|
+
readonly checked: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The generation number a load takes, from the row it has already read: `currentGen + 1` (0 with no row or no
|
|
34
|
+
* pointer yet) when no object holds that number, which one existence check proves (a zero-byte `getTail`, one
|
|
35
|
+
* metadata request on every shipped driver); otherwise one above the pointer and above everything listed, as
|
|
36
|
+
* {@link nextGeneration} numbers. The check finding the number taken (a crashed load's orphan, the object of a
|
|
37
|
+
* load still in flight, or the generations a rollback left above the pointer) and the check failing in any way
|
|
38
|
+
* other than "not found" both take the listing, so the listing stays the authority whenever the check cannot
|
|
39
|
+
* prove the number free. A failed check falls back silently: nothing records it, and what it costs is the listing,
|
|
40
|
+
* one more PUT-class request on S3, so a fault that makes every check fail shows only in the bill.
|
|
41
|
+
*
|
|
42
|
+
* A load can therefore take a number **below** an object already in the bucket: an orphan at `currentGen + 2`
|
|
43
|
+
* with `currentGen + 1` free is numbered under, not past. That is as safe as a listing's number. A load never
|
|
44
|
+
* takes a number an object holds, and a put that races onto one fails write-once, so the load reports
|
|
45
|
+
* `superseded`. The orphan above stays unpublished; the load whose check meets it numbers past it by the listing,
|
|
46
|
+
* and collection takes it once a generation above it is current. Like a listing's, the number can be one whose
|
|
47
|
+
* object was deleted (an erasure removes the generations above a rolled-back pointer that held the id), which is
|
|
48
|
+
* why nothing identifies a generation by its number alone: the reader cache, the chunk cache and a pin all key on
|
|
49
|
+
* the number and the row's token, and the token has moved on.
|
|
50
|
+
*/
|
|
51
|
+
export declare function nextLoadGeneration(ref: SegmentRef, deps: GenerationDeps, row: RegistryRecord | null): Promise<LoadNumber>;
|
|
52
|
+
/**
|
|
53
|
+
* How often a load that could collect by name lists instead: every generation divisible by this. A name-only pass
|
|
54
|
+
* takes one generation a load and never looks at the rest, so what it misses (a refused load's object that stayed
|
|
55
|
+
* because the row had changed, extras left by a `keep` that shrank, an object a rollback or an erasure left
|
|
56
|
+
* stranded below the pointer) waits for a listing. The number is a generation's, not a clock's, so the choice needs
|
|
57
|
+
* no state and no time, and a segment that loads cleanly lists once in this many loads at most. The bound is in
|
|
58
|
+
* generation numbers: a rollback moves the pointer down, and the loads that then take the numbers above it count
|
|
59
|
+
* from there.
|
|
60
|
+
*/
|
|
61
|
+
export declare const LIST_COLLECTION_CADENCE = 16;
|
|
20
62
|
/**
|
|
21
63
|
* Garbage-collect superseded Storage generations for a segment: everything strictly below `currentGen`, keeping
|
|
22
64
|
* the most recent `keep` of them as a grace window, so a read still fetching from a just-superseded
|
|
@@ -42,8 +84,8 @@ export declare function nextGeneration(ref: SegmentRef, deps: GenerationDeps): P
|
|
|
42
84
|
* an empty array is not: `eraseIdFromSegment` reads the returned list as the physical half of its erasure
|
|
43
85
|
* receipt, and would otherwise report `erased: true` over bytes still in the bucket. Re-run it.
|
|
44
86
|
*
|
|
45
|
-
* That state is reachable in practice: a `dropSegment` whose Storage sweep threw part-way, or a load that
|
|
46
|
-
*
|
|
87
|
+
* That state is reachable in practice: a `dropSegment` whose Storage sweep threw part-way, or a load that had read
|
|
88
|
+
* the row before the tombstone landed, wrote its object afterwards, and stopped before its refusal deleted it. `dropSegment` re-sweeps
|
|
47
89
|
* and reports whatever it could not reclaim in `generationsRemaining`, but a drop that was never re-run leaves a
|
|
48
90
|
* residual, and this is what eventually collects it from the retention sweep.
|
|
49
91
|
*
|
|
@@ -54,3 +96,59 @@ export declare function nextGeneration(ref: SegmentRef, deps: GenerationDeps): P
|
|
|
54
96
|
export declare function gcOrphanGenerations(ref: SegmentRef, deps: GenerationDeps, options?: {
|
|
55
97
|
keep?: number;
|
|
56
98
|
}): Promise<number[]>;
|
|
99
|
+
/**
|
|
100
|
+
* Collect after a load's publish: by name when `byName` (the load numbered its generation with one existence check,
|
|
101
|
+
* the object its row named was there when the guard looked, and its caller does not ask for a listing) and `keep` is
|
|
102
|
+
* at most one, and by listing otherwise ({@link gcOrphanGenerations}). A load lists in three cases: `keep` of 2 or
|
|
103
|
+
* more, which a name cannot serve because the window then counts generations that may be absent (a refused
|
|
104
|
+
* neighbour's number is a gap, and by name would take one the window promised to keep); a number the listing chose,
|
|
105
|
+
* so something sits above the pointer or the check could not answer, or a current object the load's check found gone; and
|
|
106
|
+
* every {@link LIST_COLLECTION_CADENCE}th generation, which collects what a name-only pass leaves. It makes no
|
|
107
|
+
* request when `keep` is at least the generation published, since no more generations than that exist below it. A
|
|
108
|
+
* load onto a `destroyed` row is refused at its publish and never reaches here; a drop that lands after the publish
|
|
109
|
+
* leaves this pass collecting the tombstone's generations, one by name or all of them by listing, and the drop's own
|
|
110
|
+
* sweep takes the rest.
|
|
111
|
+
*/
|
|
112
|
+
export declare function collectAfterLoad(ref: SegmentRef, deps: GenerationDeps, options: {
|
|
113
|
+
generation: number;
|
|
114
|
+
keep: number;
|
|
115
|
+
byName: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* The object the load's row named as current, which the caller did not open. Before it collects by name a generation
|
|
118
|
+
* the window keeps (a `keep` of 1), the pass looks for this object with one zero-byte read, and lists instead
|
|
119
|
+
* unless that finds it: a name-only pass is safe only while the object the row named is in the bucket.
|
|
120
|
+
*/
|
|
121
|
+
proveCurrent?: GenKey;
|
|
122
|
+
}): Promise<number[]>;
|
|
123
|
+
/**
|
|
124
|
+
* Collect, with no listing, the one generation a publish of `generation` pushed out of a grace window of `keep`
|
|
125
|
+
* (0 or 1): `generation - keep - 1`, when that is a number at all. Returns that name, whether or not an object was
|
|
126
|
+
* there: a delete of an absent object succeeds on every backend and says nothing, so the name returned is the one
|
|
127
|
+
* asked for, not one found, and a generation a concurrent pass, an erasure or a lifecycle rule had already taken is
|
|
128
|
+
* listed all the same. A list is not a receipt (invariant 4), and neither is this.
|
|
129
|
+
*
|
|
130
|
+
* The caller has published `generation` having numbered it with one existence check, so the row named
|
|
131
|
+
* `generation - 1` and every generation the window newly evicts is below it. With `keep` of 0 or 1 that name is
|
|
132
|
+
* the one a listing pass would also take, or one it left to a later pass: never a generation the listing keeps.
|
|
133
|
+
* That rests on the object the row named being in the bucket, and a fault can break it (a lifecycle rule or a
|
|
134
|
+
* partial restore removing it, or an erasure deleting the object of a load whose publish then landed;
|
|
135
|
+
* `checkConsistency` reports the state). The caller, which learns that from its guard's read of the current
|
|
136
|
+
* generation or, when the guard took the size from the row's summary and opened nothing, from one zero-byte read of
|
|
137
|
+
* it (see {@link collectAfterLoad}), lists instead; a load that made neither (`allowEmpty` without `minRetained`)
|
|
138
|
+
* cannot tell, and deletes by name the older generation a listing would have kept as the window. `keep` of 2 or more is refused,
|
|
139
|
+
* because there the window counts the generations that exist, which a name cannot know.
|
|
140
|
+
*
|
|
141
|
+
* Collection never touches the current generation, and the row is re-proved before the delete as every pass does.
|
|
142
|
+
* A row that is gone, or a pointer that has fallen below `generation` (a rollback, or a name purged and re-created
|
|
143
|
+
* with fewer loads), deletes nothing and returns `[]`: the publish already landed, nothing here protects it or
|
|
144
|
+
* needs the result, and a throw would hand the caller a failure for a load that took effect. A pointer that has
|
|
145
|
+
* moved above `generation` does not stop it: a publish landing in the meantime leaves routine collection working.
|
|
146
|
+
* A fault, a registry read or a delete that throws, still propagates, with the pointer at `generation`. The bound
|
|
147
|
+
* is `generation`, the pointer this load's own publish set, so the lower-of-two-pointers rule of a listing pass
|
|
148
|
+
* holds with one observation in place of two. Nothing is deleted when `generation` has nothing below its window,
|
|
149
|
+
* and no request is made.
|
|
150
|
+
*/
|
|
151
|
+
export declare function collectByName(ref: SegmentRef, deps: GenerationDeps, options: {
|
|
152
|
+
generation: number;
|
|
153
|
+
keep: number;
|
|
154
|
+
}): Promise<number[]>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a load accepts: ids, or a whole bitmap as `{ serialized }` bytes or as a `{ bitmap }` that serializes to
|
|
3
|
+
* them.
|
|
4
|
+
*
|
|
5
|
+
* Every bitmap input takes one path. The bytes are size-capped, checked structurally and decoded by the codec's
|
|
6
|
+
* safe deserializer into a bitmap of the codec's own, which then writes its chunks straight from its containers:
|
|
7
|
+
* no id passes through JavaScript. `{ bitmap }` is only a spelling of `{ serialized }`, serialized once, here, so
|
|
8
|
+
* a caller's bitmap is never trusted as a structure and a change made to it after the call is not loaded.
|
|
9
|
+
*
|
|
10
|
+
* All of it runs before the load's first request, so a malformed input costs no round trip and writes nothing.
|
|
11
|
+
*/
|
|
12
|
+
import type { CodecBitmap, CodecInterface } from './codec.js';
|
|
13
|
+
/**
|
|
14
|
+
* Anything that serializes itself to portable Roaring: `roaring`'s `RoaringBitmap32` is one. Typed by shape, so
|
|
15
|
+
* no codec's own class appears in a signature. A bitmap that can also say how large its serialization is
|
|
16
|
+
* (`getSerializationSizeInBytes`) is asked first, so one over the cap is refused before it is serialized.
|
|
17
|
+
*/
|
|
18
|
+
export interface PortableBitmap {
|
|
19
|
+
serialize(format: 'portable'): Uint8Array;
|
|
20
|
+
getSerializationSizeInBytes?(format: 'portable'): number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* What a load writes as the segment's new contents.
|
|
24
|
+
*
|
|
25
|
+
* - **ids**: an iterable or async iterable of integers in `[0, 2^32)`, consumed lazily. Not a `Uint8Array`,
|
|
26
|
+
* `Uint8ClampedArray` or `Buffer`, which a load refuses, since each byte would load as an id.
|
|
27
|
+
* - **`{ serialized }`**: one bitmap in the portable Roaring format, checked before anything is written.
|
|
28
|
+
* - **`{ bitmap }`**: an object with `serialize('portable')`, loaded as `{ serialized: bitmap.serialize('portable') }`.
|
|
29
|
+
*/
|
|
30
|
+
export type LoadInput = Iterable<number> | AsyncIterable<number> | {
|
|
31
|
+
readonly serialized: Uint8Array;
|
|
32
|
+
} | {
|
|
33
|
+
readonly bitmap: PortableBitmap;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* No canonical 32-bit Roaring bitmap serializes to more than this. Under the run cookie, the larger of the two
|
|
37
|
+
* headers, it is a 4-byte cookie, 8,192 bytes of run flags and 8 bytes of header for each of the 65,536
|
|
38
|
+
* containers, and no container's body is over 8,192 bytes: a bitset is 8,192, and a run container is kept only
|
|
39
|
+
* where it is smaller. 537,403,396 bytes, about 512.5 MiB.
|
|
40
|
+
*/
|
|
41
|
+
export declare const MAX_SERIALIZED_LOAD_BYTES: number;
|
|
42
|
+
/** A bitmap input, decoded into the codec's own bitmap: written from its chunks, never id by id. */
|
|
43
|
+
export declare class DecodedLoadInput {
|
|
44
|
+
readonly bitmap: CodecBitmap;
|
|
45
|
+
constructor(bitmap: CodecBitmap);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Check a load's input and decode a bitmap input, before the load makes any request.
|
|
49
|
+
*
|
|
50
|
+
* @returns ids unchanged, to be consumed lazily by the write, or the decoded bitmap.
|
|
51
|
+
* @throws {ValidationError} for a byte array passed as ids; bytes that are malformed, over the cap, or followed by
|
|
52
|
+
* more bytes; a `{ bitmap }` with no `serialize` method, or whose `serialize('portable')` returns no `Uint8Array`;
|
|
53
|
+
* and anything that is none of the three inputs. An error the caller's own `serialize` throws propagates as it is.
|
|
54
|
+
*/
|
|
55
|
+
export declare function prepareLoadInput(input: LoadInput, codec: CodecInterface): Iterable<number> | AsyncIterable<number> | DecodedLoadInput;
|
package/dist/core/load.d.ts
CHANGED
|
@@ -15,15 +15,17 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Refusing therefore has to clean up after itself. The object is already durable at that point, and it sits
|
|
17
17
|
* ABOVE `currentGen`, where generation collection deliberately never looks (it deletes strictly below the
|
|
18
|
-
* pointer), so a refused load deletes its own object before returning, while the segment's row is unchanged
|
|
19
|
-
*
|
|
20
|
-
* so it stays, and collection takes it like any other generation once one above it is current.
|
|
18
|
+
* pointer), so a refused load deletes its own object before returning, while the segment's row is unchanged, gone
|
|
19
|
+
* or `destroyed`. Once another write has changed the row, the object's number may name a re-created segment's live
|
|
20
|
+
* object, so it stays, and collection takes it like any other generation once one above it is current.
|
|
21
21
|
*/
|
|
22
22
|
import { type IAuditSink } from './audit.js';
|
|
23
23
|
import { type CodecInterface } from './codec.js';
|
|
24
|
-
import type { Clock } from './determinism.js';
|
|
24
|
+
import type { Clock, Rng } from './determinism.js';
|
|
25
25
|
import type { IKeystore } from './crypto.js';
|
|
26
|
-
import
|
|
26
|
+
import { type LoadInput } from './load-input.js';
|
|
27
|
+
import { type ReadRetry } from './retry.js';
|
|
28
|
+
import type { GenerationMetadata, IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
|
|
27
29
|
/** What {@link loadSegment} needs: the objects, the pointer, the codec, and key material if encrypted. */
|
|
28
30
|
export interface LoadDeps {
|
|
29
31
|
readonly storage: IStorageDriver;
|
|
@@ -32,6 +34,27 @@ export interface LoadDeps {
|
|
|
32
34
|
readonly keystore?: IKeystore;
|
|
33
35
|
readonly requireEncryption?: boolean;
|
|
34
36
|
readonly clock?: Clock;
|
|
37
|
+
/**
|
|
38
|
+
* The random source that spreads the waits between a publish's fresh compare-and-swaps (a random time under each
|
|
39
|
+
* bound). Absent, the read retry's source is used if it has one, and otherwise each wait is its bound.
|
|
40
|
+
*/
|
|
41
|
+
readonly rng?: Rng;
|
|
42
|
+
/**
|
|
43
|
+
* The store's read retry, for the read the guard makes of the current generation: a transient fault there is run
|
|
44
|
+
* again under it rather than failing the load. Absent, the read is made once. It does not govern the write: a
|
|
45
|
+
* registry write that gets no answer is settled by reading the row, and sent again only as a bounded fresh
|
|
46
|
+
* compare-and-swap.
|
|
47
|
+
*/
|
|
48
|
+
readonly readRetry?: ReadRetry;
|
|
49
|
+
/**
|
|
50
|
+
* Collect by listing the segment's objects after the publish, whatever `keep` is. Absent, a load that keeps at
|
|
51
|
+
* most one generation and found nothing above its pointer deletes by name the one generation its publish pushed
|
|
52
|
+
* out of the window, and lists every sixteenth generation to take what that leaves; the generations older than
|
|
53
|
+
* the window wait for that listing. Set it for a caller whose `keep` promises every generation below the new
|
|
54
|
+
* one beyond the window is gone when the call returns: the `*Into` verbs, whose `keep` is how an operator clears
|
|
55
|
+
* a destination that earlier materialisations kept in full.
|
|
56
|
+
*/
|
|
57
|
+
readonly collectByListing?: boolean;
|
|
35
58
|
}
|
|
36
59
|
/**
|
|
37
60
|
* What a plausible result looks like, so an implausible one is refused instead of published.
|
|
@@ -71,8 +94,27 @@ export interface LoadOptions {
|
|
|
71
94
|
* collected. Keep at least one generation for every one that can be written above the pinned one while your
|
|
72
95
|
* longest pinned job runs, on every writer that loads the segment — see "Generations and `keep`" in the loading
|
|
73
96
|
* guide.
|
|
97
|
+
*
|
|
98
|
+
* With `keep` of 0 or 1, a load that found nothing above the pointer collects without listing: it deletes the one
|
|
99
|
+
* generation its publish pushed out of the window, and lists the segment's objects on every sixteenth generation to
|
|
100
|
+
* take whatever that pass leaves, such as the generations an earlier, wider `keep` held. With `keep` of 2 or more
|
|
101
|
+
* it lists on every load, and so does one whose check found the current generation's object gone: the guard's read of
|
|
102
|
+
* it, or, when the guard took the size from the row's summary, one zero-byte read made before a `keep` of 1 takes a
|
|
103
|
+
* name. A `keep` at least the generation published collects nothing and asks for nothing.
|
|
74
104
|
*/
|
|
75
105
|
readonly keep?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Small, immutable metadata of your own for this generation: what it was computed from, such as a definition's
|
|
108
|
+
* version, the time its data landed, or a run id. A flat object of string keys and string or finite-number values,
|
|
109
|
+
* at most 1,024 bytes as canonical JSON (keys sorted by UTF-16 code unit, no whitespace), and no key longer than 128
|
|
110
|
+
* bytes. It is stored in the generation's object, and written to the segment's row by the write that makes the
|
|
111
|
+
* generation current, so whoever sees this generation as current sees its metadata. It never changes once written:
|
|
112
|
+
* a new generation is how it changes. `undefined` and the empty object store nothing. Anything else that breaks a
|
|
113
|
+
* rule (a boolean, `null`, an array, nesting, `NaN`, a key that is not well-formed text) throws `ValidationError`
|
|
114
|
+
* before a request is made. **Never put a subject's id in it**: an erasure rewrites the ids and carries the metadata
|
|
115
|
+
* over as it is. Sealed under the segment's key when the segment is encrypted.
|
|
116
|
+
*/
|
|
117
|
+
readonly metadata?: GenerationMetadata;
|
|
76
118
|
readonly audit?: IAuditSink;
|
|
77
119
|
}
|
|
78
120
|
/** Why a load did not become current. */
|
|
@@ -82,13 +124,14 @@ export type LoadRefusal = 'empty' | 'min-cardinality' | 'min-retained'
|
|
|
82
124
|
* put refused this one and it wrote nothing (`size: 0`); or the segment's registry row changed while the load was
|
|
83
125
|
* writing: another load published, a retention change, a rollback or an erasure wrote the row, or the row was
|
|
84
126
|
* deleted. Once another write has changed the row, the object stays in the bucket, because its generation number
|
|
85
|
-
* may by then name another incarnation's live object; once the row is
|
|
127
|
+
* may by then name another incarnation's live object; once the row is `destroyed` (dropped or crypto-shredded),
|
|
128
|
+
* the object is deleted, and once the row is gone, so is an object its footer proves this load's own.
|
|
86
129
|
*/
|
|
87
130
|
| 'superseded';
|
|
88
131
|
export interface LoadResult {
|
|
89
132
|
/**
|
|
90
133
|
* The generation written. Present even when refused: a refusal deletes that object while the segment's row is
|
|
91
|
-
* unchanged or gone, and leaves it in the bucket once another write has changed the row. A load that lost its
|
|
134
|
+
* unchanged or `destroyed`, or gone once its footer proves it this load's own, and leaves it in the bucket once another write has changed the row. A load that lost its
|
|
92
135
|
* generation number to another wrote nothing, and reports `size: 0`.
|
|
93
136
|
*/
|
|
94
137
|
readonly generation: number;
|
|
@@ -108,14 +151,26 @@ export interface LoadResult {
|
|
|
108
151
|
* whatever happens next.
|
|
109
152
|
*/
|
|
110
153
|
readonly cardinalityBefore: number | null;
|
|
111
|
-
/**
|
|
154
|
+
/**
|
|
155
|
+
* The superseded generations collected after publishing. Empty when nothing was published.
|
|
156
|
+
*
|
|
157
|
+
* When collection deleted by name (see {@link LoadOptions.keep}) this is the name it deleted, and that generation
|
|
158
|
+
* may have been gone already: a delete of an absent object succeeds on every backend and does not say so, so the
|
|
159
|
+
* list names what the pass asked the bucket to delete, not what it found there. A listing pass names the
|
|
160
|
+
* generations it found and deleted. Neither is a receipt, since a concurrent collector may have taken a generation
|
|
161
|
+
* first.
|
|
162
|
+
*/
|
|
112
163
|
readonly collected: readonly number[];
|
|
113
164
|
}
|
|
114
165
|
/**
|
|
115
|
-
* Replace a segment's contents with `
|
|
166
|
+
* Replace a segment's contents with `input`, as one immutable generation, and make it current.
|
|
167
|
+
*
|
|
168
|
+
* `input` is ids, or a whole bitmap as `{ serialized }` portable Roaring bytes or a `{ bitmap }` with
|
|
169
|
+
* `serialize('portable')` ({@link LoadInput}). A bitmap input is checked, decoded and serialized before the first
|
|
170
|
+
* request, and is written from its own chunks; the generation is byte for byte the one its ids would write.
|
|
116
171
|
*
|
|
117
172
|
* Returns what was written and whether it became current. A refusal is **not** an exception: `published: false`
|
|
118
173
|
* with a `reason` is a normal outcome a caller branches on, in the same shape as a successful load, because the
|
|
119
174
|
* interesting cases (a guard tripped, a racing writer won) are operational facts rather than faults.
|
|
120
175
|
*/
|
|
121
|
-
export declare function loadSegment(ref: SegmentRef,
|
|
176
|
+
export declare function loadSegment(ref: SegmentRef, input: LoadInput, deps: LoadDeps, options?: LoadOptions): Promise<LoadResult>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generation metadata: a small, flat record of strings and finite numbers attached to one generation.
|
|
3
|
+
*
|
|
4
|
+
* The rules live here once, for every boundary that meets a value: a caller's input, refused with
|
|
5
|
+
* `ValidationError`, and bytes read back from a tier, refused with `IntegrityError`. The caller passes the error
|
|
6
|
+
* to raise. Pure: no I/O.
|
|
7
|
+
*/
|
|
8
|
+
import type { GenerationMetadata } from './ports.js';
|
|
9
|
+
/** Cap on the canonical JSON of one generation's metadata, in UTF-8 bytes (braces and quotes included). */
|
|
10
|
+
export declare const MAX_METADATA_BYTES = 1024;
|
|
11
|
+
/** Cap on one metadata key, in UTF-8 bytes. */
|
|
12
|
+
export declare const MAX_METADATA_KEY_BYTES = 128;
|
|
13
|
+
/**
|
|
14
|
+
* Validate `value` as generation metadata (`GenerationMetadata` in the ports) and return its canonical JSON: RFC 8785
|
|
15
|
+
* (the JSON Canonicalization Scheme) for a flat object of strings and finite numbers. Keys are sorted by UTF-16 code
|
|
16
|
+
* unit, strings are escaped as `JSON.stringify` escapes them, numbers are written as ECMAScript's
|
|
17
|
+
* `Number.prototype.toString` writes them (so `-0` is `0`, and `1e21` is `1e+21`), and there is no whitespace. Key
|
|
18
|
+
* order therefore never changes the bytes, and `tests/golden/metadata-canonical.json` holds vectors a port can check
|
|
19
|
+
* itself against. Both caps are in UTF-8 bytes. The container is an object whose prototype is
|
|
20
|
+
* `Object.prototype` or `null`, holding only enumerable data properties with string keys. `fail` raises the error the
|
|
21
|
+
* boundary calls for. The empty object passes and returns `{}`: whether it may be stored is the caller's rule.
|
|
22
|
+
*/
|
|
23
|
+
export declare function canonicalMetadataJson(value: unknown, fail: (message: string) => never): string;
|
|
24
|
+
/**
|
|
25
|
+
* The bytes a generation stores for `value`: its canonical JSON in UTF-8, or `undefined` when it has none (`undefined`
|
|
26
|
+
* or the empty object, which store nothing). Synchronous, so what is stored is what the caller passed at the call,
|
|
27
|
+
* whatever it does with its object afterwards. `fail` raises the boundary's error.
|
|
28
|
+
*/
|
|
29
|
+
export declare function metadataBytes(value: unknown, fail: (message: string) => never): Uint8Array | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* A caller's metadata, checked against the rules and copied, so what is stored is what the caller passed at the call,
|
|
32
|
+
* whatever it does with its object afterwards: the frozen record in canonical key order, or `undefined` when there is
|
|
33
|
+
* none (`undefined`, or the empty object, which store nothing). Synchronous. `fail` raises the boundary's error.
|
|
34
|
+
*/
|
|
35
|
+
export declare function copiedMetadata(value: unknown, fail: (message: string) => never): GenerationMetadata | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Read metadata back from bytes a tier holds, which are untrusted. They must be at most {@link MAX_METADATA_BYTES},
|
|
38
|
+
* valid UTF-8, and exactly the canonical JSON of a non-empty record, so a stored copy has one spelling: whitespace, a
|
|
39
|
+
* key out of order or listed twice, and a number or an escape `JSON.stringify` would write another way are all
|
|
40
|
+
* refused. Returns a frozen record of its own. `fail` raises the boundary's error.
|
|
41
|
+
*/
|
|
42
|
+
export declare function metadataFromBytes(bytes: Uint8Array, fail: (message: string) => never): GenerationMetadata;
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* on some other handle. That is why the routing is by segment identity here rather than by which object the
|
|
22
22
|
* caller happened to start from.
|
|
23
23
|
*/
|
|
24
|
-
import type { ChunkRef, StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
|
|
24
|
+
import type { ChunkRef, GenerationSummary, StorageChunkSource, SegmentRef, SegmentSize } from './ports.js';
|
|
25
25
|
import type { CrbmStorageChunkSource } from './crbm-storage-source.js';
|
|
26
26
|
/**
|
|
27
27
|
* What a pin holds for one segment: the generation, the version identifying those exact bytes, and the pinned
|
|
@@ -67,6 +67,7 @@ export declare class PinnedStorageChunkSource implements StorageChunkSource {
|
|
|
67
67
|
listChunkKeys(ref: SegmentRef): Promise<number[]>;
|
|
68
68
|
sizeOf(ref: SegmentRef): Promise<SegmentSize | null>;
|
|
69
69
|
cardinalities(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
|
|
70
|
+
summary(ref: SegmentRef): Promise<GenerationSummary | null>;
|
|
70
71
|
currentGeneration(ref: SegmentRef): Promise<number | null>;
|
|
71
72
|
/**
|
|
72
73
|
* A pinned segment reports the version captured when it was pinned, marked as a pin's, so its decoded chunks
|