@serfab/cadre-core 1.7.0 → 1.8.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 +8 -6
- package/dist/cadre-node.d.ts +386 -114
- package/dist/cadre-node.js +887 -359
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +2 -2
- package/dist/control-authorization.js +1 -0
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +6 -0
- package/dist/control-cohort.js +6 -0
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +46 -8
- package/dist/control-database.js +97 -18
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +37 -14
- package/dist/control-formation-recorder.js +60 -10
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-read-retry.d.ts +13 -12
- package/dist/control-read-retry.js +14 -12
- package/dist/control-read-retry.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +67 -5
- package/dist/control-schema.js.map +1 -1
- package/dist/control-stream.d.ts +9 -1
- package/dist/control-stream.js +24 -1
- package/dist/control-stream.js.map +1 -1
- package/dist/control-write-retry.d.ts +12 -2
- package/dist/control-write-retry.js +13 -2
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +11 -8
- package/dist/delegate-admission.js +19 -11
- package/dist/delegate-admission.js.map +1 -1
- package/dist/diagnostics/connection-path.d.ts +0 -7
- package/dist/diagnostics/connection-path.js +0 -7
- package/dist/diagnostics/connection-path.js.map +1 -1
- package/dist/formation-approval.d.ts +7 -0
- package/dist/formation-approval.js +9 -3
- package/dist/formation-approval.js.map +1 -1
- package/dist/fs-atomic.d.ts +11 -2
- package/dist/fs-atomic.js +13 -3
- package/dist/fs-atomic.js.map +1 -1
- package/dist/hibernation-manager.d.ts +111 -8
- package/dist/hibernation-manager.js +234 -49
- package/dist/hibernation-manager.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.js +15 -9
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +90 -25
- package/dist/joined-strand-store.js +223 -55
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store-file.js +14 -5
- package/dist/key-store-file.js.map +1 -1
- package/dist/link-budget.d.ts +367 -69
- package/dist/link-budget.js +375 -72
- package/dist/link-budget.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +30 -43
- package/dist/membership-connection-gater.js +31 -42
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/peer-dial.d.ts +24 -4
- package/dist/peer-dial.js +74 -5
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +2 -1
- package/dist/peer-join-backfill.js +6 -5
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/push-fanout.d.ts +6 -0
- package/dist/push-fanout.js +6 -0
- package/dist/push-fanout.js.map +1 -1
- package/dist/relay-reservation.d.ts +3 -1
- package/dist/relay-reservation.js +3 -1
- package/dist/relay-reservation.js.map +1 -1
- package/dist/relay-server.d.ts +79 -0
- package/dist/relay-server.js +74 -0
- package/dist/relay-server.js.map +1 -0
- package/dist/seed-bootstrap.d.ts +57 -4
- package/dist/seed-bootstrap.js +107 -83
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/storage-scope.d.ts +33 -17
- package/dist/storage-scope.js +44 -23
- package/dist/storage-scope.js.map +1 -1
- package/dist/strand-addr-protocol.d.ts +58 -21
- package/dist/strand-addr-protocol.js +133 -85
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-database.d.ts +20 -2
- package/dist/strand-database.js +42 -5
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +59 -21
- package/dist/strand-first-sync-gate.js +58 -19
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +151 -0
- package/dist/strand-formation-deadlines.js +171 -0
- package/dist/strand-formation-deadlines.js.map +1 -0
- package/dist/strand-formation-manager.d.ts +63 -30
- package/dist/strand-formation-manager.js +67 -41
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +45 -35
- package/dist/strand-formation-protocol.js +59 -122
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +113 -6
- package/dist/strand-instance-manager.js +257 -41
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +6 -0
- package/dist/strand-membership-writer.js +11 -0
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-peer-book-protocol.d.ts +13 -4
- package/dist/strand-peer-book-protocol.js +3 -7
- package/dist/strand-peer-book-protocol.js.map +1 -1
- package/dist/strand-peer-book.d.ts +1 -1
- package/dist/strand-peer-book.js +8 -1
- package/dist/strand-peer-book.js.map +1 -1
- package/dist/strand-peer-observer.js +1 -1
- package/dist/strand-revocation-enforcer.js +2 -1
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +36 -20
- package/dist/strand-solicitation.js +4 -4
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-wake-protocol.d.ts +80 -23
- package/dist/strand-wake-protocol.js +89 -64
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/types.d.ts +128 -38
- package/dist/types.js +4 -3
- package/dist/types.js.map +1 -1
- package/package.json +7 -7
- package/src/cadre-node.ts +8367 -7776
- package/src/control-authorization.ts +2 -1
- package/src/control-cohort.ts +6 -0
- package/src/control-database.ts +122 -18
- package/src/control-formation-recorder.ts +354 -305
- package/src/control-read-retry.ts +14 -12
- package/src/control-schema.ts +67 -5
- package/src/control-stream.ts +25 -1
- package/src/control-write-retry.ts +13 -2
- package/src/delegate-admission.ts +23 -13
- package/src/diagnostics/connection-path.ts +0 -7
- package/src/formation-approval.ts +610 -604
- package/src/fs-atomic.ts +14 -4
- package/src/hibernation-manager.ts +638 -437
- package/src/index.ts +28 -10
- package/src/joined-strand-store.ts +247 -57
- package/src/key-store-file.ts +14 -5
- package/src/link-budget.ts +393 -74
- package/src/membership-connection-gater.ts +31 -45
- package/src/peer-dial.ts +81 -7
- package/src/peer-join-backfill.ts +8 -6
- package/src/push-fanout.ts +6 -0
- package/src/relay-reservation.ts +3 -1
- package/src/relay-server.ts +98 -0
- package/src/seed-bootstrap.ts +133 -91
- package/src/storage-scope.ts +44 -23
- package/src/strand-addr-protocol.ts +547 -458
- package/src/strand-database.ts +287 -242
- package/src/strand-first-sync-gate.ts +58 -19
- package/src/strand-formation-deadlines.ts +227 -0
- package/src/strand-formation-manager.ts +700 -647
- package/src/strand-formation-protocol.ts +103 -150
- package/src/strand-instance-manager.ts +287 -44
- package/src/strand-membership-writer.ts +12 -0
- package/src/strand-peer-book-protocol.ts +16 -13
- package/src/strand-peer-book.ts +8 -1
- package/src/strand-peer-observer.ts +1 -1
- package/src/strand-revocation-enforcer.ts +2 -1
- package/src/strand-solicitation.ts +519 -500
- package/src/strand-wake-protocol.ts +401 -355
- package/src/types.ts +130 -38
|
@@ -43,8 +43,9 @@ import type {
|
|
|
43
43
|
} from './types.js';
|
|
44
44
|
import { DEFAULT_CONNECTION_MONITOR, resolveStrandClusterSize, strandClusterPolicy } from './types.js';
|
|
45
45
|
import { strandNodeAddrs } from './strand-network-config.js';
|
|
46
|
+
import { resolveRelayServer } from './relay-server.js';
|
|
46
47
|
import { superviseRelayReservation, type RelayReservationSupervisor } from './relay-reservation.js';
|
|
47
|
-
import {
|
|
48
|
+
import { declaredCohortReadDeadlineMs, peerJoinPushBudget, relayReservationBudgetMs, resolveLinkRoundTripMs } from './link-budget.js';
|
|
48
49
|
|
|
49
50
|
const log = debug('sereus:cadre:strand-manager');
|
|
50
51
|
const timing = debug('sereus:cadre:timing');
|
|
@@ -89,8 +90,15 @@ export function liveStrandStatus(instance: StrandInstance): 'active' | 'syncing'
|
|
|
89
90
|
*/
|
|
90
91
|
export interface StartStrandConfig {
|
|
91
92
|
strandRow: StrandRow;
|
|
92
|
-
/**
|
|
93
|
-
|
|
93
|
+
/**
|
|
94
|
+
* sApp configuration provided by the hosting application. Absent for a **storage
|
|
95
|
+
* replica** — a strand this node stores and serves without the app installed
|
|
96
|
+
* (`CadreNodeConfig.hostUnclaimedStrands`): no schema signature check, no `App`
|
|
97
|
+
* schema, no `sAppInfo` on the instance, and the default latency hint. A replica is
|
|
98
|
+
* always a joiner; `StrandDatabase` refuses to found one. An app that later claims the
|
|
99
|
+
* strand upgrades the running replica in place ({@link StrandInstanceManager.attachSApp}).
|
|
100
|
+
*/
|
|
101
|
+
sAppConfig?: SAppConfig;
|
|
94
102
|
storage?: StorageConfig;
|
|
95
103
|
network?: NetworkConfig;
|
|
96
104
|
profile: NodeProfile;
|
|
@@ -311,6 +319,11 @@ export interface ResumeStrandOverrides {
|
|
|
311
319
|
servingMachines?: number;
|
|
312
320
|
}
|
|
313
321
|
|
|
322
|
+
/** A {@link StrandInstanceManager.resumeStrand} or {@link StrandInstanceManager.quiesceStrand} of one strand. */
|
|
323
|
+
type RuntimeTransition =
|
|
324
|
+
| { kind: 'resume'; operation: Promise<StrandInstance> }
|
|
325
|
+
| { kind: 'quiesce'; operation: Promise<void> };
|
|
326
|
+
|
|
314
327
|
/**
|
|
315
328
|
* Get the isolated storage path for a specific strand.
|
|
316
329
|
*
|
|
@@ -374,6 +387,26 @@ function resolveStrandStorage(
|
|
|
374
387
|
return wrapStorageWithCache(storage, strandId);
|
|
375
388
|
}
|
|
376
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Verify an sApp's schema signature (fail-closed unless `requireSignedSchemas` is
|
|
392
|
+
* false) and project the config onto the instance's `sAppInfo`.
|
|
393
|
+
*/
|
|
394
|
+
function verifiedSAppInfo(strandId: string, sAppConfig: SAppConfig, requireSignedSchemas: boolean | undefined): SAppInfo {
|
|
395
|
+
assertSchemaSignature(sAppConfig, { requireSignature: requireSignedSchemas ?? true });
|
|
396
|
+
log('Strand %s sApp schema signature verified (author: %s)', strandId, sAppConfig.id);
|
|
397
|
+
return {
|
|
398
|
+
id: sAppConfig.id,
|
|
399
|
+
version: sAppConfig.version,
|
|
400
|
+
schema: sAppConfig.schema,
|
|
401
|
+
signature: sAppConfig.signature
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Log wording for what a launch runs: the sApp and its version, or a storage replica. */
|
|
406
|
+
function describeSApp(sAppConfig: SAppConfig | undefined): string {
|
|
407
|
+
return sAppConfig ? `sApp: ${sAppConfig.id} v${sAppConfig.version}` : 'storage replica';
|
|
408
|
+
}
|
|
409
|
+
|
|
377
410
|
/**
|
|
378
411
|
* Manages individual strand instances - creates and destroys isolated libp2p nodes
|
|
379
412
|
* for each strand the cadre participates in.
|
|
@@ -479,6 +512,23 @@ export class StrandInstanceManager {
|
|
|
479
512
|
* wake becomes the better trade.
|
|
480
513
|
*/
|
|
481
514
|
private strandStorages: Map<string, IRawStorage> = new Map();
|
|
515
|
+
/**
|
|
516
|
+
* The runtime build in flight per strand id (`buildStrandRuntime`, from `startStrand` or
|
|
517
|
+
* `resumeStrand`), present until it settles. The instance is tracked for the whole build,
|
|
518
|
+
* with its database not yet constructed or not yet initialized, so a caller that must act
|
|
519
|
+
* on the FINISHED runtime ({@link attachSApp}, {@link whenRuntimeBuilt}) waits on this instead. A failed build's
|
|
520
|
+
* rejection belongs to the call that started it.
|
|
521
|
+
*/
|
|
522
|
+
private runtimeBuilds: Map<string, Promise<void>> = new Map();
|
|
523
|
+
/**
|
|
524
|
+
* The latest resume or quiesce requested per strand id, present until it settles. Each one
|
|
525
|
+
* starts only once the one requested before it has settled, so a wake that lands during a
|
|
526
|
+
* quiesce rebuilds after it, and a quiesce requested during a rebuild releases what that
|
|
527
|
+
* rebuild finished. The WHOLE operation, not its entry in {@link runtimeBuilds}: that settles
|
|
528
|
+
* before a resume's own catch records `'error'`, so a joiner awaiting it could read a stale
|
|
529
|
+
* status.
|
|
530
|
+
*/
|
|
531
|
+
private transitions: Map<string, RuntimeTransition> = new Map();
|
|
482
532
|
private stopping = false;
|
|
483
533
|
|
|
484
534
|
constructor() {
|
|
@@ -543,7 +593,7 @@ export class StrandInstanceManager {
|
|
|
543
593
|
return this.instances.get(strandId)!;
|
|
544
594
|
}
|
|
545
595
|
|
|
546
|
-
log('Starting strand instance: %s (
|
|
596
|
+
log('Starting strand instance: %s (%s)', strandId, describeSApp(sAppConfig));
|
|
547
597
|
const tTotal = performance.now();
|
|
548
598
|
|
|
549
599
|
// The id becomes two names below: the storage scope key the embedder's provider
|
|
@@ -555,18 +605,7 @@ export class StrandInstanceManager {
|
|
|
555
605
|
// still reaches buildStrandRuntime and still mints the protocol prefix.
|
|
556
606
|
assertStrandScopeKey(strandId);
|
|
557
607
|
|
|
558
|
-
|
|
559
|
-
const requireSignature = config.requireSignedSchemas ?? true;
|
|
560
|
-
assertSchemaSignature(sAppConfig, { requireSignature });
|
|
561
|
-
log('Strand %s sApp schema signature verified (author: %s)', strandId, sAppConfig.id);
|
|
562
|
-
|
|
563
|
-
// Convert SAppConfig to SAppInfo for the instance
|
|
564
|
-
const sAppInfo: SAppInfo = {
|
|
565
|
-
id: sAppConfig.id,
|
|
566
|
-
version: sAppConfig.version,
|
|
567
|
-
schema: sAppConfig.schema,
|
|
568
|
-
signature: sAppConfig.signature
|
|
569
|
-
};
|
|
608
|
+
const sAppInfo = sAppConfig ? verifiedSAppInfo(strandId, sAppConfig, config.requireSignedSchemas) : undefined;
|
|
570
609
|
|
|
571
610
|
// Resolve this strand's storage ONCE, before anything is recorded. If a factory
|
|
572
611
|
// function is provided, it is called with the strandId to create strand-specific
|
|
@@ -578,7 +617,7 @@ export class StrandInstanceManager {
|
|
|
578
617
|
}
|
|
579
618
|
|
|
580
619
|
// Determine latency hint: sApp config > default
|
|
581
|
-
const latencyHint = sAppConfig
|
|
620
|
+
const latencyHint = sAppConfig?.latencyHint ?? config.defaultLatencyHint;
|
|
582
621
|
|
|
583
622
|
const instance: StrandInstance = {
|
|
584
623
|
strandId,
|
|
@@ -597,9 +636,9 @@ export class StrandInstanceManager {
|
|
|
597
636
|
}
|
|
598
637
|
|
|
599
638
|
try {
|
|
600
|
-
await this.buildStrandRuntime(instance, config);
|
|
639
|
+
await this.trackRuntimeBuild(strandId, this.buildStrandRuntime(instance, config));
|
|
601
640
|
timing('[startStrand:%s] total: %dms', strandId, Math.round(performance.now() - tTotal));
|
|
602
|
-
log('Strand %s started successfully
|
|
641
|
+
log('Strand %s started successfully (%s)', strandId, describeSApp(sAppConfig));
|
|
603
642
|
return instance;
|
|
604
643
|
} catch (error) {
|
|
605
644
|
// Status/error first — the (now discarded) record is still what `log` reports on.
|
|
@@ -618,6 +657,35 @@ export class StrandInstanceManager {
|
|
|
618
657
|
}
|
|
619
658
|
}
|
|
620
659
|
|
|
660
|
+
/** Hold `build` in {@link runtimeBuilds} until it settles, and settle as it does. */
|
|
661
|
+
private async trackRuntimeBuild(strandId: string, build: Promise<void>): Promise<void> {
|
|
662
|
+
this.runtimeBuilds.set(strandId, build);
|
|
663
|
+
try {
|
|
664
|
+
await build;
|
|
665
|
+
} finally {
|
|
666
|
+
if (this.runtimeBuilds.get(strandId) === build) {
|
|
667
|
+
this.runtimeBuilds.delete(strandId);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Wait until no runtime build of `strandId` is in flight. Never rejects: a failed build
|
|
674
|
+
* is reported by the call that started it, and the caller re-reads the instance after.
|
|
675
|
+
*
|
|
676
|
+
* NOTE: after a failed launch the re-read sees the instance gone only because
|
|
677
|
+
* `startStrand`'s catch deletes it synchronously, and its continuation is queued before
|
|
678
|
+
* this waiter's. If that cleanup ever awaits before `instances.delete`, a waiter can act
|
|
679
|
+
* on the dying record; move the cleanup inside the tracked promise then.
|
|
680
|
+
*/
|
|
681
|
+
private async settleRuntimeBuilds(strandId: string): Promise<void> {
|
|
682
|
+
for (let build = this.runtimeBuilds.get(strandId); build; build = this.runtimeBuilds.get(strandId)) {
|
|
683
|
+
await build.catch((error: unknown) => {
|
|
684
|
+
log('Strand %s: the runtime build waited on failed (reported by its launcher): %o', strandId, error);
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
621
689
|
/**
|
|
622
690
|
* Build (or rebuild) the libp2p node + StrandDatabase for an instance and
|
|
623
691
|
* attach them, transitioning it to `active`. Shared by `startStrand` (fresh
|
|
@@ -649,9 +717,9 @@ export class StrandInstanceManager {
|
|
|
649
717
|
const networkName = `strand-${strandId}`;
|
|
650
718
|
const protocolPrefix = `/optimystic/${networkName}`;
|
|
651
719
|
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
const
|
|
720
|
+
// Whether this node runs the circuit-relay server, and its init — the same
|
|
721
|
+
// resolution the control node takes from the same config (`relay-server.ts`).
|
|
722
|
+
const relayServer = resolveRelayServer(config.network, config.profile);
|
|
655
723
|
// The strand-node VIEW of the machine's one `NetworkConfig`, not the control
|
|
656
724
|
// node's resolution: fixed direct listen ports become ephemeral (two nodes cannot
|
|
657
725
|
// bind one port), the announce config is dropped (it names the control node's
|
|
@@ -725,7 +793,12 @@ export class StrandInstanceManager {
|
|
|
725
793
|
networkName,
|
|
726
794
|
storage: strandStorage,
|
|
727
795
|
fretProfile: config.profile === 'storage' ? 'core' : 'edge',
|
|
728
|
-
relay:
|
|
796
|
+
relay: relayServer.enabled,
|
|
797
|
+
// NOTE: unlike the control node, a strand node has no unauthorized-reservation budget,
|
|
798
|
+
// so with the per-connection cap off any peer that reaches this node can hold one of
|
|
799
|
+
// `maxReservations` slots and forward uncapped through it. If strand relays are ever
|
|
800
|
+
// abused for bandwidth, give strand nodes a budget or a capped init of their own.
|
|
801
|
+
...(relayServer.enabled && { relayServerInit: relayServer.init }),
|
|
729
802
|
clusterSize: strandClusterSize,
|
|
730
803
|
// Deliberately NOT CONTROL_CLUSTER_POLICY: a strand is application data with its own
|
|
731
804
|
// breadth reasoning, and the shape match with the control policy is a coincidence.
|
|
@@ -737,11 +810,12 @@ export class StrandInstanceManager {
|
|
|
737
810
|
// returns the frozen STRAND_CLUSTER_POLICY itself, declaring nothing. Resolved
|
|
738
811
|
// HERE rather than at `startStrand`, so a wake from hibernation would pick up a
|
|
739
812
|
// serving set that changed while the strand slept.
|
|
740
|
-
// The read deadline is the same
|
|
741
|
-
// networks ride one link
|
|
813
|
+
// The read deadline is settled by the same helper the control node uses, because the
|
|
814
|
+
// two networks ride one link: the host's own, else derived from its declared
|
|
815
|
+
// `linkRoundTripMs`, else nothing, and the base policy's COHORT_READ_DEADLINE_MS stands.
|
|
742
816
|
clusterPolicy: strandClusterPolicy(strandClusterSize, {
|
|
743
817
|
servingMachines: config.servingMachines,
|
|
744
|
-
cohortQueryTimeoutMs: config.network
|
|
818
|
+
cohortQueryTimeoutMs: declaredCohortReadDeadlineMs(config.network)
|
|
745
819
|
}),
|
|
746
820
|
arachnode: {
|
|
747
821
|
enableRingZulu: config.profile === 'storage'
|
|
@@ -754,10 +828,10 @@ export class StrandInstanceManager {
|
|
|
754
828
|
// widened ping deadline has to reach the strand nodes too (see
|
|
755
829
|
// DEFAULT_CONNECTION_MONITOR).
|
|
756
830
|
connectionMonitor: config.network?.connectionMonitor ?? DEFAULT_CONNECTION_MONITOR,
|
|
757
|
-
// The same declared
|
|
758
|
-
//
|
|
759
|
-
// `
|
|
760
|
-
|
|
831
|
+
// The same declared link the control node states, and for the same reason: a strand
|
|
832
|
+
// node is also the listener for every other member's strand node, and Optimystic
|
|
833
|
+
// derives that listener's limit from this (`link-budget.ts`).
|
|
834
|
+
linkRoundTripMs: resolveLinkRoundTripMs(config.network?.linkRoundTripMs),
|
|
761
835
|
// Listen entries plus the WebSocket transport switch they imply — a strand node
|
|
762
836
|
// announces nothing the operator configured (`strand-network-config.ts`), and
|
|
763
837
|
// spreads AFTER `transports` above because the switch is a no-op whenever the
|
|
@@ -973,7 +1047,7 @@ export class StrandInstanceManager {
|
|
|
973
1047
|
// failing here would only trade that for `StrandWatcher`'s full-rebuild retry.
|
|
974
1048
|
//
|
|
975
1049
|
// NOTE: a relay that is down costs this launch one full drive — the reservation budget
|
|
976
|
-
// counted from the declared link round trip,
|
|
1050
|
+
// counted from the declared link round trip, 18 s at its default (`link-budget.ts`) — and
|
|
977
1051
|
// `StrandWatcher` launches strands one at a time, so N strands cost N of those in
|
|
978
1052
|
// bring-up during a relay outage, and MORE on a host that declared a slower link. If that
|
|
979
1053
|
// ever matters, stop awaiting here (the circuit addr then lands after `active`) rather
|
|
@@ -982,8 +1056,9 @@ export class StrandInstanceManager {
|
|
|
982
1056
|
await this.awaitFirstRelayAttempts(strandId);
|
|
983
1057
|
timing('[buildStrandRuntime:%s] relay first attempts: %dms', strandId, Math.round(performance.now() - t0));
|
|
984
1058
|
|
|
1059
|
+
// No `lastActivity` stamp: bringing a runtime up is not activity. A check-in marks
|
|
1060
|
+
// activity before its resume and must see only what landed while the strand was up.
|
|
985
1061
|
instance.status = liveStrandStatus(instance);
|
|
986
|
-
instance.lastActivity = new Date();
|
|
987
1062
|
if (instance.status === 'syncing') {
|
|
988
1063
|
log('Strand %s launched as a joiner with no Strand.Header held yet — writes are withheld until ' +
|
|
989
1064
|
'a member of the strand is reached', strandId);
|
|
@@ -1257,24 +1332,54 @@ export class StrandInstanceManager {
|
|
|
1257
1332
|
* Quiesce a strand: release its strand-network resources (stop the libp2p node,
|
|
1258
1333
|
* close the StrandDatabase) while RETAINING the instance record — identity,
|
|
1259
1334
|
* sAppInfo, keys, latency hint, metadata — and its launch config so it can be
|
|
1260
|
-
* resumed later
|
|
1261
|
-
* deletion.
|
|
1262
|
-
*
|
|
1335
|
+
* resumed later, then mark it `hibernating`. Mechanically this is `stopStrand` minus
|
|
1336
|
+
* the instance/config deletion. A tracked strand with no runtime left (a failed
|
|
1337
|
+
* rebuild already released it) is only marked; an untracked one is left alone.
|
|
1338
|
+
*
|
|
1339
|
+
* Ordered with {@link resumeStrand} (see {@link transitions}): it starts once the resume
|
|
1340
|
+
* or launch build before it has settled, so it releases a finished runtime, and the
|
|
1341
|
+
* status write is its last step, so a resume requested meanwhile writes after it. A call
|
|
1342
|
+
* made while another quiesce is the latest request joins it. A failed release leaves the
|
|
1343
|
+
* status as it was and rejects.
|
|
1263
1344
|
*/
|
|
1264
1345
|
async quiesceStrand(strandId: string): Promise<void> {
|
|
1346
|
+
const previous = this.transitions.get(strandId);
|
|
1347
|
+
if (previous?.kind === 'quiesce') {
|
|
1348
|
+
log('quiesceStrand: strand %s — joining the quiesce already requested', strandId);
|
|
1349
|
+
return previous.operation;
|
|
1350
|
+
}
|
|
1351
|
+
const operation = this.runQuiesce(strandId, previous);
|
|
1352
|
+
this.holdTransition(strandId, { kind: 'quiesce', operation });
|
|
1353
|
+
return operation;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/** Body of {@link quiesceStrand}. */
|
|
1357
|
+
private async runQuiesce(strandId: string, previous: RuntimeTransition | undefined): Promise<void> {
|
|
1358
|
+
await this.settleTransition(strandId, previous);
|
|
1359
|
+
await this.settleRuntimeBuilds(strandId);
|
|
1265
1360
|
const instance = this.instances.get(strandId);
|
|
1266
1361
|
if (!instance) {
|
|
1267
1362
|
log('quiesceStrand: strand %s not found', strandId);
|
|
1268
1363
|
return;
|
|
1269
1364
|
}
|
|
1270
|
-
if (
|
|
1365
|
+
if (instance.libp2pNode || instance.database) {
|
|
1366
|
+
log('Quiescing strand instance: %s', strandId);
|
|
1367
|
+
await this.releaseRuntime(instance);
|
|
1368
|
+
log('Strand %s quiesced (resources released, instance retained)', strandId);
|
|
1369
|
+
} else {
|
|
1271
1370
|
log('quiesceStrand: strand %s already quiesced', strandId);
|
|
1272
|
-
return;
|
|
1273
1371
|
}
|
|
1372
|
+
instance.status = 'hibernating';
|
|
1373
|
+
}
|
|
1274
1374
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1375
|
+
/**
|
|
1376
|
+
* Whether the latest runtime transition requested for `strandId` is a quiesce that has
|
|
1377
|
+
* not finished. Its handles may still be attached and its status still live, but it is on
|
|
1378
|
+
* its way to `hibernating`, so a caller that needs it up must {@link resumeStrand} it — the
|
|
1379
|
+
* resume runs after the quiesce — rather than treat it as live.
|
|
1380
|
+
*/
|
|
1381
|
+
isQuiescing(strandId: string): boolean {
|
|
1382
|
+
return this.transitions.get(strandId)?.kind === 'quiesce';
|
|
1278
1383
|
}
|
|
1279
1384
|
|
|
1280
1385
|
/**
|
|
@@ -1284,11 +1389,65 @@ export class StrandInstanceManager {
|
|
|
1284
1389
|
* launch (the cohort `bootstrapNodes` seed and the strand's `servingMachines`
|
|
1285
1390
|
* count) and updates the retained config so a later resume reuses the latest
|
|
1286
1391
|
* values. Returns the live instance unchanged if it is already running.
|
|
1392
|
+
*
|
|
1393
|
+
* Overlapping calls share one rebuild: a call made while a resume is the latest request
|
|
1394
|
+
* for the strand — a wake landing during a check-in — joins it and settles exactly as it
|
|
1395
|
+
* does, `'error'` status included. The joiner's own `overrides` are ignored; the first
|
|
1396
|
+
* resume's seed wins, and both callers resolved the cohort seed moments apart. A call made
|
|
1397
|
+
* while a quiesce is the latest request runs after that quiesce and rebuilds (see
|
|
1398
|
+
* {@link transitions}), and one made while `startStrand` is still building waits for that
|
|
1399
|
+
* build rather than starting a second.
|
|
1287
1400
|
*/
|
|
1288
1401
|
async resumeStrand(strandId: string, overrides?: ResumeStrandOverrides): Promise<StrandInstance> {
|
|
1289
1402
|
if (this.stopping) {
|
|
1290
1403
|
throw new Error('StrandInstanceManager is stopping');
|
|
1291
1404
|
}
|
|
1405
|
+
// No `await` between this read and `holdTransition`, or a second caller slips through
|
|
1406
|
+
// and builds.
|
|
1407
|
+
const previous = this.transitions.get(strandId);
|
|
1408
|
+
if (previous?.kind === 'resume') {
|
|
1409
|
+
log('resumeStrand: strand %s — joining the resume already in flight', strandId);
|
|
1410
|
+
return previous.operation;
|
|
1411
|
+
}
|
|
1412
|
+
const operation = this.runResume(strandId, previous, overrides);
|
|
1413
|
+
this.holdTransition(strandId, { kind: 'resume', operation });
|
|
1414
|
+
return operation;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/** Record `transition` as the latest for `strandId` until it settles. */
|
|
1418
|
+
private holdTransition(strandId: string, transition: RuntimeTransition): void {
|
|
1419
|
+
this.transitions.set(strandId, transition);
|
|
1420
|
+
const release = (): void => {
|
|
1421
|
+
if (this.transitions.get(strandId) === transition) {
|
|
1422
|
+
this.transitions.delete(strandId);
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
// The rejection itself reaches the caller through the operation returned to it.
|
|
1426
|
+
void transition.operation.then(release, release);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/** Wait for the transition requested before this one. Never rejects: its failure belongs to its caller. */
|
|
1430
|
+
private async settleTransition(strandId: string, previous: RuntimeTransition | undefined): Promise<void> {
|
|
1431
|
+
if (!previous) {
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
await (previous.operation as Promise<unknown>).catch((error: unknown) => {
|
|
1435
|
+
log('Strand %s: the %s queued ahead failed (reported by its caller): %o', strandId, previous.kind, error);
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/** Body of {@link resumeStrand}; at most one runs per strand at a time. */
|
|
1440
|
+
private async runResume(
|
|
1441
|
+
strandId: string,
|
|
1442
|
+
previous: RuntimeTransition | undefined,
|
|
1443
|
+
overrides?: ResumeStrandOverrides
|
|
1444
|
+
): Promise<StrandInstance> {
|
|
1445
|
+
// Before any "already live" test: a quiesce ahead still has the handles attached, and
|
|
1446
|
+
// mid-build (a launch still building) the libp2p node is attached while the database is
|
|
1447
|
+
// not. A failed launch has already dropped the instance, and the check below reports it
|
|
1448
|
+
// untracked.
|
|
1449
|
+
await this.settleTransition(strandId, previous);
|
|
1450
|
+
await this.settleRuntimeBuilds(strandId);
|
|
1292
1451
|
const instance = this.instances.get(strandId);
|
|
1293
1452
|
if (!instance) {
|
|
1294
1453
|
throw new Error(`Cannot resume strand ${strandId}: not tracked`);
|
|
@@ -1325,7 +1484,7 @@ export class StrandInstanceManager {
|
|
|
1325
1484
|
|
|
1326
1485
|
instance.status = 'starting';
|
|
1327
1486
|
try {
|
|
1328
|
-
await this.buildStrandRuntime(instance, resumeConfig);
|
|
1487
|
+
await this.trackRuntimeBuild(strandId, this.buildStrandRuntime(instance, resumeConfig));
|
|
1329
1488
|
timing('[resumeStrand:%s] total: %dms', strandId, Math.round(performance.now() - tTotal));
|
|
1330
1489
|
log('Strand %s resumed successfully', strandId);
|
|
1331
1490
|
return instance;
|
|
@@ -1353,7 +1512,8 @@ export class StrandInstanceManager {
|
|
|
1353
1512
|
* - `'already-founder'` — the retained config already founds; nothing to do.
|
|
1354
1513
|
* - `'bootstrapped'` — config flipped and the live database ran the bootstrap.
|
|
1355
1514
|
* - `'needs-resume'` — config flipped, but the instance is quiesced (no live
|
|
1356
|
-
* database)
|
|
1515
|
+
* database) or being quiesced (its database is closing), so the bootstrap could
|
|
1516
|
+
* not run here: the CALLER must wake the
|
|
1357
1517
|
* strand (`CadreNode.wakeStrand`, which owns the hibernation bookkeeping this
|
|
1358
1518
|
* manager does not) so the rebuild — which now founds — runs it.
|
|
1359
1519
|
* @param resolvePartyKey - Asked for the party's own membership key when (and only
|
|
@@ -1386,7 +1546,7 @@ export class StrandInstanceManager {
|
|
|
1386
1546
|
// A fresh object rather than mutating in place: startStrand retains the CALLER'S
|
|
1387
1547
|
// config object, which is not ours to rewrite.
|
|
1388
1548
|
this.launchConfigs.set(strandId, { ...config, founder: true, partyMemberPrivateKey });
|
|
1389
|
-
if (!instance.database && !this.firstSyncGates.has(strandId)) {
|
|
1549
|
+
if (this.isQuiescing(strandId) || (!instance.database && !this.firstSyncGates.has(strandId))) {
|
|
1390
1550
|
return 'needs-resume';
|
|
1391
1551
|
}
|
|
1392
1552
|
try {
|
|
@@ -1398,6 +1558,75 @@ export class StrandInstanceManager {
|
|
|
1398
1558
|
return 'bootstrapped';
|
|
1399
1559
|
}
|
|
1400
1560
|
|
|
1561
|
+
/**
|
|
1562
|
+
* Give a tracked storage replica ({@link StartStrandConfig.sAppConfig} absent) the app's
|
|
1563
|
+
* schema in place — the seam for an app on this machine claiming a strand this node was
|
|
1564
|
+
* already hosting. No runtime rebuild: the libp2p node, its peer id and connections, the
|
|
1565
|
+
* store and its warm cache all stay, and the new `App` tables read the blocks the replica
|
|
1566
|
+
* already holds. Callers attach BEFORE {@link foundExistingStrand}: the founder bootstrap
|
|
1567
|
+
* writes the sApp into `Strand.Header` and refuses a database that has none.
|
|
1568
|
+
*
|
|
1569
|
+
* A runtime build in flight (the replica's launch, or a hibernation wake) is waited out
|
|
1570
|
+
* first, so the attach acts on the database that build produced. Then, in order: the
|
|
1571
|
+
* schema signature is checked before anything changes; the retained launch config takes
|
|
1572
|
+
* the sApp, so every later rebuild applies it through `composeStrand`; the live database —
|
|
1573
|
+
* published, or still held by the first-sync gate — gets the schema; and only then does
|
|
1574
|
+
* the instance record `sAppInfo` and the sApp's latency hint. A quiesced instance gets the
|
|
1575
|
+
* config alone, and its next resume applies the schema.
|
|
1576
|
+
*
|
|
1577
|
+
* @returns `'already-attached'` when the instance already runs an sApp — a DIFFERENT one
|
|
1578
|
+
* is logged and left as it is, as a second claim of a claimed strand always has been —
|
|
1579
|
+
* else `'attached'`.
|
|
1580
|
+
* @throws when the strand is not tracked (a caller bug, or a launch that failed while
|
|
1581
|
+
* this waited on it); when the schema signature is refused, having changed nothing; and
|
|
1582
|
+
* whatever the live apply throws (e.g. a quiesce closed the database mid-apply) — the
|
|
1583
|
+
* retained config then still carries the sApp and `sAppInfo` stays unset, so the next
|
|
1584
|
+
* claim retries the apply.
|
|
1585
|
+
*
|
|
1586
|
+
* NOTE: two claims of one replica racing each other both pass the `sAppInfo` check and
|
|
1587
|
+
* apply concurrently; the loser of the declarative diff can reject, and its `addStrand`
|
|
1588
|
+
* retry then resolves `'already-attached'`. Needs an app calling `addStrand` twice at once
|
|
1589
|
+
* (or beside a watcher retry); if it is ever seen, chain attaches per strand id.
|
|
1590
|
+
*/
|
|
1591
|
+
async attachSApp(
|
|
1592
|
+
strandId: string,
|
|
1593
|
+
sAppConfig: SAppConfig,
|
|
1594
|
+
options: { requireSignedSchemas?: boolean } = {}
|
|
1595
|
+
): Promise<'attached' | 'already-attached'> {
|
|
1596
|
+
await this.settleRuntimeBuilds(strandId);
|
|
1597
|
+
const instance = this.instances.get(strandId);
|
|
1598
|
+
const config = this.launchConfigs.get(strandId);
|
|
1599
|
+
if (!instance || !config) {
|
|
1600
|
+
throw new Error(`Cannot attach an sApp to strand ${strandId}: not tracked`);
|
|
1601
|
+
}
|
|
1602
|
+
if (instance.sAppInfo) {
|
|
1603
|
+
if (instance.sAppInfo.id !== sAppConfig.id) {
|
|
1604
|
+
log('attachSApp: strand %s already runs sApp %s — the claim for sApp %s is ignored',
|
|
1605
|
+
strandId, instance.sAppInfo.id, sAppConfig.id);
|
|
1606
|
+
}
|
|
1607
|
+
return 'already-attached';
|
|
1608
|
+
}
|
|
1609
|
+
const sAppInfo = verifiedSAppInfo(strandId, sAppConfig, options.requireSignedSchemas);
|
|
1610
|
+
// A fresh object rather than mutating in place: startStrand retains the CALLER'S
|
|
1611
|
+
// config object, which is not ours to rewrite.
|
|
1612
|
+
this.launchConfigs.set(strandId, { ...config, sAppConfig });
|
|
1613
|
+
const database = instance.database ?? this.firstSyncGates.get(strandId)?.database;
|
|
1614
|
+
if (database) {
|
|
1615
|
+
await database.attachAppSchema(sAppConfig);
|
|
1616
|
+
} else {
|
|
1617
|
+
log('attachSApp: strand %s is quiesced — its next resume applies the sApp schema', strandId);
|
|
1618
|
+
}
|
|
1619
|
+
instance.sAppInfo = sAppInfo;
|
|
1620
|
+
if (sAppConfig.latencyHint) {
|
|
1621
|
+
// HibernationManager reads the hint whenever it arms a timer, so the app's hint governs
|
|
1622
|
+
// from the next one: a timer already armed runs once at the old duration, and a replica
|
|
1623
|
+
// launched under a realtime default was never tracked, so it stays up until relaunched.
|
|
1624
|
+
instance.latencyHint = sAppConfig.latencyHint;
|
|
1625
|
+
}
|
|
1626
|
+
log('Strand %s upgraded from storage replica to %s', strandId, describeSApp(sAppConfig));
|
|
1627
|
+
return 'attached';
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1401
1630
|
/**
|
|
1402
1631
|
* Whether `strandId` is launched but still waiting for its first sync — a joiner whose
|
|
1403
1632
|
* runtime is up while its database is withheld (status `'syncing'`, or `'idle'` after
|
|
@@ -1453,6 +1682,18 @@ export class StrandInstanceManager {
|
|
|
1453
1682
|
});
|
|
1454
1683
|
}
|
|
1455
1684
|
|
|
1685
|
+
/**
|
|
1686
|
+
* The tracked instance once no runtime build of it is in flight, or `undefined` when none
|
|
1687
|
+
* is tracked by then — never launched, or the launch waited on failed and dropped its
|
|
1688
|
+
* record. For a caller about to hand a tracked instance to an app: until its build settles
|
|
1689
|
+
* the instance is `'starting'` with no database, and a failed launch leaves no instance.
|
|
1690
|
+
* Never rejects; a failed build is reported by the call that started it.
|
|
1691
|
+
*/
|
|
1692
|
+
async whenRuntimeBuilt(strandId: string): Promise<StrandInstance | undefined> {
|
|
1693
|
+
await this.settleRuntimeBuilds(strandId);
|
|
1694
|
+
return this.instances.get(strandId);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1456
1697
|
/**
|
|
1457
1698
|
* Hand a launch's database to the app: set `instance.database`, retire the gate that
|
|
1458
1699
|
* held it, release every {@link whenWritable} waiter, and — when the launch had already
|
|
@@ -1482,9 +1723,11 @@ export class StrandInstanceManager {
|
|
|
1482
1723
|
this.firstSyncGates.delete(strandId);
|
|
1483
1724
|
}
|
|
1484
1725
|
instance.database = database;
|
|
1485
|
-
instance.lastActivity = new Date();
|
|
1486
1726
|
const wasGated = instance.status !== 'starting';
|
|
1487
1727
|
if (wasGated) {
|
|
1728
|
+
// A Header delivered by a peer is strand activity; a publish during bring-up is not
|
|
1729
|
+
// (see the build's end in `buildStrandRuntime`).
|
|
1730
|
+
instance.lastActivity = new Date();
|
|
1488
1731
|
instance.status = 'active';
|
|
1489
1732
|
log('Strand %s is now writable: Strand.Header held', strandId);
|
|
1490
1733
|
}
|
|
@@ -1919,6 +1919,18 @@ export async function sealStrand(
|
|
|
1919
1919
|
log('Sealed strand: sole manager %s stepped down', managerKeyPair.publicKeyB64);
|
|
1920
1920
|
}
|
|
1921
1921
|
|
|
1922
|
+
/**
|
|
1923
|
+
* The sApp id the strand's singleton `Strand.Header` names, or `undefined` when no
|
|
1924
|
+
* Header row is held yet. A storage replica reads it to learn which app the strand it
|
|
1925
|
+
* hosts belongs to (`CadreNode.getSAppId`).
|
|
1926
|
+
*/
|
|
1927
|
+
export async function readStrandHeaderSAppId(db: Database): Promise<string | undefined> {
|
|
1928
|
+
for await (const row of db.eval('select sAppId from Strand.Header')) {
|
|
1929
|
+
return typeof row.sAppId === 'string' ? row.sAppId : undefined;
|
|
1930
|
+
}
|
|
1931
|
+
return undefined;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1922
1934
|
/**
|
|
1923
1935
|
* Whether the strand is `Type = 'c'` (closed). Reads the singleton `Header` via
|
|
1924
1936
|
* the same `db.eval` scan idiom as {@link strandTableCount}; a strand with no
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*
|
|
29
29
|
* 1. Open strands have no shared member key at all, and the book has to work there.
|
|
30
30
|
* 2. A member-key signature proves only "some member said this", so any member could
|
|
31
|
-
* forge another member's address and cost every peer a
|
|
31
|
+
* forge another member's address and cost every peer a 16 s relayed dial per
|
|
32
32
|
* launch; a self-signature is a claim only the named peer can make.
|
|
33
33
|
* 3. On a closed strand the member key is shared by every machine of a party, so it
|
|
34
34
|
* cannot identify a machine anyway.
|
|
@@ -88,12 +88,6 @@ export const STRAND_PEER_ISSUED_AT_SKEW_MS = 5 * 60 * 1000;
|
|
|
88
88
|
*/
|
|
89
89
|
export const MAX_BOOK_FRAME_SIZE = 64 * 1024;
|
|
90
90
|
|
|
91
|
-
/** Default time the receiver waits for the inbound request frame before aborting (ms). */
|
|
92
|
-
const DEFAULT_READ_TIMEOUT_MS = 10_000;
|
|
93
|
-
|
|
94
|
-
/** Default time the client waits for the whole exchange (ms). */
|
|
95
|
-
const DEFAULT_EXCHANGE_TIMEOUT_MS = 10_000;
|
|
96
|
-
|
|
97
91
|
/** Default cap on concurrent inbound swap streams a single strand node serves. */
|
|
98
92
|
const DEFAULT_MAX_CONCURRENT = 100;
|
|
99
93
|
|
|
@@ -320,8 +314,12 @@ export interface StrandPeerBookServiceDeps {
|
|
|
320
314
|
onEntries(entries: SignedStrandPeerEntry[], fromPeerId: string): void;
|
|
321
315
|
/** Clock, for tests. Default `Date.now`. */
|
|
322
316
|
now?: () => number;
|
|
323
|
-
/**
|
|
324
|
-
|
|
317
|
+
/**
|
|
318
|
+
* How long the receiver waits for the inbound request frame (ms). Required for the same
|
|
319
|
+
* reason as {@link ExchangeStrandPeerBookOptions.timeoutMs}: the swap gives both sides one
|
|
320
|
+
* link-derived deadline.
|
|
321
|
+
*/
|
|
322
|
+
readTimeoutMs: number;
|
|
325
323
|
/** Default {@link DEFAULT_MAX_CONCURRENT}. Over it, an empty frame is returned unread. */
|
|
326
324
|
maxConcurrent?: number;
|
|
327
325
|
}
|
|
@@ -341,7 +339,7 @@ export class StrandPeerBookService {
|
|
|
341
339
|
private activeStreams = 0;
|
|
342
340
|
|
|
343
341
|
constructor(private readonly deps: StrandPeerBookServiceDeps) {
|
|
344
|
-
this.readTimeoutMs = deps.readTimeoutMs
|
|
342
|
+
this.readTimeoutMs = deps.readTimeoutMs;
|
|
345
343
|
this.maxConcurrent = deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
|
346
344
|
this.now = deps.now ?? Date.now;
|
|
347
345
|
}
|
|
@@ -427,8 +425,13 @@ async function closeQuietly(stream: ControlStream): Promise<void> {
|
|
|
427
425
|
export interface ExchangeStrandPeerBookOptions {
|
|
428
426
|
/** This strand node's own peer id: a returned entry naming it is dropped. */
|
|
429
427
|
selfPeerId: string;
|
|
430
|
-
/**
|
|
431
|
-
|
|
428
|
+
/**
|
|
429
|
+
* Whole-exchange deadline (ms). Required, with no fallback here: the exchange crosses the
|
|
430
|
+
* link, so its deadline derives from the declared link round trip (`link-budget.ts`), and
|
|
431
|
+
* the caller is the one that knows the declaration. A default in this module would be a
|
|
432
|
+
* second number free to drift from that derivation.
|
|
433
|
+
*/
|
|
434
|
+
timeoutMs: number;
|
|
432
435
|
/** Clock, for tests. Default `Date.now`. */
|
|
433
436
|
now?: () => number;
|
|
434
437
|
}
|
|
@@ -444,7 +447,7 @@ export async function exchangeStrandPeerBook(
|
|
|
444
447
|
request: StrandPeerBookFrame,
|
|
445
448
|
options: ExchangeStrandPeerBookOptions
|
|
446
449
|
): Promise<SignedStrandPeerEntry[]> {
|
|
447
|
-
const timeoutMs = options
|
|
450
|
+
const { timeoutMs } = options;
|
|
448
451
|
const remotePeerId = connection.remotePeer.toString();
|
|
449
452
|
assertBookFrameFits(request);
|
|
450
453
|
// One deadline for the stream open and the exchange together: its signal aborts the
|
package/src/strand-peer-book.ts
CHANGED
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* cohort each held only self, the strand reported `active`, local writes
|
|
15
15
|
* succeeded, nothing replicated.
|
|
16
16
|
*
|
|
17
|
+
* NOTE: interim, to be replaced (maintainer decision, 2026-09-28). FRET's
|
|
18
|
+
* `plan/10-feat-address-hints-in-neighbor-exchange` carries signed peer records in
|
|
19
|
+
* its neighbour exchange and its saved routing table. Once a FRET release with it
|
|
20
|
+
* reaches `@optimystic/db-p2p`, remove this book and the swap and pass db-p2p
|
|
21
|
+
* `persistence` for strand nodes instead; `blocked/retire-strand-peer-book-for-fret-address-hints`
|
|
22
|
+
* tracks that.
|
|
23
|
+
*
|
|
17
24
|
* NOTE: accepted design boundary (maintainer decision, 2026-09-27) — this is a
|
|
18
25
|
* per-machine record in the machine's own storage, NOT a registry inside the
|
|
19
26
|
* strand database. An in-strand registry (`Strand.MemberPeer` rows carrying
|
|
@@ -60,7 +67,7 @@ const log = debug('sereus:cadre:strand-peer-book');
|
|
|
60
67
|
* Cap on the peers remembered per strand. When full, the entry with the smallest
|
|
61
68
|
* `max(issuedAt, lastSeenAt)` is evicted. Sixteen is generous for the parties a
|
|
62
69
|
* strand has today (two, occasionally a few), and it bounds the worst case of a
|
|
63
|
-
* launch dialing dead entries: a failed relayed dial costs up to
|
|
70
|
+
* launch dialing dead entries: a failed relayed dial costs up to 16 s at the
|
|
64
71
|
* declared link, so the cap — with aging — is what keeps a stale book from
|
|
65
72
|
* stalling bring-up for minutes.
|
|
66
73
|
*/
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Throttled to one report per peer per {@link STRAND_PEER_OBSERVE_THROTTLE_MS}
|
|
18
18
|
* unless the address set changed, so a flapping relayed connection — identify
|
|
19
|
-
* fires on every reconnect, every
|
|
19
|
+
* fires on every reconnect, every 16 s at worst — does not rewrite the node-local
|
|
20
20
|
* slot each time. Best-effort throughout: nothing here throws into a libp2p event
|
|
21
21
|
* handler.
|
|
22
22
|
*
|
|
@@ -118,7 +118,8 @@
|
|
|
118
118
|
import debug from 'debug';
|
|
119
119
|
import type { ConnectionGater, PeerId, MultiaddrConnection } from '@libp2p/interface';
|
|
120
120
|
import type { Database } from '@quereus/quereus';
|
|
121
|
-
import {
|
|
121
|
+
import { decideWithinDeadline } from './membership-connection-gater.js';
|
|
122
|
+
import { ADMISSION_DECISION_TIMEOUT_MS } from './link-budget.js';
|
|
122
123
|
|
|
123
124
|
const log = debug('sereus:cadre:strand-revocation');
|
|
124
125
|
|