@serfab/cadre-core 1.12.0 → 1.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.
@@ -1154,6 +1154,13 @@ export declare class CadreNode implements SAppIdLookup {
1154
1154
  * failure and is re-driven by the next membership write and by the timed reconcile.
1155
1155
  */
1156
1156
  private refreshAuthorizedControlPeers;
1157
+ /**
1158
+ * Add the configured `controlNetwork.bootstrapNodes` to the control node's address book
1159
+ * and dial each peer once — what `@libp2p/bootstrap` would have done had the node been
1160
+ * built with them. Best-effort: a failed dial is logged, and the connection manager and
1161
+ * FRET reconnect from the address book as they do for any known peer.
1162
+ */
1163
+ private dialControlBootstrapPeers;
1157
1164
  /**
1158
1165
  * PeerIds of the configured control bootstrap nodes (relay/bootstrap
1159
1166
  * infrastructure — always admitted inbound). Parsed once, lazily; an
@@ -895,22 +895,20 @@ export class CadreNode {
895
895
  // only way to declare it at all — see `enrolled-machine-store.ts`.
896
896
  this.initializeEnrolledMachineStore();
897
897
  // Arm the connection gate's BRING-UP QUIET PERIOD before the libp2p node
898
- // exists, so no connection can form ahead of it: `@libp2p/bootstrap` emits
899
- // its discovery events ~1 s after `libp2p.start()` and the connection
900
- // manager auto-dials from there, which is INSIDE createControlNode below.
898
+ // exists, so no connection can form ahead of it: the node is built without
899
+ // its bootstrap peers (dialed after bring-up), but a peer it already knows
900
+ // from a previous run, or one that dials in, could otherwise connect inside
901
+ // createControlNode below.
901
902
  // The invariant being protected is "zero control connections during
902
903
  // control-database bring-up" — see the field's doc and
903
904
  // `membership-connection-gater.ts` → "The bring-up quiet period".
904
905
  //
905
- // NOTE: without this gate the bootstrap arm is a RACE, not a guarantee — its
906
- // safety margin is bring-up duration versus @libp2p/bootstrap's 1 s discovery
907
- // fuse, and bring-up duration is (raw-storage operations) × per-operation
908
- // latency. The cold-start operation count and the 50-90 ms/op figure a loaded
909
- // phone actually sees live in `control-database.ts`'s `loadSchema` note (and
910
- // are pinned by `control-start-storage-op-budget.spec.ts`): at the top of that
911
- // range bring-up is 9-15 s, far past the fuse. So if this gate is ever removed
912
- // or bypassed, that arm reopens for exactly the nodes most likely to hit it —
913
- // phones joining a party, whose membership row has not replicated yet.
906
+ // NOTE: the gate, not the ordering, is what holds the invariant. Bring-up
907
+ // duration is (raw-storage operations) × per-operation latency; the cold-start
908
+ // operation count and the 50-90 ms/op figure a loaded phone sees live in
909
+ // `control-database.ts`'s `loadSchema` note (pinned by
910
+ // `control-start-storage-op-budget.spec.ts`), so bring-up can take 9-15 s —
911
+ // ample time for a remembered or inbound peer to connect without the gate.
914
912
  this.controlBringUpInFlight = true;
915
913
  // Create the control network libp2p node
916
914
  let t0 = performance.now();
@@ -948,6 +946,10 @@ export class CadreNode {
948
946
  // bring-up hazard for boot-time strand seeds that are always empty.
949
947
  this.controlBringUpInFlight = false;
950
948
  log('Control database initialized');
949
+ // Now that the gate is open, make contact with the configured bootstrap peers the
950
+ // node was built without (see buildControlNodeOptions). Not awaited: an unreachable
951
+ // relay must not hold up start().
952
+ void this.dialControlBootstrapPeers();
951
953
  // Attach the per-stream gate to the control DB's membership hub, so every
952
954
  // committed `CadrePeer` write re-materializes the authorized snapshot on its
953
955
  // own. Wired before anything can write. `_running` is still false for the
@@ -1528,7 +1530,7 @@ export class CadreNode {
1528
1530
  * only caller in production is `createControlNode`.
1529
1531
  */
1530
1532
  buildControlNodeOptions() {
1531
- const { controlNetwork, network, profile } = this.config;
1533
+ const { network, profile } = this.config;
1532
1534
  const identityKey = this.identityKey;
1533
1535
  // `network.relayAddrs` contributes the bare `/p2p-circuit` SEARCH entry, which
1534
1536
  // opens no connection — the reservation is driven explicitly at the END of
@@ -1545,7 +1547,11 @@ export class CadreNode {
1545
1547
  const controlStorageProvider = this.resolveControlStorage();
1546
1548
  const nodeOptions = {
1547
1549
  port: 0,
1548
- bootstrapNodes: controlNetwork.bootstrapNodes,
1550
+ // Empty on purpose: Optimystic refuses to treat a block as never-created until it has
1551
+ // heard from every `bootstrapNodes` peer, and the bring-up quiet period refuses those
1552
+ // dials, so a non-empty list fails start() with `cohort-unreachable`. The configured
1553
+ // peers are dialed once bring-up ends ({@link dialControlBootstrapPeers}).
1554
+ bootstrapNodes: [],
1549
1555
  networkName: this.controlNetworkName(),
1550
1556
  storage: controlStorageProvider,
1551
1557
  fretProfile: profile === 'storage' ? 'core' : 'edge',
@@ -2003,6 +2009,31 @@ export class CadreNode {
2003
2009
  log('refreshAuthorizedControlPeers(%s) failed — keeping previous snapshot: %o', reason, error);
2004
2010
  }
2005
2011
  }
2012
+ /**
2013
+ * Add the configured `controlNetwork.bootstrapNodes` to the control node's address book
2014
+ * and dial each peer once — what `@libp2p/bootstrap` would have done had the node been
2015
+ * built with them. Best-effort: a failed dial is logged, and the connection manager and
2016
+ * FRET reconnect from the address book as they do for any known peer.
2017
+ */
2018
+ async dialControlBootstrapPeers() {
2019
+ const node = this.controlNode;
2020
+ if (!node) {
2021
+ return;
2022
+ }
2023
+ const groups = groupAddrsByPeerId(this.config.controlNetwork.bootstrapNodes);
2024
+ await Promise.all([...groups].map(async ([peerId, addrs]) => {
2025
+ if (peerId === node.peerId.toString()) {
2026
+ return;
2027
+ }
2028
+ await mergePeerAddrs(node, peerId, addrs);
2029
+ try {
2030
+ await node.dial(peerIdFromString(peerId));
2031
+ }
2032
+ catch (error) {
2033
+ log('dialControlBootstrapPeers: dial to bootstrap peer %s failed: %o', peerId, error);
2034
+ }
2035
+ }));
2036
+ }
2006
2037
  /**
2007
2038
  * PeerIds of the configured control bootstrap nodes (relay/bootstrap
2008
2039
  * infrastructure — always admitted inbound). Parsed once, lazily; an