@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.
Files changed (121) hide show
  1. package/README.md +3 -2
  2. package/dist/announce-addrs.d.ts +49 -0
  3. package/dist/announce-addrs.js +78 -0
  4. package/dist/announce-addrs.js.map +1 -0
  5. package/dist/cadre-node.d.ts +607 -53
  6. package/dist/cadre-node.js +1268 -157
  7. package/dist/cadre-node.js.map +1 -1
  8. package/dist/control-database.d.ts +174 -23
  9. package/dist/control-database.js +317 -107
  10. package/dist/control-database.js.map +1 -1
  11. package/dist/control-read-retry.d.ts +103 -0
  12. package/dist/control-read-retry.js +145 -0
  13. package/dist/control-read-retry.js.map +1 -0
  14. package/dist/control-retry.d.ts +101 -0
  15. package/dist/control-retry.js +108 -0
  16. package/dist/control-retry.js.map +1 -0
  17. package/dist/control-schema.d.ts +1 -1
  18. package/dist/control-schema.js +50 -6
  19. package/dist/control-schema.js.map +1 -1
  20. package/dist/control-write-retry.d.ts +59 -35
  21. package/dist/control-write-retry.js +21 -92
  22. package/dist/control-write-retry.js.map +1 -1
  23. package/dist/delegate-admission.d.ts +17 -15
  24. package/dist/delegate-admission.js +17 -15
  25. package/dist/delegate-admission.js.map +1 -1
  26. package/dist/enrolled-machine-store-file.d.ts +13 -0
  27. package/dist/enrolled-machine-store-file.js +41 -0
  28. package/dist/enrolled-machine-store-file.js.map +1 -0
  29. package/dist/enrolled-machine-store.d.ts +81 -0
  30. package/dist/enrolled-machine-store.js +225 -0
  31. package/dist/enrolled-machine-store.js.map +1 -0
  32. package/dist/file-durable-slot.js +5 -3
  33. package/dist/file-durable-slot.js.map +1 -1
  34. package/dist/index.d.ts +10 -6
  35. package/dist/index.js +39 -12
  36. package/dist/index.js.map +1 -1
  37. package/dist/membership-connection-gater.d.ts +226 -26
  38. package/dist/membership-connection-gater.js +316 -30
  39. package/dist/membership-connection-gater.js.map +1 -1
  40. package/dist/node-local-snapshot.d.ts +6 -0
  41. package/dist/node-local-snapshot.js +7 -1
  42. package/dist/node-local-snapshot.js.map +1 -1
  43. package/dist/{strand-backfill.d.ts → peer-join-backfill.d.ts} +49 -20
  44. package/dist/{strand-backfill.js → peer-join-backfill.js} +146 -56
  45. package/dist/peer-join-backfill.js.map +1 -0
  46. package/dist/peer-record.d.ts +26 -0
  47. package/dist/peer-record.js +32 -0
  48. package/dist/peer-record.js.map +1 -1
  49. package/dist/relay-addrs.d.ts +145 -22
  50. package/dist/relay-addrs.js +314 -31
  51. package/dist/relay-addrs.js.map +1 -1
  52. package/dist/relay-reservation.d.ts +20 -15
  53. package/dist/relay-reservation.js +18 -14
  54. package/dist/relay-reservation.js.map +1 -1
  55. package/dist/strand-addr-protocol.js +16 -0
  56. package/dist/strand-addr-protocol.js.map +1 -1
  57. package/dist/strand-database.d.ts +12 -0
  58. package/dist/strand-database.js +16 -0
  59. package/dist/strand-database.js.map +1 -1
  60. package/dist/strand-formation-manager.d.ts +22 -9
  61. package/dist/strand-formation-manager.js +20 -12
  62. package/dist/strand-formation-manager.js.map +1 -1
  63. package/dist/strand-formation-protocol.d.ts +83 -9
  64. package/dist/strand-formation-protocol.js +86 -10
  65. package/dist/strand-formation-protocol.js.map +1 -1
  66. package/dist/strand-instance-manager.d.ts +151 -17
  67. package/dist/strand-instance-manager.js +237 -32
  68. package/dist/strand-instance-manager.js.map +1 -1
  69. package/dist/strand-membership-writer.d.ts +118 -14
  70. package/dist/strand-membership-writer.js +203 -15
  71. package/dist/strand-membership-writer.js.map +1 -1
  72. package/dist/strand-network-config.d.ts +84 -0
  73. package/dist/strand-network-config.js +136 -0
  74. package/dist/strand-network-config.js.map +1 -0
  75. package/dist/strand-revocation-enforcer.d.ts +392 -0
  76. package/dist/strand-revocation-enforcer.js +533 -0
  77. package/dist/strand-revocation-enforcer.js.map +1 -0
  78. package/dist/strand-solicitation.d.ts +10 -3
  79. package/dist/strand-solicitation.js +12 -7
  80. package/dist/strand-solicitation.js.map +1 -1
  81. package/dist/strand-wake-protocol.d.ts +47 -2
  82. package/dist/strand-wake-protocol.js +76 -6
  83. package/dist/strand-wake-protocol.js.map +1 -1
  84. package/dist/strand-watcher.js +6 -1
  85. package/dist/strand-watcher.js.map +1 -1
  86. package/dist/types.d.ts +282 -43
  87. package/dist/types.js +7 -1
  88. package/dist/types.js.map +1 -1
  89. package/package.json +16 -8
  90. package/src/announce-addrs.ts +93 -0
  91. package/src/cadre-node.ts +1366 -171
  92. package/src/control-database.ts +2628 -2406
  93. package/src/control-read-retry.ts +167 -0
  94. package/src/control-retry.ts +190 -0
  95. package/src/control-schema.ts +50 -6
  96. package/src/control-write-retry.ts +30 -130
  97. package/src/delegate-admission.ts +17 -15
  98. package/src/enrolled-machine-store-file.ts +43 -0
  99. package/src/enrolled-machine-store.ts +280 -0
  100. package/src/file-durable-slot.ts +5 -3
  101. package/src/index.ts +477 -407
  102. package/src/membership-connection-gater.ts +539 -204
  103. package/src/node-local-snapshot.ts +13 -1
  104. package/src/peer-addr-book.ts +251 -250
  105. package/src/{strand-backfill.ts → peer-join-backfill.ts} +187 -75
  106. package/src/peer-record.ts +33 -0
  107. package/src/relay-addrs.ts +436 -111
  108. package/src/relay-reservation.ts +828 -823
  109. package/src/strand-addr-protocol.ts +17 -0
  110. package/src/strand-database.ts +17 -0
  111. package/src/strand-formation-manager.ts +511 -491
  112. package/src/strand-formation-protocol.ts +808 -692
  113. package/src/strand-instance-manager.ts +899 -611
  114. package/src/strand-membership-writer.ts +232 -15
  115. package/src/strand-network-config.ts +172 -0
  116. package/src/strand-revocation-enforcer.ts +653 -0
  117. package/src/strand-solicitation.ts +484 -472
  118. package/src/strand-wake-protocol.ts +94 -8
  119. package/src/strand-watcher.ts +319 -314
  120. package/src/types.ts +1554 -1309
  121. package/dist/strand-backfill.js.map +0 -1
