@serfab/cadre-core 0.11.0 → 0.13.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/announce-addrs.d.ts +49 -0
- package/dist/announce-addrs.js +78 -0
- package/dist/announce-addrs.js.map +1 -0
- package/dist/cadre-node.d.ts +607 -53
- package/dist/cadre-node.js +1268 -157
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-database.d.ts +174 -23
- package/dist/control-database.js +317 -107
- package/dist/control-database.js.map +1 -1
- package/dist/control-read-retry.d.ts +103 -0
- package/dist/control-read-retry.js +145 -0
- package/dist/control-read-retry.js.map +1 -0
- package/dist/control-retry.d.ts +101 -0
- package/dist/control-retry.js +108 -0
- package/dist/control-retry.js.map +1 -0
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +50 -6
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +59 -35
- package/dist/control-write-retry.js +21 -92
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +17 -15
- package/dist/delegate-admission.js +17 -15
- package/dist/delegate-admission.js.map +1 -1
- package/dist/enrolled-machine-store-file.d.ts +13 -0
- package/dist/enrolled-machine-store-file.js +41 -0
- package/dist/enrolled-machine-store-file.js.map +1 -0
- package/dist/enrolled-machine-store.d.ts +81 -0
- package/dist/enrolled-machine-store.js +225 -0
- package/dist/enrolled-machine-store.js.map +1 -0
- package/dist/file-durable-slot.js +5 -3
- package/dist/file-durable-slot.js.map +1 -1
- package/dist/index.d.ts +10 -6
- package/dist/index.js +39 -12
- package/dist/index.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +226 -26
- package/dist/membership-connection-gater.js +316 -30
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-local-snapshot.d.ts +6 -0
- package/dist/node-local-snapshot.js +7 -1
- package/dist/node-local-snapshot.js.map +1 -1
- package/dist/{strand-backfill.d.ts → peer-join-backfill.d.ts} +49 -20
- package/dist/{strand-backfill.js → peer-join-backfill.js} +146 -56
- package/dist/peer-join-backfill.js.map +1 -0
- package/dist/peer-record.d.ts +26 -0
- package/dist/peer-record.js +32 -0
- package/dist/peer-record.js.map +1 -1
- package/dist/relay-addrs.d.ts +145 -22
- package/dist/relay-addrs.js +314 -31
- package/dist/relay-addrs.js.map +1 -1
- package/dist/relay-reservation.d.ts +20 -15
- package/dist/relay-reservation.js +18 -14
- package/dist/relay-reservation.js.map +1 -1
- package/dist/strand-addr-protocol.js +16 -0
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-database.d.ts +12 -0
- package/dist/strand-database.js +16 -0
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-formation-manager.d.ts +22 -9
- package/dist/strand-formation-manager.js +20 -12
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +83 -9
- package/dist/strand-formation-protocol.js +86 -10
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +151 -17
- package/dist/strand-instance-manager.js +237 -32
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +118 -14
- package/dist/strand-membership-writer.js +203 -15
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-network-config.d.ts +84 -0
- package/dist/strand-network-config.js +136 -0
- package/dist/strand-network-config.js.map +1 -0
- package/dist/strand-revocation-enforcer.d.ts +392 -0
- package/dist/strand-revocation-enforcer.js +533 -0
- package/dist/strand-revocation-enforcer.js.map +1 -0
- package/dist/strand-solicitation.d.ts +10 -3
- package/dist/strand-solicitation.js +12 -7
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-wake-protocol.d.ts +47 -2
- package/dist/strand-wake-protocol.js +76 -6
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/strand-watcher.js +6 -1
- package/dist/strand-watcher.js.map +1 -1
- package/dist/types.d.ts +282 -43
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/package.json +16 -8
- package/src/announce-addrs.ts +93 -0
- package/src/cadre-node.ts +1366 -171
- package/src/control-database.ts +2628 -2406
- package/src/control-read-retry.ts +167 -0
- package/src/control-retry.ts +190 -0
- package/src/control-schema.ts +50 -6
- package/src/control-write-retry.ts +30 -130
- package/src/delegate-admission.ts +17 -15
- package/src/enrolled-machine-store-file.ts +43 -0
- package/src/enrolled-machine-store.ts +280 -0
- package/src/file-durable-slot.ts +5 -3
- package/src/index.ts +477 -407
- package/src/membership-connection-gater.ts +539 -204
- package/src/node-local-snapshot.ts +13 -1
- package/src/peer-addr-book.ts +251 -250
- package/src/{strand-backfill.ts → peer-join-backfill.ts} +187 -75
- package/src/peer-record.ts +33 -0
- package/src/relay-addrs.ts +436 -111
- package/src/relay-reservation.ts +828 -823
- package/src/strand-addr-protocol.ts +17 -0
- package/src/strand-database.ts +17 -0
- package/src/strand-formation-manager.ts +511 -491
- package/src/strand-formation-protocol.ts +808 -692
- package/src/strand-instance-manager.ts +899 -611
- package/src/strand-membership-writer.ts +232 -15
- package/src/strand-network-config.ts +172 -0
- package/src/strand-revocation-enforcer.ts +653 -0
- package/src/strand-solicitation.ts +484 -472
- package/src/strand-wake-protocol.ts +94 -8
- package/src/strand-watcher.ts +319 -314
- package/src/types.ts +1554 -1309
- package/dist/strand-backfill.js.map +0 -1
|
@@ -1,611 +1,899 @@
|
|
|
1
|
-
import debug from 'debug';
|
|
2
|
-
import type { PrivateKey } from '@libp2p/interface';
|
|
3
|
-
import { createLibp2pNode, type IRawStorage } from '@optimystic/db-p2p';
|
|
4
|
-
import { wrapStorageWithCache, disposeStorageCache } from '@serfab/quereus-plugin-sereus';
|
|
5
|
-
import { StrandDatabase } from './strand-database.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*/
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*/
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
//
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
await
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
try {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import type { PrivateKey } from '@libp2p/interface';
|
|
3
|
+
import { createLibp2pNode, type IRawStorage } from '@optimystic/db-p2p';
|
|
4
|
+
import { wrapStorageWithCache, disposeStorageCache } from '@serfab/quereus-plugin-sereus';
|
|
5
|
+
import { StrandDatabase } from './strand-database.js';
|
|
6
|
+
import { PeerJoinBackfill, type PeerJoinBackfillConfig } from './peer-join-backfill.js';
|
|
7
|
+
import {
|
|
8
|
+
StrandRevocationEnforcer,
|
|
9
|
+
createRevocationConnectionGater,
|
|
10
|
+
readStrandRevocationRows,
|
|
11
|
+
type StrandRevocationEnforcementConfig
|
|
12
|
+
} from './strand-revocation-enforcer.js';
|
|
13
|
+
import { assertSchemaSignature } from './schema-verification.js';
|
|
14
|
+
import type {
|
|
15
|
+
StrandInstance,
|
|
16
|
+
StrandRow,
|
|
17
|
+
StorageConfig,
|
|
18
|
+
NetworkConfig,
|
|
19
|
+
LatencyHint,
|
|
20
|
+
NodeProfile,
|
|
21
|
+
SAppConfig,
|
|
22
|
+
SAppInfo,
|
|
23
|
+
RawStorageProvider,
|
|
24
|
+
Libp2pNodeWithRepo
|
|
25
|
+
} from './types.js';
|
|
26
|
+
import { resolveStrandClusterSize, strandClusterPolicy } from './types.js';
|
|
27
|
+
import { strandNodeAddrs } from './strand-network-config.js';
|
|
28
|
+
|
|
29
|
+
const log = debug('sereus:cadre:strand-manager');
|
|
30
|
+
const timing = debug('sereus:cadre:timing');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Configuration for starting a strand instance
|
|
34
|
+
*/
|
|
35
|
+
export interface StartStrandConfig {
|
|
36
|
+
strandRow: StrandRow;
|
|
37
|
+
/** sApp configuration provided by the hosting application */
|
|
38
|
+
sAppConfig: SAppConfig;
|
|
39
|
+
storage?: StorageConfig;
|
|
40
|
+
network?: NetworkConfig;
|
|
41
|
+
profile: NodeProfile;
|
|
42
|
+
defaultLatencyHint: LatencyHint;
|
|
43
|
+
privateKey?: PrivateKey;
|
|
44
|
+
/** Cohort-derived discovery seed (multiaddr strings). Defaults to [] when omitted. */
|
|
45
|
+
bootstrapNodes?: string[];
|
|
46
|
+
/**
|
|
47
|
+
* Require a valid author signature on the sApp schema before bring-up.
|
|
48
|
+
* Defaults to true (fail closed) when omitted; set false only for dev/test
|
|
49
|
+
* with unsigned demo schemas. Mirrors {@link CadreNodeConfig.requireSignedSchemas}.
|
|
50
|
+
*/
|
|
51
|
+
requireSignedSchemas?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Whether this node founds the strand (vs. joins it). Forwarded to the
|
|
54
|
+
* StrandDatabase so the founder bootstrap (Header, founding Member/Manager)
|
|
55
|
+
* runs once at bring-up. Joiners leave this unset and write nothing. Callers
|
|
56
|
+
* resolve it BEFORE calling `startStrand` (`CadreNode.launchStrand` derives it
|
|
57
|
+
* from the row's `FounderOwnerKey` when no explicit flag is given — see
|
|
58
|
+
* {@link StrandConfig.founder}); a founder request that arrives while the
|
|
59
|
+
* instance is already tracked goes through {@link StrandInstanceManager.foundExistingStrand},
|
|
60
|
+
* never through a repeat `startStrand`.
|
|
61
|
+
*/
|
|
62
|
+
founder?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Number of nodes Optimystic is told this strand's replication cluster should
|
|
65
|
+
* have. Same rule as {@link CadreNodeConfig.strandClusterSize}, which CadreNode
|
|
66
|
+
* forwards here: every node on the strand should use the same value, and it is
|
|
67
|
+
* frozen when the strand's libp2p node is created. Defaults to
|
|
68
|
+
* `DEFAULT_STRAND_CLUSTER_SIZE` (4); values below `MIN_CLUSTER_SIZE` (2) are
|
|
69
|
+
* rejected by `resolveStrandClusterSize`. Strand-only — the
|
|
70
|
+
* control network's breadth is the fixed `CONTROL_REPLICATION_BREADTH`.
|
|
71
|
+
*/
|
|
72
|
+
clusterSize?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Machines that SERVE THIS STRAND (this node included), for the strand node's
|
|
75
|
+
* block-repair corroboration yardstick — see `resolveRepairYardstick`. It must be
|
|
76
|
+
* an authenticated per-strand count: the machines that actually run a node for this
|
|
77
|
+
* strand, not the machines that exist.
|
|
78
|
+
*
|
|
79
|
+
* **The party's enrolled-machine count is NOT this number and must never be passed
|
|
80
|
+
* here.** A strand launches only on machines whose embedding app registered its sApp
|
|
81
|
+
* config (`CadreNode.addStrand`), so a closed strand shared by two machines of a
|
|
82
|
+
* three-machine party is served by two. Passing three over-declares, and
|
|
83
|
+
* over-declaring is the unsafe direction: at a declaration of 3 or more Optimystic
|
|
84
|
+
* pins the repair corroboration floor at two corroborating peers, which a cohort
|
|
85
|
+
* that can only ever field one peer can never reach — so the strand can never repair
|
|
86
|
+
* a block (`cluster-fetch:no-quorum`, surfacing as reads failing with `Missing
|
|
87
|
+
* block`). That regression is why this field was renamed off "enrolledMachines":
|
|
88
|
+
* `bug-strand-yardstick-counts-party-machines`.
|
|
89
|
+
*
|
|
90
|
+
* **Nothing feeds it in production today.** No authenticated per-strand serving count
|
|
91
|
+
* exists yet, so `CadreNode` passes nothing and the strand node runs the frozen
|
|
92
|
+
* `STRAND_CLUSTER_POLICY` — declaring no yardstick, which leaves the known,
|
|
93
|
+
* upstream-tracked single-voter exposure
|
|
94
|
+
* (`backlog/debt-read-repair-single-voter-corroboration`). Building the count is
|
|
95
|
+
* `backlog/feat-strand-yardstick-from-serving-machines`; this field and its
|
|
96
|
+
* threading are the seam it plugs into.
|
|
97
|
+
*
|
|
98
|
+
* Volatile when a source does exist: re-resolve it on every resume beside the cohort
|
|
99
|
+
* seed, since machines join and leave a strand while it hibernates. Omitting it means
|
|
100
|
+
* "this node does not know", which declares nothing.
|
|
101
|
+
*/
|
|
102
|
+
servingMachines?: number;
|
|
103
|
+
/**
|
|
104
|
+
* Tuning for the strand peer-join block catch-up ({@link PeerJoinBackfill}),
|
|
105
|
+
* forwarded from {@link CadreNodeConfig.strandBackfill}. When the strand's
|
|
106
|
+
* libp2p node connects to a peer this runtime has not yet caught up, every
|
|
107
|
+
* block in the strand's own raw store is pushed to it. Runs only on strands
|
|
108
|
+
* with per-strand storage; `{ enabled: false }` restores the pre-existing
|
|
109
|
+
* no-backfill behaviour.
|
|
110
|
+
*/
|
|
111
|
+
backfill?: PeerJoinBackfillConfig;
|
|
112
|
+
/**
|
|
113
|
+
* Tuning for the CLOSED-strand revoked-peer gate
|
|
114
|
+
* ({@link StrandRevocationEnforcer}), forwarded from
|
|
115
|
+
* {@link CadreNodeConfig.strandRevocationEnforcement}. Armed only for
|
|
116
|
+
* `strandRow.Type === 'c'` (an open strand has no membership rows to derive a
|
|
117
|
+
* deny set from); `{ enabled: false }` restores the pre-existing behaviour
|
|
118
|
+
* (a removed party's peers keep being served).
|
|
119
|
+
*/
|
|
120
|
+
revocationEnforcement?: StrandRevocationEnforcementConfig;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Called when THIS node's own peer id turns up in the strand's revoked set —
|
|
124
|
+
* this node's party was removed from the closed strand, or left it. Fires once
|
|
125
|
+
* per removal (re-armed if the party is re-admitted; a resume also rebuilds the
|
|
126
|
+
* enforcer and may legitimately re-fire), and only for closed strands with the
|
|
127
|
+
* gate armed. `CadreNode`
|
|
128
|
+
* wires it to its `strand:revoked` event; nothing is stopped or torn down on
|
|
129
|
+
* this node's behalf — what to do about it is the app's call.
|
|
130
|
+
*/
|
|
131
|
+
onSelfRevoked?: (strandId: string) => void;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Volatile inputs re-resolved when resuming a quiesced strand — the ones that can
|
|
136
|
+
* have moved since it last ran, and that the rebuilt libp2p node freezes again. Each
|
|
137
|
+
* one omitted keeps the value the retained launch config already holds, so a resume
|
|
138
|
+
* that passes nothing rebuilds the strand exactly as it last ran.
|
|
139
|
+
*/
|
|
140
|
+
export interface ResumeStrandOverrides {
|
|
141
|
+
/**
|
|
142
|
+
* Freshly-resolved cohort discovery seed (multiaddr strings). Grows as peers are
|
|
143
|
+
* learned since the strand first launched.
|
|
144
|
+
*/
|
|
145
|
+
bootstrapNodes?: string[];
|
|
146
|
+
/**
|
|
147
|
+
* Freshly-read count of the machines serving this strand, for the repair yardstick
|
|
148
|
+
* (see {@link StartStrandConfig.servingMachines} — including why the party's
|
|
149
|
+
* enrolled-machine count is not it). Moves whenever a machine starts or stops
|
|
150
|
+
* serving the strand, which a hibernating strand does not otherwise notice. Nothing
|
|
151
|
+
* passes it today. Omitting it here does NOT mean "unknown" the way omitting it from
|
|
152
|
+
* {@link StartStrandConfig} does — it retains the last value; see `resumeStrand`.
|
|
153
|
+
*/
|
|
154
|
+
servingMachines?: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get the isolated storage path for a specific strand.
|
|
159
|
+
*
|
|
160
|
+
* @deprecated This helper is Node-only and throws in React Native (it assumes a
|
|
161
|
+
* filesystem layout). Use a storage provider factory function instead, which
|
|
162
|
+
* receives the strandId and can create strand-specific storage paths using
|
|
163
|
+
* platform-appropriate methods.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* // Instead of using getStrandStoragePath, use a storage provider factory:
|
|
167
|
+
* const storage = {
|
|
168
|
+
* provider: (strandId: string) => new FileRawStorage(`./data/strands/${strandId}`)
|
|
169
|
+
* };
|
|
170
|
+
*/
|
|
171
|
+
export function getStrandStoragePath(basePath: string, strandId: string): string {
|
|
172
|
+
// Check if we're in a Node.js environment
|
|
173
|
+
if (typeof process === 'undefined' || !process.versions?.node) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
'getStrandStoragePath is not available in React Native. ' +
|
|
176
|
+
'Use a storage provider factory function instead.'
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Sanitize strandId for filesystem safety (UUIDs should be safe, but just in case)
|
|
181
|
+
const safeId = strandId.replace(/[^a-zA-Z0-9-]/g, '_');
|
|
182
|
+
|
|
183
|
+
// Build the path with plain string joins rather than the Node `path` module.
|
|
184
|
+
// A static `require('path')` forces RN bundlers (e.g. Metro) to *resolve* the
|
|
185
|
+
// module at bundle time even though this Node-only helper throws above before
|
|
186
|
+
// ever reaching here — joining by hand keeps the module free of any Node
|
|
187
|
+
// built-in reference, so RN bundles need no `path` shim.
|
|
188
|
+
const trimmedBase = basePath.replace(/[\\/]+$/, '');
|
|
189
|
+
return `${trimmedBase}/strands/${safeId}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Resolve a storage provider for a specific strand.
|
|
194
|
+
* If the provider is a factory function, call it with the strandId.
|
|
195
|
+
*
|
|
196
|
+
* Called only from {@link StrandInstanceManager.startStrand}, which owns the result
|
|
197
|
+
* for the instance's lifetime — see `strandStorages`.
|
|
198
|
+
*
|
|
199
|
+
* @param provider - Storage provider (instance or factory)
|
|
200
|
+
* @param strandId - The strand ID to create storage for
|
|
201
|
+
* @returns The resolved IRawStorage instance, or undefined if no provider
|
|
202
|
+
*/
|
|
203
|
+
function resolveStrandStorage(
|
|
204
|
+
provider: RawStorageProvider | undefined,
|
|
205
|
+
strandId: string
|
|
206
|
+
): IRawStorage | undefined {
|
|
207
|
+
if (!provider) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
const storage = typeof provider === 'function' ? provider(strandId) : provider;
|
|
211
|
+
// Wrapped in the write-through raw-storage cache (quereus-plugin-sereus's cached-storage.ts).
|
|
212
|
+
// Called ONCE per strand launch — `startStrand` keeps the result for the instance's
|
|
213
|
+
// lifetime — so the wrap survives every runtime rebuild the instance goes through.
|
|
214
|
+
return wrapStorageWithCache(storage, strandId);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Manages individual strand instances - creates and destroys isolated libp2p nodes
|
|
219
|
+
* for each strand the cadre participates in.
|
|
220
|
+
*/
|
|
221
|
+
export class StrandInstanceManager {
|
|
222
|
+
private instances: Map<string, StrandInstance> = new Map();
|
|
223
|
+
/**
|
|
224
|
+
* Retained launch config per strand, captured in `startStrand` and cleared in
|
|
225
|
+
* `stopStrand`. `resumeStrand` reuses it to rebuild a quiesced strand's runtime
|
|
226
|
+
* without the caller re-threading storage/network/profile/key/sApp config.
|
|
227
|
+
*/
|
|
228
|
+
private launchConfigs: Map<string, StartStrandConfig> = new Map();
|
|
229
|
+
/**
|
|
230
|
+
* The per-strand peer-join block catch-up, keyed by strand id. Private (not on
|
|
231
|
+
* the public {@link StrandInstance}) because it is runtime plumbing with the
|
|
232
|
+
* same lifetime as the strand's libp2p node: created in `buildStrandRuntime`,
|
|
233
|
+
* stopped and dropped in `releaseRuntime` — so quiesce → resume rebuilds it
|
|
234
|
+
* with a fresh caught-up-peer memo, which is intended (a resumed node may have
|
|
235
|
+
* missed writes).
|
|
236
|
+
*/
|
|
237
|
+
private backfills: Map<string, PeerJoinBackfill> = new Map();
|
|
238
|
+
/**
|
|
239
|
+
* The per-strand revoked-peer enforcer (closed strands only), keyed by strand
|
|
240
|
+
* id. Same lifecycle rationale as {@link backfills}: created in
|
|
241
|
+
* `buildStrandRuntime`, stopped and dropped in `releaseRuntime`, so quiesce →
|
|
242
|
+
* resume rebuilds it with a fresh (initially empty, fail-open) snapshot.
|
|
243
|
+
*/
|
|
244
|
+
private revocationEnforcers: Map<string, StrandRevocationEnforcer> = new Map();
|
|
245
|
+
/**
|
|
246
|
+
* The resolved (cache-wrapped) raw storage per strand id — the instance's OWN
|
|
247
|
+
* store, resolved once in `startStrand` and held until `stopStrand` disposes it.
|
|
248
|
+
* Private for the same reason `backfills` is: runtime plumbing, not part of the
|
|
249
|
+
* public {@link StrandInstance}.
|
|
250
|
+
*
|
|
251
|
+
* Deliberately NOT touched by `releaseRuntime`: that is what lets a quiesce →
|
|
252
|
+
* resume cycle rebuild the libp2p node over the SAME store, keeping its
|
|
253
|
+
* write-through cache warm (and, on an in-memory backend, keeping the strand's
|
|
254
|
+
* blocks at all). An entry exists iff `instances` does AND the launch config
|
|
255
|
+
* supplied a storage provider.
|
|
256
|
+
*
|
|
257
|
+
* NOTE: a hibernating strand therefore keeps its store — and its share of the
|
|
258
|
+
* process-wide cache pool — resident for as long as the instance is tracked. That
|
|
259
|
+
* is the point (a warm wake), and the pool evicts under pressure, so the cost is
|
|
260
|
+
* one map entry per hibernating strand today. If a device ever hibernates strands
|
|
261
|
+
* by the hundred, revisit: dropping the store at quiesce and paying for a cold
|
|
262
|
+
* wake becomes the better trade.
|
|
263
|
+
*/
|
|
264
|
+
private strandStorages: Map<string, IRawStorage> = new Map();
|
|
265
|
+
private stopping = false;
|
|
266
|
+
|
|
267
|
+
constructor() {
|
|
268
|
+
log('StrandInstanceManager created');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get all current strand instances
|
|
273
|
+
*/
|
|
274
|
+
getInstances(): Map<string, StrandInstance> {
|
|
275
|
+
return new Map(this.instances);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get a specific strand instance
|
|
280
|
+
*/
|
|
281
|
+
getInstance(strandId: string): StrandInstance | undefined {
|
|
282
|
+
return this.instances.get(strandId);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Check if a strand is currently running
|
|
287
|
+
*/
|
|
288
|
+
hasStrand(strandId: string): boolean {
|
|
289
|
+
return this.instances.has(strandId);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Start a new strand instance.
|
|
294
|
+
*
|
|
295
|
+
* A failed launch leaves NOTHING tracked: the instance and its retained launch
|
|
296
|
+
* config are both dropped before the error is rethrown, so the strand id is
|
|
297
|
+
* free for a genuine retry. This matches the pre-registration failure path
|
|
298
|
+
* (a rejected schema signature, which throws before anything is recorded) —
|
|
299
|
+
* both failure modes of this call leave the same residue: none. Callers learn
|
|
300
|
+
* of the failure from the rejected promise (and, on the control-discovered
|
|
301
|
+
* path, from CadreNode's `strand:error` event), not from an error record left
|
|
302
|
+
* behind in `instances`.
|
|
303
|
+
*
|
|
304
|
+
* The strand's raw storage is resolved HERE, once, and held for the instance's
|
|
305
|
+
* lifetime (see `strandStorages`) — `buildStrandRuntime` only reads it, so a
|
|
306
|
+
* hibernation wake never re-enters the embedder's provider.
|
|
307
|
+
*/
|
|
308
|
+
async startStrand(config: StartStrandConfig): Promise<StrandInstance> {
|
|
309
|
+
const { strandRow, sAppConfig } = config;
|
|
310
|
+
const strandId = strandRow.Id;
|
|
311
|
+
|
|
312
|
+
if (this.stopping) {
|
|
313
|
+
throw new Error('StrandInstanceManager is stopping');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (this.instances.has(strandId)) {
|
|
317
|
+
// Callers resolve founder-ness BEFORE reaching here (CadreNode.launchStrand) and
|
|
318
|
+
// honor a founder request on a tracked instance via foundExistingStrand — so a
|
|
319
|
+
// founder flag arriving at this early return against a non-founder retained
|
|
320
|
+
// config is a dropped bootstrap, and must never again be silent.
|
|
321
|
+
if (config.founder === true && this.launchConfigs.get(strandId)?.founder !== true) {
|
|
322
|
+
log('startStrand: strand %s is already running but was NOT launched as founder — ' +
|
|
323
|
+
'this early return DROPS the founder request; use foundExistingStrand', strandId);
|
|
324
|
+
}
|
|
325
|
+
log('Strand %s already running', strandId);
|
|
326
|
+
return this.instances.get(strandId)!;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
log('Starting strand instance: %s (sApp: %s v%s)', strandId, sAppConfig.id, sAppConfig.version);
|
|
330
|
+
const tTotal = performance.now();
|
|
331
|
+
|
|
332
|
+
// Verify schema signature before proceeding (fail-closed by default)
|
|
333
|
+
const requireSignature = config.requireSignedSchemas ?? true;
|
|
334
|
+
assertSchemaSignature(sAppConfig, { requireSignature });
|
|
335
|
+
log('Strand %s sApp schema signature verified (author: %s)', strandId, sAppConfig.id);
|
|
336
|
+
|
|
337
|
+
// Convert SAppConfig to SAppInfo for the instance
|
|
338
|
+
const sAppInfo: SAppInfo = {
|
|
339
|
+
id: sAppConfig.id,
|
|
340
|
+
version: sAppConfig.version,
|
|
341
|
+
schema: sAppConfig.schema,
|
|
342
|
+
signature: sAppConfig.signature
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// Resolve this strand's storage ONCE, before anything is recorded. If a factory
|
|
346
|
+
// function is provided, it is called with the strandId to create strand-specific
|
|
347
|
+
// storage (e.g. strand-isolated directories). A provider that throws therefore
|
|
348
|
+
// fails the launch alongside the schema-signature check, leaving nothing tracked.
|
|
349
|
+
const strandStorage = resolveStrandStorage(config.storage?.provider, strandId);
|
|
350
|
+
if (strandStorage) {
|
|
351
|
+
log('Strand %s using provided storage provider', strandId);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Determine latency hint: sApp config > default
|
|
355
|
+
const latencyHint = sAppConfig.latencyHint ?? config.defaultLatencyHint;
|
|
356
|
+
|
|
357
|
+
const instance: StrandInstance = {
|
|
358
|
+
strandId,
|
|
359
|
+
status: 'starting',
|
|
360
|
+
sAppInfo,
|
|
361
|
+
memberPrivateKey: strandRow.MemberPrivateKey ?? undefined,
|
|
362
|
+
connectedPeers: 0,
|
|
363
|
+
lastActivity: new Date(),
|
|
364
|
+
latencyHint
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
this.instances.set(strandId, instance);
|
|
368
|
+
this.launchConfigs.set(strandId, config);
|
|
369
|
+
if (strandStorage) {
|
|
370
|
+
this.strandStorages.set(strandId, strandStorage);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
await this.buildStrandRuntime(instance, config);
|
|
375
|
+
timing('[startStrand:%s] total: %dms', strandId, Math.round(performance.now() - tTotal));
|
|
376
|
+
log('Strand %s started successfully with sApp %s', strandId, sAppConfig.id);
|
|
377
|
+
return instance;
|
|
378
|
+
} catch (error) {
|
|
379
|
+
// Status/error first — the (now discarded) record is still what `log` reports on.
|
|
380
|
+
instance.status = 'error';
|
|
381
|
+
instance.error = error instanceof Error ? error.message : String(error);
|
|
382
|
+
log('Failed to start strand %s: %s', strandId, instance.error);
|
|
383
|
+
// Drop the dead record so this strand id can be launched again. Keep the
|
|
384
|
+
// `launchConfigs` has an entry iff `instances` does invariant — resumeStrand
|
|
385
|
+
// reads both, and a config without an instance would strand the config.
|
|
386
|
+
this.instances.delete(strandId);
|
|
387
|
+
this.launchConfigs.delete(strandId);
|
|
388
|
+
// The store this launch resolved goes with it: nothing owns it any more, and a
|
|
389
|
+
// retained cache wrapper would be handed back (already retired) on a retry.
|
|
390
|
+
await this.disposeStrandStorage(strandId);
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Build (or rebuild) the libp2p node + StrandDatabase for an instance and
|
|
397
|
+
* attach them, transitioning it to `active`. Shared by `startStrand` (fresh
|
|
398
|
+
* launch) and `resumeStrand` (rehydrating a quiesced instance). Reads all
|
|
399
|
+
* volatile inputs (bootstrapNodes, servingMachines, network, profile,
|
|
400
|
+
* privateKey, sApp config) from `config`, so the caller controls the
|
|
401
|
+
* cohort-derived values. Storage is the
|
|
402
|
+
* one input it does NOT re-read from `config`: that belongs to the instance and
|
|
403
|
+
* comes from `strandStorages`.
|
|
404
|
+
*/
|
|
405
|
+
private async buildStrandRuntime(instance: StrandInstance, config: StartStrandConfig): Promise<void> {
|
|
406
|
+
const strandId = instance.strandId;
|
|
407
|
+
const { sAppConfig } = config;
|
|
408
|
+
|
|
409
|
+
// The store the instance OWNS (resolved once in `startStrand`), not a fresh
|
|
410
|
+
// resolution: a rebuild must reach the same backend through the same warm cache.
|
|
411
|
+
const strandStorage = this.strandStorages.get(strandId);
|
|
412
|
+
|
|
413
|
+
// db-p2p namespaces every one of the node's protocol ids by network name
|
|
414
|
+
// (`/optimystic/<networkName>/...`), so anything dialing this node's own
|
|
415
|
+
// services must derive its prefix from the SAME string the node was built
|
|
416
|
+
// with — hence one binding for both, not two literals that can drift.
|
|
417
|
+
const networkName = `strand-${strandId}`;
|
|
418
|
+
const protocolPrefix = `/optimystic/${networkName}`;
|
|
419
|
+
|
|
420
|
+
// Determine relay mode: if explicitly set in config, use that;
|
|
421
|
+
// otherwise default to true for storage profile nodes.
|
|
422
|
+
const enableRelay = config.network?.enableRelay ?? (config.profile === 'storage');
|
|
423
|
+
// The CONFIGURED relay route (the default), deliberately NOT the control node's
|
|
424
|
+
// `'search'` route: nothing drives an explicit reservation for a strand node, so
|
|
425
|
+
// a bare `/p2p-circuit` search entry would register a pending reservation that
|
|
426
|
+
// never gets filled and leave the strand node undialable. The ordering hazard
|
|
427
|
+
// that pushed the control node onto the search route does not exist here — a
|
|
428
|
+
// strand node's protocol ids are namespaced `/optimystic/strand-<id>/…`, so a
|
|
429
|
+
// relay dialed from inside `libp2p.start()` is never in the strand's cohort and
|
|
430
|
+
// cannot refuse its database bring-up. See `relay-addrs.ts`.
|
|
431
|
+
//
|
|
432
|
+
// The strand-node VIEW of the machine's one `NetworkConfig`, not the control
|
|
433
|
+
// node's resolution: fixed direct listen ports become ephemeral (two nodes cannot
|
|
434
|
+
// bind one port) and the announce config is dropped (it names the control node's
|
|
435
|
+
// address). See `strand-network-config.ts` for both, and for the tradeoff.
|
|
436
|
+
const addrOptions = strandNodeAddrs(config.network);
|
|
437
|
+
|
|
438
|
+
// The CLOSED-strand revoked-peer gate (see strand-revocation-enforcer.ts).
|
|
439
|
+
// Constructed BEFORE the libp2p node because the node's options embed its
|
|
440
|
+
// predicates; its deny set starts empty (admit everything — fail-open, same
|
|
441
|
+
// posture as bring-up everywhere here) and is first populated after the
|
|
442
|
+
// strand database initializes below. Registered in the map immediately so
|
|
443
|
+
// the failure rollback (releaseRuntime) tears it down like every other
|
|
444
|
+
// runtime component. Open strands skip it entirely: their Member/MemberPeer
|
|
445
|
+
// tables are empty by schema (`OnlyClosed`), so there is nothing to derive
|
|
446
|
+
// a deny set from — arming would be pointless polling.
|
|
447
|
+
const revocationEnforcer =
|
|
448
|
+
config.strandRow.Type === 'c' && config.revocationEnforcement?.enabled !== false
|
|
449
|
+
? new StrandRevocationEnforcer({
|
|
450
|
+
label: strandId,
|
|
451
|
+
readRows: () => {
|
|
452
|
+
const database = instance.database;
|
|
453
|
+
if (!database) {
|
|
454
|
+
throw new Error(`strand ${strandId} has no live database`);
|
|
455
|
+
}
|
|
456
|
+
return readStrandRevocationRows(database.getDatabase());
|
|
457
|
+
},
|
|
458
|
+
// Read per sweep, never captured: this closure is built BEFORE the
|
|
459
|
+
// libp2p node exists (its options embed the enforcer's predicates),
|
|
460
|
+
// and `releaseRuntime` clears the field again on quiesce. Undefined
|
|
461
|
+
// therefore means "no transport right now", which the enforcer
|
|
462
|
+
// treats as "nothing to tear down".
|
|
463
|
+
getNetwork: () => instance.libp2pNode,
|
|
464
|
+
onSelfRevoked: () => config.onSelfRevoked?.(strandId)
|
|
465
|
+
}, config.revocationEnforcement)
|
|
466
|
+
: undefined;
|
|
467
|
+
if (revocationEnforcer) {
|
|
468
|
+
this.revocationEnforcers.set(strandId, revocationEnforcer);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Closed strands compose revoked-peer denial onto the caller's gater and arm
|
|
472
|
+
// the fail-closed per-stream gate; open strands keep the raw configured gater
|
|
473
|
+
// (their peers are legitimately cross-party and nothing is ever revoked).
|
|
474
|
+
const revocationGateOptions = revocationEnforcer
|
|
475
|
+
? {
|
|
476
|
+
connectionGater: createRevocationConnectionGater(revocationEnforcer, config.network?.connectionGater),
|
|
477
|
+
authorizeInboundStream: (remotePeerId: string, protocol: string) =>
|
|
478
|
+
revocationEnforcer.authorizeStream(remotePeerId, protocol)
|
|
479
|
+
}
|
|
480
|
+
: config.network?.connectionGater
|
|
481
|
+
? { connectionGater: config.network.connectionGater }
|
|
482
|
+
: {};
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
// Bound once: the breadth is also the ceiling on the repair yardstick below, and the
|
|
486
|
+
// two must be derived from the same resolution. Inside the `try` deliberately — a
|
|
487
|
+
// rejected clusterSize is a build failure that runs the same cleanup as any other.
|
|
488
|
+
const strandClusterSize = resolveStrandClusterSize(config.clusterSize);
|
|
489
|
+
|
|
490
|
+
let t0 = performance.now();
|
|
491
|
+
const node = await createLibp2pNode({
|
|
492
|
+
port: 0, // Random port
|
|
493
|
+
bootstrapNodes: config.bootstrapNodes ?? [],
|
|
494
|
+
networkName,
|
|
495
|
+
storage: strandStorage,
|
|
496
|
+
fretProfile: config.profile === 'storage' ? 'core' : 'edge',
|
|
497
|
+
relay: enableRelay,
|
|
498
|
+
clusterSize: strandClusterSize,
|
|
499
|
+
// Deliberately NOT CONTROL_CLUSTER_POLICY: a strand is application data with its own
|
|
500
|
+
// breadth reasoning, and the shape match with the control policy is a coincidence.
|
|
501
|
+
//
|
|
502
|
+
// The builder declares this node's block-repair corroboration yardstick from the
|
|
503
|
+
// count of machines SERVING this strand, capped at the breadth above (a block
|
|
504
|
+
// never lives on more machines than the cohort is wide). Given no count — the
|
|
505
|
+
// production path today, since no per-strand serving count exists yet — it
|
|
506
|
+
// returns the frozen STRAND_CLUSTER_POLICY itself, declaring nothing. Resolved
|
|
507
|
+
// HERE rather than at `startStrand`, so a wake from hibernation would pick up a
|
|
508
|
+
// serving set that changed while the strand slept.
|
|
509
|
+
clusterPolicy: strandClusterPolicy(strandClusterSize, config.servingMachines),
|
|
510
|
+
arachnode: {
|
|
511
|
+
enableRingZulu: config.profile === 'storage'
|
|
512
|
+
},
|
|
513
|
+
...(config.privateKey && { privateKey: config.privateKey }),
|
|
514
|
+
...(config.network?.transports && { transports: config.network.transports }),
|
|
515
|
+
// Listen entries plus the WebSocket transport switch they imply — a strand node
|
|
516
|
+
// announces nothing the operator configured (`strand-network-config.ts`), and
|
|
517
|
+
// spreads AFTER `transports` above because the switch is a no-op whenever the
|
|
518
|
+
// embedder supplied its own factories. An inherited configured `/p2p-circuit` entry is
|
|
519
|
+
// deliberate and survives the derivation untouched: it is what gives a NAT'd
|
|
520
|
+
// strand node a reachable relay slot, and it works because the launch path
|
|
521
|
+
// announces this strand's derived peerId to the relay first (delegate admission;
|
|
522
|
+
// see cadre-node.ts). Those circuit entries come either from a hand-written
|
|
523
|
+
// `network.listenAddrs` or from `network.relayAddrs`, which the resolution folds
|
|
524
|
+
// into the same list on the configured route this call takes (see above).
|
|
525
|
+
...addrOptions,
|
|
526
|
+
// The raw configured gater (open strands), or the revocation-composed
|
|
527
|
+
// gater plus the fail-closed per-stream revoked-peer gate (closed
|
|
528
|
+
// strands) — resolved above, before the try.
|
|
529
|
+
...revocationGateOptions
|
|
530
|
+
}) as Libp2pNodeWithRepo;
|
|
531
|
+
timing('[buildStrandRuntime:%s] createLibp2pNode: %dms', strandId, Math.round(performance.now() - t0));
|
|
532
|
+
|
|
533
|
+
instance.libp2pNode = node;
|
|
534
|
+
|
|
535
|
+
// Create and initialize the StrandDatabase.
|
|
536
|
+
//
|
|
537
|
+
// Attach before initialize so a failed init is cleaned up by
|
|
538
|
+
// releaseRuntime below (close() is safe on a partially-initialized db).
|
|
539
|
+
t0 = performance.now();
|
|
540
|
+
const strandDb = new StrandDatabase({
|
|
541
|
+
strandId,
|
|
542
|
+
sAppConfig,
|
|
543
|
+
libp2pNode: node,
|
|
544
|
+
coordinatedRepo: node.coordinatedRepo,
|
|
545
|
+
// Founder bootstrap inputs: the strand's type drives which membership rows
|
|
546
|
+
// are written, and the closed-strand MemberPrivateKey derives the founding
|
|
547
|
+
// Member/Manager key. Both come off the control-network strand row.
|
|
548
|
+
strandType: config.strandRow.Type,
|
|
549
|
+
memberPrivateKey: config.strandRow.MemberPrivateKey ?? undefined,
|
|
550
|
+
founder: config.founder
|
|
551
|
+
});
|
|
552
|
+
instance.database = strandDb;
|
|
553
|
+
await strandDb.initialize();
|
|
554
|
+
timing('[buildStrandRuntime:%s] strandDatabase.initialize: %dms', strandId, Math.round(performance.now() - t0));
|
|
555
|
+
|
|
556
|
+
// Arm the revoked-peer deny-set poll now that the database it reads
|
|
557
|
+
// exists. start() kicks an immediate refresh WITHOUT awaiting it —
|
|
558
|
+
// bring-up and resume are never blocked on a membership read; until the
|
|
559
|
+
// first read lands the empty snapshot admits everyone (fail-open).
|
|
560
|
+
revocationEnforcer?.start();
|
|
561
|
+
|
|
562
|
+
// Peer-join block catch-up: push this strand's own blocks to each newly
|
|
563
|
+
// connected peer, so a machine that joined after blocks were committed
|
|
564
|
+
// still ends up physically holding them (without per-strand storage there
|
|
565
|
+
// is nothing to copy). Armed for EVERY stored strand: `PeerJoinBackfill`
|
|
566
|
+
// only does work when the strand's libp2p node reports a peer connection,
|
|
567
|
+
// so on a device that is genuinely alone it is inert, and arming it at
|
|
568
|
+
// launch is what closes the "founded alone, never replicates" hole — a
|
|
569
|
+
// peer that joins later gets the founder's blocks without any relaunch.
|
|
570
|
+
// No `authorizePeer` gate, deliberately — see the module comment in
|
|
571
|
+
// peer-join-backfill.ts for the strand-side argument (and why the control
|
|
572
|
+
// network, which DOES gate, is different).
|
|
573
|
+
//
|
|
574
|
+
// NOTE: cost is one PeerJoinBackfill object + one `connection:open` listener
|
|
575
|
+
// per running strand — linear in strand count, negligible at the handful a
|
|
576
|
+
// device or host runs today. If a node ever hosts strands by the hundred,
|
|
577
|
+
// move to one shared listener that dispatches by strand id.
|
|
578
|
+
if (strandStorage && config.backfill?.enabled !== false) {
|
|
579
|
+
if (node.keyNetwork) {
|
|
580
|
+
const backfill = new PeerJoinBackfill({
|
|
581
|
+
label: strandId,
|
|
582
|
+
libp2p: node,
|
|
583
|
+
peerNetwork: node.keyNetwork,
|
|
584
|
+
storage: strandStorage,
|
|
585
|
+
// The same prefix the receiver registered its block-transfer handler
|
|
586
|
+
// under — derived from networkName above, never re-spelled here.
|
|
587
|
+
protocolPrefix
|
|
588
|
+
}, config.backfill);
|
|
589
|
+
backfill.start();
|
|
590
|
+
this.backfills.set(strandId, backfill);
|
|
591
|
+
} else {
|
|
592
|
+
log('Strand %s: libp2p node exposes no keyNetwork; peer-join block catch-up is inert', strandId);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
instance.status = 'active';
|
|
597
|
+
instance.lastActivity = new Date();
|
|
598
|
+
} catch (error) {
|
|
599
|
+
// Roll back any partially-attached runtime so the instance is left with
|
|
600
|
+
// NEITHER handle. Otherwise the `libp2pNode || database` "already live"
|
|
601
|
+
// guard in resumeStrand/handleStrandWake would treat a half-built strand
|
|
602
|
+
// as healthy — leaking the libp2p node and never retrying the rebuild.
|
|
603
|
+
await this.releaseRuntime(instance).catch((cleanupErr) => {
|
|
604
|
+
log('buildStrandRuntime cleanup for strand %s also failed: %o', strandId, cleanupErr);
|
|
605
|
+
});
|
|
606
|
+
throw error;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Release an instance's strand-network runtime: close the StrandDatabase, then
|
|
612
|
+
* stop the libp2p node (construction order in reverse), clearing both fields
|
|
613
|
+
* and zeroing connectedPeers. Tolerant of partially-built state — either handle
|
|
614
|
+
* may be absent — so it doubles as rollback for a failed `buildStrandRuntime`.
|
|
615
|
+
* Shared by `quiesceStrand`, `stopStrand`, and that rollback path.
|
|
616
|
+
*
|
|
617
|
+
* Leaves `strandStorages` untouched by design — the store outlives the runtime it
|
|
618
|
+
* was built into, which is what makes a resume warm. Only `stopStrand` disposes it.
|
|
619
|
+
*/
|
|
620
|
+
private async releaseRuntime(instance: StrandInstance): Promise<void> {
|
|
621
|
+
// Backfill first — before the database closes and the libp2p node stops — so
|
|
622
|
+
// no NEW catch-up push is issued against a torn-down transport. A push already
|
|
623
|
+
// in flight is not awaited; it fails into the module's own per-chunk catch.
|
|
624
|
+
const backfill = this.backfills.get(instance.strandId);
|
|
625
|
+
if (backfill) {
|
|
626
|
+
backfill.stop();
|
|
627
|
+
this.backfills.delete(instance.strandId);
|
|
628
|
+
}
|
|
629
|
+
// The revoked-peer enforcer goes with the runtime it gated: a resume
|
|
630
|
+
// rebuilds it with a fresh (initially empty, fail-open) snapshot.
|
|
631
|
+
const revocationEnforcer = this.revocationEnforcers.get(instance.strandId);
|
|
632
|
+
if (revocationEnforcer) {
|
|
633
|
+
revocationEnforcer.stop();
|
|
634
|
+
this.revocationEnforcers.delete(instance.strandId);
|
|
635
|
+
}
|
|
636
|
+
if (instance.database) {
|
|
637
|
+
await instance.database.close();
|
|
638
|
+
instance.database = undefined;
|
|
639
|
+
}
|
|
640
|
+
if (instance.libp2pNode) {
|
|
641
|
+
await instance.libp2pNode.stop();
|
|
642
|
+
instance.libp2pNode = undefined;
|
|
643
|
+
}
|
|
644
|
+
instance.connectedPeers = 0;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Drop the strand's owned store and release this strand's claim on its cache. The
|
|
649
|
+
* wrapper counts holders, so the cache is emptied and unregistered from the shared
|
|
650
|
+
* pool only if no other scope still holds it. Called only where the strand's whole
|
|
651
|
+
* lifetime ends (`stopStrand`, and the failed-launch rollback) — never on a quiesce.
|
|
652
|
+
*
|
|
653
|
+
* A dispose failure is logged, not thrown: the store is already unreferenced here,
|
|
654
|
+
* and failing the stop over a cache-bookkeeping error would leave the caller unable
|
|
655
|
+
* to tear the strand down. `disposeStorageCache` no-ops for an unwrapped store
|
|
656
|
+
* (e.g. `MemoryRawStorage`), so no instanceof test is needed.
|
|
657
|
+
*/
|
|
658
|
+
private async disposeStrandStorage(strandId: string): Promise<void> {
|
|
659
|
+
const storage = this.strandStorages.get(strandId);
|
|
660
|
+
if (!storage) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
this.strandStorages.delete(strandId);
|
|
664
|
+
try {
|
|
665
|
+
await disposeStorageCache(storage);
|
|
666
|
+
} catch (error) {
|
|
667
|
+
log('Failed to dispose storage cache for strand %s: %o', strandId, error);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Refresh a strand's revoked-peer deny set NOW — and, with it, run the
|
|
673
|
+
* teardown sweep that hangs up every connected revoked peer.
|
|
674
|
+
*
|
|
675
|
+
* The gate is otherwise poll-driven (default
|
|
676
|
+
* `DEFAULT_REVOCATION_POLL_INTERVAL_MS`), because neither a replicated
|
|
677
|
+
* revocation nor a local `revokeMember`/`leaveStrand` raises anything this
|
|
678
|
+
* runtime can hook. A caller that just wrote a revocation against the
|
|
679
|
+
* strand's database should follow it with this call, which makes the cut
|
|
680
|
+
* immediate instead of waiting out the interval.
|
|
681
|
+
*
|
|
682
|
+
* Quiet no-op — matching `quiesceStrand`'s posture — when the strand is not
|
|
683
|
+
* tracked, is quiesced, is open, or has the gate disabled: in every one of
|
|
684
|
+
* those cases there is no enforcer to refresh. Never rejects (the enforcer's
|
|
685
|
+
* refresh contains its own failures), and resolves only once the sweep has
|
|
686
|
+
* finished.
|
|
687
|
+
*/
|
|
688
|
+
async refreshRevocationEnforcement(strandId: string): Promise<void> {
|
|
689
|
+
const enforcer = this.revocationEnforcers.get(strandId);
|
|
690
|
+
if (!enforcer) {
|
|
691
|
+
log('refreshRevocationEnforcement: strand %s has no armed revocation enforcer', strandId);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
await enforcer.refresh();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Quiesce a strand: release its strand-network resources (stop the libp2p node,
|
|
699
|
+
* close the StrandDatabase) while RETAINING the instance record — identity,
|
|
700
|
+
* sAppInfo, keys, latency hint, metadata — and its launch config so it can be
|
|
701
|
+
* resumed later. Mechanically this is `stopStrand` minus the instance/config
|
|
702
|
+
* deletion. The caller sets the post-quiesce status (e.g. `hibernating`).
|
|
703
|
+
* No-ops when the strand is missing or already quiesced.
|
|
704
|
+
*/
|
|
705
|
+
async quiesceStrand(strandId: string): Promise<void> {
|
|
706
|
+
const instance = this.instances.get(strandId);
|
|
707
|
+
if (!instance) {
|
|
708
|
+
log('quiesceStrand: strand %s not found', strandId);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
if (!instance.libp2pNode && !instance.database) {
|
|
712
|
+
log('quiesceStrand: strand %s already quiesced', strandId);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
log('Quiescing strand instance: %s', strandId);
|
|
717
|
+
await this.releaseRuntime(instance);
|
|
718
|
+
log('Strand %s quiesced (resources released, instance retained)', strandId);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Resume a previously-quiesced strand: rebuild its libp2p node + StrandDatabase
|
|
723
|
+
* from the retained launch config and re-attach them, transitioning it back to
|
|
724
|
+
* `active`. `overrides` re-applies volatile inputs that may have changed since
|
|
725
|
+
* launch (the cohort `bootstrapNodes` seed and the strand's `servingMachines`
|
|
726
|
+
* count) and updates the retained config so a later resume reuses the latest
|
|
727
|
+
* values. Returns the live instance unchanged if it is already running.
|
|
728
|
+
*/
|
|
729
|
+
async resumeStrand(strandId: string, overrides?: ResumeStrandOverrides): Promise<StrandInstance> {
|
|
730
|
+
if (this.stopping) {
|
|
731
|
+
throw new Error('StrandInstanceManager is stopping');
|
|
732
|
+
}
|
|
733
|
+
const instance = this.instances.get(strandId);
|
|
734
|
+
if (!instance) {
|
|
735
|
+
throw new Error(`Cannot resume strand ${strandId}: not tracked`);
|
|
736
|
+
}
|
|
737
|
+
const launchConfig = this.launchConfigs.get(strandId);
|
|
738
|
+
if (!launchConfig) {
|
|
739
|
+
throw new Error(`Cannot resume strand ${strandId}: no retained launch config`);
|
|
740
|
+
}
|
|
741
|
+
if (instance.libp2pNode || instance.database) {
|
|
742
|
+
log('resumeStrand: strand %s already live', strandId);
|
|
743
|
+
return instance;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
log('Resuming strand instance: %s', strandId);
|
|
747
|
+
const tTotal = performance.now();
|
|
748
|
+
|
|
749
|
+
// Re-apply volatile inputs and persist them so a subsequent resume reuses them.
|
|
750
|
+
// Each `??` matters: a resume that passes no override must keep the retained value,
|
|
751
|
+
// and a resume that passes one must leave it retained for the next resume — otherwise
|
|
752
|
+
// a later no-override wake silently reverts to whatever launch time saw.
|
|
753
|
+
const resumeConfig: StartStrandConfig = {
|
|
754
|
+
...launchConfig,
|
|
755
|
+
bootstrapNodes: overrides?.bootstrapNodes ?? launchConfig.bootstrapNodes,
|
|
756
|
+
// NOTE: `??` retains, so an override can raise or lower the count but cannot CLEAR it
|
|
757
|
+
// back to "this node no longer knows" — the direction that would declare nothing. Moot
|
|
758
|
+
// while nothing feeds `servingMachines` at all; if a source lands
|
|
759
|
+
// (`backlog/feat-strand-yardstick-from-serving-machines`) that can legitimately lose the
|
|
760
|
+
// count — a strand whose member rows became unreadable — this merge must gain an explicit
|
|
761
|
+
// clear rather than silently declaring a stale number over a serving set it can no longer
|
|
762
|
+
// see. Same applies to `bootstrapNodes` above, where a stale seed is harmless.
|
|
763
|
+
servingMachines: overrides?.servingMachines ?? launchConfig.servingMachines
|
|
764
|
+
};
|
|
765
|
+
this.launchConfigs.set(strandId, resumeConfig);
|
|
766
|
+
|
|
767
|
+
instance.status = 'starting';
|
|
768
|
+
try {
|
|
769
|
+
await this.buildStrandRuntime(instance, resumeConfig);
|
|
770
|
+
timing('[resumeStrand:%s] total: %dms', strandId, Math.round(performance.now() - tTotal));
|
|
771
|
+
log('Strand %s resumed successfully', strandId);
|
|
772
|
+
return instance;
|
|
773
|
+
} catch (error) {
|
|
774
|
+
instance.status = 'error';
|
|
775
|
+
instance.error = error instanceof Error ? error.message : String(error);
|
|
776
|
+
log('Failed to resume strand %s: %s', strandId, instance.error);
|
|
777
|
+
throw error;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Honor a founder request against an ALREADY-TRACKED strand — the seam that
|
|
783
|
+
* closes the "whoever launches first decides whether the bootstrap runs" gap:
|
|
784
|
+
* an instance first launched as a joiner (an app's own attach, or a watcher
|
|
785
|
+
* poll winning the launch race) used to swallow a later founder request
|
|
786
|
+
* silently, leaving the strand active with no `Strand.Header`.
|
|
787
|
+
*
|
|
788
|
+
* Flips the RETAINED launch config's `founder` to true, so every later
|
|
789
|
+
* quiesce → resume rebuild founds as well (the bootstrap is insert-if-absent —
|
|
790
|
+
* {@link StrandDatabase.ensureFounderBootstrap} — so re-running it per rebuild
|
|
791
|
+
* writes nothing twice), and runs the bootstrap against the live database now.
|
|
792
|
+
*
|
|
793
|
+
* @returns how the request resolved:
|
|
794
|
+
* - `'already-founder'` — the retained config already founds; nothing to do.
|
|
795
|
+
* - `'bootstrapped'` — config flipped and the live database ran the bootstrap.
|
|
796
|
+
* - `'needs-resume'` — config flipped, but the instance is quiesced (no live
|
|
797
|
+
* database), so the bootstrap could not run here: the CALLER must wake the
|
|
798
|
+
* strand (`CadreNode.wakeStrand`, which owns the hibernation bookkeeping this
|
|
799
|
+
* manager does not) so the rebuild — which now founds — runs it.
|
|
800
|
+
* @throws when the strand is not tracked — this seam exists only for the
|
|
801
|
+
* tracked-instance launch path; an untracked id is a caller bug.
|
|
802
|
+
*/
|
|
803
|
+
async foundExistingStrand(strandId: string): Promise<'already-founder' | 'bootstrapped' | 'needs-resume'> {
|
|
804
|
+
const instance = this.instances.get(strandId);
|
|
805
|
+
const config = this.launchConfigs.get(strandId);
|
|
806
|
+
if (!instance || !config) {
|
|
807
|
+
throw new Error(`Cannot found strand ${strandId}: not tracked`);
|
|
808
|
+
}
|
|
809
|
+
if (config.founder === true) {
|
|
810
|
+
return 'already-founder';
|
|
811
|
+
}
|
|
812
|
+
// A fresh object rather than mutating in place: startStrand retains the CALLER'S
|
|
813
|
+
// config object, which is not ours to rewrite.
|
|
814
|
+
this.launchConfigs.set(strandId, { ...config, founder: true });
|
|
815
|
+
if (!instance.database) {
|
|
816
|
+
return 'needs-resume';
|
|
817
|
+
}
|
|
818
|
+
await this.ensureFounderBootstrap(strandId);
|
|
819
|
+
return 'bootstrapped';
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Run the (idempotent) founder bootstrap against a tracked strand's LIVE
|
|
824
|
+
* database, independently of what the retained launch config says.
|
|
825
|
+
*
|
|
826
|
+
* Separate from {@link foundExistingStrand} because a caller that resolved
|
|
827
|
+
* `'needs-resume'` and woke the strand must not assume the wake's own rebuild
|
|
828
|
+
* founded it: `HibernationManager` COALESCES wakes, so a wake already in flight
|
|
829
|
+
* when the config flipped had already read the PRE-flip config and rebuilt as a
|
|
830
|
+
* joiner. Re-running the bootstrap costs one insert-if-absent probe per table
|
|
831
|
+
* and is the only thing that makes "founding resolves once the Header is
|
|
832
|
+
* written" true on that path.
|
|
833
|
+
*
|
|
834
|
+
* @throws when the strand is not tracked, or is still quiesced (no live
|
|
835
|
+
* database) — both mean the bootstrap did NOT run, which a founder request
|
|
836
|
+
* must never swallow.
|
|
837
|
+
*/
|
|
838
|
+
async ensureFounderBootstrap(strandId: string): Promise<void> {
|
|
839
|
+
const instance = this.instances.get(strandId);
|
|
840
|
+
if (!instance) {
|
|
841
|
+
throw new Error(`Cannot run the founder bootstrap for strand ${strandId}: not tracked`);
|
|
842
|
+
}
|
|
843
|
+
if (!instance.database) {
|
|
844
|
+
throw new Error(
|
|
845
|
+
`Cannot run the founder bootstrap for strand ${strandId}: it is quiesced, so there ` +
|
|
846
|
+
'is no live database to write to — resume it first.'
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
await instance.database.ensureFounderBootstrap();
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Stop a strand instance
|
|
854
|
+
*/
|
|
855
|
+
async stopStrand(strandId: string): Promise<void> {
|
|
856
|
+
const instance = this.instances.get(strandId);
|
|
857
|
+
if (!instance) {
|
|
858
|
+
log('Strand %s not found', strandId);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
log('Stopping strand instance: %s', strandId);
|
|
863
|
+
instance.status = 'stopping';
|
|
864
|
+
|
|
865
|
+
try {
|
|
866
|
+
await this.releaseRuntime(instance);
|
|
867
|
+
instance.status = 'stopped';
|
|
868
|
+
this.instances.delete(strandId);
|
|
869
|
+
this.launchConfigs.delete(strandId);
|
|
870
|
+
// Storage is released only here — NOT in releaseRuntime, which a quiesce shares.
|
|
871
|
+
await this.disposeStrandStorage(strandId);
|
|
872
|
+
log('Strand %s stopped successfully', strandId);
|
|
873
|
+
} catch (error) {
|
|
874
|
+
instance.status = 'error';
|
|
875
|
+
instance.error = error instanceof Error ? error.message : String(error);
|
|
876
|
+
log('Error stopping strand %s: %s', strandId, instance.error);
|
|
877
|
+
throw error;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Stop all strand instances
|
|
883
|
+
*/
|
|
884
|
+
async stopAll(): Promise<void> {
|
|
885
|
+
this.stopping = true;
|
|
886
|
+
log('Stopping all strand instances (%d)', this.instances.size);
|
|
887
|
+
|
|
888
|
+
const stopPromises = Array.from(this.instances.keys()).map(id =>
|
|
889
|
+
this.stopStrand(id).catch(err => {
|
|
890
|
+
log('Error stopping strand %s during shutdown: %s', id, err);
|
|
891
|
+
})
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
await Promise.all(stopPromises);
|
|
895
|
+
this.stopping = false;
|
|
896
|
+
log('All strand instances stopped');
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|