@serfab/cadre-core 1.16.0 → 1.18.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 (268) hide show
  1. package/README.md +4 -3
  2. package/dist/bootstrap-peer-store-file.js +1 -1
  3. package/dist/bootstrap-peer-store.d.ts +1 -1
  4. package/dist/bootstrap-peer-store.js +4 -4
  5. package/dist/cadre-invite-protocol.d.ts +81 -142
  6. package/dist/cadre-invite-protocol.js +168 -458
  7. package/dist/cadre-invite-protocol.js.map +1 -1
  8. package/dist/cadre-join-request.d.ts +15 -2
  9. package/dist/cadre-join-request.js +22 -3
  10. package/dist/cadre-join-request.js.map +1 -1
  11. package/dist/cadre-node.d.ts +1089 -475
  12. package/dist/cadre-node.js +2170 -1037
  13. package/dist/cadre-node.js.map +1 -1
  14. package/dist/control-authorization.d.ts +36 -14
  15. package/dist/control-authorization.js +20 -3
  16. package/dist/control-authorization.js.map +1 -1
  17. package/dist/control-cohort.d.ts +88 -32
  18. package/dist/control-cohort.js +91 -33
  19. package/dist/control-cohort.js.map +1 -1
  20. package/dist/control-database.d.ts +856 -318
  21. package/dist/control-database.js +1422 -701
  22. package/dist/control-database.js.map +1 -1
  23. package/dist/control-formation-recorder.d.ts +46 -43
  24. package/dist/control-formation-recorder.js +77 -78
  25. package/dist/control-formation-recorder.js.map +1 -1
  26. package/dist/control-protocol-guard.d.ts +11 -4
  27. package/dist/control-protocol-guard.js +20 -5
  28. package/dist/control-protocol-guard.js.map +1 -1
  29. package/dist/control-schema-version.d.ts +97 -0
  30. package/dist/control-schema-version.js +225 -0
  31. package/dist/control-schema-version.js.map +1 -0
  32. package/dist/control-schema.d.ts +45 -5
  33. package/dist/control-schema.js +262 -78
  34. package/dist/control-schema.js.map +1 -1
  35. package/dist/control-write-retry.d.ts +45 -10
  36. package/dist/control-write-retry.js +92 -22
  37. package/dist/control-write-retry.js.map +1 -1
  38. package/dist/delegate-admission.d.ts +13 -0
  39. package/dist/delegate-admission.js +27 -0
  40. package/dist/delegate-admission.js.map +1 -1
  41. package/dist/ed25519-key.d.ts +10 -7
  42. package/dist/ed25519-key.js +23 -20
  43. package/dist/ed25519-key.js.map +1 -1
  44. package/dist/edge-node/config.d.ts +122 -0
  45. package/dist/edge-node/config.js +199 -0
  46. package/dist/edge-node/config.js.map +1 -0
  47. package/dist/edge-node/dissolve.d.ts +95 -0
  48. package/dist/edge-node/dissolve.js +184 -0
  49. package/dist/edge-node/dissolve.js.map +1 -0
  50. package/dist/edge-node/edge-join.d.ts +44 -0
  51. package/dist/edge-node/edge-join.js +227 -0
  52. package/dist/edge-node/edge-join.js.map +1 -0
  53. package/dist/edge-node/edge-node.d.ts +295 -0
  54. package/dist/edge-node/edge-node.js +704 -0
  55. package/dist/edge-node/edge-node.js.map +1 -0
  56. package/dist/edge-node/index.d.ts +18 -0
  57. package/dist/edge-node/index.js +18 -0
  58. package/dist/edge-node/index.js.map +1 -0
  59. package/dist/edge-node/join-credential.d.ts +11 -0
  60. package/dist/edge-node/join-credential.js +11 -0
  61. package/dist/edge-node/join-credential.js.map +1 -0
  62. package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
  63. package/dist/edge-node/lazy-raw-storage.js +134 -0
  64. package/dist/edge-node/lazy-raw-storage.js.map +1 -0
  65. package/dist/edge-node/node-local.d.ts +60 -0
  66. package/dist/edge-node/node-local.js +58 -0
  67. package/dist/edge-node/node-local.js.map +1 -0
  68. package/dist/edge-node/options.d.ts +83 -0
  69. package/dist/edge-node/options.js +110 -0
  70. package/dist/edge-node/options.js.map +1 -0
  71. package/dist/edge-node/owner-grant.d.ts +41 -0
  72. package/dist/edge-node/owner-grant.js +38 -0
  73. package/dist/edge-node/owner-grant.js.map +1 -0
  74. package/dist/edge-node/platform.d.ts +140 -0
  75. package/dist/edge-node/platform.js +12 -0
  76. package/dist/edge-node/platform.js.map +1 -0
  77. package/dist/edge-node/strand-move.d.ts +115 -0
  78. package/dist/edge-node/strand-move.js +223 -0
  79. package/dist/edge-node/strand-move.js.map +1 -0
  80. package/dist/edge-node/strands.d.ts +28 -0
  81. package/dist/edge-node/strands.js +60 -0
  82. package/dist/edge-node/strands.js.map +1 -0
  83. package/dist/enrolled-machine-store.d.ts +3 -3
  84. package/dist/enrolled-machine-store.js +3 -3
  85. package/dist/formation-token.d.ts +48 -0
  86. package/dist/formation-token.js +26 -0
  87. package/dist/formation-token.js.map +1 -0
  88. package/dist/index.d.ts +27 -17
  89. package/dist/index.js +39 -17
  90. package/dist/index.js.map +1 -1
  91. package/dist/joined-strand-store.d.ts +29 -12
  92. package/dist/joined-strand-store.js +66 -27
  93. package/dist/joined-strand-store.js.map +1 -1
  94. package/dist/key-store.d.ts +9 -4
  95. package/dist/key-store.js +9 -4
  96. package/dist/key-store.js.map +1 -1
  97. package/dist/link-budget.d.ts +9 -7
  98. package/dist/link-budget.js +9 -7
  99. package/dist/link-budget.js.map +1 -1
  100. package/dist/machine-removal.d.ts +19 -39
  101. package/dist/machine-removal.js +24 -98
  102. package/dist/machine-removal.js.map +1 -1
  103. package/dist/member-redemption.d.ts +256 -0
  104. package/dist/member-redemption.js +490 -0
  105. package/dist/member-redemption.js.map +1 -0
  106. package/dist/membership-view.d.ts +91 -0
  107. package/dist/membership-view.js +110 -0
  108. package/dist/membership-view.js.map +1 -0
  109. package/dist/owner-anchor-sync.d.ts +4 -4
  110. package/dist/owner-anchor-sync.js +16 -15
  111. package/dist/owner-anchor-sync.js.map +1 -1
  112. package/dist/owner-grant-protocol.d.ts +251 -0
  113. package/dist/owner-grant-protocol.js +344 -0
  114. package/dist/owner-grant-protocol.js.map +1 -0
  115. package/dist/owner-key-export.d.ts +70 -0
  116. package/dist/owner-key-export.js +279 -0
  117. package/dist/owner-key-export.js.map +1 -0
  118. package/dist/owner-key-flows.d.ts +55 -0
  119. package/dist/owner-key-flows.js +60 -0
  120. package/dist/owner-key-flows.js.map +1 -0
  121. package/dist/owner-key-store.d.ts +31 -0
  122. package/dist/owner-key-store.js +81 -0
  123. package/dist/owner-key-store.js.map +1 -0
  124. package/dist/owner-keys.d.ts +78 -0
  125. package/dist/owner-keys.js +133 -0
  126. package/dist/owner-keys.js.map +1 -0
  127. package/dist/owner-signer.d.ts +54 -0
  128. package/dist/owner-signer.js +51 -0
  129. package/dist/owner-signer.js.map +1 -0
  130. package/dist/peer-addr-book.d.ts +10 -1
  131. package/dist/peer-addr-book.js +4 -3
  132. package/dist/peer-addr-book.js.map +1 -1
  133. package/dist/peer-authorization.d.ts +83 -18
  134. package/dist/peer-authorization.js +123 -53
  135. package/dist/peer-authorization.js.map +1 -1
  136. package/dist/peer-join-backfill.d.ts +89 -20
  137. package/dist/peer-join-backfill.js +265 -108
  138. package/dist/peer-join-backfill.js.map +1 -1
  139. package/dist/pending-join-runner.d.ts +48 -18
  140. package/dist/pending-join-runner.js +74 -56
  141. package/dist/pending-join-runner.js.map +1 -1
  142. package/dist/provider/rent-node.js +1 -1
  143. package/dist/provider/rent-node.js.map +1 -1
  144. package/dist/seed-bootstrap.d.ts +162 -133
  145. package/dist/seed-bootstrap.js +300 -283
  146. package/dist/seed-bootstrap.js.map +1 -1
  147. package/dist/seed-trust-policy.js +4 -4
  148. package/dist/strand-addr-protocol.d.ts +4 -1
  149. package/dist/strand-addr-protocol.js +8 -2
  150. package/dist/strand-addr-protocol.js.map +1 -1
  151. package/dist/strand-cohort.d.ts +3 -0
  152. package/dist/strand-cohort.js.map +1 -1
  153. package/dist/strand-database.d.ts +24 -2
  154. package/dist/strand-database.js +29 -6
  155. package/dist/strand-database.js.map +1 -1
  156. package/dist/strand-first-sync-gate.d.ts +176 -88
  157. package/dist/strand-first-sync-gate.js +288 -121
  158. package/dist/strand-first-sync-gate.js.map +1 -1
  159. package/dist/strand-formation-deadlines.d.ts +9 -9
  160. package/dist/strand-formation-deadlines.js +9 -9
  161. package/dist/strand-formation-manager.d.ts +23 -17
  162. package/dist/strand-formation-manager.js +46 -44
  163. package/dist/strand-formation-manager.js.map +1 -1
  164. package/dist/strand-formation-protocol.d.ts +5 -8
  165. package/dist/strand-formation-protocol.js +101 -16
  166. package/dist/strand-formation-protocol.js.map +1 -1
  167. package/dist/strand-instance-manager.d.ts +27 -18
  168. package/dist/strand-instance-manager.js +119 -50
  169. package/dist/strand-instance-manager.js.map +1 -1
  170. package/dist/strand-membership-reconciler.d.ts +16 -13
  171. package/dist/strand-membership-reconciler.js +30 -22
  172. package/dist/strand-membership-reconciler.js.map +1 -1
  173. package/dist/strand-membership-writer.d.ts +45 -22
  174. package/dist/strand-membership-writer.js +226 -131
  175. package/dist/strand-membership-writer.js.map +1 -1
  176. package/dist/strand-move.d.ts +6 -5
  177. package/dist/strand-move.js +36 -32
  178. package/dist/strand-move.js.map +1 -1
  179. package/dist/strand-peer-redial.js +2 -3
  180. package/dist/strand-peer-redial.js.map +1 -1
  181. package/dist/strand-revocation-enforcer.d.ts +134 -60
  182. package/dist/strand-revocation-enforcer.js +144 -71
  183. package/dist/strand-revocation-enforcer.js.map +1 -1
  184. package/dist/strand-solicitation.d.ts +50 -39
  185. package/dist/strand-solicitation.js +10 -3
  186. package/dist/strand-solicitation.js.map +1 -1
  187. package/dist/strand-watcher.d.ts +80 -13
  188. package/dist/strand-watcher.js +152 -48
  189. package/dist/strand-watcher.js.map +1 -1
  190. package/dist/table-change-watch.d.ts +63 -0
  191. package/dist/table-change-watch.js +209 -0
  192. package/dist/table-change-watch.js.map +1 -0
  193. package/dist/timeout-scheduler.d.ts +4 -3
  194. package/dist/timeout-scheduler.js +4 -3
  195. package/dist/timeout-scheduler.js.map +1 -1
  196. package/dist/types.d.ts +374 -150
  197. package/dist/types.js +4 -4
  198. package/dist/types.js.map +1 -1
  199. package/package.json +17 -8
  200. package/src/bootstrap-peer-store-file.ts +44 -44
  201. package/src/bootstrap-peer-store.ts +214 -214
  202. package/src/cadre-invite-protocol.ts +740 -1052
  203. package/src/cadre-join-request.ts +24 -3
  204. package/src/cadre-node.ts +2402 -1079
  205. package/src/control-authorization.ts +47 -14
  206. package/src/control-cohort.ts +204 -128
  207. package/src/control-database.ts +1749 -820
  208. package/src/control-formation-recorder.ts +359 -353
  209. package/src/control-protocol-guard.ts +23 -6
  210. package/src/control-schema-version.ts +301 -0
  211. package/src/control-schema.ts +273 -78
  212. package/src/control-write-retry.ts +641 -567
  213. package/src/delegate-admission.ts +351 -314
  214. package/src/ed25519-key.ts +174 -171
  215. package/src/edge-node/config.ts +277 -0
  216. package/src/edge-node/dissolve.ts +251 -0
  217. package/src/edge-node/edge-join.ts +319 -0
  218. package/src/edge-node/edge-node.ts +1045 -0
  219. package/src/edge-node/index.ts +59 -0
  220. package/src/edge-node/join-credential.ts +16 -0
  221. package/src/edge-node/lazy-raw-storage.ts +178 -0
  222. package/src/edge-node/node-local.ts +84 -0
  223. package/src/edge-node/options.ts +179 -0
  224. package/src/edge-node/owner-grant.ts +63 -0
  225. package/src/edge-node/platform.ts +151 -0
  226. package/src/edge-node/strand-move.ts +270 -0
  227. package/src/edge-node/strands.ts +72 -0
  228. package/src/enrolled-machine-store.ts +3 -3
  229. package/src/formation-token.ts +63 -0
  230. package/src/index.ts +899 -789
  231. package/src/joined-strand-store.ts +73 -28
  232. package/src/key-store.ts +10 -4
  233. package/src/link-budget.ts +9 -7
  234. package/src/machine-removal.ts +26 -111
  235. package/src/member-redemption.ts +649 -0
  236. package/src/membership-view.ts +144 -0
  237. package/src/owner-anchor-sync.ts +19 -18
  238. package/src/owner-grant-protocol.ts +547 -0
  239. package/src/owner-key-export.ts +348 -0
  240. package/src/owner-key-flows.ts +109 -0
  241. package/src/owner-key-store.ts +85 -0
  242. package/src/owner-keys.ts +178 -0
  243. package/src/owner-signer.ts +94 -0
  244. package/src/peer-addr-book.ts +4 -3
  245. package/src/peer-authorization.ts +558 -488
  246. package/src/peer-join-backfill.ts +971 -761
  247. package/src/pending-join-runner.ts +76 -53
  248. package/src/provider/rent-node.ts +1 -1
  249. package/src/seed-bootstrap.ts +343 -349
  250. package/src/seed-trust-policy.ts +4 -4
  251. package/src/strand-addr-protocol.ts +547 -538
  252. package/src/strand-cohort.ts +4 -0
  253. package/src/strand-database.ts +352 -319
  254. package/src/strand-first-sync-gate.ts +334 -133
  255. package/src/strand-formation-deadlines.ts +227 -227
  256. package/src/strand-formation-manager.ts +731 -725
  257. package/src/strand-formation-protocol.ts +122 -24
  258. package/src/strand-instance-manager.ts +2092 -2026
  259. package/src/strand-membership-reconciler.ts +33 -25
  260. package/src/strand-membership-writer.ts +2093 -1974
  261. package/src/strand-move.ts +50 -38
  262. package/src/strand-peer-redial.ts +2 -3
  263. package/src/strand-revocation-enforcer.ts +740 -654
  264. package/src/strand-solicitation.ts +61 -34
  265. package/src/strand-watcher.ts +175 -56
  266. package/src/table-change-watch.ts +285 -0
  267. package/src/timeout-scheduler.ts +29 -28
  268. package/src/types.ts +391 -152
