@serfab/cadre-core 1.3.0 → 1.5.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 (90) hide show
  1. package/README.md +5 -3
  2. package/dist/cadre-node.d.ts +70 -34
  3. package/dist/cadre-node.js +197 -60
  4. package/dist/cadre-node.js.map +1 -1
  5. package/dist/control-database.d.ts +24 -10
  6. package/dist/control-database.js +48 -16
  7. package/dist/control-database.js.map +1 -1
  8. package/dist/control-formation-recorder.d.ts +4 -4
  9. package/dist/control-formation-recorder.js +6 -6
  10. package/dist/control-formation-recorder.js.map +1 -1
  11. package/dist/control-read-retry.d.ts +12 -0
  12. package/dist/control-read-retry.js +12 -0
  13. package/dist/control-read-retry.js.map +1 -1
  14. package/dist/control-write-retry.d.ts +5 -4
  15. package/dist/control-write-retry.js +24 -7
  16. package/dist/control-write-retry.js.map +1 -1
  17. package/dist/index.d.ts +5 -4
  18. package/dist/index.js +10 -5
  19. package/dist/index.js.map +1 -1
  20. package/dist/link-budget.d.ts +182 -0
  21. package/dist/link-budget.js +195 -0
  22. package/dist/link-budget.js.map +1 -0
  23. package/dist/membership-connection-gater.d.ts +18 -5
  24. package/dist/membership-connection-gater.js +50 -10
  25. package/dist/membership-connection-gater.js.map +1 -1
  26. package/dist/peer-dial.d.ts +36 -17
  27. package/dist/peer-dial.js +37 -17
  28. package/dist/peer-dial.js.map +1 -1
  29. package/dist/peer-join-backfill.d.ts +58 -4
  30. package/dist/peer-join-backfill.js +165 -20
  31. package/dist/peer-join-backfill.js.map +1 -1
  32. package/dist/relay-reservation.d.ts +59 -11
  33. package/dist/relay-reservation.js +169 -43
  34. package/dist/relay-reservation.js.map +1 -1
  35. package/dist/seed-bootstrap.js +2 -1
  36. package/dist/seed-bootstrap.js.map +1 -1
  37. package/dist/storage-scope.d.ts +50 -12
  38. package/dist/storage-scope.js +85 -12
  39. package/dist/storage-scope.js.map +1 -1
  40. package/dist/strand-first-sync-gate.d.ts +43 -5
  41. package/dist/strand-first-sync-gate.js +55 -7
  42. package/dist/strand-first-sync-gate.js.map +1 -1
  43. package/dist/strand-formation-manager.js +2 -1
  44. package/dist/strand-formation-manager.js.map +1 -1
  45. package/dist/strand-id.d.ts +35 -0
  46. package/dist/strand-id.js +46 -0
  47. package/dist/strand-id.js.map +1 -0
  48. package/dist/strand-instance-manager.d.ts +27 -2
  49. package/dist/strand-instance-manager.js +75 -15
  50. package/dist/strand-instance-manager.js.map +1 -1
  51. package/dist/strand-membership-reconciler.d.ts +200 -45
  52. package/dist/strand-membership-reconciler.js +302 -86
  53. package/dist/strand-membership-reconciler.js.map +1 -1
  54. package/dist/strand-membership-writer.d.ts +3 -2
  55. package/dist/strand-membership-writer.js +67 -90
  56. package/dist/strand-membership-writer.js.map +1 -1
  57. package/dist/strand-network-config.d.ts +3 -3
  58. package/dist/strand-network-config.js +3 -3
  59. package/dist/strand-solicitation.js +2 -1
  60. package/dist/strand-solicitation.js.map +1 -1
  61. package/dist/strand-watcher.d.ts +45 -0
  62. package/dist/strand-watcher.js +74 -13
  63. package/dist/strand-watcher.js.map +1 -1
  64. package/dist/types.d.ts +190 -24
  65. package/dist/types.js +73 -4
  66. package/dist/types.js.map +1 -1
  67. package/package.json +8 -8
  68. package/src/cadre-node.ts +7445 -7302
  69. package/src/control-database.ts +48 -16
  70. package/src/control-formation-recorder.ts +6 -6
  71. package/src/control-read-retry.ts +214 -202
  72. package/src/control-write-retry.ts +24 -7
  73. package/src/index.ts +29 -2
  74. package/src/link-budget.ts +217 -0
  75. package/src/membership-connection-gater.ts +56 -11
  76. package/src/peer-dial.ts +39 -17
  77. package/src/peer-join-backfill.ts +192 -23
  78. package/src/relay-reservation.ts +1119 -951
  79. package/src/seed-bootstrap.ts +2 -1
  80. package/src/storage-scope.ts +163 -78
  81. package/src/strand-first-sync-gate.ts +55 -7
  82. package/src/strand-formation-manager.ts +647 -646
  83. package/src/strand-id.ts +48 -0
  84. package/src/strand-instance-manager.ts +1516 -1440
  85. package/src/strand-membership-reconciler.ts +334 -92
  86. package/src/strand-membership-writer.ts +1962 -1987
  87. package/src/strand-network-config.ts +250 -250
  88. package/src/strand-solicitation.ts +500 -499
  89. package/src/strand-watcher.ts +388 -325
  90. package/src/types.ts +192 -23