@@ -44,9 +44,35 @@ export const WAKE_PROTOCOL = '/sereus/strand-wake/1.0.0';
44
44
  */
45
45
  const MAX_WAKE_SIZE = 64 * 1024;
46
46
 
47
- /** Default time to wait for the ack before abandoning a wake dial (ms). */
47
+ /** Default time to wait for the ack before abandoning ONE wake dial attempt (ms). */
48
48
  const DEFAULT_WAKE_TIMEOUT_MS = 10_000;
49
49
 
50
+ /**
51
+ * Default budget for a WHOLE {@link dialWake} call, in ms — every candidate
52
+ * address together, not each one.
53
+ *
54
+ * Without it the cost of a wake is (candidate count × {@link
55
+ * DEFAULT_WAKE_TIMEOUT_MS}), a number nothing chooses or bounds. That is not
56
+ * hypothetical: an address behind a dropped NAT mapping, or any host that
57
+ * blackholes rather than sending a RST, burns its full attempt timeout instead
58
+ * of failing in milliseconds, so a two-address peer costs 20 s and a
59
+ * five-address one costs 50 s.
60
+ *
61
+ * This makes the TARGET PEER the unit rather than the address — the same
62
+ * decision, for the same reason, as
63
+ * `DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS` in `control-cohort.ts`, and the same
64
+ * chosen number. 20 s is deliberately wider than one attempt timeout so a
65
+ * reachable peer whose signaling address is stale still gets a genuine try at
66
+ * its direct one; a peer needing longer than that is not "asleep and reachable",
67
+ * which is the only case a wake is for. The last attempt inside the budget gets
68
+ * whatever remains of it, so the call returns at the budget, not past it.
69
+ *
70
+ * Override per call with {@link DialWakeOptions.budgetMs} — tests that drive
71
+ * dead addresses on purpose set it low so a dial's duration is a chosen number
72
+ * rather than a transitive libp2p default stretched by machine load.
73
+ */
74
+ export const DEFAULT_WAKE_DIAL_BUDGET_MS = 20_000;
75
+
50
76
  /** Default time the receiver waits for an inbound wake frame before aborting (ms). */
51
77
  const DEFAULT_WAKE_READ_TIMEOUT_MS = 10_000;
52
78
 