@@ -6,10 +6,11 @@
6
6
  * is described in `docs/strands.md` → "Joining while the inviter is offline".
7
7
  *
8
8
  * `CadreNode` owns every side effect and hands this module plain functions
9
- * ({@link PendingJoinRunnerDeps}): read the joins, run one attempt (its own `formStrand`), write
10
- * an outcome, say whether this machine is an owner, and the clock. This module owns the policy:
11
- * when to attempt, how to read an attempt's failure, and how to settle an outcome write that
12
- * another machine's write beat.
9
+ * ({@link PendingJoinRunnerDeps}): read the joins, hear a change to the tables they are read
10
+ * from, run one attempt (its own `formStrand`), write an outcome, say whether this machine is an
11
+ * owner, and the clock. This module owns the policy: when to read the joins, when to attempt,
12
+ * how to read an attempt's failure, and how to settle an outcome write that another machine's
13
+ * write beat.
13
14
  *
14
15
  * Only owner machines run it, because every pending-join write is owner-signed. Letting a
15
16
  * machine without an owner key finish a join is `blocked/decide-non-owner-machine-completes-a-pending-join`;
@@ -30,12 +31,28 @@ import { PendingJoinChangedError, type JoinRequestFields, type PendingJoinFields
30
31
  import { FormationPostApprovalError, FormationRejectedError, FormationUnreachableError } from './strand-formation-rejection.js';
31
32
  import { formationDeadlines } from './strand-formation-deadlines.js';
32
33
  import { MEMBERSHIP_INVITE_TTL_MS } from './strand-formation-manager.js';
34
+ import { scheduleRuns, type TableChangeWatch, type TableChangeWatchOptions } from './table-change-watch.js';
33
35
  import { defaultTimeoutScheduler, type TimeoutScheduler } from './timeout-scheduler.js';
34
36
 
35
37
  const log = debug('sereus:cadre:pending-join');
36
38
 
37
- /** How often an owner machine re-reads the party's pending joins (it also reads once right after start). */
38
- export const PENDING_JOIN_POLL_MS = 30_000;
39
+ /**
40
+ * The longest an owner machine goes without re-reading the party's pending joins when no change
41
+ * to their tables is heard. It covers a change that arrives without a storage event, and the
42
+ * time-driven removal of a finished join, which lands up to this much late.
43
+ *
44
+ * NOTE: on a cadre larger than `CONTROL_REPLICATION_BREADTH` (16), a machine outside a join
45
+ * table's cohort hears no change event, so a dismissal or another machine's outcome reaches it
46
+ * up to this much late. The worst effect is one more attempt on a dismissed join, whose outcome
47
+ * write then finds the request gone, or a `token-spent` answer whose re-read adopts the other
48
+ * machine's outcome. A 30 s pass while any join is pending was declined: a join can stay pending
49
+ * for {@link MAX_PENDING_JOIN_MS}, and those reads are what this backstop removes. Revisit if a
50
+ * cadre above 16 machines is seen attempting dismissed joins.
51
+ */
52
+ export const PENDING_JOIN_BACKSTOP_MS = 10 * 60_000;
53
+
54
+ /** How soon a pass runs again after one failed, or after an outcome write gave up to another machine's writes. */
55
+ const PENDING_JOIN_PASS_RETRY_MS = 30_000;
39
56
 
40
57
  /** Ceiling on the wait between two attempts of one join on one machine, before jitter. */
41
58
  export const PENDING_JOIN_MAX_BACKOFF_MS = 10 * 60_000;
@@ -52,7 +69,7 @@ const MAX_BACKGROUND_ATTEMPTS = 2;
52
69
  /** Each retry delay is moved by up to this fraction either way, so machines that failed together do not retry together. */
53
70
  const RETRY_JITTER = 0.2;
54
71
 
55
- /** Re-read-and-rewrite rounds after another machine's write won, before the next pass decides instead. */
72
+ /** Re-read-and-rewrite rounds after another machine's write won, before a pass {@link PENDING_JOIN_PASS_RETRY_MS} later decides instead. */
56
73
  const MAX_OUTCOME_WRITE_ROUNDS = 3;
57
74
 
58
75
  type AttemptError = NonNullable<PendingJoinStatus['lastError']>;
@@ -68,6 +85,10 @@ export interface PendingJoinRunnerDeps {
68
85
  /** Whether this machine may write pending joins now. Asked every pass. */
69
86
  isOwner(): Promise<boolean>;
70
87
  readRows(): Promise<PendingJoin[]>;
88
+ /** Subscribe `onChange` to the tables a pass reads. Absent: passes come from `start`, `kick`, retries and the backstop only (tests). */
89
+ watchChanges?(onChange: () => Promise<void>, options: TableChangeWatchOptions): TableChangeWatch;
90
+ /** Run a pass at least this long after the last one settled; default {@link PENDING_JOIN_BACKSTOP_MS}. */
91
+ backstopMs?: number;
71
92
  readRow(id: string): Promise<PendingJoin | null>;
72
93
  /** One formation attempt for the request's invitation and disclosure (`CadreNode.formStrand`). */
73
94
  attempt(join: PendingJoin): Promise<FormStrandResult>;
@@ -77,7 +98,7 @@ export interface PendingJoinRunnerDeps {
77
98
  rewrite(expectedStampId: string, next: PendingJoinFields): Promise<PendingJoin>;
78
99
  /** `ControlDatabase.deletePendingJoin`, owner-signed. */
79
100
  remove(id: string): Promise<boolean>;
80
- /** Whether this machine has no control connection now, so a write it commits reaches no other machine. */
101
+ /** Whether this machine reaches no sibling now (`CadreNode.hasReachableControlSibling`), so a write it commits reaches no other machine. */
81
102
  isAlone(): boolean;
82
103
  /** The sApp the request's invitation names. */
83
104
  sAppIdOf(join: PendingJoin): string;
@@ -231,7 +252,7 @@ function stableFraction(key: string): number {
231
252
 
232
253
  export class PendingJoinRunner {
233
254
  private readonly tracked = new Map<string, TrackedJoin>();
234
- /** Joins whose latest write by this machine committed with no control connection: id → the request stamp written. */
255
+ /** Joins whose latest write by this machine committed with no sibling reachable: id → the request stamp written. */
235
256
  private readonly writtenAlone = new Map<string, string>();
236
257
  private readonly dueQueue: TrackedJoin[] = [];
237
258
  private readonly scheduler: TimeoutScheduler;
@@ -242,9 +263,8 @@ export class PendingJoinRunner {
242
263
  /** How long a `token-spent` answer waits before its confirming attempt: long enough for a sibling's `joined` write to replicate. */
243
264
  private readonly spentConfirmDelayMs: number;
244
265
  private running = false;
245
- private pollTimer: unknown;
246
- private passInFlight: Promise<void> | null = null;
247
- private passAgain = false;
266
+ /** Every pass runs on it, one at a time; present between {@link start} and {@link stop}. */
267
+ private watch: TableChangeWatch | null = null;
248
268
  private backgroundAttempts = 0;
249
269
 
250
270
  constructor(private readonly deps: PendingJoinRunnerDeps) {
@@ -256,11 +276,21 @@ export class PendingJoinRunner {
256
276
  this.spentConfirmDelayMs = 2 * deadlines.sessionMs;
257
277
  }
258
278
 
259
- /** Start passes: one at once, then every {@link PENDING_JOIN_POLL_MS}. */
279
+ /**
280
+ * Start passes: one at once, then one whenever a table a pass reads changes, and at the latest
281
+ * {@link PENDING_JOIN_BACKSTOP_MS} after the last. Throws when the change subscription cannot
282
+ * be made.
283
+ *
284
+ * NOTE: a pass from before a {@link stop} that is still in flight runs alongside the new
285
+ * watch's passes, since each watch keeps only its own runs to one at a time. `CadreNode` builds
286
+ * a fresh runner per start, so nothing restarts one; if something comes to, make that pass
287
+ * check which start it belongs to.
288
+ */
260
289
  start(): void {
261
290
  if (this.running) return;
291
+ this.watch = this.subscribe();
262
292
  this.running = true;
263
- this.schedulePass(0);
293
+ this.watch.trigger(0);
264
294
  }
265
295
 
266
296
  /**
@@ -274,18 +304,21 @@ export class PendingJoinRunner {
274
304
  */
275
305
  stop(): void {
276
306
  this.running = false;
277
- this.clearPollTimer();
307
+ // A pass already in flight finishes; `running` keeps it from scheduling anything further.
308
+ this.watch?.dispose();
309
+ this.watch = null;
278
310
  for (const entry of this.tracked.values()) this.cancelTimer(entry);
279
311
  this.tracked.clear();
280
312
  this.dueQueue.length = 0;
281
313
  this.writtenAlone.clear();
282
314
  }
283
315
 
284
- /** Run a pass now, for a change that may make one useful (this machine just became an owner). */
316
+ /**
317
+ * Run a pass now, for a change the watched tables do not show (this machine's owner signer was
318
+ * just wired). A pass in flight is followed by one more.
319
+ */
285
320
  kick(): void {
286
- if (!this.running) return;
287
- this.clearPollTimer();
288
- void this.pass();
321
+ this.watch?.trigger(0);
289
322
  }
290
323
 
291
324
  /**
@@ -327,8 +360,8 @@ export class PendingJoinRunner {
327
360
 
328
361
  /**
329
362
  * Re-write, with identical content under a fresh stamp, each join this machine last wrote while
330
- * it had no control connection, so the other machines receive it. Called on the control
331
- * connection growth edge. A join someone wrote since is left alone.
363
+ * no sibling was reachable, so the other machines receive it. Called on the control cohort's
364
+ * growth edge, when a sibling becomes reachable. A join someone wrote since is left alone.
332
365
  *
333
366
  * NOTE: in memory only, so a join written alone by a process that stopped before it reconnected
334
367
  * reaches the party only with that join's next write. A sweep of every join this machine wrote,
@@ -359,41 +392,29 @@ export class PendingJoinRunner {
359
392
  }
360
393
  }
361
394
 
362
- private schedulePass(delayMs: number): void {
363
- if (!this.running) return;
364
- this.clearPollTimer();
365
- this.pollTimer = this.scheduler.setTimeout(() => {
366
- this.pollTimer = undefined;
367
- void this.pass();
368
- }, delayMs);
395
+ /** The watch every pass runs on: the deps' change subscription, or a timer-only one. */
396
+ private subscribe(): TableChangeWatch {
397
+ const onChange = (): Promise<void> => this.pass();
398
+ const options: TableChangeWatchOptions = {
399
+ label: 'pending-join',
400
+ backstopMs: this.deps.backstopMs ?? PENDING_JOIN_BACKSTOP_MS,
401
+ scheduler: this.scheduler,
402
+ };
403
+ return this.deps.watchChanges?.(onChange, options) ?? scheduleRuns(onChange, options);
369
404
  }
370
405
 
371
- private clearPollTimer(): void {
372
- if (this.pollTimer !== undefined) {
373
- this.scheduler.clearTimeout(this.pollTimer);
374
- this.pollTimer = undefined;
406
+ /** One pass; a failed one runs again {@link PENDING_JOIN_PASS_RETRY_MS} later. */
407
+ private async pass(): Promise<void> {
408
+ try {
409
+ await this.runPass();
410
+ } catch (error) {
411
+ log('pending-join pass failed; retried in 30 s: %s', errorText(error));
412
+ this.retryPass();
375
413
  }
376
414
  }
377
415
 
378
- /** One pass at a time; a kick during a pass runs one more after it. */
379
- private pass(): Promise<void> {
380
- if (this.passInFlight) {
381
- this.passAgain = true;
382
- return this.passInFlight;
383
- }
384
- const run = this.runPass()
385
- .catch((error: unknown) => log('pending-join pass failed; the next one retries: %s', errorText(error)))
386
- .finally(() => {
387
- this.passInFlight = null;
388
- if (this.passAgain && this.running) {
389
- this.passAgain = false;
390
- void this.pass();
391
- return;
392
- }
393
- this.schedulePass(PENDING_JOIN_POLL_MS);
394
- });
395
- this.passInFlight = run;
396
- return run;
416
+ private retryPass(): void {
417
+ this.watch?.trigger(PENDING_JOIN_PASS_RETRY_MS);
397
418
  }
398
419
 
399
420
  private async runPass(): Promise<void> {
@@ -415,7 +436,8 @@ export class PendingJoinRunner {
415
436
  try {
416
437
  await this.consider(join);
417
438
  } catch (error) {
418
- log('pending join %s: this pass could not handle it; the next one retries: %s', join.Id, errorText(error));
439
+ log('pending join %s: this pass could not handle it; retried in 30 s: %s', join.Id, errorText(error));
440
+ this.retryPass();
419
441
  }
420
442
  }
421
443
  }
@@ -663,7 +685,8 @@ export class PendingJoinRunner {
663
685
  }
664
686
  expected = live!;
665
687
  }
666
- log('pending join %s: another machine kept changing the join; the next pass decides', entry.row.Id);
688
+ log('pending join %s: another machine kept changing the join; a pass in 30 s decides', entry.row.Id);
689
+ this.retryPass();
667
690
  }
668
691
 
669
692
  private settle(join: PendingJoin): void {
@@ -143,7 +143,7 @@ async function requireOwner(node: RentingNode): Promise<void> {
143
143
 
144
144
  async function rentByInvitation(node: RentingNode, client: ProviderClient, options: RentNodeOptions): Promise<RentNodeResult> {
145
145
  report(options, 'inviting');
146
- const { invitation, encoded } = await node.createCadreInvitation({ grantsOwner: false, uses: 1, expiresInMs: RENTAL_INVITATION_TTL_MS });
146
+ const { invitation, encoded } = await node.createCadreInvitation({ uses: 1, expiresInMs: RENTAL_INVITATION_TTL_MS });
147
147
  report(options, 'creating');
148
148
  let container;
149
149
  try {