package/README.md CHANGED
@@ -236,9 +236,11 @@ A **scope key** is what the factory receives. It is the strand id for each stran
236
236
  which holds one party's own records and must not be shared between parties. Every key is
237
237
  opaque and already safe as a file, directory or database name (always within
238
238
  `[A-Za-z0-9._-]`): use it verbatim, do not parse it. `isControlStorageScope(scope)` tells
239
- the two kinds apart. Caveat, until `bug-strand-scope-key-charset-unenforced` lands: the
240
- charset is enforced for the control key but only *conventional* for a strand id, which
241
- reaches the factory verbatim even when the strand row replicated in from another node.
239
+ the two kinds apart. The control key holds the charset by base64url encoding; a strand
240
+ id holds it by check — `assertStrandScopeKey` runs on every strand launch, so a strand
241
+ whose row replicated in from another node with an unusable id is refused rather than
242
+ handed to the factory, and on `publishStrand`, so this node never writes such an id into
243
+ the party's control database to begin with.
242
244
 
243
245
  Available storage implementations:
244
246
  | Package | Environment | Description |
@@ -279,8 +279,10 @@ export declare class CadreNode implements SAppIdLookup {
279
279
  * dead entry behind; recovery today is a fresh invitation.
280
280
  *
281
281
  * NOTE: entries are kept for the node's lifetime, including across a
282
- * {@link stopStrand} — a stopped strand may be rediscovered by the watcher and must
283
- * still get its seed. Each strand's list is capped at `MAX_STRAND_ADDRS`, but the
282
+ * {@link stopStrand} — a stopped strand may be claimed again with {@link addStrand}
283
+ * (the watcher itself will not re-offer it: the stop suppresses the id for the rest of
284
+ * the session) and must still get its seed. Each strand's list is capped at
285
+ * `MAX_STRAND_ADDRS`, but the
284
286
  * number of KEYS is bounded only by the strands this node has ever formed as an
285
287
  * initiator (not by time); if a node ever forms strands at scale, evict on
286
288
  * `unpublishStrand` or cap the map.
@@ -306,8 +308,10 @@ export declare class CadreNode implements SAppIdLookup {
306
308
  * (`strand-membership-reconciler.ts`, wired via `StartStrandConfig.pendingMembershipInvite`
307
309
  * in {@link launchStrand}): it deletes the entry once the invitation is redeemed, burned
308
310
  * against an already-seated member, or found dead (expired / cancelled / consumed
309
- * elsewhere / the strand sealed). Until the strand is actually launched here the entry
310
- * just waits.
311
+ * elsewhere / the strand sealed) — naming the invitation it settled, so a re-formation
312
+ * that replaced the entry meanwhile keeps its fresh one
313
+ * ({@link unstageMembershipInvite}). Until the strand is actually launched here the entry
314
+ * just waits; once launched, staging notifies the manager, which re-arms a finished loop.
311
315
  *
312
316
  * NOTE: entries live for the node's lifetime (one small pair per formed closed
313
317
  * strand), the same unbounded-keys tripwire {@link crossPartyStrandAddrs} documents —
