@serfab/cadre-core 0.7.1 → 0.9.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 (170) hide show
  1. package/README.md +11 -10
  2. package/dist/cadre-node.d.ts +818 -19
  3. package/dist/cadre-node.js +1952 -99
  4. package/dist/cadre-node.js.map +1 -1
  5. package/dist/canonical-datetime.d.ts +33 -0
  6. package/dist/canonical-datetime.js +38 -0
  7. package/dist/canonical-datetime.js.map +1 -0
  8. package/dist/canonical-json.d.ts +12 -0
  9. package/dist/canonical-json.js +33 -0
  10. package/dist/canonical-json.js.map +1 -0
  11. package/dist/control-cohort.d.ts +63 -0
  12. package/dist/control-cohort.js +80 -0
  13. package/dist/control-cohort.js.map +1 -0
  14. package/dist/control-database.d.ts +279 -8
  15. package/dist/control-database.js +563 -134
  16. package/dist/control-database.js.map +1 -1
  17. package/dist/control-formation-recorder.d.ts +89 -0
  18. package/dist/control-formation-recorder.js +139 -0
  19. package/dist/control-formation-recorder.js.map +1 -0
  20. package/dist/control-schema.d.ts +12 -0
  21. package/dist/control-schema.js +223 -0
  22. package/dist/control-schema.js.map +1 -0
  23. package/dist/control-stream.d.ts +54 -0
  24. package/dist/control-stream.js +104 -0
  25. package/dist/control-stream.js.map +1 -0
  26. package/dist/device-token.d.ts +50 -0
  27. package/dist/device-token.js +66 -0
  28. package/dist/device-token.js.map +1 -0
  29. package/dist/diagnostics/connection-path.d.ts +118 -0
  30. package/dist/diagnostics/connection-path.js +204 -0
  31. package/dist/diagnostics/connection-path.js.map +1 -0
  32. package/dist/diagnostics/webrtc-turn-tracker.d.ts +50 -0
  33. package/dist/diagnostics/webrtc-turn-tracker.js +196 -0
  34. package/dist/diagnostics/webrtc-turn-tracker.js.map +1 -0
  35. package/dist/ed25519-key.d.ts +42 -0
  36. package/dist/ed25519-key.js +51 -0
  37. package/dist/ed25519-key.js.map +1 -0
  38. package/dist/hibernation-manager.d.ts +79 -0
  39. package/dist/hibernation-manager.js +173 -23
  40. package/dist/hibernation-manager.js.map +1 -1
  41. package/dist/index.d.ts +25 -4
  42. package/dist/index.js +59 -4
  43. package/dist/index.js.map +1 -1
  44. package/dist/key-store-file.d.ts +52 -0
  45. package/dist/key-store-file.js +194 -0
  46. package/dist/key-store-file.js.map +1 -0
  47. package/dist/key-store.d.ts +75 -0
  48. package/dist/key-store.js +66 -0
  49. package/dist/key-store.js.map +1 -0
  50. package/dist/peer-authorization.d.ts +48 -0
  51. package/dist/peer-authorization.js +66 -0
  52. package/dist/peer-authorization.js.map +1 -0
  53. package/dist/peer-record.d.ts +89 -0
  54. package/dist/peer-record.js +117 -0
  55. package/dist/peer-record.js.map +1 -0
  56. package/dist/push-credentials.d.ts +34 -0
  57. package/dist/push-credentials.js +81 -0
  58. package/dist/push-credentials.js.map +1 -0
  59. package/dist/push-fanout.d.ts +137 -0
  60. package/dist/push-fanout.js +197 -0
  61. package/dist/push-fanout.js.map +1 -0
  62. package/dist/push-node.d.ts +38 -0
  63. package/dist/push-node.js +48 -0
  64. package/dist/push-node.js.map +1 -0
  65. package/dist/push-notifier-apns.d.ts +61 -0
  66. package/dist/push-notifier-apns.js +199 -0
  67. package/dist/push-notifier-apns.js.map +1 -0
  68. package/dist/push-notifier-fcm.d.ts +44 -0
  69. package/dist/push-notifier-fcm.js +182 -0
  70. package/dist/push-notifier-fcm.js.map +1 -0
  71. package/dist/push-notifier-shared.d.ts +18 -0
  72. package/dist/push-notifier-shared.js +25 -0
  73. package/dist/push-notifier-shared.js.map +1 -0
  74. package/dist/push-notifier.d.ts +52 -0
  75. package/dist/push-notifier.js +22 -0
  76. package/dist/push-notifier.js.map +1 -0
  77. package/dist/schema-verification.d.ts +11 -2
  78. package/dist/schema-verification.js +15 -4
  79. package/dist/schema-verification.js.map +1 -1
  80. package/dist/seed-bootstrap.d.ts +228 -21
  81. package/dist/seed-bootstrap.js +489 -168
  82. package/dist/seed-bootstrap.js.map +1 -1
  83. package/dist/seed-trust-policy.d.ts +63 -0
  84. package/dist/seed-trust-policy.js +56 -0
  85. package/dist/seed-trust-policy.js.map +1 -0
  86. package/dist/strand-addr-protocol.d.ts +132 -0
  87. package/dist/strand-addr-protocol.js +313 -0
  88. package/dist/strand-addr-protocol.js.map +1 -0
  89. package/dist/strand-cohort.d.ts +54 -0
  90. package/dist/strand-cohort.js +38 -0
  91. package/dist/strand-cohort.js.map +1 -0
  92. package/dist/strand-database.d.ts +54 -6
  93. package/dist/strand-database.js +88 -100
  94. package/dist/strand-database.js.map +1 -1
  95. package/dist/strand-formation-manager.d.ts +79 -19
  96. package/dist/strand-formation-manager.js +196 -104
  97. package/dist/strand-formation-manager.js.map +1 -1
  98. package/dist/strand-formation-protocol.d.ts +162 -0
  99. package/dist/strand-formation-protocol.js +279 -0
  100. package/dist/strand-formation-protocol.js.map +1 -0
  101. package/dist/strand-instance-manager.d.ts +73 -4
  102. package/dist/strand-instance-manager.js +167 -41
  103. package/dist/strand-instance-manager.js.map +1 -1
  104. package/dist/strand-member-key.d.ts +29 -0
  105. package/dist/strand-member-key.js +38 -0
  106. package/dist/strand-member-key.js.map +1 -0
  107. package/dist/strand-member-registry.d.ts +98 -0
  108. package/dist/strand-member-registry.js +129 -0
  109. package/dist/strand-member-registry.js.map +1 -0
  110. package/dist/strand-membership-writer.d.ts +361 -0
  111. package/dist/strand-membership-writer.js +449 -0
  112. package/dist/strand-membership-writer.js.map +1 -0
  113. package/dist/strand-solicitation.d.ts +77 -2
  114. package/dist/strand-solicitation.js +25 -4
  115. package/dist/strand-solicitation.js.map +1 -1
  116. package/dist/strand-wake-payload.d.ts +30 -0
  117. package/dist/strand-wake-payload.js +20 -0
  118. package/dist/strand-wake-payload.js.map +1 -0
  119. package/dist/strand-wake-protocol.d.ts +114 -0
  120. package/dist/strand-wake-protocol.js +257 -0
  121. package/dist/strand-wake-protocol.js.map +1 -0
  122. package/dist/strand-watcher.d.ts +6 -2
  123. package/dist/strand-watcher.js +57 -17
  124. package/dist/strand-watcher.js.map +1 -1
  125. package/dist/types.d.ts +436 -33
  126. package/dist/types.js +22 -5
  127. package/dist/types.js.map +1 -1
  128. package/package.json +18 -8
  129. package/src/cadre-node.ts +2981 -948
  130. package/src/canonical-datetime.ts +39 -0
  131. package/src/canonical-json.ts +30 -0
  132. package/src/control-cohort.ts +103 -0
  133. package/src/control-database.ts +948 -383
  134. package/src/control-formation-recorder.ts +151 -0
  135. package/src/control-schema.ts +222 -0
  136. package/src/control-stream.ts +128 -0
  137. package/src/device-token.ts +89 -0
  138. package/src/diagnostics/connection-path.ts +277 -0
  139. package/src/diagnostics/webrtc-turn-tracker.ts +222 -0
  140. package/src/ed25519-key.ts +69 -0
  141. package/src/hibernation-manager.ts +197 -28
  142. package/src/index.ts +275 -79
  143. package/src/key-store-file.ts +203 -0
  144. package/src/key-store.ts +99 -0
  145. package/src/peer-authorization.ts +81 -0
  146. package/src/peer-record.ts +146 -0
  147. package/src/push-credentials.ts +84 -0
  148. package/src/push-fanout.ts +272 -0
  149. package/src/push-node.ts +64 -0
  150. package/src/push-notifier-apns.ts +247 -0
  151. package/src/push-notifier-fcm.ts +220 -0
  152. package/src/push-notifier-shared.ts +28 -0
  153. package/src/push-notifier.ts +52 -0
  154. package/src/schema-verification.ts +18 -4
  155. package/src/seed-bootstrap.ts +563 -223
  156. package/src/seed-trust-policy.ts +107 -0
  157. package/src/strand-addr-protocol.ts +410 -0
  158. package/src/strand-cohort.ts +77 -0
  159. package/src/strand-database.ts +113 -141
  160. package/src/strand-formation-manager.ts +233 -142
  161. package/src/strand-formation-protocol.ts +391 -0
  162. package/src/strand-instance-manager.ts +207 -56
  163. package/src/strand-member-key.ts +39 -0
  164. package/src/strand-member-registry.ts +177 -0
  165. package/src/strand-membership-writer.ts +640 -0
  166. package/src/strand-solicitation.ts +93 -4
  167. package/src/strand-wake-payload.ts +32 -0
  168. package/src/strand-wake-protocol.ts +331 -0
  169. package/src/strand-watcher.ts +61 -17
  170. package/src/types.ts +1032 -582
