@serfab/cadre-core 0.12.0 → 1.0.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.
Files changed (155) hide show
  1. package/README.md +23 -8
  2. package/dist/bootstrap-peer-store-file.js +2 -2
  3. package/dist/bootstrap-peer-store.d.ts +32 -12
  4. package/dist/bootstrap-peer-store.js +41 -15
  5. package/dist/bootstrap-peer-store.js.map +1 -1
  6. package/dist/cadre-node.d.ts +883 -102
  7. package/dist/cadre-node.js +1800 -209
  8. package/dist/cadre-node.js.map +1 -1
  9. package/dist/control-authorization.d.ts +2 -2
  10. package/dist/control-authorization.js +1 -0
  11. package/dist/control-authorization.js.map +1 -1
  12. package/dist/control-cohort.d.ts +13 -26
  13. package/dist/control-cohort.js +0 -26
  14. package/dist/control-cohort.js.map +1 -1
  15. package/dist/control-database.d.ts +318 -44
  16. package/dist/control-database.js +516 -74
  17. package/dist/control-database.js.map +1 -1
  18. package/dist/control-read-retry.d.ts +40 -11
  19. package/dist/control-read-retry.js +48 -15
  20. package/dist/control-read-retry.js.map +1 -1
  21. package/dist/control-retry.d.ts +58 -3
  22. package/dist/control-retry.js +58 -3
  23. package/dist/control-retry.js.map +1 -1
  24. package/dist/control-schema.d.ts +1 -1
  25. package/dist/control-schema.js +133 -21
  26. package/dist/control-schema.js.map +1 -1
  27. package/dist/control-write-retry.d.ts +80 -11
  28. package/dist/control-write-retry.js +168 -29
  29. package/dist/control-write-retry.js.map +1 -1
  30. package/dist/enrolled-machine-store-file.d.ts +13 -0
  31. package/dist/enrolled-machine-store-file.js +41 -0
  32. package/dist/enrolled-machine-store-file.js.map +1 -0
  33. package/dist/enrolled-machine-store.d.ts +81 -0
  34. package/dist/enrolled-machine-store.js +225 -0
  35. package/dist/enrolled-machine-store.js.map +1 -0
  36. package/dist/file-durable-slot.js +5 -3
  37. package/dist/file-durable-slot.js.map +1 -1
  38. package/dist/index.d.ts +17 -8
  39. package/dist/index.js +54 -9
  40. package/dist/index.js.map +1 -1
  41. package/dist/membership-connection-gater.d.ts +15 -1
  42. package/dist/membership-connection-gater.js +9 -3
  43. package/dist/membership-connection-gater.js.map +1 -1
  44. package/dist/node-local-snapshot.d.ts +21 -7
  45. package/dist/node-local-snapshot.js +30 -9
  46. package/dist/node-local-snapshot.js.map +1 -1
  47. package/dist/peer-dial.d.ts +143 -0
  48. package/dist/peer-dial.js +176 -0
  49. package/dist/peer-dial.js.map +1 -0
  50. package/dist/{strand-backfill.d.ts → peer-join-backfill.d.ts} +49 -20
  51. package/dist/{strand-backfill.js → peer-join-backfill.js} +133 -52
  52. package/dist/peer-join-backfill.js.map +1 -0
  53. package/dist/relay-addrs.d.ts +160 -49
  54. package/dist/relay-addrs.js +287 -60
  55. package/dist/relay-addrs.js.map +1 -1
  56. package/dist/relay-reservation.d.ts +71 -3
  57. package/dist/relay-reservation.js +116 -8
  58. package/dist/relay-reservation.js.map +1 -1
  59. package/dist/seed-bootstrap.d.ts +30 -1
  60. package/dist/seed-bootstrap.js +62 -17
  61. package/dist/seed-bootstrap.js.map +1 -1
  62. package/dist/storage-scope.d.ts +62 -0
  63. package/dist/storage-scope.js +75 -0
  64. package/dist/storage-scope.js.map +1 -0
  65. package/dist/strand-database.d.ts +41 -11
  66. package/dist/strand-database.js +48 -18
  67. package/dist/strand-database.js.map +1 -1
  68. package/dist/strand-first-sync-gate.d.ts +152 -0
  69. package/dist/strand-first-sync-gate.js +208 -0
  70. package/dist/strand-first-sync-gate.js.map +1 -0
  71. package/dist/strand-formation-manager.d.ts +99 -5
  72. package/dist/strand-formation-manager.js +119 -8
  73. package/dist/strand-formation-manager.js.map +1 -1
  74. package/dist/strand-formation-protocol.d.ts +97 -3
  75. package/dist/strand-formation-protocol.js +108 -2
  76. package/dist/strand-formation-protocol.js.map +1 -1
  77. package/dist/strand-instance-manager.d.ts +389 -19
  78. package/dist/strand-instance-manager.js +678 -65
  79. package/dist/strand-instance-manager.js.map +1 -1
  80. package/dist/strand-member-key.d.ts +20 -12
  81. package/dist/strand-member-key.js +20 -12
  82. package/dist/strand-member-key.js.map +1 -1
  83. package/dist/strand-member-registry.js +3 -0
  84. package/dist/strand-member-registry.js.map +1 -1
  85. package/dist/strand-membership-reconciler.d.ts +361 -0
  86. package/dist/strand-membership-reconciler.js +548 -0
  87. package/dist/strand-membership-reconciler.js.map +1 -0
  88. package/dist/strand-membership-writer.d.ts +143 -11
  89. package/dist/strand-membership-writer.js +419 -135
  90. package/dist/strand-membership-writer.js.map +1 -1
  91. package/dist/strand-network-config.d.ts +116 -0
  92. package/dist/strand-network-config.js +192 -0
  93. package/dist/strand-network-config.js.map +1 -0
  94. package/dist/strand-revocation-enforcer.d.ts +392 -0
  95. package/dist/strand-revocation-enforcer.js +533 -0
  96. package/dist/strand-revocation-enforcer.js.map +1 -0
  97. package/dist/strand-solicitation.d.ts +17 -1
  98. package/dist/strand-solicitation.js +13 -2
  99. package/dist/strand-solicitation.js.map +1 -1
  100. package/dist/strand-wake-protocol.d.ts +11 -7
  101. package/dist/strand-wake-protocol.js +20 -52
  102. package/dist/strand-wake-protocol.js.map +1 -1
  103. package/dist/strand-watcher.js +12 -1
  104. package/dist/strand-watcher.js.map +1 -1
  105. package/dist/timed-step.d.ts +12 -0
  106. package/dist/timed-step.js +28 -0
  107. package/dist/timed-step.js.map +1 -0
  108. package/dist/timeout-scheduler.d.ts +19 -0
  109. package/dist/timeout-scheduler.js +22 -0
  110. package/dist/timeout-scheduler.js.map +1 -0
  111. package/dist/types.d.ts +472 -46
  112. package/dist/types.js +7 -1
  113. package/dist/types.js.map +1 -1
  114. package/package.json +16 -8
  115. package/src/bootstrap-peer-store-file.ts +2 -2
  116. package/src/bootstrap-peer-store.ts +69 -27
  117. package/src/cadre-node.ts +1926 -228
  118. package/src/control-authorization.ts +2 -1
  119. package/src/control-cohort.ts +122 -130
  120. package/src/control-database.ts +586 -74
  121. package/src/control-read-retry.ts +202 -167
  122. package/src/control-retry.ts +297 -190
  123. package/src/control-schema.ts +133 -21
  124. package/src/control-write-retry.ts +525 -383
  125. package/src/enrolled-machine-store-file.ts +43 -0
  126. package/src/enrolled-machine-store.ts +280 -0
  127. package/src/file-durable-slot.ts +5 -3
  128. package/src/index.ts +134 -15
  129. package/src/membership-connection-gater.ts +9 -3
  130. package/src/node-local-snapshot.ts +38 -9
  131. package/src/peer-addr-book.ts +251 -250
  132. package/src/peer-dial.ts +215 -0
  133. package/src/{strand-backfill.ts → peer-join-backfill.ts} +478 -389
  134. package/src/relay-addrs.ts +458 -218
  135. package/src/relay-reservation.ts +951 -828
  136. package/src/seed-bootstrap.ts +76 -18
  137. package/src/storage-scope.ts +78 -0
  138. package/src/strand-database.ts +61 -21
  139. package/src/strand-first-sync-gate.ts +258 -0
  140. package/src/strand-formation-manager.ts +162 -8
  141. package/src/strand-formation-protocol.ts +850 -692
  142. package/src/strand-instance-manager.ts +877 -74
  143. package/src/strand-member-key.ts +20 -12
  144. package/src/strand-member-registry.ts +3 -0
  145. package/src/strand-membership-reconciler.ts +680 -0
  146. package/src/strand-membership-writer.ts +550 -190
  147. package/src/strand-network-config.ts +250 -0
  148. package/src/strand-revocation-enforcer.ts +653 -0
  149. package/src/strand-solicitation.ts +499 -471
  150. package/src/strand-wake-protocol.ts +21 -66
  151. package/src/strand-watcher.ts +325 -314
  152. package/src/timed-step.ts +28 -0
  153. package/src/timeout-scheduler.ts +28 -0
  154. package/src/types.ts +487 -52
  155. package/dist/strand-backfill.js.map +0 -1
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Dialing one peer from a list of candidate addresses, so that an address that
3
+ * never answers cannot use up the time the others needed.
4
+ *
5
+ * libp2p's own multi-address dial (`libp2p.dial(addrs)`, libp2p 3.1.3's
6
+ * `DialQueue.dialPeer`) tries the addresses ONE AT A TIME under ONE signal, with
7
+ * no time limit per address. An address whose connection attempt is silently
8
+ * dropped — a firewall, a subnet the dialer cannot route to, a virtual adapter's
9
+ * address a Windows host reports alongside its real ones — holds that dial until
10
+ * the whole signal expires, and every address sorted after it is never tried. It
11
+ * also sorts the list itself (`defaultAddressSorter`), putting loopback addresses
12
+ * last, behind exactly the LAN addresses most likely to be dropped. A phone
13
+ * borrowing a node from a cadre-host whose only reachable address was the
14
+ * forwarded loopback one never connected for this reason.
15
+ *
16
+ * So the addresses are dialed here, each as its own `dial()` with its own time
17
+ * limit ({@link PeerDialBudget.perAddressMs}), inside a limit for the whole peer
18
+ * ({@link PeerDialBudget.totalMs}). One address per call also means libp2p's
19
+ * sorter has nothing to reorder, so the order is chosen here
20
+ * ({@link directBeforeRelayed}).
21
+ *
22
+ * Dependency-free apart from `control-stream.ts`, so any module may import it
23
+ * without creating an import cycle.
24
+ */
25
+
26
+ import debug from 'debug';
27
+ import type { Connection } from '@libp2p/interface';
28
+ import type { Multiaddr } from '@multiformats/multiaddr';
29
+ import { withDeadline } from './control-stream.js';
30
+
31
+ const log = debug('sereus:cadre:peer-dial');
32
+
33
+ /**
34
+ * Default limit on ONE address's dial attempt, in ms.
35
+ *
36
+ * It has to cover the slowest address that should succeed, because an attempt
37
+ * that needs longer fails the same way on every retry. That is a relayed dial on
38
+ * a mobile link: connect to the relay (transport, encryption and multiplexer
39
+ * handshakes, when no relay connection is open yet), open the circuit, then run
40
+ * both handshakes again end to end through the relay at roughly twice the
41
+ * round-trip time. At a few hundred ms per round trip that is several seconds.
42
+ * A direct dial that works is far quicker — a phone's WebSocket dial to a node
43
+ * forwarded over `adb reverse` measured 1.6 s end to end. 8 s covers the relayed
44
+ * case with some margin while still abandoning a silently dropped address
45
+ * sooner than libp2p's own 10 s dial timeout would.
46
+ *
47
+ * NOTE: a `/p2p-circuit/webrtc` address also negotiates a WebRTC session after
48
+ * the relay leg. If such dials are ever seen failing at exactly this limit, raise
49
+ * it (or `network.controlCohort.perAddressDialTimeoutMs` on the affected nodes)
50
+ * rather than the per-peer limit, which cannot help an address that is itself
51
+ * cut off.
52
+ *
53
+ * Override per node with `network.controlCohort.perAddressDialTimeoutMs`.
54
+ */
55
+ export const DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS = 8_000;
56
+
57
+ /**
58
+ * Default limit on dialing ONE peer — every candidate address together — in ms.
59
+ *
60
+ * Without it, an offline peer costs (address count × up to
61
+ * {@link DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS}), a number that
62
+ * depends on how many addresses the peer happens to report. Callers dial peers
63
+ * one after another (the control-cohort reconcile pass dials each sibling in
64
+ * turn), so that cost also delays every later peer. With the limit, a reconcile
65
+ * pass costs at most (dialed siblings) × this.
66
+ *
67
+ * Sized to fit a typical list with dead addresses ahead of the one that works:
68
+ * a lent cadre-host node reports two to four addresses a phone can dial
69
+ * (loopback and LAN WebSocket addresses, plus any virtual adapters). Three
70
+ * silently dropped addresses cost 3 × 8 s = 24 s, leaving 6 s for the working
71
+ * address after them — well over the 1.6 s the phone measured. A peer whose
72
+ * working address sits behind four or more dropped ones does not fit, and fails
73
+ * each pass the same way; if that shows up, the remedy is ordering (try the
74
+ * address that last connected first), not a larger number here.
75
+ *
76
+ * Override per node with `network.controlCohort.dialTimeoutMs` — tests that
77
+ * drive dead addresses on purpose set it low so a pass's duration is a chosen
78
+ * number rather than a transitive libp2p default stretched by machine load.
79
+ */
80
+ export const DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS = 30_000;
81
+
82
+ /** Time limits for dialing one peer from a list of candidate addresses. */
83
+ export interface PeerDialBudget {
84
+ /** Limit on one address's attempt, in ms. */
85
+ perAddressMs: number;
86
+ /** Limit on the whole peer — every address together — in ms. */
87
+ totalMs: number;
88
+ }
89
+
90
+ /** {@link PeerDialBudget} built from the two defaults above. */
91
+ export const DEFAULT_PEER_DIAL_BUDGET: Readonly<PeerDialBudget> = {
92
+ perAddressMs: DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS,
93
+ totalMs: DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS,
94
+ };
95
+
96
+ /** The slice of a libp2p node {@link dialPeerAddrs} uses. */
97
+ export interface AddrDialer {
98
+ dial(addr: Multiaddr, options: { signal: AbortSignal }): Promise<Connection>;
99
+ }
100
+
101
+ /** One candidate's outcome, kept so the thrown error can name every attempt. */
102
+ interface AddrAttemptFailure {
103
+ addr: Multiaddr;
104
+ error: Error;
105
+ }
106
+
107
+ /**
108
+ * Dial one peer from `addrs`, one address per `dial()` call, and return the
109
+ * first connection that forms. Direct addresses are tried before relayed ones
110
+ * ({@link directBeforeRelayed}); otherwise the given order is kept.
111
+ *
112
+ * Addresses naming a transport this node lacks are not filtered out first: a
113
+ * one-address `dial()` rejects those at once (`NoValidAddressesError`), so they
114
+ * cost a log line, not time.
115
+ *
116
+ * Throws when no address connects — see {@link tryAddrsInTurn} for the error.
117
+ *
118
+ * @param label Names the dial in timeout messages and logs, e.g.
119
+ * `reconcileControlCohort dial of sibling <peerId>`; each attempt reads
120
+ * `<label> via <addr>`.
121
+ */
122
+ export function dialPeerAddrs(
123
+ dialer: AddrDialer,
124
+ addrs: readonly Multiaddr[],
125
+ budget: PeerDialBudget,
126
+ label: string,
127
+ ): Promise<Connection> {
128
+ return tryAddrsInTurn(directBeforeRelayed(addrs), budget, label, (addr, signal) => dialer.dial(addr, { signal }));
129
+ }
130
+
131
+ /**
132
+ * Run `attempt` against each address in the given order until one succeeds, and
133
+ * return its result.
134
+ *
135
+ * Each attempt is limited to `budget.perAddressMs`, or to whatever remains of
136
+ * `budget.totalMs` when that is less. The attempt's signal is aborted when its
137
+ * limit passes, so a dial in progress is cancelled rather than left running.
138
+ * Addresses the total leaves no time for are reported as not tried.
139
+ *
140
+ * Throws when every address fails. With one address the error is that attempt's
141
+ * own, unchanged. With several it is one error naming each address and why it
142
+ * failed, in order, with the first failure as `cause` — so the report shows the
143
+ * address that mattered, not only whichever happened to be tried last.
144
+ *
145
+ * Between attempts this waits for one macrotask (`setTimeout(0)`). libp2p's dial
146
+ * queue lets a new `dial()` join any queued dial job for the same peer id
147
+ * WITHOUT checking whether that job has already finished, and an aborted job
148
+ * leaves the queue a few microtasks after its callers are rejected. Timing out
149
+ * one address and immediately dialing the next for the same peer therefore
150
+ * joins the dying job, which rejects the new dial with `AbortError` before it
151
+ * touches the network — the next address is never tried. Seen against libp2p
152
+ * 3.1.3; `test/peer-dial.spec.ts` dials real nodes so an upgrade that changes it
153
+ * shows up there. All of the queue's cleanup runs as microtasks, so it has
154
+ * finished by the time a macrotask runs.
155
+ */
156
+ export async function tryAddrsInTurn<T>(
157
+ addrs: readonly Multiaddr[],
158
+ budget: PeerDialBudget,
159
+ label: string,
160
+ attempt: (addr: Multiaddr, signal: AbortSignal, attemptMs: number) => Promise<T>,
161
+ ): Promise<T> {
162
+ const deadline = Date.now() + budget.totalMs;
163
+ const failures: AddrAttemptFailure[] = [];
164
+ for (const addr of addrs) {
165
+ const remaining = deadline - Date.now();
166
+ if (remaining <= 0) {
167
+ failures.push({ addr, error: new Error(`not tried — the ${budget.totalMs}ms budget for ${label} was spent on earlier addresses`) });
168
+ continue;
169
+ }
170
+ if (failures.length > 0) {
171
+ await nextMacrotask();
172
+ }
173
+ const attemptMs = Math.min(budget.perAddressMs, remaining);
174
+ try {
175
+ return await withDeadline(attemptMs, `${label} via ${addr.toString()}`, (signal) => attempt(addr, signal, attemptMs));
176
+ } catch (err) {
177
+ const error = err instanceof Error ? err : new Error(String(err));
178
+ failures.push({ addr, error });
179
+ log('%s via %s failed: %o', label, addr.toString(), error);
180
+ }
181
+ }
182
+ throw allAttemptsFailed(label, failures);
183
+ }
184
+
185
+ /**
186
+ * `addrs` with every circuit-relay address (one containing `/p2p-circuit`) moved
187
+ * after every direct one, each group keeping its given order.
188
+ *
189
+ * The one part of libp2p's own ordering worth keeping: a relayed connection is
190
+ * slower and limited in how much data and time it may carry, so a direct
191
+ * address is always worth trying first. libp2p's other rules are deliberately
192
+ * dropped — it puts loopback addresses last and public ones before private ones,
193
+ * which is how a forwarded loopback address ended up behind addresses that
194
+ * never answer. With a per-address limit, trying a dead address first costs a
195
+ * bounded amount of time, so the caller's order is the better guide.
196
+ */
197
+ export function directBeforeRelayed(addrs: readonly Multiaddr[]): Multiaddr[] {
198
+ const relayed = (addr: Multiaddr): boolean => addr.getComponents().some((c) => c.name === 'p2p-circuit');
199
+ return [...addrs.filter((addr) => !relayed(addr)), ...addrs.filter(relayed)];
200
+ }
201
+
202
+ function allAttemptsFailed(label: string, failures: AddrAttemptFailure[]): Error {
203
+ if (failures.length === 0) {
204
+ return new Error(`${label}: no candidate addresses`);
205
+ }
206
+ if (failures.length === 1) {
207
+ return failures[0].error;
208
+ }
209
+ const detail = failures.map((f) => `${f.addr.toString()} — ${f.error.message}`).join('; ');
210
+ return new Error(`${label} failed for all ${failures.length} candidate addresses: ${detail}`, { cause: failures[0].error });
211
+ }
212
+
213
+ function nextMacrotask(): Promise<void> {
214
+ return new Promise((resolve) => setTimeout(resolve, 0));
215
+ }