instar 1.3.522 → 1.3.523
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/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +56 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.js +1 -1
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/MeshRpc.d.ts +5 -0
- package/dist/core/MeshRpc.d.ts.map +1 -1
- package/dist/core/MeshRpc.js +8 -0
- package/dist/core/MeshRpc.js.map +1 -1
- package/dist/core/StoreSnapshot.d.ts +455 -0
- package/dist/core/StoreSnapshot.d.ts.map +1 -0
- package/dist/core/StoreSnapshot.js +786 -0
- package/dist/core/StoreSnapshot.js.map +1 -0
- package/dist/core/stateSyncConfig.js +1 -1
- package/dist/core/stateSyncConfig.js.map +1 -1
- package/dist/core/storeSnapshotBuild.worker.d.ts +2 -0
- package/dist/core/storeSnapshotBuild.worker.d.ts.map +1 -0
- package/dist/core/storeSnapshotBuild.worker.js +32 -0
- package/dist/core/storeSnapshotBuild.worker.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.523.md +31 -0
- package/upgrades/side-effects/hlc-step3-snapshot-tail.md +133 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StoreSnapshot — single-origin snapshot-then-tail machinery (WS2 replicated-store
|
|
3
|
+
* foundation, Component 4 / build-order step 3).
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/specs/multi-machine-replicated-store-foundation.md §6 (snapshot-then-
|
|
6
|
+
* tail), §6.1 (single-origin anti-forgery), §6.2 (snapshot format + watermark
|
|
7
|
+
* VECTOR), §6.3 (the cutover apply path), §6.4 (HLC demoted to SECONDARY dedup),
|
|
8
|
+
* §6.5 (tombstone high-water seed), §6.6 (why a per-(origin,kind) seq-watermark
|
|
9
|
+
* vector, not a scalar), §8.2 (snapshot-cache fixed ceiling).
|
|
10
|
+
*
|
|
11
|
+
* GENERIC foundation machinery ONLY — there is NO concrete store kind here
|
|
12
|
+
* (preferences/relationships are later consumers, WS2.1+). Everything ships DARK
|
|
13
|
+
* behind `multiMachine.stateSync.<store>.enabled` (default false); a single-machine
|
|
14
|
+
* agent (no peers to pull from) is a strict no-op.
|
|
15
|
+
*
|
|
16
|
+
* This module is PURE LOGIC. The 67MB-class whole-store materialization happens OFF
|
|
17
|
+
* the event loop in `storeSnapshotBuild.worker.ts` (the instar#1069 requirement,
|
|
18
|
+
* mirroring CartographerSweepEngine / cartographerDetect.worker.ts) — this file
|
|
19
|
+
* exports the pure `materializeSnapshot()` the worker dispatches to, plus the
|
|
20
|
+
* non-worker cache/breaker/cutover seams that run on the main thread but do only
|
|
21
|
+
* bounded, O(records-in-one-batch) work. No fs, no Date directly, no network.
|
|
22
|
+
*
|
|
23
|
+
* THE CORRECTNESS BORROW (§6.3): the whole no-gap/no-double-apply guarantee is
|
|
24
|
+
* inherited from the EXISTING seq-contiguous transport. This module:
|
|
25
|
+
* 1. builds a single-origin snapshot (origin === serving machine, §6.1),
|
|
26
|
+
* 2. computes a per-(origin,kind) seq-watermark VECTOR from the entries that
|
|
27
|
+
* actually materialized (§6.2 — it cannot lie),
|
|
28
|
+
* 3. seeds `PeerMeta.kinds[kind].lastHeldSeq = snapshotSeq` (§6.3 step 3),
|
|
29
|
+
* 4. then TAILS via the UNCHANGED `buildServeBatch(kind, snapshotSeq, M)` — the
|
|
30
|
+
* existing applier seq-contiguity (lastHeldSeq+1) does the gap/dup work.
|
|
31
|
+
* There is NO new gap-detection here. HLC is a SECONDARY dedup hint only (§6.4).
|
|
32
|
+
*/
|
|
33
|
+
import { type HlcTimestamp } from './HybridLogicalClock.js';
|
|
34
|
+
/**
|
|
35
|
+
* One materialized record in a single-origin snapshot (§6.2). Carries its `hlc`
|
|
36
|
+
* (the merge total order), `op` (put/delete tombstone), `recordKey`, `origin`
|
|
37
|
+
* (=== the serving machine, §6.1), and the opaque store-specific `data`. A delete
|
|
38
|
+
* is a TOMBSTONE record (kept within the tombstone horizon, §6.5), never a physical
|
|
39
|
+
* removal — so the apply path can resolve a delete↔put race deterministically.
|
|
40
|
+
*/
|
|
41
|
+
export interface SnapshotRecord {
|
|
42
|
+
recordKey: string;
|
|
43
|
+
hlc: HlcTimestamp;
|
|
44
|
+
op: 'put' | 'delete';
|
|
45
|
+
origin: string;
|
|
46
|
+
/** The store-specific portion of the journal `data` (envelope fields stripped). */
|
|
47
|
+
data: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The per-(origin,kind) seq-watermark — a VECTOR component, not a scalar (§6.6).
|
|
51
|
+
* `snapshotSeq` is the highest journal `seq` of an origin-M-authored entry that
|
|
52
|
+
* materialized into the snapshot. It is THE TAIL CURSOR: the cutover seeds
|
|
53
|
+
* `lastHeldSeq = snapshotSeq` then tails `buildServeBatch(kind, snapshotSeq, M)`.
|
|
54
|
+
*/
|
|
55
|
+
export interface SnapshotKindWatermark {
|
|
56
|
+
/** The highest seq this snapshot materialized from for (origin, kind). */
|
|
57
|
+
snapshotSeq: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The snapshot's seq-watermark — a per-(origin,kind) VECTOR (§6.2 / §6.6). One
|
|
61
|
+
* snapshot is single-origin, so `origin` is a scalar; the VECTOR is over the
|
|
62
|
+
* contributing KINDS (a store may ride more than one journal kind). `maxHlc` is
|
|
63
|
+
* SECONDARY (§6.4): an idempotency/dedup hint at cutover, NEVER the tail cursor.
|
|
64
|
+
*/
|
|
65
|
+
export interface SnapshotWatermark {
|
|
66
|
+
/** === the serving machine (single-origin, §6.1). */
|
|
67
|
+
origin: string;
|
|
68
|
+
/** Per contributing (origin, kind) stream: the highest seq materialized. */
|
|
69
|
+
kinds: Record<string, SnapshotKindWatermark>;
|
|
70
|
+
/** SECONDARY dedup hint only — the max HLC over all records (§6.4). */
|
|
71
|
+
maxHlc: HlcTimestamp;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A built single-origin snapshot of store S from origin M (§6.2): the CURRENT
|
|
75
|
+
* materialized state (latest record per recordKey; deletes tombstoned), plus the
|
|
76
|
+
* watermark vector. `deleteWatermarks` carries the per-recordKey deleted-keys
|
|
77
|
+
* high-water SEED (§6.5) — the HLC of the highest delete the snapshot reflects per
|
|
78
|
+
* key — so the receiver can seed its resurrection guard from the snapshot.
|
|
79
|
+
*/
|
|
80
|
+
export interface StoreSnapshot {
|
|
81
|
+
/** The store key (config sub-key / advert suffix, e.g. 'pref'). */
|
|
82
|
+
store: string;
|
|
83
|
+
/** === the serving machine (single-origin invariant, §6.1). */
|
|
84
|
+
origin: string;
|
|
85
|
+
/** The materialized records (latest per recordKey, deletes tombstoned). */
|
|
86
|
+
records: SnapshotRecord[];
|
|
87
|
+
/** The per-(origin,kind) seq-watermark VECTOR + the secondary maxHlc (§6.2). */
|
|
88
|
+
watermark: SnapshotWatermark;
|
|
89
|
+
/**
|
|
90
|
+
* The tombstone high-water SEED (§6.5): per recordKey that is currently a
|
|
91
|
+
* tombstone in this snapshot, the HLC of that delete. A receiver seeds its
|
|
92
|
+
* deleted-keys high-water from these so a stale pre-delete put cannot resurrect
|
|
93
|
+
* the key even after the tombstone record itself rotates out.
|
|
94
|
+
*/
|
|
95
|
+
deleteWatermarks: Record<string, HlcTimestamp>;
|
|
96
|
+
/** Total serialized byte size — for the cache byte ceiling (§8.2). */
|
|
97
|
+
sizeBytes: number;
|
|
98
|
+
/**
|
|
99
|
+
* True iff the materialization hit `maxSnapshotBytes` and DROPPED records while
|
|
100
|
+
* keeping the full watermark (§6.3 boundedness). A truncated snapshot is a
|
|
101
|
+
* SUB-WATERMARK GAP TRAP: it seeds `lastHeldSeq = snapshotSeq` but does NOT
|
|
102
|
+
* contain every record at-or-below that seq, so the subsequent tail (which serves
|
|
103
|
+
* only `seq > snapshotSeq`) would NEVER replay the dropped records — a silent gap
|
|
104
|
+
* the seq-contiguity cannot catch (it starts above them). Therefore a truncated
|
|
105
|
+
* snapshot is structurally REFUSED, never applied: `applySnapshotCutover` throws on
|
|
106
|
+
* it and `StoreSnapshotEngine.serveSnapshot` returns `build-truncated` so the caller
|
|
107
|
+
* falls back to a from-genesis tail (the safe, complete path). The flag travels ON
|
|
108
|
+
* the snapshot (not just the serve-result envelope) so the refusal cannot be
|
|
109
|
+
* bypassed by a consumer that consumes a bare `StoreSnapshot`.
|
|
110
|
+
*/
|
|
111
|
+
truncated: boolean;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The minimal journal-entry shape the materializer reads. Matches `JournalEntry`
|
|
115
|
+
* (CoherenceJournal) but typed locally so this module stays import-light and the
|
|
116
|
+
* worker can pass plain objects across the thread boundary. `data` must carry the
|
|
117
|
+
* replicated-record envelope fields (recordKey/hlc/op/origin) — already validated
|
|
118
|
+
* by the applier before they landed; the materializer re-narrows defensively.
|
|
119
|
+
*/
|
|
120
|
+
export interface RawJournalEntry {
|
|
121
|
+
seq: number;
|
|
122
|
+
ts: string;
|
|
123
|
+
machine: string;
|
|
124
|
+
kind: string;
|
|
125
|
+
data: Record<string, unknown>;
|
|
126
|
+
}
|
|
127
|
+
/** Input to the (worker-dispatched) pure materializer. */
|
|
128
|
+
export interface MaterializeInput {
|
|
129
|
+
/** The store key (advert suffix). */
|
|
130
|
+
store: string;
|
|
131
|
+
/** The single origin (=== serving machine, §6.1) this snapshot is built for. */
|
|
132
|
+
origin: string;
|
|
133
|
+
/**
|
|
134
|
+
* The own-stream entries to materialize, keyed by journal kind. EVERY entry MUST
|
|
135
|
+
* have `entry.machine === origin` (the §6.1 single-origin invariant) — the
|
|
136
|
+
* materializer DROPS (counts) any cross-origin entry rather than trusting it, so
|
|
137
|
+
* a buggy caller cannot smuggle a foreign-origin record into a single-origin
|
|
138
|
+
* snapshot. Within a kind, order does not matter — the materializer resolves by
|
|
139
|
+
* HLC-max per recordKey.
|
|
140
|
+
*/
|
|
141
|
+
entriesByKind: Record<string, RawJournalEntry[]>;
|
|
142
|
+
/**
|
|
143
|
+
* The per-kind byte ceiling for the materialized snapshot. A materialization
|
|
144
|
+
* that would exceed it is TRUNCATED deterministically (highest-seq-first kept)
|
|
145
|
+
* and the result is flagged `truncated` — bounded, never an unbounded build
|
|
146
|
+
* (instar#1069). 0/absent ⇒ no per-build truncation (the cache byte ceiling
|
|
147
|
+
* still bounds what is RETAINED).
|
|
148
|
+
*/
|
|
149
|
+
maxSnapshotBytes?: number;
|
|
150
|
+
}
|
|
151
|
+
/** Result of the pure materializer (what the worker posts back). */
|
|
152
|
+
export interface MaterializeResult {
|
|
153
|
+
snapshot: StoreSnapshot;
|
|
154
|
+
/** Count of entries dropped for `entry.machine !== origin` (anti-forgery, §6.1). */
|
|
155
|
+
crossOriginDropped: number;
|
|
156
|
+
/** Count of entries dropped for a malformed/missing envelope (defensive re-narrow). */
|
|
157
|
+
malformedDropped: number;
|
|
158
|
+
/** True iff the materialization hit `maxSnapshotBytes` and was truncated. */
|
|
159
|
+
truncated: boolean;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Materialize a single-origin snapshot (§6.1 + §6.2). PURE — no I/O, no Date, no
|
|
163
|
+
* network. This is the function the worker dispatches to (the heavy walk runs on
|
|
164
|
+
* the worker thread).
|
|
165
|
+
*
|
|
166
|
+
* Algorithm:
|
|
167
|
+
* 1. For every contributing kind, fold the own-stream entries into a per-recordKey
|
|
168
|
+
* LATEST record (HLC-max via HybridLogicalClock.compare). A cross-origin entry
|
|
169
|
+
* (`entry.machine !== origin`) is DROPPED + counted — the §6.1 anti-forgery
|
|
170
|
+
* invariant enforced at materialization, not trusted to the caller.
|
|
171
|
+
* 2. Track the per-(origin,kind) `snapshotSeq` = the highest origin-authored
|
|
172
|
+
* `entry.seq` reflected (§6.2 — computed from what materialized, cannot lie).
|
|
173
|
+
* 3. Track the per-recordKey delete high-water (§6.5) for the seed.
|
|
174
|
+
* 4. Emit the materialized set (latest per key, deletes tombstoned) + the
|
|
175
|
+
* watermark vector + the secondary maxHlc.
|
|
176
|
+
*
|
|
177
|
+
* Boundedness (instar#1069): the per-key fold is O(entries); the result is the
|
|
178
|
+
* live key count (≤ entries). `maxSnapshotBytes` deterministically truncates a
|
|
179
|
+
* pathologically-large materialization (highest-seq-first kept).
|
|
180
|
+
*/
|
|
181
|
+
export declare function materializeSnapshot(input: MaterializeInput): MaterializeResult;
|
|
182
|
+
/** A cache key (§8.2): keyed by (origin, store, maxHlc). Deterministic string. */
|
|
183
|
+
export declare function snapshotCacheKey(origin: string, store: string, maxHlc: HlcTimestamp): string;
|
|
184
|
+
/**
|
|
185
|
+
* The snapshot cache — a FIXED-ceiling ring (§8.2), NOT pool-size-scaled. Bounded
|
|
186
|
+
* by BOTH `maxCachedSnapshots` (count) AND `maxCacheBytes` (bytes), whichever
|
|
187
|
+
* binds first; LRU eviction with a monotonic `cacheLossCounter` (mirroring the
|
|
188
|
+
* quarantine ring's lossCounter). An evicted snapshot is just rebuilt on next
|
|
189
|
+
* demand (breaker-gated, §6.3) — eviction is a recompute, never a correctness
|
|
190
|
+
* loss; the counter makes the recompute visible in degradation.
|
|
191
|
+
*
|
|
192
|
+
* A stale entry (its source stream advanced past the cached maxHlc) is dropped on
|
|
193
|
+
* the next put for the same (origin, store) — the cache key embeds maxHlc, so a
|
|
194
|
+
* fresher build is a DIFFERENT key; `invalidateOlder` drops the superseded ones.
|
|
195
|
+
*/
|
|
196
|
+
export declare class SnapshotCache {
|
|
197
|
+
private readonly maxCount;
|
|
198
|
+
private readonly maxBytes;
|
|
199
|
+
private readonly entries;
|
|
200
|
+
private clock;
|
|
201
|
+
private bytes;
|
|
202
|
+
private _cacheLossCounter;
|
|
203
|
+
constructor(opts: {
|
|
204
|
+
maxCachedSnapshots: number;
|
|
205
|
+
maxCacheBytes: number;
|
|
206
|
+
});
|
|
207
|
+
/** Monotonic count of LRU evictions (§8.2 — surfaced in degradation). */
|
|
208
|
+
get cacheLossCounter(): number;
|
|
209
|
+
/** Current live entry count. */
|
|
210
|
+
get size(): number;
|
|
211
|
+
/** Current total cached bytes. */
|
|
212
|
+
get byteSize(): number;
|
|
213
|
+
/** Get a cached snapshot by key (refreshes its LRU position). */
|
|
214
|
+
get(key: string): StoreSnapshot | undefined;
|
|
215
|
+
/**
|
|
216
|
+
* Put a freshly-built snapshot. Drops any STALE entry for the same (origin,
|
|
217
|
+
* store) whose maxHlc is older than this one (the source stream advanced past
|
|
218
|
+
* it, §8.2) — those are superseded, not LRU-evicted (no lossCounter bump). Then
|
|
219
|
+
* enforces the count + byte ceilings via LRU eviction (lossCounter bumped).
|
|
220
|
+
*/
|
|
221
|
+
put(snapshot: StoreSnapshot): void;
|
|
222
|
+
/** Drop cached entries for the same (origin, store) with a strictly-older maxHlc. */
|
|
223
|
+
private invalidateOlder;
|
|
224
|
+
/** Evict LRU entries until BOTH the count and byte ceilings hold (§8.2). */
|
|
225
|
+
private evictToBounds;
|
|
226
|
+
/** Iterate cached snapshots for a given (origin, store) — bounded by maxCount.
|
|
227
|
+
* Does NOT touch LRU (a read-only scan; the caller touches LRU on a real serve). */
|
|
228
|
+
entriesFor(origin: string, store: string): Iterable<StoreSnapshot>;
|
|
229
|
+
/** Drop ALL cached entries for an origin (the §7.4 rollback-unmerge hook). */
|
|
230
|
+
dropOrigin(origin: string): void;
|
|
231
|
+
}
|
|
232
|
+
/** Options for the rebuild breaker (§6.3 minimum-rebuild window + frequency cap). */
|
|
233
|
+
export interface RebuildBreakerOptions {
|
|
234
|
+
/** Minimum ms between actual rebuilds for one (peer, origin, store) — a flapping
|
|
235
|
+
* peer is served the CACHED snapshot within this window. Default 30s. */
|
|
236
|
+
minRebuildIntervalMs?: number;
|
|
237
|
+
/** Max rebuilds allowed within `windowMs` before the breaker trips. Default 5. */
|
|
238
|
+
maxRebuildsPerWindow?: number;
|
|
239
|
+
/** The rolling window for the frequency cap. Default 5min. */
|
|
240
|
+
windowMs?: number;
|
|
241
|
+
/** How long the breaker stays tripped once it fires. Default 1min. */
|
|
242
|
+
cooldownMs?: number;
|
|
243
|
+
}
|
|
244
|
+
/** A rebuild decision (§6.3): allow a fresh build, or serve the cache / refuse. */
|
|
245
|
+
export type RebuildDecision = {
|
|
246
|
+
allow: true;
|
|
247
|
+
} | {
|
|
248
|
+
allow: false;
|
|
249
|
+
reason: 'within-min-interval' | 'breaker-open';
|
|
250
|
+
serveCache: boolean;
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* Per-peer snapshot-build-frequency breaker (§6.3). Prevents rebuild storms from a
|
|
254
|
+
* flapping peer: within `minRebuildIntervalMs` the cached snapshot is served; more
|
|
255
|
+
* than `maxRebuildsPerWindow` actual rebuilds in `windowMs` trips a breaker that
|
|
256
|
+
* stays open for `cooldownMs`. Bounded (No-Unbounded-Loops). PURE-ish: the clock
|
|
257
|
+
* is injected so it is unit-testable across simulated windows.
|
|
258
|
+
*
|
|
259
|
+
* Keyed by (peer, origin, store) so one peer flapping on store A does not throttle
|
|
260
|
+
* its store-B rebuilds, and two peers requesting the same snapshot are independent.
|
|
261
|
+
*/
|
|
262
|
+
export declare class SnapshotRebuildBreaker {
|
|
263
|
+
private readonly minIntervalMs;
|
|
264
|
+
private readonly maxPerWindow;
|
|
265
|
+
private readonly windowMs;
|
|
266
|
+
private readonly cooldownMs;
|
|
267
|
+
private readonly now;
|
|
268
|
+
/** Per key: the recent rebuild timestamps (bounded by the window) + breaker state.
|
|
269
|
+
* `lastRebuildAt === null` means "never rebuilt" — distinct from a real now()=0
|
|
270
|
+
* timestamp, so the min-interval check is correct even when the clock starts at 0. */
|
|
271
|
+
private readonly state;
|
|
272
|
+
constructor(opts: RebuildBreakerOptions & {
|
|
273
|
+
now: () => number;
|
|
274
|
+
});
|
|
275
|
+
private keyOf;
|
|
276
|
+
/**
|
|
277
|
+
* Decide whether a fresh rebuild is allowed RIGHT NOW for (peer, origin, store).
|
|
278
|
+
* Returns `{ allow: true }` to build; otherwise a refusal with `serveCache`
|
|
279
|
+
* (true = serve the cached snapshot instead of building). This does NOT record
|
|
280
|
+
* the rebuild — call `recordRebuild()` after an ACTUAL build completes, so a
|
|
281
|
+
* cache-served request never counts against the frequency cap.
|
|
282
|
+
*/
|
|
283
|
+
shouldRebuild(peer: string, origin: string, store: string): RebuildDecision;
|
|
284
|
+
/** Record that an actual rebuild completed for (peer, origin, store). */
|
|
285
|
+
recordRebuild(peer: string, origin: string, store: string): void;
|
|
286
|
+
/** Is the breaker currently open for (peer, origin, store)? (for tests/observability). */
|
|
287
|
+
isOpen(peer: string, origin: string, store: string): boolean;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* The receiver-side seams the cutover needs from the existing applier — injected
|
|
291
|
+
* so the cutover is unit-testable with in-memory fakes and never duplicates the
|
|
292
|
+
* applier's seq-contiguity logic (§6.3: "rides the EXISTING seq transport"). The
|
|
293
|
+
* real wiring binds these to JournalSyncApplier's PeerMeta + buildServeBatch.
|
|
294
|
+
*/
|
|
295
|
+
export interface CutoverApplierSeams {
|
|
296
|
+
/**
|
|
297
|
+
* Apply ONE materialized snapshot record into store S's (origin M) replicated
|
|
298
|
+
* namespace via a per-recordKey HLC-max merge (§6.3 step 3). Returns whether the
|
|
299
|
+
* record landed as the new winner (false = an already-present newer record was
|
|
300
|
+
* not overwritten — idempotent). A `put` whose hlc is below the deleted-keys
|
|
301
|
+
* high-water for its key is the receiver's resurrection-drop (§6.5 guard 1) and
|
|
302
|
+
* returns false.
|
|
303
|
+
*/
|
|
304
|
+
applySnapshotRecord(store: string, origin: string, rec: SnapshotRecord): boolean;
|
|
305
|
+
/**
|
|
306
|
+
* SEED, per contributing (origin, kind) stream, `PeerMeta.kinds[kind].lastHeldSeq
|
|
307
|
+
* = snapshotSeq` (§6.3 step 3 — the load-bearing cursor placement). Idempotent:
|
|
308
|
+
* re-seeding to the same seq is a no-op; the real applier never LOWERS a
|
|
309
|
+
* lastHeldSeq it already advanced past (a re-cutover with a stale snapshot does
|
|
310
|
+
* not rewind the cursor).
|
|
311
|
+
*/
|
|
312
|
+
seedLastHeldSeq(origin: string, kind: string, snapshotSeq: number): void;
|
|
313
|
+
/**
|
|
314
|
+
* Seed the per-recordKey deleted-keys high-water from the snapshot (§6.5). The
|
|
315
|
+
* receiver MERGES (HLC-max) — never lowers an existing high-water.
|
|
316
|
+
*/
|
|
317
|
+
seedDeleteWatermark(store: string, origin: string, recordKey: string, hlc: HlcTimestamp): void;
|
|
318
|
+
/**
|
|
319
|
+
* Record an HLC-identity (recordKey, origin, hlc) as already-merged (the §6.4
|
|
320
|
+
* SECONDARY dedup set). Returns whether the identity was NEW (false = already
|
|
321
|
+
* seen ⇒ the caller drops the duplicate). This is a redundant safety net layered
|
|
322
|
+
* ON the seq contiguity, never a substitute for it.
|
|
323
|
+
*/
|
|
324
|
+
recordHlcIdentity(store: string, recordKey: string, origin: string, hlc: HlcTimestamp): boolean;
|
|
325
|
+
}
|
|
326
|
+
/** The verdict for one applied snapshot record (for the cutover result tally). */
|
|
327
|
+
export interface CutoverApplyTally {
|
|
328
|
+
/** Records that landed as the new winner. */
|
|
329
|
+
applied: number;
|
|
330
|
+
/** Records skipped as an HLC-identity duplicate (the §6.4 secondary net). */
|
|
331
|
+
dedupSkipped: number;
|
|
332
|
+
/** Records the applier did not land (older than the present winner, or a
|
|
333
|
+
* resurrection drop — §6.5). */
|
|
334
|
+
notWinner: number;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Apply a single-origin snapshot at cutover (§6.3 steps 3–5 + §6.4 + §6.5). This
|
|
338
|
+
* is the load-bearing seam: it (a) applies each record via HLC-max merge through
|
|
339
|
+
* the injected applier, (b) seeds the deleted-keys high-water from the snapshot,
|
|
340
|
+
* (c) seeds `lastHeldSeq = snapshotSeq` per contributing kind so the NEXT ordinary
|
|
341
|
+
* tail is already in-contiguity, and (d) uses the §6.4 secondary HLC-identity set
|
|
342
|
+
* as a belt-and-suspenders dedup.
|
|
343
|
+
*
|
|
344
|
+
* It does NOT do the tail — that is the UNCHANGED `buildServeBatch(kind,
|
|
345
|
+
* snapshotSeq, M)` path the caller drives next. There is NO gap-detection here;
|
|
346
|
+
* the seq contiguity already there does the work (§6.3 step 4).
|
|
347
|
+
*
|
|
348
|
+
* Idempotency (§6.3 step 5): re-running is safe — re-seeding the cursor to the
|
|
349
|
+
* same snapshotSeq + re-applying is a per-recordKey HLC-max merge (an
|
|
350
|
+
* already-present newer record is not overwritten) and the seq tail drops
|
|
351
|
+
* at-or-below entries as duplicates.
|
|
352
|
+
*
|
|
353
|
+
* PURE-ish: all state mutation is funnelled through the injected seams.
|
|
354
|
+
*/
|
|
355
|
+
export declare function applySnapshotCutover(snapshot: StoreSnapshot, seams: CutoverApplierSeams): CutoverApplyTally;
|
|
356
|
+
/**
|
|
357
|
+
* Compute the tail request cursor for a contributing (origin, kind) AFTER a
|
|
358
|
+
* cutover (§6.3 step 4). The tail rides the UNCHANGED transport:
|
|
359
|
+
* `buildServeBatch(kind, snapshotSeq, origin)`. This helper is the single place
|
|
360
|
+
* that names the contract — the cursor is the snapshot's per-kind `snapshotSeq`,
|
|
361
|
+
* NOTHING HLC-derived (HLC is demoted to secondary dedup, §6.4).
|
|
362
|
+
*/
|
|
363
|
+
export declare function tailCursorAfterCutover(snapshot: StoreSnapshot, kind: string): number;
|
|
364
|
+
/** Validate + narrow an untrusted wire snapshot (the receiver side of the mesh
|
|
365
|
+
* verb). Returns null to REJECT the whole snapshot (single-origin violated, a
|
|
366
|
+
* malformed record, etc.) — the §6.1 anti-forgery + §6.2 format gate at the door.
|
|
367
|
+
* `expectedOrigin` is the AUTHENTICATED sender (the mesh layer proved it); a
|
|
368
|
+
* snapshot whose `origin` or any record `origin` disagrees is rejected wholesale
|
|
369
|
+
* (the cross-origin-snapshot attack, §6.1). */
|
|
370
|
+
export declare function validateWireSnapshot(raw: unknown, expectedOrigin: string): StoreSnapshot | null;
|
|
371
|
+
/** Result of a snapshot-serve request (§6.3): the snapshot + how it was produced. */
|
|
372
|
+
export type SnapshotServeResult = {
|
|
373
|
+
ok: true;
|
|
374
|
+
snapshot: StoreSnapshot;
|
|
375
|
+
source: 'built' | 'cache';
|
|
376
|
+
durationMs: number;
|
|
377
|
+
truncated: false;
|
|
378
|
+
} | {
|
|
379
|
+
ok: false;
|
|
380
|
+
reason: 'breaker-open' | 'build-failed' | 'build-timeout' | 'no-entries' | 'build-truncated';
|
|
381
|
+
durationMs: number;
|
|
382
|
+
};
|
|
383
|
+
/** The seams the engine needs to LOAD a store's own-stream entries (injected so
|
|
384
|
+
* the engine is testable without touching disk; the real wiring reads the
|
|
385
|
+
* CoherenceJournal own streams for the contributing kinds). */
|
|
386
|
+
export interface SnapshotEngineSeams {
|
|
387
|
+
/** Load the own-stream entries for (origin, store), keyed by contributing kind.
|
|
388
|
+
* The caller (real wiring) returns ONLY this machine's own entries (single-
|
|
389
|
+
* origin, §6.1); the materializer ALSO enforces it (drops cross-origin). */
|
|
390
|
+
loadOwnEntries(store: string, origin: string): Record<string, RawJournalEntry[]>;
|
|
391
|
+
/** Injected wall clock (ms) — for the breaker + duration. */
|
|
392
|
+
now(): number;
|
|
393
|
+
}
|
|
394
|
+
/** Engine construction options. */
|
|
395
|
+
export interface StoreSnapshotEngineOptions {
|
|
396
|
+
cache: SnapshotCache;
|
|
397
|
+
breaker: SnapshotRebuildBreaker;
|
|
398
|
+
seams: SnapshotEngineSeams;
|
|
399
|
+
/** Per-build worker timeout (ms). Default 120s (mirrors CartographerSweepEngine). */
|
|
400
|
+
buildTimeoutMs?: number;
|
|
401
|
+
/** Worker heap ceiling (MB). Default 1536 (mirrors CartographerSweepEngine). */
|
|
402
|
+
workerHeapMb?: number;
|
|
403
|
+
/** The per-build snapshot byte ceiling passed to the materializer (deterministic
|
|
404
|
+
* truncation). Default 0 ⇒ no per-build truncation (cache byte ceiling still
|
|
405
|
+
* bounds retention). */
|
|
406
|
+
maxSnapshotBytes?: number;
|
|
407
|
+
/** Optional structured logger for build observability (default no-op). */
|
|
408
|
+
log?: (event: string, detail: Record<string, unknown>) => void;
|
|
409
|
+
/** Test seam: run the materializer INLINE instead of spawning a worker (the unit
|
|
410
|
+
* tests assert the cache/breaker orchestration without a real thread). Default
|
|
411
|
+
* false ⇒ the real off-event-loop worker is spawned (the instar#1069 path). */
|
|
412
|
+
runInline?: boolean;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* StoreSnapshotEngine — the §6.3 serve orchestrator. For a (peer, origin, store)
|
|
416
|
+
* request it:
|
|
417
|
+
* 1. asks the rebuild breaker whether a fresh build is allowed (a flapping peer
|
|
418
|
+
* is served the cached snapshot; a tripped breaker refuses — §6.3),
|
|
419
|
+
* 2. on a build: loads the own-stream entries (single-origin), runs the
|
|
420
|
+
* materializer OFF THE EVENT LOOP in a worker thread (instar#1069), bounded
|
|
421
|
+
* by `buildTimeoutMs`, caches the result (LRU + cacheLossCounter, §8.2), and
|
|
422
|
+
* records the rebuild against the breaker,
|
|
423
|
+
* 3. on a within-window / breaker-open decision: serves the cached snapshot if
|
|
424
|
+
* present, else refuses (the caller falls back / retries later).
|
|
425
|
+
*
|
|
426
|
+
* The HEAVY work (whole-store materialization) is the worker's; the engine itself
|
|
427
|
+
* does only bounded orchestration on the main thread.
|
|
428
|
+
*/
|
|
429
|
+
export declare class StoreSnapshotEngine {
|
|
430
|
+
private readonly cache;
|
|
431
|
+
private readonly breaker;
|
|
432
|
+
private readonly seams;
|
|
433
|
+
private readonly buildTimeoutMs;
|
|
434
|
+
private readonly workerHeapMb;
|
|
435
|
+
private readonly maxSnapshotBytes;
|
|
436
|
+
private readonly log;
|
|
437
|
+
private readonly runInline;
|
|
438
|
+
constructor(opts: StoreSnapshotEngineOptions);
|
|
439
|
+
/** The snapshot cache (for observability / the rollback-unmerge dropOrigin hook). */
|
|
440
|
+
getCache(): SnapshotCache;
|
|
441
|
+
/**
|
|
442
|
+
* Serve a single-origin snapshot of (origin, store) to `peer` (§6.3). Builds off
|
|
443
|
+
* the event loop, reuses the cache within the rebuild window, breaker-gated.
|
|
444
|
+
*/
|
|
445
|
+
serveSnapshot(peer: string, origin: string, store: string): Promise<SnapshotServeResult>;
|
|
446
|
+
/** The most-recent cached snapshot for (origin, store), if any. */
|
|
447
|
+
private mostRecentCached;
|
|
448
|
+
/** Run the materializer — off the event loop (worker), or inline for tests. */
|
|
449
|
+
private runBuild;
|
|
450
|
+
/** Minimal env allowlist for a spawned worker — NEVER the parent process.env. */
|
|
451
|
+
private workerEnv;
|
|
452
|
+
/** Spawn the build worker, await its single message, bound by buildTimeoutMs. */
|
|
453
|
+
private runWorker;
|
|
454
|
+
}
|
|
455
|
+
//# sourceMappingURL=StoreSnapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StoreSnapshot.d.ts","sourceRoot":"","sources":["../../src/core/StoreSnapshot.ts"],"names":[],"mappings":"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"}
|