@@ -16,6 +16,17 @@ export interface HibernationCallbacks {
16
16
  onIdle: (strandId: string) => Promise<void>;
17
17
  onHibernate: (strandId: string) => Promise<void>;
18
18
  onWake: (strandId: string) => Promise<void>;
19
+ /**
20
+ * Perform a real cohort check-in for a hibernating strand. The implementation
21
+ * (`CadreNode.handleStrandCheckIn`) resumes the strand, gives it a bounded
22
+ * window to connect to reachable cohort peers and surface pending activity,
23
+ * then re-hibernates if still idle. The manager AWAITS this before scheduling
24
+ * the next (longer-delayed) check-in, so a slow check-in never overlaps the
25
+ * next tick. After it resolves the manager inspects `instance.status`: a
26
+ * strand left non-`hibernating` is treated as woken (backoff resets on the
27
+ * next hibernation); a strand left `hibernating` escalates the backoff.
28
+ */
29
+ onCheckIn: (strandId: string) => Promise<void>;
19
30
  }
20
31
 
21
32
  /**
@@ -31,7 +42,20 @@ export class HibernationManager {
31
42
  private readonly config: HibernationConfig;
32
43
  private readonly callbacks: HibernationCallbacks;
33
44
  private readonly timers: Map<string, ReturnType<typeof setTimeout>> = new Map();
34
- private readonly checkInTimers: Map<string, ReturnType<typeof setInterval>> = new Map();
45
+ /**
46
+ * Pending check-in timers, one per hibernating strand. Unlike the old fixed
47
+ * `setInterval`, these are single-shot `setTimeout`s rescheduled by
48
+ * {@link runCheckIn} with an escalating delay — so a long-running `onCheckIn`
49
+ * can never overlap the next tick, and the period adapts to the backoff.
50
+ */
51
+ private readonly checkInTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
52
+ /**
53
+ * In-flight wake promises keyed by strandId. Coalesces overlapping wake
54
+ * triggers (two near-simultaneous activities, or activity racing a force wake)
55
+ * so `onWake` — and the libp2p-node rebuild it drives — runs at most once per
56
+ * concurrent wake.
57
+ */
58
+ private readonly wakePromises: Map<string, Promise<void>> = new Map();
35
59
  private running = false;
