@serfab/cadre-core 1.2.0 → 1.4.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 (46) hide show
  1. package/README.md +5 -3
  2. package/dist/cadre-node.d.ts +32 -19
  3. package/dist/cadre-node.js +121 -31
  4. package/dist/cadre-node.js.map +1 -1
  5. package/dist/control-formation-recorder.d.ts +4 -4
  6. package/dist/control-formation-recorder.js +6 -6
  7. package/dist/control-formation-recorder.js.map +1 -1
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +5 -3
  10. package/dist/index.js.map +1 -1
  11. package/dist/membership-connection-gater.d.ts +18 -5
  12. package/dist/membership-connection-gater.js +50 -10
  13. package/dist/membership-connection-gater.js.map +1 -1
  14. package/dist/storage-scope.d.ts +50 -12
  15. package/dist/storage-scope.js +85 -12
  16. package/dist/storage-scope.js.map +1 -1
  17. package/dist/strand-formation-manager.js +2 -1
  18. package/dist/strand-formation-manager.js.map +1 -1
  19. package/dist/strand-id.d.ts +35 -0
  20. package/dist/strand-id.js +46 -0
  21. package/dist/strand-id.js.map +1 -0
  22. package/dist/strand-instance-manager.js +20 -2
  23. package/dist/strand-instance-manager.js.map +1 -1
  24. package/dist/strand-network-config.d.ts +3 -3
  25. package/dist/strand-network-config.js +3 -3
  26. package/dist/strand-solicitation.js +2 -1
  27. package/dist/strand-solicitation.js.map +1 -1
  28. package/dist/strand-watcher.d.ts +45 -0
  29. package/dist/strand-watcher.js +74 -13
  30. package/dist/strand-watcher.js.map +1 -1
  31. package/dist/types.d.ts +118 -15
  32. package/dist/types.js +59 -0
  33. package/dist/types.js.map +1 -1
  34. package/package.json +7 -7
  35. package/src/cadre-node.ts +127 -31
  36. package/src/control-formation-recorder.ts +6 -6
  37. package/src/index.ts +563 -554
  38. package/src/membership-connection-gater.ts +56 -11
  39. package/src/storage-scope.ts +163 -78
  40. package/src/strand-formation-manager.ts +647 -646
  41. package/src/strand-id.ts +48 -0
  42. package/src/strand-instance-manager.ts +21 -2
  43. package/src/strand-network-config.ts +250 -250
  44. package/src/strand-solicitation.ts +500 -499
  45. package/src/strand-watcher.ts +388 -325
  46. package/src/types.ts +121 -15
package/src/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ConnectionGater, Libp2p, PeerId, PrivateKey } from '@libp2p/interface';
2
- import type { IRawStorage, Libp2pTransports } from '@optimystic/db-p2p';
2
+ import type { IRawStorage, Libp2pConnectionMonitorInit, Libp2pTransports, NoiseCryptoInterface } from '@optimystic/db-p2p';
3
3
  import type { IPeerNetwork, IRepo } from '@optimystic/db-core';
4
4
  import type { PeerJoinBackfillConfig } from './peer-join-backfill.js';
5
5
  import type { StrandRevocationEnforcementConfig } from './strand-revocation-enforcer.js';