@@ -732,6 +736,12 @@ export declare class CadreNode implements SAppIdLookup {
732
736
  * derivation from it (`db-p2p` namespaces all of the node's protocol ids as
733
737
  * `/optimystic/<networkName>/...`), so the node options and the block-transfer
734
738
  * protocol prefix the control backfill dials can never drift apart.
739
+ *
740
+ * NOTE: the party id goes in UNENCODED here, unlike in `controlStorageScope`. Safe
741
+ * today because a party id is locally configured rather than replicated in, and both
742
+ * ends of a connection derive this string identically — an odd party id yields an odd
743
+ * but consistent protocol id, not a mismatch or an escaped name. If a party id ever
744
+ * arrives from the network, encode it here as the storage scope key already does.
735
745
  */
736
746
  private controlNetworkName;
737
747
  /**
@@ -1349,9 +1359,14 @@ export declare class CadreNode implements SAppIdLookup {
1349
1359
  * {@link SeedBootstrapService} this node builds — `applySeed`'s owner dials and
1350
1360
  * `dialInvite`.
1351
1361
  *
1352
- * See {@link DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS} for why a peer's dial is
1353
- * bounded as a whole, and {@link DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS}
1354
- * for why each address is bounded as well.
1362
+ * See `peer-dial.ts`'s `DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS` for why a peer's dial is
1363
+ * bounded as a whole, and `DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS` for why each
1364
+ * address is bounded as well.
1365
+ *
1366
+ * Both are DERIVED from `network.linkRoundTripMs` rather than fixed, because the slowest
1367
+ * address either has to cover is a relayed dial and that costs a fixed number of exchanges —
1368
+ * `link-budget.ts` has the counts. A host's explicit `controlCohort` values still win, so a
1369
+ * test that drives dead addresses on purpose keeps the duration it chose.
1355
1370
  */
1356
1371
  private controlDialBudget;
1357
1372
  /**
@@ -1862,17 +1877,17 @@ export declare class CadreNode implements SAppIdLookup {
1862
1877
  *
1863
1878
  * A rejected call leaves nothing running but DOES leave the sApp config
1864
1879
  * registered, deliberately: both an explicit retry and the {@link StrandWatcher}'s
1865
- * automatic relaunch need it. That background relaunch covers only a strand the
1866
- * WATCHER launched: it forgets a row whose `onStrandAdded` threw and re-attempts it
1867
- * on a later poll (each failure re-emitting `strand:error`) until it succeeds. A
1868
- * strand claimed here after it was DISCOVERED gets no such retry — the watcher
1869
- * already recorded it in its `knownStrands` when it offered it as
1870
- * `strand:discovered`, and no later poll re-offers a strand it knows. This call has
1871
- * also already dropped it from {@link getDiscoveredStrands}, so a failed claim
1872
- * leaves the strand dormant until the caller retries or the node restarts (see
1873
- * `backlog/bug-discovered-strand-lost-when-claim-fails`).
1874
- * {@link detachStrand} (reached via {@link stopStrand}) is what abandons a strand
1875
- * for good.
1880
+ * automatic relaunch need it. A failed launch here hands the strand back to that
1881
+ * relaunch — `StrandWatcher.forgetStrand` drops the id from the watcher's
1882
+ * `knownStrands` and records the backoff, so a later poll re-attempts it on the same
1883
+ * `pollInterval * 2^(failures-1)` ladder a watcher-driven failure gets. Because the
1884
+ * config stays registered, the retry takes `handleStrandAdded`'s auto-launch branch:
1885
+ * what the app sees is `strand:error` per failed retry and `strand:started` when one
1886
+ * succeeds, never a second `strand:discovered`. {@link detachStrand} (reached via
1887
+ * {@link stopStrand}) is what abandons a strand for good, and its stop suppresses the
1888
+ * strand in the watcher so this retry cannot resurrect it — a call to THIS method lifts
1889
+ * that suppression again, since an explicit claim is a deliberate reversal of the
1890
+ * deliberate stop.
1876
1891
  */
1877
1892
  addStrand(config: StrandConfig): Promise<StrandInstance>;
1878
1893
  /**
@@ -1945,7 +1960,8 @@ export declare class CadreNode implements SAppIdLookup {
1945
1960
  * @returns The live `Strand` row — the one just inserted, or the matching one already
1946
1961
  * there. A closed strand's caller should carry THIS row's `MemberPrivateKey` forward:
1947
1962
  * on a repeat it is the stored key, not the argument.
1948
- * @throws if the node is not started, exposes no owner signing key, the id is blank, a
1963
+ * @throws if the node is not started, exposes no owner signing key, the id is blank or
1964
+ * unusable as a storage scope key (`InvalidStrandIdError` — see `storage-scope.ts`), a
1949
1965
  * row with the same id holds different content, or the control DB rejects the
1950
1966
  * (unauthorized) insert.
1951
1967
  */
@@ -2372,8 +2388,10 @@ export declare class CadreNode implements SAppIdLookup {
2372
2388
  * caller runs the drive regardless).
2373
2389
  *
2374
2390
  * NOTE: against a relay that is DOWN this hook costs up to two strand-addr
2375
- * timeouts (10 s each: dial by peer id, then by addr) before the 10 s drive even
2376
- * starts, so one failed re-drive can hold the supervisor `driving` for ~30 s.
2391
+ * timeouts (10 s each: dial by peer id, then by addr) before the reservation drive even
2392
+ * starts, so one failed re-drive holds the supervisor `driving` for those 20 s plus the
2393
+ * drive's own deadline — 28 s at the default declared link round trip, and longer on a host
2394
+ * that declared a slower one (`link-budget.ts`).
2377
2395
  * Bounded and harmless while the relay is unreachable anyway; if recovery
2378
2396
  * latency after a relay comes back ever matters, skip the announce when the
2379
2397
  * control node holds no connection to the relay (the drive's own dial fails
@@ -2465,11 +2483,11 @@ export declare class CadreNode implements SAppIdLookup {
2465
2483
  * Stop a strand on THIS node only: untrack it from hibernation, drop its sApp config,
2466
2484
  * stop the local instance, and emit `strand:stopped`. The shared `Strand` row is left
2467
2485
  * intact, so on the next node RESTART the strand is rediscovered and surfaces as
2468
- * `strand:discovered` again — not on the next watcher poll, which never re-offers a
2469
- * strand it has already seen (`StrandWatcher.knownStrands` retains the id for the life
2470
- * of the process). The stop also drops the strand from {@link getDiscoveredStrands},
2471
- * so a drain cannot undo a deliberate stop. Party-wide removal is
2472
- * {@link unpublishStrand}.
2486
+ * `strand:discovered` again — but never again in THIS session: the stop suppresses the
2487
+ * id in the watcher (`StrandWatcher.suppressStrand`) and drops it from
2488
+ * {@link getDiscoveredStrands}, so neither a later poll nor a drain can undo a
2489
+ * deliberate stop. Only an explicit {@link addStrand} does, which is the caller
2490
+ * reversing its own decision. Party-wide removal is {@link unpublishStrand}.
2473
2491
  */
2474
2492
  stopStrand(strandId: string): Promise<void>;
2475
2493
  /**
@@ -2490,6 +2508,10 @@ export declare class CadreNode implements SAppIdLookup {
2490
2508
  * readings are the intended one: `handleStrandRemoved` arrives because the control row
2491
2509
  * is gone, and an explicit {@link stopStrand} is a deliberate abandonment — neither
2492
2510
  * strand may be re-offered to a later `getDiscoveredStrands()` drain.
2511
+ *
2512
+ * The watcher-level suppression that makes a stop permanent is deliberately NOT here,
2513
+ * only in {@link stopStrand}: the two callers diverge on it. A vanished control row
2514
+ * that reappears is a fresh strand and must be offered again.
2493
2515
  */
2494
2516
  private detachStrand;
2495
2517
  /**
@@ -2589,13 +2611,15 @@ export declare class CadreNode implements SAppIdLookup {
2589
2611
  * local), and only then does it reach out. Everything above in {@link start}
2590
2612
  * has completed by the time this runs.
2591
2613
  *
2592
- * BUDGET: the supervisor's first attempt — `relay-reservation.ts`'s
2593
- * `DEFAULT_RELAY_RESERVE_TIMEOUT_MS`, 10 s — is what `start()` waits on — deliberately the module default rather
2594
- * than a boot-specific one. A healthy dial-plus-reserve is sub-second even over
2595
- * a WAN, so 10 s is slack for a slow link; going much longer would make a dead
2596
- * relay indistinguishable from a hung start, and much shorter would fail nodes
2597
- * on links that were merely slow. The retries carry on in the background after
2598
- * this resolves, exactly as they do for a {@link reserveRelays} caller.
2614
+ * BUDGET: the supervisor's first attempt is what `start()` waits on — deliberately the drive's
2615
+ * ordinary deadline rather than a boot-specific one. That deadline is now COUNTED, four link
2616
+ * round trips at the declared `network.linkRoundTripMs` (8 s at its default, where it was a
2617
+ * fixed 10 s): a healthy dial-plus-reserve is sub-second even over a WAN, so this is slack for
2618
+ * a slow link, while going much longer would make a dead relay indistinguishable from a hung
2619
+ * start and much shorter would fail nodes on links that were merely slow. A host that declares
2620
+ * a slower link lengthens it without touching this path — `link-budget.ts`. The retries carry
2621
+ * on in the background after this resolves, exactly as they do for a {@link reserveRelays}
2622
+ * caller.
2599
2623
  *
2600
2624
  * `network.requireRelay === false` softens only the outcome below: a first
2601
2625
  * attempt that lands no `/p2p-circuit` address is logged instead of thrown, and
@@ -3044,7 +3068,11 @@ export declare class CadreNode implements SAppIdLookup {
3044
3068
  * an intact control DB) and mints + persists a fresh one otherwise. The invitation
3045
3069
  * will admit THIS key's public half as the `Strand.Member`.
3046
3070
  * 2. Stage the invitation in {@link pendingMembershipInvites} for the strand
3047
- * bring-up flow (`strand-node-binds-member-peer`) to redeem via `consumeInvite`.
3071
+ * bring-up flow (`strand-node-binds-member-peer`) to redeem via `consumeInvite`,
3072
+ * and tell the instance manager, which re-arms an already-finished membership
3073
+ * reconciler so a RE-formation against a launched strand is attempted at once — the
3074
+ * removed-party case, where the loop finished long before the removal. A strand not
3075
+ * launched here yet has no loop to re-arm, and its bring-up finds the entry.
3048
3076
  *
3049
3077
  * Throws — failing the whole {@link formStrand} — when the identity cannot be
3050
3078
  * persisted: a joiner "joined" without a persistable identity could never become a
@@ -3054,6 +3082,14 @@ export declare class CadreNode implements SAppIdLookup {
3054
3082
  * rejected) and redeeming a FRESH invitation.
3055
3083
  */
3056
3084
  private adoptFormationMembershipInvite;
3085
+ /**
3086
+ * The reconciler's half of the {@link pendingMembershipInvites} seam: drop `settled`
3087
+ * (spent, burned, or dead) only while it is still the staged entry. A re-formation
3088
+ * replaces the entry between a pass's read and its settle, and the fresh invitation it
3089
+ * staged is the one the re-armed loop is about to redeem — deleting it here would lose
3090
+ * it silently, the very outcome the re-arm exists to prevent.
3091
+ */
3092
+ private unstageMembershipInvite;
3057
3093
  /**
3058
3094
  * The pending single-use membership invitation a closed-strand formation carried back
3059
3095
  * for `strandId`, or `undefined` when none is staged — the seam the strand bring-up