36
60
 
37
61
  constructor(config: HibernationConfig, callbacks: HibernationCallbacks) {
@@ -48,11 +72,13 @@ export class HibernationManager {
48
72
  const custom = this.config.customTimeouts?.[hint];
49
73
 
50
74
  if (!custom) return defaults;
51
-
75
+
52
76
  return {
53
77
  idleTimeout: custom.idleTimeout ?? defaults.idleTimeout,
54
78
  hibernateTimeout: custom.hibernateTimeout ?? defaults.hibernateTimeout,
55
- checkInInterval: custom.checkInInterval ?? defaults.checkInInterval
79
+ checkInInterval: custom.checkInInterval ?? defaults.checkInInterval,
80
+ checkInBackoffFactor: custom.checkInBackoffFactor ?? defaults.checkInBackoffFactor,
81
+ checkInMaxInterval: custom.checkInMaxInterval ?? defaults.checkInMaxInterval
56
82
  };
57
83
  }
58
84
 
@@ -79,13 +105,58 @@ export class HibernationManager {
79
105
  this.timers.clear();
80
106
 
81
107
  for (const timer of this.checkInTimers.values()) {
82
- clearInterval(timer);
108
+ clearTimeout(timer);
83
109
  }
84
110
  this.checkInTimers.clear();
85
-
111
+
112
+ // In-flight wakes clean themselves up via their finally; drop the references
113
+ // so a fresh start coalesces cleanly.
114
+ this.wakePromises.clear();
115
+
86
116
  log('HibernationManager stopped');
87
117
  }
88
118
 
119
+ /**
120
+ * Whether a strand with this instance's latency hint ever hibernates — `false`
121
+ * for realtime (Infinity idle timeout, see {@link HIBERNATION_TIMEOUTS}), also
122
+ * honouring any per-hint `customTimeouts` override. Imperative callers
123
+ * (`CadreNode.hibernateStrand` / `hibernateAll`) use this as the single source
124
+ * of truth for "skip realtime", consistent with {@link trackStrand} declining
125
+ * to track Infinity-timeout strands.
126
+ */
127
+ hibernates(instance: StrandInstance): boolean {
128
+ return this.getTimeouts(instance.latencyHint).idleTimeout !== Infinity;
129
+ }
130
+
131
+ /**
132
+ * Imperatively hibernate a tracked strand now, bypassing the idle/hibernate
133
+ * timers — the mobile background-entry path. Cancels the strand's pending
134
+ * idle/hibernate AND check-in timers so none can later re-fire `onHibernate`
135
+ * on the already-quiesced strand or resurrect one the caller means to keep
136
+ * down, then invokes `onHibernate` (quiesce + mark hibernating).
137
+ *
138
+ * Unlike the timer path ({@link handleHibernateTimeout}) it deliberately does
139
+ * NOT re-arm the check-in chain: a force-hibernate keeps the strand down until
140
+ * the caller drives the next wake on demand (push-delivered on mobile), so a
141
+ * stray check-in timer must not bring it back up.
142
+ *
143
+ * @returns `true` if the strand was hibernated, `false` (no-op) for a realtime
144
+ * strand that never hibernates.
145
+ */
146
+ async forceHibernate(instance: StrandInstance): Promise<boolean> {
147
+ if (!this.hibernates(instance)) {
148
+ log('forceHibernate: strand %s is realtime; no-op', instance.strandId);
149
+ return false;
150
+ }
151
+ // Cancel idle/hibernate + check-in timers BEFORE quiescing so nothing fights
152
+ // the imperative hibernate (a stale hibernate timer firing on the quiesced
153
+ // strand, or a check-in resuming a strand the caller wants kept down).
154
+ this.clearTimers(instance.strandId);
155
+ await this.callbacks.onHibernate(instance.strandId);
156
+ log('forceHibernate: strand %s hibernated (timers cancelled, not re-armed)', instance.strandId);
157
+ return true;
158
+ }
159
+
89
160
  /**
90
161
  * Register a strand for hibernation management
91
162
  */
@@ -122,13 +193,33 @@ export class HibernationManager {
122
193
  const { strandId, status, latencyHint } = instance;
123
194
  instance.lastActivity = new Date();
124
195
 
125
- // If idle or hibernating, wake up
196
+ // If idle or hibernating, wake up. Coalesce so two near-simultaneous
197
+ // activities don't each fire onWake (which would rebuild two libp2p nodes).
126
198
  if (status === 'idle' || status === 'hibernating') {
127
199
  log('Activity on %s strand %s - waking', status, strandId);
128
200
  this.clearTimers(strandId);
129
- void this.callbacks.onWake(strandId);
201
+ // Fire-and-forget; force-wake awaiters see errors, so swallow (and log)
202
+ // here to avoid an unhandled rejection on this best-effort path.
203
+ //
204
+ // Once the wake settles (CadreNode rebuilds the runtime and marks the
205
+ // strand `active`), re-arm the idle→hibernate→check-in cycle. Without this
206
+ // a strand that wakes then goes quiet stays `active` forever and never
207
+ // re-hibernates — so the check-in backoff could never restart at base.
208
+ // Guarded on `active` so a wake that did not transition (a coalesced
209
+ // no-op, or a still-mid-flight rebuild) never arms a stray timer.
210
+ void this.beginWake(strandId).then(() => {
211
+ if (this.running && instance.status === 'active') {
212
+ const timeouts = this.getTimeouts(instance.latencyHint);
213
+ if (timeouts.idleTimeout !== Infinity) {
214
+ this.scheduleIdleTransition(instance);
215
+ }
216
+ }
217
+ }).catch((err) => {
218
+ log('Activity-driven wake failed for strand %s: %o', strandId, err);
219
+ });
220
+ return;
130
221
  }
131
-
222
+
132
223
  // Reschedule idle transition if active
133
224
  if (status === 'active') {
134
225
  const timeouts = this.getTimeouts(latencyHint);
@@ -143,7 +234,30 @@ export class HibernationManager {
143
234
  */
144
235
  async wakeStrand(strandId: string): Promise<void> {
145
236
  this.clearTimers(strandId);
146
- await this.callbacks.onWake(strandId);
237
+ await this.beginWake(strandId);
238
+ }
239
+
240
+ /**
241
+ * Begin a wake for a strand, or coalesce with one already in flight. Ensures
242
+ * `onWake` runs at most once per concurrent wake — the returned promise is
243
+ * shared by all overlapping callers and cleared once it settles. Force-wake
244
+ * callers await it; activity-driven callers fire-and-forget.
245
+ */
246
+ private beginWake(strandId: string): Promise<void> {
247
+ const existing = this.wakePromises.get(strandId);
248
+ if (existing) {
249
+ return existing;
250
+ }
251
+
252
+ const wake = (async () => {
253
+ try {
254
+ await this.callbacks.onWake(strandId);
255
+ } finally {
256
+ this.wakePromises.delete(strandId);
257
+ }
258
+ })();
259
+ this.wakePromises.set(strandId, wake);
260
+ return wake;
147
261
  }
148
262
 
149
263
  private scheduleIdleTransition(instance: StrandInstance): void {
@@ -168,13 +282,16 @@ export class HibernationManager {
168
282
 
169
283
  log('Idle timeout for strand %s', strandId);
170
284
 
171
- // Transition to idle
285
+ // Transition to idle. The callback can reject (e.g. a future idle handler
286
+ // that releases resources); catch so the timer chain never unhandled-rejects.
172
287
  void this.callbacks.onIdle(strandId).then(() => {
173
288
  // Schedule hibernate transition
174
289
  const timeouts = this.getTimeouts(latencyHint);
175
290
  if (timeouts.hibernateTimeout !== Infinity) {
176
291
  this.scheduleHibernateTransition(instance);
177
292
  }
293
+ }).catch((err) => {
294
+ log('onIdle failed for strand %s: %o', strandId, err);
178
295
  });
179
296
  }
180
297
 
@@ -200,42 +317,94 @@ export class HibernationManager {
200
317
 
201
318
  log('Hibernate timeout for strand %s', strandId);
202
319
 
203
- // Transition to hibernating
320
+ // Transition to hibernating. onHibernate now releases strand-network
321
+ // resources (quiesce), so its close()/stop() can reject — catch so a failed
322
+ // hibernate logs instead of producing an unhandled rejection.
204
323
  void this.callbacks.onHibernate(strandId).then(() => {
205
324
  // Schedule periodic check-ins
206
325
  const timeouts = this.getTimeouts(latencyHint);
207
326
  if (timeouts.checkInInterval !== Infinity) {
208
327
  this.scheduleCheckIn(instance);
209
328
  }
329
+ }).catch((err) => {
330
+ log('onHibernate failed for strand %s: %o', strandId, err);
210
331
  });
211
332
  }
212
333
 
213
- private scheduleCheckIn(instance: StrandInstance): void {
334
+ /**
335
+ * Arm the next check-in for a hibernating strand. Each call is a single-shot
336
+ * `setTimeout` (not a fixed `setInterval`) so the period escalates per
337
+ * {@link runCheckIn} and a slow `onCheckIn` never overlaps the next tick.
338
+ *
339
+ * `delay` is omitted by the chain start ({@link handleHibernateTimeout}),
340
+ * defaulting to the base `checkInInterval` — which is why backoff naturally
341
+ * resets to base each fresh hibernation cycle, with no per-strand counter to
342
+ * clear on wake. Subsequent ticks pass the escalated, capped delay.
343
+ */
344
+ private scheduleCheckIn(instance: StrandInstance, delay?: number): void {
214
345
  const { strandId, latencyHint } = instance;
215
346
  const timeouts = this.getTimeouts(latencyHint);
347
+ const currentDelay = delay ?? timeouts.checkInInterval;
216
348
 
217
- // Clear existing check-in timer
349
+ // Replace any existing check-in timer (no-op if the firing timer rescheduled us).
218
350
  const existing = this.checkInTimers.get(strandId);
219
351
  if (existing) {
220
- clearInterval(existing);
352
+ clearTimeout(existing);
221
353
  }
222
354
 
223
- // Schedule periodic check-ins
224
- const timer = setInterval(() => {
225
- if (!this.running) {
226
- clearInterval(timer);
227
- return;
228
- }
355
+ const timer = setTimeout(() => {
356
+ void this.runCheckIn(instance, currentDelay);
357
+ }, currentDelay);
358
+
359
+ this.checkInTimers.set(strandId, timer);
360
+ instance.nextCheckIn = new Date(Date.now() + currentDelay);
361
+ }
362
+
363
+ /**
364
+ * Run a single check-in tick: invoke `onCheckIn` (a real resume → bounded
365
+ * sync window → re-hibernate-if-idle cycle in `CadreNode`) and AWAIT it before
366
+ * deciding the next step.
367
+ *
368
+ * - If the strand woke during the check-in (`onCheckIn` left it non-
369
+ * `hibernating`), stop the chain — the activity path has re-armed the
370
+ * idle/hibernate timers, and the next hibernation restarts the chain at the
371
+ * base delay (backoff reset).
372
+ * - Otherwise escalate the delay by `checkInBackoffFactor`, capped at
373
+ * `checkInMaxInterval`, and reschedule.
374
+ */
375
+ private async runCheckIn(instance: StrandInstance, currentDelay: number): Promise<void> {
376
+ const { strandId, latencyHint } = instance;
377
+ if (!this.running) return;
229
378
 
230
- log('Check-in for hibernating strand %s', strandId);
231
- instance.nextCheckIn = new Date(Date.now() + timeouts.checkInInterval);
379
+ log('Check-in for hibernating strand %s (delay=%dms)', strandId, currentDelay);
232
380
 
233
- // In a real implementation, this would query the cohort for pending activity
234
- // For now, we just update the nextCheckIn timestamp
235
- }, timeouts.checkInInterval);
381
+ try {
382
+ await this.callbacks.onCheckIn(strandId);
383
+ } catch (err) {
384
+ // A failed check-in (e.g. resume threw) must not break the chain — log and
385
+ // fall through to reschedule the next, longer-delayed attempt.
386
+ log('onCheckIn failed for strand %s: %o', strandId, err);
387
+ }
236
388
 
237
- this.checkInTimers.set(strandId, timer);
238
- instance.nextCheckIn = new Date(Date.now() + timeouts.checkInInterval);
389
+ if (!this.running) return;
390
+
391
+ // The check-in either woke the strand (CadreNode left it active) or left it
392
+ // hibernating. Inspect the shared instance the callback just mutated.
393
+ if (instance.status !== 'hibernating') {
394
+ log('Check-in woke strand %s; backoff resets on next hibernation', strandId);
395
+ this.checkInTimers.delete(strandId);
396
+ // The chain is stopping; drop the now-stale next-check-in advertisement so
397
+ // `getStrand` doesn't report a phantom check-in for a strand that is awake.
398
+ instance.nextCheckIn = undefined;
399
+ return;
400
+ }
401
+
402
+ const timeouts = this.getTimeouts(latencyHint);
403
+ const nextDelay = Math.min(
404
+ currentDelay * timeouts.checkInBackoffFactor,
405
+ timeouts.checkInMaxInterval
406
+ );
407
+ this.scheduleCheckIn(instance, nextDelay);
239
408
  }
240
409
 
241
410
  private clearTimer(strandId: string): void {
@@ -251,7 +420,7 @@ export class HibernationManager {
251
420
 
252
421
  const checkInTimer = this.checkInTimers.get(strandId);
253
422
  if (checkInTimer) {
254
- clearInterval(checkInTimer);
423
+ clearTimeout(checkInTimer);
255
424
  this.checkInTimers.delete(strandId);
256
425
  }
257
426
  }
package/src/index.ts CHANGED
@@ -1,79 +1,275 @@
1
- // Core types
2
- export * from './types.js';
3
-
4
- // Main CadreNode class
5
- export { CadreNode } from './cadre-node.js';
6
-
7
- // Control database
8
- export { ControlDatabase, type ControlDatabaseConfig } from './control-database.js';
9
-
10
- // Strand database
11
- export { StrandDatabase, type StrandDatabaseConfig } from './strand-database.js';
12
-
13
- // Strand management
14
- export {
15
- StrandWatcher,
16
- type StrandWatcherCallbacks,
17
- type StrandQueryable,
18
- type SAppIdLookup,
19
- type StrandRowWithApp
20
- } from './strand-watcher.js';
21
- export {
22
- StrandInstanceManager,
23
- type StartStrandConfig,
24
- getStrandStoragePath
25
- } from './strand-instance-manager.js';
26
-
27
- // Hibernation
28
- export {
29
- HibernationManager,
30
- type HibernationCallbacks
31
- } from './hibernation-manager.js';
32
-
33
- // Arachnode (stub)
34
- export {
35
- ArachnodeStub,
36
- createArachnodeStub,
37
- type RingConfig
38
- } from './arachnode-stub.js';
39
-
40
- // Enrollment
41
- export {
42
- EnrollmentService,
43
- type MemberVerifier,
44
- type MemberRegistry
45
- } from './enrollment.js';
46
-
47
- // Strand Solicitation
48
- export {
49
- StrandSolicitationService,
50
- type DisclosureValidator,
51
- type FormationUsageRecorder,
52
- type StrandProvisioner,
53
- type FormationSigner,
54
- type StrandSolicitationServiceOptions
55
- } from './strand-solicitation.js';
56
-
57
- // Seed Bootstrap
58
- export {
59
- SeedBootstrapService,
60
- SEED_PROTOCOL,
61
- type SeedBootstrapConfig,
62
- type SeedEventCallbacks
63
- } from './seed-bootstrap.js';
64
-
65
- // Schema Verification
66
- export {
67
- signSchema,
68
- verifySchema,
69
- assertSchemaSignature,
70
- SchemaVerificationError
71
- } from './schema-verification.js';
72
-
73
- // Strand Formation (strand-proto integration)
74
- export {
75
- StrandFormationManager,
76
- createStrandFormationManager,
77
- type StrandFormationManagerConfig,
78
- type StrandFormationManagerOptions
79
- } from './strand-formation-manager.js';
1
+ // Core types
2
+ export * from './types.js';
3
+
4
+ // Canonical JSON serialization (shared signing payload format)
5
+ export { canonicalJson } from './canonical-json.js';
6
+
7
+ // Main CadreNode class
8
+ export { CadreNode } from './cadre-node.js';
9
+
10
+ // Control database
11
+ export { ControlDatabase, buildAuthorizationMessage, type ControlDatabaseConfig, type ControlTable } from './control-database.js';
12
+
13
+ // Ed25519 key bridge (libp2p Ed25519 -> base64url keypair)
14
+ export { ed25519KeyPairFromLibp2p, ed25519PublicKeyFromPrivate, type Ed25519KeyPair } from './ed25519-key.js';
15
+
16
+ // Pluggable key store (backend-agnostic identity/owner key material seam).
17
+ // Dependency-free: the interface, error, default slot id, and in-memory backend
18
+ // are safe in every (RN/browser/Node) entry graph. The Node FileKeyStore is a
19
+ // separate subpath module ('@serfab/cadre-core/key-store-file') so its node:fs
20
+ // import never lands here.
21
+ export {
22
+ InMemoryKeyStore,
23
+ KeyStoreAccessError,
24
+ DEFAULT_IDENTITY_KEY_ID,
25
+ type KeyStore,
26
+ type KeyId
27
+ } from './key-store.js';
28
+
29
+ // Strand database
30
+ export { StrandDatabase, type StrandDatabaseConfig } from './strand-database.js';
31
+
32
+ // Strand management
33
+ export {
34
+ StrandWatcher,
35
+ type StrandWatcherCallbacks,
36
+ type StrandQueryable,
37
+ type SAppIdLookup,
38
+ type StrandRowWithApp
39
+ } from './strand-watcher.js';
40
+ export {
41
+ StrandInstanceManager,
42
+ type StartStrandConfig,
43
+ type ResumeStrandOverrides,
44
+ getStrandStoragePath
45
+ } from './strand-instance-manager.js';
46
+
47
+ // Hibernation
48
+ export {
49
+ HibernationManager,
50
+ type HibernationCallbacks
51
+ } from './hibernation-manager.js';
52
+
53
+ // Arachnode (stub)
54
+ export {
55
+ ArachnodeStub,
56
+ createArachnodeStub,
57
+ type RingConfig
58
+ } from './arachnode-stub.js';
59
+
60
+ // Enrollment
61
+ export {
62
+ EnrollmentService,
63
+ type MemberVerifier,
64
+ type MemberRegistry
65
+ } from './enrollment.js';
66
+
67
+ // Strand Solicitation
68
+ export {
69
+ StrandSolicitationService,
70
+ createDefaultFormationResponseValidator,
71
+ type DisclosureValidator,
72
+ type FormationUsageRecorder,
73
+ type ResolvedHostStrand,
74
+ type StrandProvisioner,
75
+ type FormationSigner,
76
+ type FormationResponseValidator,
77
+ type StrandSolicitationServiceOptions
78
+ } from './strand-solicitation.js';
79
+
80
+ // DB-backed FormationUsageRecorder (reads/writes the real CadreControl tables)
81
+ export { ControlFormationUsageRecorder } from './control-formation-recorder.js';
82
+
83
+ // Closed-strand member key generation (ed25519 protobuf, base64) + the
84
+ // protobuf->base64url bridge that derives the founding Member/Manager key.
85
+ export { generateStrandMemberKey, strandMemberKeyPair } from './strand-member-key.js';
86
+
87
+ // Strand membership writer (founder bootstrap, invite issuance/consumption,
88
+ // manager-admit, member-peer registration, manager rotation, + shared signing
89
+ // primitives reused across the flows).
90
+ export {
91
+ signStrandPayload,
92
+ verifyStrandPayload,
93
+ bootstrapFounderMembership,
94
+ issueInvite,
95
+ consumeInvite,
96
+ addMemberByManager,
97
+ registerMemberPeer,
98
+ addManager,
99
+ removeManager,
100
+ STRAND_ENGINE,
101
+ STRAND_ENGINE_VERSION,
102
+ type FounderBootstrapParams,
103
+ type IssueInviteParams,
104
+ type IssuedInvite,
105
+ type ConsumeInviteParams,
106
+ type AddMemberByManagerParams,
107
+ type RegisterMemberPeerParams,
108
+ type AddManagerParams,
109
+ type RemoveManagerParams
110
+ } from './strand-membership-writer.js';
111
+
112
+ // Engine-canonical datetime helper (shared by control + strand signed-write flows
113
+ // so a signed timestamp byte-matches the datetime-coerced column the CHECK sees).
114
+ export { canonicalDatetime } from './canonical-datetime.js';
115
+
116
+ // Strand-DB-backed EnrollmentService backing: concrete MemberRegistry +
117
+ // MemberVerifier that write/read the real Strand.* membership tables.
118
+ export {
119
+ StrandMemberRegistry,
120
+ StrandMemberVerifier,
121
+ memberRegistrationPayload,
122
+ type StrandAdmission
123
+ } from './strand-member-registry.js';
124
+
125
+ // Seed Bootstrap
126
+ export {
127
+ SeedBootstrapService,
128
+ SEED_PROTOCOL,
129
+ ed25519PublicKeyB64FromPeerId,
130
+ type SeedBootstrapConfig,
131
+ type SeedEventCallbacks
132
+ } from './seed-bootstrap.js';
133
+
134
+ // Peer Authorization (shared owner-signature digest + offline verifier)
135
+ export {
136
+ peerAuthorizationDigest,
137
+ cadrePeerVoucherDigest,
138
+ cadrePeerRemoveDigest,
139
+ verifyPeerAuthorization
140
+ } from './peer-authorization.js';
141
+
142
+ // Strand Wake (control-network push-wake protocol)
143
+ export {
144
+ StrandWakeService,
145
+ dialWake,
146
+ WAKE_PROTOCOL,
147
+ type StrandWakeServiceOptions,
148
+ type DialWakeOptions
149
+ } from './strand-wake-protocol.js';
150
+
151
+ // Strand Address (control-network strand-address RPC) — wire types come via
152
+ // `export * from './types.js'` (StrandAddrRequest / StrandAddrResponse).
153
+ export {
154
+ StrandAddrService,
155
+ collectStrandAddrs,
156
+ STRAND_ADDR_PROTOCOL,
157
+ type StrandAddrServiceOptions,
158
+ type StrandAddrPeer,
159
+ type CollectStrandAddrsOptions
160
+ } from './strand-addr-protocol.js';
161
+
162
+ // Peer-address record (self-published, signed, freshness-stamped CadrePeer row)
163
+ export {
164
+ peerRecordSignedPayload,
165
+ signPeerRecord,
166
+ verifyPeerRecordSignature,
167
+ isPeerRecordFresh,
168
+ isSignalingAddr,
169
+ orderSignalingFirst,
170
+ currentMemberTrustPolicy,
171
+ DEFAULT_PEER_RECORD_MAX_AGE_MS,
172
+ DEFAULT_PEER_RECORD_HEARTBEAT_MS
173
+ } from './peer-record.js';
174
+
175
+ // Device-token record (self-published, signed FCM/APNs push token — CadrePeer sibling)
176
+ export {
177
+ deviceTokenSignedPayload,
178
+ signDeviceTokenRecord,
179
+ verifyDeviceTokenSignature,
180
+ isPushPlatform
181
+ } from './device-token.js';
182
+
183
+ // Strand-wake payload contract (canonical; shared by the server sender + RN receiver)
184
+ export { STRAND_WAKE_TYPE, type StrandWakePayload } from './strand-wake-payload.js';
185
+
186
+ // Platform push delivery — the cross-platform `PushNotifier` *contract* only.
187
+ // The FCM/APNs implementations (node:http2/node:crypto) and the
188
+ // `createPushNotifier` router that builds them live behind the Node-only subpath
189
+ // `@serfab/cadre-core/push-node` (`push-node.ts`), never in this graph. A Node
190
+ // host constructs a notifier from that subpath and injects the instance into
191
+ // `CadreNodeConfig.push.notifier`. These re-exports are type-only (erased at
192
+ // emit), so the RN/browser entry graph never pulls the Node modules in.
193
+ export type { PushNotifier, PushMessage, PushSendResult } from './push-notifier.js';
194
+
195
+ // Push-credential validation + log redaction. Dependency-free (type-only imports),
196
+ // so a provisioner (cadre-host / cadre-provider) can reject a partial credential
197
+ // set and produce a key-safe log view without pulling the FCM/APNs sender graph.
198
+ export { validatePushCredentials, redactPushCredentials, REDACTED } from './push-credentials.js';
199
+
200
+ // Server-side push-wake trigger policy + fan-out (who/when to wake). Cross-platform
201
+ // clean — it imports only the PushNotifier *type*, so exporting it as a runtime
202
+ // value pulls no node:http2/node:crypto edge into the RN/browser graph.
203
+ export {
204
+ PushFanoutService,
205
+ DEFAULT_PUSH_COOLDOWN_MS,
206
+ DEFAULT_PUSH_DEBOUNCE_MS,
207
+ type PushFanoutOptions,
208
+ type FanoutMember
209
+ } from './push-fanout.js';
210
+
211
+ // Control-cohort dial selection (backbone-preferential, bounded out-degree) +
212
+ // the cadence/degree defaults the proactive reconcile routine reads.
213
+ export {
214
+ selectControlCohortDials,
215
+ DEFAULT_CONTROL_COHORT_RECONCILE_MS,
216
+ DEFAULT_CONTROL_COHORT_TARGET_DEGREE,
217
+ type ControlCohortSelection
218
+ } from './control-cohort.js';
219
+
220
+ // Seed trust policy (trust anchor for incoming seeds)
221
+ export {
222
+ dbAnchoredTrustPolicy,
223
+ pinnedKeyTrustPolicy,
224
+ tofuTrustPolicy,
225
+ type SeedTrustPolicy,
226
+ type SeedTrustContext,
227
+ type SeedTrustDecision
228
+ } from './seed-trust-policy.js';
229
+
230
+ // Schema Verification
231
+ export {
232
+ signSchema,
233
+ verifySchema,
234
+ assertSchemaSignature,
235
+ SchemaVerificationError
236
+ } from './schema-verification.js';
237
+
238
+ // Strand Formation transport (native cadre-core protocol)
239
+ export {
240
+ FormationListener,
241
+ dialFormation,
242
+ isValidResponderCreatesResult,
243
+ FORMATION_PROTOCOL,
244
+ type FormationParty,
245
+ type FormationContactMessage,
246
+ type FormationResultMessage,
247
+ type FormationProvisionResult,
248
+ type FormationStrandInfo,
249
+ type FormationDbConnectionInfo,
250
+ type FormationListenerOptions,
251
+ type FormationDialOptions,
252
+ type ResponderProvisionOutcome
253
+ } from './strand-formation-protocol.js';
254
+
255
+ // Strand Formation manager (drives the native transport)
256
+ export {
257
+ StrandFormationManager,
258
+ createStrandFormationManager,
259
+ type StrandFormationManagerConfig,
260
+ type StrandFormationManagerOptions
261
+ } from './strand-formation-manager.js';
262
+
263
+ // Connection-path diagnostics (relayed vs direct classification + summary)
264
+ export {
265
+ classifyTransport,
266
+ classifyConnectionPath,
267
+ summarizeConnectionPaths,
268
+ emptyConnectionPathSummary,
269
+ DEFAULT_SETTLE_WINDOW_MS,
270
+ type ConnectionPathKind,
271
+ type ConnectionTransport,
272
+ type ConnectionPath,
273
+ type ConnectionPathSummary,
274
+ type ConnectionLike
275
+ } from './diagnostics/connection-path.js';