@@ -121,13 +121,13 @@ export const HIBERNATION_TIMEOUTS: Record<LatencyHint, HibernationTimeouts> = {
121
121
  * Factory functions are useful for creating per-scope storage instances.
122
122
  *
123
123
  * **The argument is an opaque scope key.** Use it directly as a file name, directory
124
- * name or database name: every key cadre-core mints stays within `[A-Za-z0-9._-]`, so
125
- * no escaping is needed and none should be assumed. Do not parse it;
126
- * `controlStorageScope` / `isControlStorageScope` (`storage-scope.ts`) are the
127
- * supported way to mint and recognize the control key. The strand arm of that charset
128
- * rule is intended but not yet checked — a strand id replicated in from another node
129
- * in the party reaches this callback verbatim; see
130
- * `tickets/backlog/bug-strand-scope-key-charset-unenforced`.
124
+ * name or database name: every key stays within `[A-Za-z0-9._-]`, so no escaping is
125
+ * needed and none should be assumed. Do not parse it; `controlStorageScope` /
126
+ * `isControlStorageScope` (`storage-scope.ts`) are the supported way to mint and
127
+ * recognize the control key. The control key holds the charset by base64url encoding;
128
+ * a strand's key — its strand id, which may have replicated in from another node in
129
+ * the party — holds it because `StrandInstanceManager.startStrand` runs
130
+ * `assertStrandScopeKey` on every launch and refuses a strand that fails it.
131
131
  *
132
132
  * **Called once per scope per runtime lifetime.** The scopes are the control database
133
133
  * (once per `CadreNode.start()`) and each strand id (once per `startStrand`).
@@ -366,6 +366,46 @@ export interface NetworkConfig {
366
366
  * ```
367
367
  */
368
368
  transports?: Libp2pTransports;
369
+ /**
370
+ * Crypto primitives for the Noise handshake and the encrypted connection that
371
+ * follows, threaded to both the control node and every strand's cohort node — every
372
+ * node pays the handshake, so one setting covers them all. When omitted, libp2p-noise
373
+ * picks its own default.
374
+ *
375
+ * React Native is the reason this exists: Metro resolves `@chainsafe/libp2p-noise`'s
376
+ * browser build, whose default is pure-JS crypto, and on Hermes (no JIT) that
377
+ * dominates connection setup on a slow phone. An app with native crypto supplies it
378
+ * here. It must implement every member of the interface; the usual shape spreads
379
+ * `noisePureJsCrypto` and overrides the hashing and ChaCha20-Poly1305 functions:
380
+ * ```typescript
381
+ * import { noisePureJsCrypto } from '@optimystic/db-p2p';
382
+ *
383
+ * network: {
384
+ * noiseCrypto: { ...noisePureJsCrypto, hashSHA256: nativeSha256, chaCha20Poly1305Encrypt: …, chaCha20Poly1305Decrypt: … }
385
+ * }
386
+ * ```
387
+ *
388
+ * Only local primitives change — the wire protocol does not, so a node with native
389
+ * crypto interoperates with one without. See `@optimystic/db-p2p`'s
390
+ * `NodeOptions.noiseCrypto`.
391
+ */
392
+ noiseCrypto?: NoiseCryptoInterface;
393
+ /**
394
+ * libp2p's connection monitor — the liveness ping it runs on every connection — for
395
+ * the control node and every strand node, as {@link noiseCrypto} is. When omitted,
396
+ * cadre-core applies {@link DEFAULT_CONNECTION_MONITOR} rather than leaving libp2p's
397
+ * own defaults in place; an explicit value REPLACES that default wholesale, so `{}`
398
+ * is how an app asks for libp2p's stock behaviour back.
399
+ *
400
+ * Typed from `@optimystic/db-p2p`'s re-export of libp2p's `ConnectionMonitorInit`, so
401
+ * an app does not need a direct `libp2p` dependency, and handed to db-p2p's
402
+ * `NodeOptions.connectionMonitor` unchanged.
403
+ *
404
+ * An app that widens the ping deadline itself must raise `pingInterval` above it in
405
+ * the same object, or libp2p aborts the connection on the second overlapping ping —
406
+ * see {@link DEFAULT_CONNECTION_MONITOR} for why.
407
+ */
408
+ connectionMonitor?: Libp2pConnectionMonitorInit;
369
409
  /**
370
410
  * Optional async resolver returning the multiaddrs to embed in invites
371
411
  * (and other owner-address contexts). When unset, `libp2pNode.getMultiaddrs()`
@@ -447,6 +487,66 @@ export interface NetworkConfig {
447
487
  };
448
488
  }
449
489
 
490
+ /**
491
+ * The connection-monitor settings every cadre node runs with when
492
+ * {@link NetworkConfig.connectionMonitor} is unset: a 30 second ping deadline in place
493
+ * of libp2p's 5 second one, and a 35 second gap between pings so a ping is never
494
+ * outstanding when the next one starts.
495
+ *
496
+ * WHY this is a default and not opt-in. libp2p's monitor pings every connection and,
497
+ * with its own `abortConnectionOnPingFailure: true`, aborts the connection on the FIRST
498
+ * timeout. A peer whose event loop is saturated by pure-JS Noise crypto — a slow phone
499
+ * under React Native, see {@link NetworkConfig.noiseCrypto} — misses that deadline while
500
+ * perfectly healthy; the other end aborts, the peer redials, and the new handshake
501
+ * saturates it further. Measured on gotchoices/sereus#13 at a Galaxy S7's crypto cost, a
502
+ * two-party bring-up failed 3 of 3 runs on stock settings, with 30 `aborting connection
503
+ * due to ping failure` entries in the relay's log for one run, and passed 4 of 4 in about
504
+ * 90 seconds once the deadline was widened on every node. The monitor runs on BOTH ends
505
+ * of a connection and either end's abort closes it for both, so a setting the phone alone
506
+ * applies does not cover the peer dropping it. That is what rules out scoping this to
507
+ * React Native.
508
+ *
509
+ * WHY `pingInterval` MOVES WITH THE DEADLINE, and is not left at libp2p's 10 seconds.
510
+ * The monitor opens a ping stream per connection per interval whether or not the previous
511
+ * ping has answered, and `/ipfs/ping/1.0.0` is registered by `@libp2p/ping` with
512
+ * `maxOutboundStreams: 1`. A second concurrent ping stream on one connection therefore
513
+ * fails in `Connection.newStream` with `TooManyOutboundProtocolStreamsError`, which
514
+ * reaches the monitor's own catch and aborts the connection exactly as a timeout does. A
515
+ * widened deadline alone is thus capped by the ping interval: measured against two local
516
+ * libp2p 3.1.3 nodes whose ping handler answered 600ms late, an interval of 300ms with a
517
+ * 900ms deadline aborted the connection, while a 900ms interval with the same deadline
518
+ * kept it (the same pair aborted at a 200ms stall only when the deadline was 300ms). An
519
+ * interval strictly above the deadline is what makes the 30 seconds real.
520
+ *
521
+ * WHAT IT COSTS. A dead peer is reclaimed 30 to 65 seconds after it stops answering — the
522
+ * deadline, plus up to one interval of waiting for the ping that will fail — where
523
+ * libp2p's defaults took about 5 to 15 seconds. `db-p2p` caps a node at 16 connections,
524
+ * so the worst case is those slots held about a minute longer than before; at that scale
525
+ * it is not a starvation risk.
526
+ *
527
+ * WHY THE DEADLINE IS PINNED rather than given room to adapt. `pingTimeout` is an
528
+ * adaptive-timeout init, and equal `minTimeout`/`maxTimeout` clamp it to one value on
529
+ * every libp2p version. Under libp2p 3.1.3, which sereus resolves today, it is already
530
+ * flat at `minTimeout`: `ConnectionMonitor` asks its `AdaptiveTimeout` for a deadline but
531
+ * never calls `cleanUp` to report how long the ping took, so the moving average the
532
+ * deadline derives from stays at zero. libp2p 3.3 does report ping durations back, and a
533
+ * ceiling above `pingInterval` would then let the deadline grow past the interval and put
534
+ * the overlapping-ping abort above straight back. Pinning both ends keeps the interval's
535
+ * margin true on the version bump instead of making it something to remember. It also
536
+ * sidesteps 3.3's other surprise: the monitor keeps one `AdaptiveTimeout` for all of a
537
+ * node's connections, so one slow peer would otherwise lengthen the deadline for every
538
+ * connection on that node.
539
+ */
540
+ export const DEFAULT_CONNECTION_MONITOR = Object.freeze({
541
+ // Strictly greater than the deadline below, so the previous ping is always resolved or
542
+ // aborted before the next one opens a stream. `types.spec.ts` holds the two apart.
543
+ pingInterval: 35_000,
544
+ // Frozen at both levels, as `CONTROL_CLUSTER_POLICY` is: one object reaches every
545
+ // libp2p node this process builds, so a mutation anywhere would move the deadline
546
+ // for all of them.
547
+ pingTimeout: Object.freeze({ minTimeout: 30_000, maxTimeout: 30_000 })
548
+ } satisfies Libp2pConnectionMonitorInit);
549
+
450
550
  /**
451
551
  * Hibernation configuration
452
552
  */
@@ -1231,13 +1331,19 @@ export interface CadreNodeEvents {
1231
1331
  * Carries the full {@link StrandRow} so the app can join without re-querying
1232
1332
  * the control DB.
1233
1333
  *
1234
- * **Fired exactly ONCE per strand per session, and it can fire before your
1235
- * listener is attached.** The strand watcher's first poll runs inside
1236
- * `CadreNode.start()` (100 ms after the watcher starts), so every strand
1237
- * already stored for this party is normally offered while the embedding app is
1238
- * still inside its own `start()` await. The watcher then records the strand as
1239
- * seen and no later poll re-offers it, so a listener attached a moment late
1240
- * misses those strands for the life of the process.
1334
+ * **Fired once per strand per session, and it can fire before your listener is
1335
+ * attached.** The strand watcher's first poll runs inside `CadreNode.start()`
1336
+ * (100 ms after the watcher starts), so every strand already stored for this
1337
+ * party is normally offered while the embedding app is still inside its own
1338
+ * `start()` await. The watcher then records the strand as seen and no later poll
1339
+ * re-offers it, so a listener attached a moment late misses those strands for the
1340
+ * life of the process.
1341
+ *
1342
+ * One exception, and it does NOT produce a second event: when an `addStrand` that
1343
+ * claims a discovered strand FAILS, the watcher is told to forget the strand so a
1344
+ * later poll retries it. That claim left the sApp config registered, so the retry
1345
+ * takes the auto-launch branch — what the app sees is `strand:error` per failed
1346
+ * retry and `strand:started` when one succeeds.
1241
1347
  *
1242
1348
  * So an app that auto-joins discovered strands must **subscribe first, then
1243
1349
  * drain `CadreNode.getDiscoveredStrands()`** — the map of strands no local