@@ -234,19 +260,45 @@ export class StrandWakeService {
234
260
 
235
261
  /** Options for {@link dialWake}. */
236
262
  export interface DialWakeOptions {
237
- /** Per-dial timeout in ms (default {@link DEFAULT_WAKE_TIMEOUT_MS}). */
263
+ /** Per-ATTEMPT timeout in ms (default {@link DEFAULT_WAKE_TIMEOUT_MS}). */
238
264
  timeoutMs?: number;
265
+ /**
266
+ * Budget for the whole call — every candidate together (default
267
+ * {@link DEFAULT_WAKE_DIAL_BUDGET_MS}).
268
+ */
269
+ budgetMs?: number;
239
270
  /** Override the protocol id (defaults to {@link WAKE_PROTOCOL}). */
240
271
  protocolId?: string;
241
272
  }
242
273
 
274
+ /** One candidate's outcome, kept so the thrown error can name every attempt. */
275
+ interface WakeDialFailure {
276
+ addr: Multiaddr;
277
+ error: Error;
278
+ }
279
+
243
280
  /**
244
281
  * Sender side: dial a target's control-network address(es), send a
245
282
  * {@link WakeRequest} over `WAKE_PROTOCOL`, and return the peer's {@link WakeAck}.
246
283
  *
247
284
  * Tries each candidate address in order (signaling/relay first, as produced by
248
285
  * `CadreNode.resolvePeerAddrs`) until one dials, so a NAT'd peer is reachable via
249
- * its circuit-relay address. Throws if no address is dialable.
286
+ * its circuit-relay address. Each attempt is bounded by `timeoutMs` and the call
287
+ * as a whole by `budgetMs` (see {@link DEFAULT_WAKE_DIAL_BUDGET_MS}); a candidate
288
+ * the budget leaves no room for is reported as untried rather than silently
289
+ * dropped.
290
+ *
291
+ * Throws if no address is dialable — with an error naming EVERY candidate and
292
+ * why it failed, not merely the last one. That distinction is not cosmetic:
293
+ * with only the last candidate's message the failure that mattered (usually the
294
+ * signaling address, tried first) is invisible outside a debug log, and the
295
+ * surfaced message points at whichever address happened to be tried last.
296
+ *
297
+ * The candidate loop is deliberately explicit rather than one
298
+ * `dialProtocol(addrs)` call. libp2p sorts any multi-address dial with
299
+ * `defaultAddressSorter`, whose `circuitRelayAddressesLast` pass would demote
300
+ * exactly the signaling address this ordering puts first — silently inverting
301
+ * it, with no per-dial sorter override to opt out of.
250
302
  */
251
303
  export async function dialWake(
252
304
  node: Libp2p,
@@ -259,23 +311,57 @@ export async function dialWake(
259
311
  }
260
312
  const protocolId = options.protocolId ?? WAKE_PROTOCOL;
261
313
  const timeoutMs = options.timeoutMs ?? DEFAULT_WAKE_TIMEOUT_MS;
314
+ const budgetMs = options.budgetMs ?? DEFAULT_WAKE_DIAL_BUDGET_MS;
315
+ const deadline = Date.now() + budgetMs;
262
316
 
263
- let lastError: Error | null = null;
317
+ const failures: WakeDialFailure[] = [];
264
318
  for (const addr of addrs) {
319
+ // Whatever is left of the whole-call budget caps this attempt, so the last
320
+ // candidate inside the budget still gets a real (if shortened) try.
321
+ const remaining = deadline - Date.now();
322
+ if (remaining <= 0) {
323
+ failures.push({ addr, error: new Error(`not tried — the ${budgetMs}ms wake dial budget was spent on earlier addresses`) });
324
+ continue;
325
+ }
326
+ const attemptMs = Math.min(timeoutMs, remaining);
265
327
  try {
266
328
  // One deadline per attempt: its signal aborts the in-flight dialProtocol and
267
329
  // resets the live stream, so neither the connect nor the ack-read leaks.
268
330
  return await withDeadline(
269
- timeoutMs,
331
+ attemptMs,
270
332
  `Wake dial ${addr.toString()}`,
271
- (signal) => sendWake(node, addr, protocolId, request, timeoutMs, signal),
333
+ (signal) => sendWake(node, addr, protocolId, request, attemptMs, signal),
272
334
  );
273
335
  } catch (err) {
274
- lastError = err instanceof Error ? err : new Error(String(err));
336
+ failures.push({ addr, error: err instanceof Error ? err : new Error(String(err)) });
275
337
  log('Wake dial to %s failed: %o', addr.toString(), err);
276
338
  }
277
339
  }
278
- throw lastError ?? new Error('Wake dial failed');
340
+ throw wakeDialError(failures);
341
+ }
342
+
343
+ /**
344
+ * Fold every candidate's failure into the one error {@link dialWake} throws.
345
+ *
346
+ * A single candidate throws its own error unchanged, so a one-address wake reads
347
+ * exactly as it always has (and `rejects.toThrow(/timed out/)` still means what
348
+ * it says). Several candidates produce one message naming each address and its
349
+ * cause, with the FIRST failure as `cause` — the first candidate is the
350
+ * signaling address, which is the one that matters when a relayed peer goes
351
+ * unreachable.
352
+ */
353
+ function wakeDialError(failures: WakeDialFailure[]): Error {
354
+ if (failures.length === 0) {
355
+ return new Error('Wake dial failed');
356
+ }
357
+ if (failures.length === 1) {
358
+ return failures[0].error;
359
+ }
360
+ const detail = failures.map((f) => `${f.addr.toString()} — ${f.error.message}`).join('; ');
361
+ return new Error(
362
+ `Wake dial failed for all ${failures.length} candidate addresses: ${detail}`,
363
+ { cause: failures[0].error },
364
+ );
279
365
  }
280
366
 
281
367
  /**