@serfab/cadre-core 1.17.0 → 1.19.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 +3 -2
- package/dist/cadre-invite-protocol.d.ts +35 -13
- package/dist/cadre-invite-protocol.js +90 -25
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-node.d.ts +586 -255
- package/dist/cadre-node.js +1182 -566
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-cohort.d.ts +66 -9
- package/dist/control-cohort.js +67 -9
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +451 -193
- package/dist/control-database.js +701 -359
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +46 -43
- package/dist/control-formation-recorder.js +77 -78
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +6 -0
- package/dist/control-protocol-guard.js +14 -2
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +48 -31
- package/dist/control-schema-version.js +67 -45
- package/dist/control-schema-version.js.map +1 -1
- package/dist/control-schema.d.ts +8 -8
- package/dist/control-schema.js +16 -16
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +42 -7
- package/dist/control-write-retry.js +87 -19
- package/dist/control-write-retry.js.map +1 -1
- package/dist/enrolled-machine-store.d.ts +3 -3
- package/dist/enrolled-machine-store.js +3 -3
- package/dist/formation-token.d.ts +48 -0
- package/dist/formation-token.js +26 -0
- package/dist/formation-token.js.map +1 -0
- package/dist/index.d.ts +15 -14
- package/dist/index.js +17 -14
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +26 -9
- package/dist/joined-strand-store.js +63 -24
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/link-budget.d.ts +9 -7
- package/dist/link-budget.js +9 -7
- package/dist/link-budget.js.map +1 -1
- package/dist/member-redemption.d.ts +15 -9
- package/dist/member-redemption.js +13 -8
- package/dist/member-redemption.js.map +1 -1
- package/dist/membership-view.d.ts +91 -0
- package/dist/membership-view.js +110 -0
- package/dist/membership-view.js.map +1 -0
- package/dist/owner-grant-protocol.d.ts +11 -7
- package/dist/owner-grant-protocol.js +31 -17
- package/dist/owner-grant-protocol.js.map +1 -1
- package/dist/peer-addr-book.d.ts +10 -1
- package/dist/peer-addr-book.js +4 -3
- package/dist/peer-addr-book.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +96 -30
- package/dist/peer-join-backfill.js +272 -128
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/pending-join-runner.d.ts +48 -18
- package/dist/pending-join-runner.js +74 -56
- package/dist/pending-join-runner.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +47 -40
- package/dist/seed-bootstrap.js +66 -61
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/self-address-update.d.ts +13 -0
- package/dist/self-address-update.js +19 -0
- package/dist/self-address-update.js.map +1 -0
- package/dist/strand-database.d.ts +23 -2
- package/dist/strand-database.js +26 -6
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +176 -89
- package/dist/strand-first-sync-gate.js +288 -122
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +9 -9
- package/dist/strand-formation-deadlines.js +9 -9
- package/dist/strand-formation-manager.d.ts +23 -17
- package/dist/strand-formation-manager.js +46 -44
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +5 -8
- package/dist/strand-formation-protocol.js +101 -16
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +26 -17
- package/dist/strand-instance-manager.js +119 -50
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +16 -13
- package/dist/strand-membership-reconciler.js +28 -22
- package/dist/strand-membership-reconciler.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +40 -28
- package/dist/strand-membership-writer.js +212 -153
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.js +5 -2
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-revocation-enforcer.d.ts +134 -60
- package/dist/strand-revocation-enforcer.js +144 -71
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +50 -39
- package/dist/strand-solicitation.js +10 -3
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-watcher.d.ts +80 -13
- package/dist/strand-watcher.js +152 -48
- package/dist/strand-watcher.js.map +1 -1
- package/dist/table-change-watch.d.ts +63 -0
- package/dist/table-change-watch.js +209 -0
- package/dist/table-change-watch.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +4 -3
- package/dist/timeout-scheduler.js +4 -3
- package/dist/timeout-scheduler.js.map +1 -1
- package/dist/types.d.ts +90 -49
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -1
- package/package.json +8 -8
- package/src/cadre-invite-protocol.ts +119 -31
- package/src/cadre-node.ts +1289 -588
- package/src/control-cohort.ts +86 -9
- package/src/control-database.ts +848 -403
- package/src/control-formation-recorder.ts +359 -353
- package/src/control-protocol-guard.ts +16 -2
- package/src/control-schema-version.ts +101 -51
- package/src/control-schema.ts +16 -16
- package/src/control-write-retry.ts +91 -19
- package/src/delegate-admission.ts +351 -351
- package/src/enrolled-machine-store.ts +3 -3
- package/src/formation-token.ts +63 -0
- package/src/index.ts +30 -18
- package/src/joined-strand-store.ts +69 -24
- package/src/link-budget.ts +9 -7
- package/src/member-redemption.ts +20 -13
- package/src/membership-view.ts +144 -0
- package/src/owner-grant-protocol.ts +33 -18
- package/src/peer-addr-book.ts +4 -3
- package/src/peer-join-backfill.ts +961 -761
- package/src/pending-join-runner.ts +76 -53
- package/src/seed-bootstrap.ts +72 -67
- package/src/self-address-update.ts +19 -0
- package/src/strand-database.ts +352 -322
- package/src/strand-first-sync-gate.ts +334 -134
- package/src/strand-formation-deadlines.ts +227 -227
- package/src/strand-formation-manager.ts +731 -725
- package/src/strand-formation-protocol.ts +122 -24
- package/src/strand-instance-manager.ts +2092 -2026
- package/src/strand-membership-reconciler.ts +31 -25
- package/src/strand-membership-writer.ts +2093 -2019
- package/src/strand-move.ts +5 -2
- package/src/strand-revocation-enforcer.ts +740 -654
- package/src/strand-solicitation.ts +61 -34
- package/src/strand-watcher.ts +175 -56
- package/src/table-change-watch.ts +285 -0
- package/src/timeout-scheduler.ts +29 -28
- package/src/types.ts +89 -51
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One way for cadre-core to be told that a control or strand table changed, so an idle loop
|
|
3
|
+
* can wait for a change instead of re-reading on a timer. Built on Quereus's
|
|
4
|
+
* `Database.watch`, which hears both a commit made on this `Database` and, through the
|
|
5
|
+
* Optimystic plugin's `notifyExternalChange` bridge, every change this node's storage takes
|
|
6
|
+
* in from elsewhere: a consensus commit it took part in as a cohort member, a block-transfer
|
|
7
|
+
* push such as join backfill, and a read-repair landing. Events are table-wide; there are no
|
|
8
|
+
* row keys.
|
|
9
|
+
*
|
|
10
|
+
* The consumer's `onChange` never runs inside a commit. The watch handler only sets flags
|
|
11
|
+
* and arms a timer, and runs are single-flight: changes inside the coalescing window share
|
|
12
|
+
* one run, a change during a run produces exactly one follow-up run, and a consumer's own
|
|
13
|
+
* `trigger` (start, retry backoff) and the backstop go through the same rule.
|
|
14
|
+
* `docs/architecture.md` → "Table change subscriptions" says what the seam does not hear,
|
|
15
|
+
* which is why consumers keep a backstop.
|
|
16
|
+
*/
|
|
17
|
+
import debug from 'debug';
|
|
18
|
+
import type { ChangeScope, Database, Subscription, TableWatch, WatchEvent } from '@quereus/quereus';
|
|
19
|
+
import { defaultTimeoutScheduler } from './timeout-scheduler.js';
|
|
20
|
+
import type { TimeoutScheduler } from './timeout-scheduler.js';
|
|
21
|
+
|
|
22
|
+
const log = debug('sereus:cadre:table-watch');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default coalescing window. One local commit fires the watch twice (Quereus's own
|
|
26
|
+
* post-commit dispatch, then the plugin's storage event); this folds both into one run.
|
|
27
|
+
*/
|
|
28
|
+
export const TABLE_CHANGE_COALESCE_MS = 50;
|
|
29
|
+
|
|
30
|
+
export interface TableChangeWatchOptions {
|
|
31
|
+
/** Short label for log lines (e.g. 'strand-watcher'). */
|
|
32
|
+
label: string;
|
|
33
|
+
/** Coalescing window after the first change; default {@link TABLE_CHANGE_COALESCE_MS}. */
|
|
34
|
+
coalesceMs?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Run anyway this long after the last run settled, change or not. Omit for none.
|
|
37
|
+
* The path for a change that arrives without a storage event (see the module doc).
|
|
38
|
+
*/
|
|
39
|
+
backstopMs?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Called inline, synchronously, from the watch handler — inside the committing call,
|
|
42
|
+
* possibly with the control write lock held — once per changed table per event, with the
|
|
43
|
+
* name as the consumer passed it. Only for setting a flag (cache invalidation). Must not
|
|
44
|
+
* read, write or await.
|
|
45
|
+
*/
|
|
46
|
+
onInvalidate?: (qualifiedTable: string) => void;
|
|
47
|
+
/** Timer seam; default real, unref'd timers. */
|
|
48
|
+
scheduler?: TimeoutScheduler;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface TableChangeWatch {
|
|
52
|
+
/** Schedule a run after `delayMs` (default 0) through the same single-flight rule; an earlier pending run wins. */
|
|
53
|
+
trigger(delayMs?: number): void;
|
|
54
|
+
/** Stop: unsubscribe, clear timers; no run starts afterwards. Idempotent. Does not wait for a run in flight. */
|
|
55
|
+
dispose(): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Subscribe `onChange` to changes in `tables` (qualified `'Schema.Table'` names, e.g.
|
|
60
|
+
* `'CadreControl.Strand'`). Throws synchronously on a name that is not qualified, or a table
|
|
61
|
+
* that does not exist.
|
|
62
|
+
*
|
|
63
|
+
* NOTE: the plugin arms its storage bridge for a table on that table's first full
|
|
64
|
+
* initialization on this `Database`, so a table not yet touched hears local commits only,
|
|
65
|
+
* not replicated ones. Consumers' start runs touch their tables; the seam does not, because
|
|
66
|
+
* touching a never-written collection consults the network.
|
|
67
|
+
*
|
|
68
|
+
* NOTE: Quereus silently disposes a subscription whose table is dropped or altered. Control
|
|
69
|
+
* and strand schemas are fixed for a database's life (an app-schema attach adds `App` tables
|
|
70
|
+
* and alters no `Strand` one), so nothing re-subscribes; revisit if that stops holding.
|
|
71
|
+
*/
|
|
72
|
+
export function watchTables(
|
|
73
|
+
db: Database,
|
|
74
|
+
tables: readonly string[],
|
|
75
|
+
onChange: () => Promise<void>,
|
|
76
|
+
options: TableChangeWatchOptions,
|
|
77
|
+
): TableChangeWatch {
|
|
78
|
+
return new TableChangeWatcher({ db, tables }, onChange, options);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* {@link watchTables}' schedule — single-flight runs, `trigger`, the backstop — with no table
|
|
83
|
+
* subscription, for a consumer with no database to watch (tests, fakes).
|
|
84
|
+
*/
|
|
85
|
+
export function scheduleRuns(onChange: () => Promise<void>, options: TableChangeWatchOptions): TableChangeWatch {
|
|
86
|
+
return new TableChangeWatcher(undefined, onChange, options);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The subscriptions one database owner has handed out, so its `close()` can dispose those
|
|
91
|
+
* still live — synchronously, ahead of its own awaits, so nothing a closing commit does
|
|
92
|
+
* schedules a run. (`Database.close()` disposes them too, but only after its own awaits.)
|
|
93
|
+
*/
|
|
94
|
+
export class TableWatchRegistry {
|
|
95
|
+
private readonly live = new Set<TableChangeWatch>();
|
|
96
|
+
|
|
97
|
+
watch(
|
|
98
|
+
db: Database,
|
|
99
|
+
tables: readonly string[],
|
|
100
|
+
onChange: () => Promise<void>,
|
|
101
|
+
options: TableChangeWatchOptions,
|
|
102
|
+
): TableChangeWatch {
|
|
103
|
+
const inner = watchTables(db, tables, onChange, options);
|
|
104
|
+
const tracked: TableChangeWatch = {
|
|
105
|
+
trigger: delayMs => inner.trigger(delayMs),
|
|
106
|
+
dispose: () => {
|
|
107
|
+
this.live.delete(tracked);
|
|
108
|
+
inner.dispose();
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
this.live.add(tracked);
|
|
112
|
+
return tracked;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
disposeAll(): void {
|
|
116
|
+
for (const watch of [...this.live]) {
|
|
117
|
+
watch.dispose();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** One armed timer; an object of its own so the scheduler's handles need not be unique. */
|
|
123
|
+
interface Wake {
|
|
124
|
+
handle: unknown;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The tables a {@link TableChangeWatcher} subscribes to; absent for {@link scheduleRuns}. */
|
|
128
|
+
interface WatchedTables {
|
|
129
|
+
db: Database;
|
|
130
|
+
tables: readonly string[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
class TableChangeWatcher implements TableChangeWatch {
|
|
134
|
+
private readonly subscription: Subscription | undefined;
|
|
135
|
+
/** The consumer's name for each watched table, by Quereus's lowercased `schema.table` key. */
|
|
136
|
+
private readonly names = new Map<string, string>();
|
|
137
|
+
private readonly scheduler: TimeoutScheduler;
|
|
138
|
+
private readonly coalesceMs: number;
|
|
139
|
+
/**
|
|
140
|
+
* Every armed timer: the coalescing window, `trigger` delays and the backstop. Whichever
|
|
141
|
+
* fires first starts the run, and starting a run clears the rest.
|
|
142
|
+
*/
|
|
143
|
+
private readonly wakes = new Set<Wake>();
|
|
144
|
+
/** The coalescing window, while armed: a change joins it instead of extending it. */
|
|
145
|
+
private coalesceWake: Wake | undefined;
|
|
146
|
+
private running = false;
|
|
147
|
+
/** A timer fired while a run was in flight: one more run, a window after it settles. */
|
|
148
|
+
private rerun = false;
|
|
149
|
+
private disposed = false;
|
|
150
|
+
private warnedThenable = false;
|
|
151
|
+
|
|
152
|
+
constructor(
|
|
153
|
+
watched: WatchedTables | undefined,
|
|
154
|
+
private readonly onChange: () => Promise<void>,
|
|
155
|
+
private readonly options: TableChangeWatchOptions,
|
|
156
|
+
) {
|
|
157
|
+
this.scheduler = options.scheduler ?? defaultTimeoutScheduler;
|
|
158
|
+
this.coalesceMs = options.coalesceMs ?? TABLE_CHANGE_COALESCE_MS;
|
|
159
|
+
this.subscription = watched && this.subscribe(watched);
|
|
160
|
+
this.armBackstop();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
trigger(delayMs = 0): void {
|
|
164
|
+
if (!this.disposed) {
|
|
165
|
+
this.arm(delayMs);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
dispose(): void {
|
|
170
|
+
if (this.disposed) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.disposed = true;
|
|
174
|
+
this.subscription?.unsubscribe();
|
|
175
|
+
this.clearWakes();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private subscribe({ db, tables }: WatchedTables): Subscription {
|
|
179
|
+
const watches = tables.map(fullTableWatch);
|
|
180
|
+
watches.forEach((watch, i) => this.names.set(tableKey(watch), tables[i]));
|
|
181
|
+
const scope: ChangeScope = { watches, nonDeterministicSources: [], unboundParameters: [] };
|
|
182
|
+
// Returns undefined, so Quereus never awaits consumer work inside a commit.
|
|
183
|
+
return db.watch(scope, event => this.handleChange(event));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Runs inside the committing call, possibly under the control write lock: flags and a timer only. */
|
|
187
|
+
private handleChange(event: WatchEvent): void {
|
|
188
|
+
if (this.disposed) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this.coalesceWake ??= this.arm(this.coalesceMs);
|
|
192
|
+
for (const { watch } of event.matched) {
|
|
193
|
+
this.invalidate(this.names.get(tableKey(watch)) ?? tableKey(watch));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private invalidate(table: string): void {
|
|
198
|
+
let result: unknown;
|
|
199
|
+
try {
|
|
200
|
+
result = this.options.onInvalidate?.(table);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
log('%s: onInvalidate threw for %s: %O', this.options.label, table, err);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!this.warnedThenable && isThenable(result)) {
|
|
206
|
+
this.warnedThenable = true;
|
|
207
|
+
log('%s: misuse — onInvalidate returned a promise for %s; it is never awaited, and must only set a flag', this.options.label, table);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private arm(delayMs: number): Wake {
|
|
212
|
+
const wake: Wake = { handle: undefined };
|
|
213
|
+
wake.handle = this.scheduler.setTimeout(() => this.fire(wake), delayMs);
|
|
214
|
+
this.wakes.add(wake);
|
|
215
|
+
return wake;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private armBackstop(): void {
|
|
219
|
+
if (this.options.backstopMs !== undefined) {
|
|
220
|
+
this.arm(this.options.backstopMs);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private clearWakes(): void {
|
|
225
|
+
for (const wake of this.wakes) {
|
|
226
|
+
this.scheduler.clearTimeout(wake.handle);
|
|
227
|
+
}
|
|
228
|
+
this.wakes.clear();
|
|
229
|
+
this.coalesceWake = undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private fire(wake: Wake): void {
|
|
233
|
+
if (!this.wakes.delete(wake)) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (wake === this.coalesceWake) {
|
|
237
|
+
this.coalesceWake = undefined;
|
|
238
|
+
}
|
|
239
|
+
if (this.running) {
|
|
240
|
+
this.rerun = true;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
this.clearWakes();
|
|
244
|
+
this.rerun = false;
|
|
245
|
+
this.running = true;
|
|
246
|
+
void this.run();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private async run(): Promise<void> {
|
|
250
|
+
try {
|
|
251
|
+
await this.onChange();
|
|
252
|
+
} catch (err) {
|
|
253
|
+
log('%s: change run failed: %O', this.options.label, err);
|
|
254
|
+
}
|
|
255
|
+
this.running = false;
|
|
256
|
+
this.settled();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private settled(): void {
|
|
260
|
+
if (this.disposed) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (this.rerun) {
|
|
264
|
+
this.coalesceWake ??= this.arm(this.coalesceMs);
|
|
265
|
+
}
|
|
266
|
+
this.armBackstop();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function fullTableWatch(qualifiedTable: string): TableWatch {
|
|
271
|
+
const [schema, table, ...rest] = qualifiedTable.split('.');
|
|
272
|
+
if (!schema || !table || rest.length > 0) {
|
|
273
|
+
throw new Error(`watchTables: '${qualifiedTable}' is not a qualified 'Schema.Table' name`);
|
|
274
|
+
}
|
|
275
|
+
return { table: { schema, table }, columns: 'all', scope: { kind: 'full' } };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Quereus's own matching key for a watched table. */
|
|
279
|
+
function tableKey(watch: TableWatch): string {
|
|
280
|
+
return `${watch.table.schema}.${watch.table.table}`.toLowerCase();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isThenable(value: unknown): boolean {
|
|
284
|
+
return typeof value === 'object' && value !== null && typeof (value as { then?: unknown }).then === 'function';
|
|
285
|
+
}
|
package/src/timeout-scheduler.ts
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The timer seam shared by the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The timer seam shared by the loops that re-arm themselves one step at a time — the
|
|
3
|
+
* membership reconciler's retry ladder (`strand-membership-reconciler.ts`) — and by the table
|
|
4
|
+
* change subscription's coalescing, trigger and backstop timers (`table-change-watch.ts`), on
|
|
5
|
+
* which the first-sync write gate's probes run (`strand-first-sync-gate.ts`). `setTimeout`
|
|
6
|
+
* rather than a repeating interval because the next step is armed only once the previous
|
|
7
|
+
* one settles, so a slow network read never stacks steps behind it, and because the delay
|
|
8
|
+
* can differ per step.
|
|
9
|
+
*
|
|
10
|
+
* Tests inject a hand-cranked clock; production omits it and gets real timers, `unref`'d so
|
|
11
|
+
* a loop that is only waiting never holds a Node process open (the call is optional —
|
|
12
|
+
* browsers and React Native return a plain handle with no `unref`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Timer seam for a self-rescheduling loop; omit for real (unref'd) timeouts. */
|
|
16
|
+
export interface TimeoutScheduler {
|
|
17
|
+
setTimeout(fn: () => void, ms: number): unknown;
|
|
18
|
+
clearTimeout(handle: unknown): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Real timers, `unref`'d where the runtime supports it. */
|
|
22
|
+
export const defaultTimeoutScheduler: TimeoutScheduler = {
|
|
23
|
+
setTimeout: (fn, ms) => {
|
|
24
|
+
const handle = setTimeout(fn, ms);
|
|
25
|
+
(handle as { unref?: () => void }).unref?.();
|
|
26
|
+
return handle;
|
|
27
|
+
},
|
|
28
|
+
clearTimeout: (handle) => clearTimeout(handle as Parameters<typeof clearTimeout>[0])
|
|
29
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ConnectionGater, Libp2p, PeerId, PrivateKey } from '@libp2p/interface';
|
|
2
2
|
import type { CircuitRelayServerInit } from '@libp2p/circuit-relay-v2';
|
|
3
3
|
import type { IRawStorage, Libp2pConnectionMonitorInit, Libp2pTransports, NoiseCryptoInterface } from '@optimystic/db-p2p';
|
|
4
|
-
import type { IPeerNetwork, IRepo } from '@optimystic/db-core';
|
|
4
|
+
import type { CollectionChangeListener, IPeerNetwork, IRepo } from '@optimystic/db-core';
|
|
5
5
|
import type { PeerJoinBackfillConfig } from './peer-join-backfill.js';
|
|
6
6
|
import type { StrandReactivityConfig } from './strand-reactivity.js';
|
|
7
7
|
import type { StrandRevocationEnforcementConfig } from './strand-revocation-enforcer.js';
|
|
@@ -18,6 +18,7 @@ import type { EnrolledMachineStore } from './enrolled-machine-store.js';
|
|
|
18
18
|
import type { JoinedStrandStore } from './joined-strand-store.js';
|
|
19
19
|
import type { PushNotifier } from './push-notifier.js';
|
|
20
20
|
import type { CadreInviteSignedFields, OwnerGrantSignedFields, RevocableTable } from './control-authorization.js';
|
|
21
|
+
import type { FoundingInvite } from './control-database.js';
|
|
21
22
|
import type { CadreInvitation } from './cadre-invite-protocol.js';
|
|
22
23
|
import type { OwnerGrantBundle } from './owner-grant-protocol.js';
|
|
23
24
|
import type { RedemptionAddressOutcome } from './member-redemption.js';
|
|
@@ -39,6 +40,21 @@ export interface Libp2pNodeWithRepo extends Libp2p {
|
|
|
39
40
|
* backfill (`peer-join-backfill.ts`) logs once and stays inert when it is absent.
|
|
40
41
|
*/
|
|
41
42
|
keyNetwork?: IPeerNetwork;
|
|
43
|
+
/**
|
|
44
|
+
* The node's block store, attached by the same factory (Optimystic's
|
|
45
|
+
* `OptimysticNodeAttachments.storageRepo`), narrowed to the storage-wide change feed: it
|
|
46
|
+
* fires for every block that lands in this node's store, a push or a commit. Optional for
|
|
47
|
+
* the same reason as `keyNetwork`; the first-sync gate (`strand-first-sync-gate.ts`) wakes
|
|
48
|
+
* on it, and logs once and probes on its backstop alone when it is absent.
|
|
49
|
+
*/
|
|
50
|
+
storageRepo?: { onAnyCollectionChange(listener: CollectionChangeListener): () => void };
|
|
51
|
+
/**
|
|
52
|
+
* Tells the node a peer holds a block at a revision or later (Optimystic's
|
|
53
|
+
* `OptimysticNodeAttachments.reportBlockHeld`), so its under-replication drain and rebalance
|
|
54
|
+
* monitor do not push that peer the same block again. Optional for the same reason as
|
|
55
|
+
* `keyNetwork`; the join catch-up (`peer-join-backfill.ts`) reports each block a peer accepted.
|
|
56
|
+
*/
|
|
57
|
+
reportBlockHeld?: (blockId: string, rev: number, peerId: string) => Promise<void>;
|
|
42
58
|
}
|
|
43
59
|
|
|
44
60
|
/**
|
|
@@ -578,8 +594,13 @@ export interface NetworkConfig {
|
|
|
578
594
|
*/
|
|
579
595
|
targetDegree?: number;
|
|
580
596
|
/**
|
|
581
|
-
*
|
|
582
|
-
*
|
|
597
|
+
* The reconcile pass's backstop in ms: a pass runs this long after the last one
|
|
598
|
+
* settled when nothing has started one sooner (a membership table change, a sibling
|
|
599
|
+
* connecting or disconnecting, an address change, a redial or strand-address wake).
|
|
600
|
+
* It also caps the redial ladder. Defaults to
|
|
601
|
+
* {@link DEFAULT_CONTROL_COHORT_RECONCILE_MS} (2 minutes). The membership rows view's
|
|
602
|
+
* maximum age (`MEMBERSHIP_VIEW_MAX_AGE_MS`, 15 s) does not follow this field: a backstop
|
|
603
|
+
* longer than it reads storage on every backstop pass, a shorter one every 15 s at most.
|
|
583
604
|
*/
|
|
584
605
|
reconcileMs?: number;
|
|
585
606
|
/**
|
|
@@ -648,10 +669,10 @@ export interface NetworkConfig {
|
|
|
648
669
|
* pinging node's monitor sees the reset and aborts the connection exactly as it would on its
|
|
649
670
|
* own timeout. So this deadline is only real up to the handler timeout of the node being
|
|
650
671
|
* pinged, which has to stay above it. The deployed relay sets its handler to 60 s for that
|
|
651
|
-
* reason (`ops/docker/libp2p-infra/src/main.ts`). Every cadre node's handler is Optimystic's
|
|
652
|
-
*
|
|
653
|
-
* (`
|
|
654
|
-
*
|
|
672
|
+
* reason (`ops/docker/libp2p-infra/src/main.ts`). Every cadre node's handler is Optimystic's,
|
|
673
|
+
* which derives it from this object: twice `pingTimeout.maxTimeout`, so 60 s
|
|
674
|
+
* (`NodeOptions.pingHandlerTimeoutMs`, unset here, since Optimystic 1.16.0). An app that
|
|
675
|
+
* replaces this object without a `pingTimeout` gets `@libp2p/ping`'s 10 s handler back.
|
|
655
676
|
*
|
|
656
677
|
* WHAT THE LINK NEEDS OF IT. One ping opens a fresh stream and echoes over it: a protocol
|
|
657
678
|
* negotiation plus the echo, two link round trips by `link-budget.ts`'s counts (a `newStream`
|
|
@@ -845,10 +866,11 @@ export interface CadreNodeConfig {
|
|
|
845
866
|
|
|
846
867
|
/**
|
|
847
868
|
* Tuning for the strand peer-join block catch-up (see `peer-join-backfill.ts`):
|
|
848
|
-
* when a strand's libp2p node connects to a peer
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
869
|
+
* when a strand's libp2p node connects (or reconnects) to a peer, every
|
|
870
|
+
* block in the strand's own raw store that the peer is not
|
|
871
|
+
* known to hold is pushed to it, each block once per peer, so a machine that
|
|
872
|
+
* joined the strand after blocks were committed still ends up physically
|
|
873
|
+
* holding them. Applies to every strand this node starts.
|
|
852
874
|
* Omit for the defaults (`DEFAULT_PEER_JOIN_BACKFILL`); `{ enabled: false }`
|
|
853
875
|
* restores the no-backfill behaviour. The control network runs its own,
|
|
854
876
|
* separately tuned catch-up — see {@link controlBackfill}.
|
|
@@ -877,13 +899,14 @@ export interface CadreNodeConfig {
|
|
|
877
899
|
/**
|
|
878
900
|
* Tuning for the CONTROL network's peer-join block catch-up (same module,
|
|
879
901
|
* `peer-join-backfill.ts`, wired in `CadreNode.start`): when the control
|
|
880
|
-
* libp2p node connects to an AUTHORIZED party member
|
|
881
|
-
*
|
|
882
|
-
* it. This is what makes a control
|
|
883
|
-
*
|
|
884
|
-
* that joined later — without it,
|
|
885
|
-
* the affected tables as empty.
|
|
886
|
-
*
|
|
902
|
+
* libp2p node connects (or reconnects) to an AUTHORIZED party member, every
|
|
903
|
+
* block in the control database's own raw store that the
|
|
904
|
+
* member is not known to hold is pushed to it. This is what makes a control
|
|
905
|
+
* block committed while the writer was alone (collection headers written at
|
|
906
|
+
* genesis above all) physically reach members that joined later — without it,
|
|
907
|
+
* such a member that restarts offline reads the affected tables as empty.
|
|
908
|
+
* Pushes are gated on the node's authorized-member snapshot (the rule
|
|
909
|
+
* `CadreNode.isAuthorizedMember` reads), checked at push time, because the control
|
|
887
910
|
* network's inbound gate deliberately admits non-members in several states
|
|
888
911
|
* (seed delivery, enrollment, bootstrap/relay peers). Omit for the defaults
|
|
889
912
|
* (`DEFAULT_PEER_JOIN_BACKFILL`); `{ enabled: false }` disables it.
|
|
@@ -896,9 +919,10 @@ export interface CadreNodeConfig {
|
|
|
896
919
|
* starts: each strand node materializes the peer ids bound to REMOVED members
|
|
897
920
|
* (orphaned `Strand.MemberPeer` rows) and refuses them at the stream and
|
|
898
921
|
* connection layers, so removing a party cuts the network to its machines
|
|
899
|
-
* rather than only deleting its row. Open strands never arm it.
|
|
900
|
-
*
|
|
901
|
-
*
|
|
922
|
+
* rather than only deleting its row. Open strands never arm it. Each strand
|
|
923
|
+
* re-reads its membership when `Strand.Member` or `Strand.MemberPeer` changes,
|
|
924
|
+
* and at the latest every `DEFAULT_REVOCATION_BACKSTOP_MS`. Omit for the
|
|
925
|
+
* defaults; `{ enabled: false }` restores the pre-existing behaviour.
|
|
902
926
|
*/
|
|
903
927
|
strandRevocationEnforcement?: StrandRevocationEnforcementConfig;
|
|
904
928
|
|
|
@@ -911,9 +935,8 @@ export interface CadreNodeConfig {
|
|
|
911
935
|
* background without blocking bring-up. `pollIntervalMs` is the IDLE cadence
|
|
912
936
|
* (how often a machine nobody has admitted re-checks) and the cap of the
|
|
913
937
|
* unfinished-join retry ladder, NOT the retry cadence itself — an unfinished
|
|
914
|
-
* join retries from 1 s, doubling. Omit for the defaults
|
|
915
|
-
* {
|
|
916
|
-
* `{ enabled: false }` disarms the loop — for test fixtures that hand-drive
|
|
938
|
+
* join retries from 1 s, doubling. Omit for the defaults
|
|
939
|
+
* (`DEFAULT_MEMBERSHIP_RECONCILE_INTERVAL_MS`); `{ enabled: false }` disarms the loop — for test fixtures that hand-drive
|
|
917
940
|
* the membership writers and assert exact row sets.
|
|
918
941
|
*/
|
|
919
942
|
strandMembershipReconciliation?: StrandMembershipReconciliationConfig;
|
|
@@ -923,15 +946,17 @@ export interface CadreNodeConfig {
|
|
|
923
946
|
* (`strand-first-sync-gate.ts`), applied to every strand this node launches as a
|
|
924
947
|
* non-founder: the strand's database is withheld from the app (`StrandInstance.database`
|
|
925
948
|
* unset, status `'syncing'`) until the strand's `Strand.Header` has been received from
|
|
926
|
-
* another member and every `App` table has been read once, because a
|
|
927
|
-
* to a table it has never fetched creates a private copy of it that
|
|
949
|
+
* another member and every `Strand` and `App` table has been read once, because a
|
|
950
|
+
* machine that writes to a table it has never fetched creates a private copy of it that
|
|
951
|
+
* never merges.
|
|
928
952
|
* `timeoutMs` bounds how long {@link CadreNode.addStrand} waits before rejecting with
|
|
929
|
-
* `StrandAwaitingFirstSyncError` (retryable — the launch stays up and keeps probing)
|
|
930
|
-
*
|
|
953
|
+
* `StrandAwaitingFirstSyncError` (retryable — the launch stays up and keeps probing).
|
|
954
|
+
* While gated the machine probes whenever data lands in the strand's store, and
|
|
955
|
+
* `backstopMs` after its last probe when none has. Omit for the defaults (`DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS`,
|
|
931
956
|
* sized for a machine re-attaching through a relay on a slow link, the slowest attach
|
|
932
957
|
* measured; that constant's doc comment carries the number, the measurement behind it and
|
|
933
|
-
* what the budget costs, and is the only copy of them; `
|
|
934
|
-
*
|
|
958
|
+
* what the budget costs, and is the only copy of them; `DEFAULT_STRAND_FIRST_SYNC_BACKSTOP_MS`,
|
|
959
|
+
* 2 s). There is deliberately no way to disable the gate: a machine whose store already
|
|
935
960
|
* holds the Header is never gated, so nothing that works today is blocked by it.
|
|
936
961
|
*/
|
|
937
962
|
strandFirstSync?: StrandFirstSyncConfig;
|
|
@@ -939,7 +964,11 @@ export interface CadreNodeConfig {
|
|
|
939
964
|
/** Hibernation configuration */
|
|
940
965
|
hibernation?: HibernationConfig;
|
|
941
966
|
|
|
942
|
-
/**
|
|
967
|
+
/**
|
|
968
|
+
* The strand watcher's backstop in ms (default 300000): the longest it goes without
|
|
969
|
+
* re-reading `Strand` and `JoinedStrand` when no change to them is heard. It otherwise runs
|
|
970
|
+
* on a change. Also caps the first step of its retry ladders, which is 5 s otherwise.
|
|
971
|
+
*/
|
|
943
972
|
strandWatchInterval?: number;
|
|
944
973
|
|
|
945
974
|
/**
|
|
@@ -1463,6 +1492,14 @@ export interface FoundStrandConfig {
|
|
|
1463
1492
|
memberPrivateKey?: string;
|
|
1464
1493
|
/** sApp configuration the hosting application provides, as for {@link StrandConfig}. */
|
|
1465
1494
|
sAppConfig: SAppConfig;
|
|
1495
|
+
/**
|
|
1496
|
+
* A formation invitation to publish bound to this strand, for the app `sAppConfig.id`, in the
|
|
1497
|
+
* same control transaction and owner approval as the strand row. Mint it first with
|
|
1498
|
+
* `createOpenInvitation` (its `expiration` is `expiresAtMs`). When the strand is already
|
|
1499
|
+
* published the invitation is published on its own, under its own approval, as
|
|
1500
|
+
* `publishFormationInvite` would; one already stored under `token` for this strand is kept.
|
|
1501
|
+
*/
|
|
1502
|
+
formationInvite?: Omit<FoundingInvite, 'sAppId'>;
|
|
1466
1503
|
/**
|
|
1467
1504
|
* Forwarded to the attach ({@link StrandConfig.awaitFirstSync}). Matters only when the
|
|
1468
1505
|
* call ATTACHES rather than founds — the stored row was published by a sibling machine
|
|
@@ -1783,10 +1820,10 @@ export interface CadreNodeEvents {
|
|
|
1783
1820
|
* row is the product of the formation's consent, so claiming it needs no second one.
|
|
1784
1821
|
*
|
|
1785
1822
|
* **Fired once per strand per session, and it can fire before your listener is
|
|
1786
|
-
* attached.** The strand watcher's first
|
|
1823
|
+
* attached.** The strand watcher's first run is inside `CadreNode.start()`
|
|
1787
1824
|
* (100 ms after the watcher starts), so every strand already stored for this
|
|
1788
1825
|
* party is normally offered while the embedding app is still inside its own
|
|
1789
|
-
* `start()` await. The watcher then records the strand as seen and no later
|
|
1826
|
+
* `start()` await. The watcher then records the strand as seen and no later run
|
|
1790
1827
|
* re-offers it, so a listener attached a moment late misses those strands for the
|
|
1791
1828
|
* life of the process.
|
|
1792
1829
|
*
|
|
@@ -1841,8 +1878,14 @@ export interface CadreNodeEvents {
|
|
|
1841
1878
|
'pendingJoin:changed': PendingJoinStatus;
|
|
1842
1879
|
/** Emitted when a seed is received via the seed protocol */
|
|
1843
1880
|
'seed:received': { partyId: string; peerId: string };
|
|
1844
|
-
/**
|
|
1845
|
-
|
|
1881
|
+
/**
|
|
1882
|
+
* Emitted when a seed delivered over the seed protocol has been verified, merged and
|
|
1883
|
+
* acknowledged to its sender. The dials of its preferred peers follow this event, so it
|
|
1884
|
+
* waits on no connection. `dialTargets` are those peers (`seedDialTargets`): an embedder
|
|
1885
|
+
* that restarts into another party's stores carries them there, since this node retained
|
|
1886
|
+
* them in its own.
|
|
1887
|
+
*/
|
|
1888
|
+
'seed:applied': { partyId: string; peersAdded: number; dialTargets: SeedDialTarget[] };
|
|
1846
1889
|
/** Emitted when seed application fails */
|
|
1847
1890
|
'seed:error': { partyId: string; error: string };
|
|
1848
1891
|
/**
|
|
@@ -1850,10 +1893,10 @@ export interface CadreNodeEvents {
|
|
|
1850
1893
|
* valid claim proof has been recorded (`claim.record`) and has durably anchored its
|
|
1851
1894
|
* signer as this node's owner. Carries the claim's party beside the owner key: the
|
|
1852
1895
|
* party this node should serve from now on, which its own config did not know. Fires
|
|
1853
|
-
* BEFORE the seed is acknowledged to the claimant
|
|
1854
|
-
*
|
|
1855
|
-
*
|
|
1856
|
-
*
|
|
1896
|
+
* BEFORE the seed is acknowledged to the claimant and its peers merged, so it precedes
|
|
1897
|
+
* that seed's `seed:applied`; an embedder that restarts into the claimed party on this
|
|
1898
|
+
* event should wait for that `seed:applied` (or `seed:error`) so the acknowledgement is
|
|
1899
|
+
* not lost with the node. `CadreNode.isAwaitingClaim` is the
|
|
1857
1900
|
* matching predicate for a reader that subscribed too late to see the event.
|
|
1858
1901
|
*/
|
|
1859
1902
|
'claim:accepted': NodeClaimRecord;
|
|
@@ -2113,7 +2156,7 @@ export interface CadreOwnerKey {
|
|
|
2113
2156
|
|
|
2114
2157
|
/** Why `CadreNode.addOwner` refuses by name (`OwnerKeyAddRefusedError.code`). */
|
|
2115
2158
|
export type OwnerKeyAddRefusal =
|
|
2116
|
-
/** No
|
|
2159
|
+
/** No sibling reachable (`CadreNode.hasReachableControlSibling`) while the party has other members: the row would stay on this node, and nothing re-issues an `OwnerKey` add. */
|
|
2117
2160
|
| 'alone';
|
|
2118
2161
|
|
|
2119
2162
|
/** Why `CadreNode.removeMachine` refuses by name (`MachineRemovalRefusedError.code`). */
|
|
@@ -2154,7 +2197,7 @@ export type OwnerKeyRemovalRefusal =
|
|
|
2154
2197
|
| 'last-owner'
|
|
2155
2198
|
/** The key is this node's own owner key: an owner cannot sign its own row's removal (`A.Key <> old.Key`), so ask another owner key. */
|
|
2156
2199
|
| 'self'
|
|
2157
|
-
/** A re-vouch of an `OwnerKey` row was asked for while this node
|
|
2200
|
+
/** A re-vouch of an `OwnerKey` row was asked for while this node reaches no sibling and the party has other members: the re-added row has no re-issue path. Decided at call time, so never planned. */
|
|
2158
2201
|
| 'alone';
|
|
2159
2202
|
|
|
2160
2203
|
/** The rows sharing the owner keys' voucher column that a removed owner key strands unless re-vouched first. */
|
|
@@ -2268,8 +2311,8 @@ export interface OwnerGrantRedemptionResult {
|
|
|
2268
2311
|
* `ControlDatabase.queryRevocations`: the identity triple plus the `ReissuedAt`
|
|
2269
2312
|
* counter an owner bumps (`ControlDatabase.reissueRevocations`) to re-write — and
|
|
2270
2313
|
* therefore re-broadcast — a tombstone that committed while the node was alone.
|
|
2271
|
-
* Never the ledger marker: that row names no guarded table, and
|
|
2272
|
-
* skips it, which is what keeps `tableName` a {@link RevocableTable}.
|
|
2314
|
+
* Never the ledger marker an older party may hold: that row names no guarded table, and
|
|
2315
|
+
* `queryRevocations` skips it, which is what keeps `tableName` a {@link RevocableTable}.
|
|
2273
2316
|
*/
|
|
2274
2317
|
export interface RevocationRow {
|
|
2275
2318
|
/** Which guarded table's stamp was retired. */
|
|
@@ -2286,14 +2329,6 @@ export interface RevocationRow {
|
|
|
2286
2329
|
signerSig: string;
|
|
2287
2330
|
}
|
|
2288
2331
|
|
|
2289
|
-
/**
|
|
2290
|
-
* Outcome of `ControlDatabase.openRevocationLedger`: `'opened'` when this call filed the
|
|
2291
|
-
* singleton `Revocation` ledger marker, `'already-open'` when it was already filed (seen
|
|
2292
|
-
* by the in-lock guard, or filed first by another owner and refused on the primary key).
|
|
2293
|
-
* Either way the marker exists afterwards.
|
|
2294
|
-
*/
|
|
2295
|
-
export type RevocationLedgerOpenResult = 'opened' | 'already-open';
|
|
2296
|
-
|
|
2297
2332
|
/**
|
|
2298
2333
|
* Outcome of {@link CadreNode.registerSelf}, surfaced so callers (e.g. the CLI
|
|
2299
2334
|
* `--owner` branch) can log what happened:
|
|
@@ -2493,6 +2528,9 @@ export interface SeedPeer {
|
|
|
2493
2528
|
preferred: boolean;
|
|
2494
2529
|
}
|
|
2495
2530
|
|
|
2531
|
+
/** A peer a seed nominates for dialing, and the addresses to dial it at (`seedDialTargets`). */
|
|
2532
|
+
export type SeedDialTarget = Pick<SeedPeer, 'peerId' | 'multiaddrs'>;
|
|
2533
|
+
|
|
2496
2534
|
/**
|
|
2497
2535
|
* Control network seed for bootstrapping new nodes.
|
|
2498
2536
|
* Pre-populates a new node's cache to solve the cold-start problem:
|