@pollen-robotics/reachy-mini-sdk 1.10.0-dev.0.main.b50d97e → 1.10.0-dev.0.main.c8367cd

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 (128) hide show
  1. package/APP_CREATION_GUIDE.md +98 -0
  2. package/dist/lib/central-listener.d.ts +71 -0
  3. package/dist/lib/central-listener.d.ts.map +1 -0
  4. package/dist/lib/central-listener.js +277 -0
  5. package/dist/lib/central-listener.js.map +1 -0
  6. package/dist/lib/central-listener.test.d.ts +2 -0
  7. package/dist/lib/central-listener.test.d.ts.map +1 -0
  8. package/dist/lib/central-listener.test.js +378 -0
  9. package/dist/lib/central-listener.test.js.map +1 -0
  10. package/dist/lib/dc-watchdog.test.d.ts +2 -0
  11. package/dist/lib/dc-watchdog.test.d.ts.map +1 -0
  12. package/dist/lib/dc-watchdog.test.js +154 -0
  13. package/dist/lib/dc-watchdog.test.js.map +1 -0
  14. package/dist/lib/ensure-awake.test.d.ts +2 -0
  15. package/dist/lib/ensure-awake.test.d.ts.map +1 -0
  16. package/dist/lib/ensure-awake.test.js +117 -0
  17. package/dist/lib/ensure-awake.test.js.map +1 -0
  18. package/dist/lib/ice-grace.test.d.ts +2 -0
  19. package/dist/lib/ice-grace.test.d.ts.map +1 -0
  20. package/dist/lib/ice-grace.test.js +211 -0
  21. package/dist/lib/ice-grace.test.js.map +1 -0
  22. package/dist/lib/imu.test.d.ts +2 -0
  23. package/dist/lib/imu.test.d.ts.map +1 -0
  24. package/dist/lib/imu.test.js +78 -0
  25. package/dist/lib/imu.test.js.map +1 -0
  26. package/dist/lib/logger.d.ts +38 -0
  27. package/dist/lib/logger.d.ts.map +1 -0
  28. package/dist/lib/logger.js +81 -0
  29. package/dist/lib/logger.js.map +1 -0
  30. package/dist/lib/pending-replies.d.ts +127 -0
  31. package/dist/lib/pending-replies.d.ts.map +1 -0
  32. package/dist/lib/pending-replies.js +255 -0
  33. package/dist/lib/pending-replies.js.map +1 -0
  34. package/dist/lib/pending-replies.test.d.ts +2 -0
  35. package/dist/lib/pending-replies.test.d.ts.map +1 -0
  36. package/dist/lib/pending-replies.test.js +217 -0
  37. package/dist/lib/pending-replies.test.js.map +1 -0
  38. package/dist/lib/reachy-mini.d.ts +202 -88
  39. package/dist/lib/reachy-mini.d.ts.map +1 -1
  40. package/dist/lib/reachy-mini.js +651 -566
  41. package/dist/lib/reachy-mini.js.map +1 -1
  42. package/dist/lib/redial.test.d.ts +2 -0
  43. package/dist/lib/redial.test.d.ts.map +1 -0
  44. package/dist/lib/redial.test.js +206 -0
  45. package/dist/lib/redial.test.js.map +1 -0
  46. package/dist/lib/request.test.d.ts +2 -0
  47. package/dist/lib/request.test.d.ts.map +1 -0
  48. package/dist/lib/request.test.js +90 -0
  49. package/dist/lib/request.test.js.map +1 -0
  50. package/dist/lib/session-supervisor.d.ts +201 -0
  51. package/dist/lib/session-supervisor.d.ts.map +1 -0
  52. package/dist/lib/session-supervisor.js +520 -0
  53. package/dist/lib/session-supervisor.js.map +1 -0
  54. package/dist/lib/token-store.d.ts +50 -0
  55. package/dist/lib/token-store.d.ts.map +1 -0
  56. package/dist/lib/token-store.js +120 -0
  57. package/dist/lib/token-store.js.map +1 -0
  58. package/dist/lib/token-store.test.d.ts +2 -0
  59. package/dist/lib/token-store.test.d.ts.map +1 -0
  60. package/dist/lib/token-store.test.js +82 -0
  61. package/dist/lib/token-store.test.js.map +1 -0
  62. package/dist/lib/types.d.ts +280 -6
  63. package/dist/lib/types.d.ts.map +1 -1
  64. package/dist/lib/url-helpers.d.ts.map +1 -1
  65. package/dist/lib/url-helpers.js +3 -1
  66. package/dist/lib/url-helpers.js.map +1 -1
  67. package/dist/lib/version.d.ts +2 -0
  68. package/dist/lib/version.d.ts.map +1 -0
  69. package/dist/lib/version.js +5 -0
  70. package/dist/lib/version.js.map +1 -0
  71. package/dist/reachy-mini-sdk.d.ts +15 -1
  72. package/dist/reachy-mini-sdk.d.ts.map +1 -1
  73. package/dist/reachy-mini-sdk.js +12 -0
  74. package/dist/reachy-mini-sdk.js.map +1 -1
  75. package/host/dist/ReachyHost.d.ts.map +1 -1
  76. package/host/dist/chunks/centralRest-uXNMidlL.js +2315 -0
  77. package/host/dist/chunks/index-Beu4GNzi.js +821 -0
  78. package/host/dist/chunks/{mountHost-DjE5zE9R.js → mountHost-BTTF4Uu9.js} +24933 -27488
  79. package/host/dist/components/ConnectingView.d.ts.map +1 -1
  80. package/host/dist/components/DaemonUpdateGate.d.ts +65 -0
  81. package/host/dist/components/DaemonUpdateGate.d.ts.map +1 -0
  82. package/host/dist/components/ErrorView.d.ts.map +1 -1
  83. package/host/dist/components/HostErrorBoundary.d.ts +30 -0
  84. package/host/dist/components/HostErrorBoundary.d.ts.map +1 -0
  85. package/host/dist/components/LeavingView.d.ts.map +1 -1
  86. package/host/dist/components/PickerView.d.ts +16 -14
  87. package/host/dist/components/PickerView.d.ts.map +1 -1
  88. package/host/dist/components/PostOAuthSplash.d.ts +14 -9
  89. package/host/dist/components/PostOAuthSplash.d.ts.map +1 -1
  90. package/host/dist/components/ReachyHostShell.d.ts.map +1 -1
  91. package/host/dist/components/SignInView.d.ts.map +1 -1
  92. package/host/dist/components/TopBar.d.ts +5 -6
  93. package/host/dist/components/TopBar.d.ts.map +1 -1
  94. package/host/dist/components/WelcomeBackOverlay.d.ts.map +1 -1
  95. package/host/dist/embed/index.d.ts +68 -5
  96. package/host/dist/embed/index.d.ts.map +1 -1
  97. package/host/dist/entry/auto.js +2 -2
  98. package/host/dist/entry/embed.d.ts +5 -4
  99. package/host/dist/entry/embed.d.ts.map +1 -1
  100. package/host/dist/entry/embed.js +5 -4
  101. package/host/dist/hooks/useHostBridge.d.ts +35 -1
  102. package/host/dist/hooks/useHostBridge.d.ts.map +1 -1
  103. package/host/dist/hooks/useLatestDaemonVersion.d.ts +7 -0
  104. package/host/dist/hooks/useLatestDaemonVersion.d.ts.map +1 -0
  105. package/host/dist/hooks/useOAuth.d.ts +18 -0
  106. package/host/dist/hooks/useOAuth.d.ts.map +1 -1
  107. package/host/dist/hooks/useRobots.d.ts +0 -2
  108. package/host/dist/hooks/useRobots.d.ts.map +1 -1
  109. package/host/dist/index.d.ts +2 -2
  110. package/host/dist/index.d.ts.map +1 -1
  111. package/host/dist/index.js +11 -10
  112. package/host/dist/lib/daemonRelease.d.ts +84 -0
  113. package/host/dist/lib/daemonRelease.d.ts.map +1 -0
  114. package/host/dist/lib/protocol.d.ts +141 -7
  115. package/host/dist/lib/protocol.d.ts.map +1 -1
  116. package/host/dist/lib/rebootWatch.d.ts +51 -0
  117. package/host/dist/lib/rebootWatch.d.ts.map +1 -0
  118. package/host/dist/lib/sdk-types.d.ts +1 -1
  119. package/host/dist/lib/sdk-types.d.ts.map +1 -1
  120. package/host/dist/lib/sdkStaleness.d.ts +78 -0
  121. package/host/dist/lib/sdkStaleness.d.ts.map +1 -0
  122. package/host/dist/lib/settings.d.ts +16 -5
  123. package/host/dist/lib/settings.d.ts.map +1 -1
  124. package/package.json +11 -4
  125. package/host/dist/chunks/index-lMs2sCXG.js +0 -439
  126. package/host/dist/chunks/reachy-mini-CdsdQLIU.js +0 -1523
  127. package/host/dist/lib/centralListener.d.ts +0 -73
  128. package/host/dist/lib/centralListener.d.ts.map +0 -1
@@ -0,0 +1,520 @@
1
+ /**
2
+ * Session resilience supervisor for the ReachyMini SDK.
3
+ *
4
+ * Owns the three transport-resilience mechanisms so the main class only
5
+ * has to forward browser/pc events and expose narrow accessors:
6
+ *
7
+ * 1. ICE-blip debounce + network awareness — grace windows around
8
+ * `iceConnectionState` transitions (visibility-deferred while the
9
+ * tab is hidden) and `online`/`offline`/`connection.change`
10
+ * forwarding as public events.
11
+ * 2. Automatic session re-dial — the daemon's `webrtcbin` cannot do a
12
+ * standards ICE restart (missing upstream support), so the recovery
13
+ * unit is the whole SESSION: tear down the dead RTCPeerConnection
14
+ * and dial the same robot again through central. Runs inside the
15
+ * SDK so every consumer (host embed, mobile app, third-party apps)
16
+ * inherits it.
17
+ * 3. Data-channel silence watchdog — application-level heartbeat
18
+ * without any daemon change: a streaming session already receives
19
+ * constant inbound traffic (get_state poll replies and/or pushed
20
+ * pose frames), so prolonged TOTAL silence is a dead transport that
21
+ * ICE hasn't noticed yet.
22
+ *
23
+ * The supervisor never touches the RTCPeerConnection, data channel or
24
+ * pending promises directly: every side effect goes through
25
+ * `SessionSupervisorDeps`, implemented by `ReachyMini` as closures over
26
+ * its own private state. That keeps the ownership boundary honest — the
27
+ * class owns the transport, the supervisor owns the RECOVERY POLICY.
28
+ */
29
+ import { createLogger } from './logger.js';
30
+ const log = createLogger('session');
31
+ /* ─── Tunables ─────────────────────────────────────────────────────── */
32
+ // ICE grace windows. `disconnected` is transient per spec (browsers
33
+ // usually heal it in 1-2 s on WiFi blips / AP roams), so debounce it.
34
+ // `failed` IS terminal per spec, but we've observed real
35
+ // `failed → connected` flips on rapid AP roams and iOS BT route
36
+ // changes — 1 s of debounce absorbs those without noticeably delaying
37
+ // a real failure.
38
+ const ICE_DISCONNECT_GRACE_MS = 3000;
39
+ const ICE_FAILED_GRACE_MS = 1000;
40
+ /**
41
+ * Ceiling on how long `armIceGraceOnVisibility` waits for a hidden tab
42
+ * to come back. The daemon's `webrtcsink` runs a STUN consent-freshness
43
+ * check (RFC 7675, ~30 s default) and unilaterally tears its side of
44
+ * the session down past that window, releasing the producer slot on
45
+ * central — so after a long background stint the transport is gone and
46
+ * another foreground grace would be a lie. 60 s gives a 2× margin over
47
+ * the daemon-side timeout: long enough to absorb "phone in pocket for
48
+ * 45 s", short enough to be honest with the user.
49
+ */
50
+ const MAX_VISIBILITY_DEFER_MS = 60_000;
51
+ // Re-dial backoff (one entry per attempt, in ms of wait BEFORE that
52
+ // attempt): ~22 s window + dial time. Each attempt is capped because
53
+ // past ~15 s the robot side likely still holds the previous (dead)
54
+ // session — tear the attempt down and retry against a freed slot.
55
+ const REDIAL_BACKOFF_MS = [0, 2000, 4000, 8000, 8000];
56
+ const REDIAL_DIAL_TIMEOUT_MS = 15_000;
57
+ // Data-channel silence watchdog. A streaming session receives constant
58
+ // inbound traffic (`get_state` replies every ≤500 ms and/or ~30 Hz pose
59
+ // frames), so prolonged TOTAL silence is a dead transport even while
60
+ // ICE still reports 'connected' (half-open links dodge STUN consent
61
+ // checks for tens of seconds). Past the nudge threshold send one extra
62
+ // `get_state`; past the fatal threshold escalate — still ~4× faster
63
+ // than the RFC 7675 consent-freshness teardown (~30 s). Hidden /
64
+ // throttled tabs re-baseline instead of judging stale data.
65
+ const DC_WATCHDOG_TICK_MS = 1000;
66
+ const DC_SILENCE_NUDGE_MS = 2500;
67
+ const DC_SILENCE_FATAL_MS = 8000;
68
+ /* ─── Supervisor ───────────────────────────────────────────────────── */
69
+ export class SessionSupervisor {
70
+ _deps;
71
+ // ICE-blip debounce. All handler slots are scoped to the lifetime
72
+ // of a live session (armed from the pc's state handler, cleared by
73
+ // every teardown path via `clearIceGrace`).
74
+ _iceGraceTimer = null;
75
+ _iceGraceReason = null;
76
+ _pendingVisibilityHandler = null;
77
+ // Network awareness (installed in startSession, removed on teardown).
78
+ _onlineHandler = null;
79
+ _offlineHandler = null;
80
+ _connectionChangeHandler = null;
81
+ // Automatic re-dial. Mutable via `setAutoReconnect()` so flows that
82
+ // EXPECT a transport death (the daemon self-update reboot) can stand
83
+ // the machinery down.
84
+ _autoReconnect;
85
+ _redialing = false;
86
+ _redialWake = null;
87
+ // Data-channel silence watchdog.
88
+ _dcWatchdogId = null;
89
+ _lastDcInboundAt = 0;
90
+ _dcWatchdogLastTickAt = 0;
91
+ _dcSilenceNudged = false;
92
+ constructor(deps, options) {
93
+ this._deps = deps;
94
+ this._autoReconnect = options.autoReconnect;
95
+ }
96
+ /** True while the re-dial loop is running. */
97
+ get redialing() {
98
+ return this._redialing;
99
+ }
100
+ /**
101
+ * Enable/disable the automatic session re-dial at runtime.
102
+ * Disabling also aborts any re-dial already in flight.
103
+ */
104
+ setAutoReconnect(enabled) {
105
+ this._autoReconnect = enabled;
106
+ if (!enabled)
107
+ this.cancelRedial();
108
+ }
109
+ /** Fatal errors carry the classic webrtc `error` payload shape. */
110
+ _emitError(error) {
111
+ this._deps.emit('error', { source: 'webrtc', error });
112
+ }
113
+ /* ─── ICE-blip debounce + network awareness ──────────────────────
114
+ *
115
+ * Both halves below are intentionally generic (they don't know
116
+ * about motion, audio, or the FSM): they just smooth out
117
+ * browser-level events so the consumer's own state machine doesn't
118
+ * get torn down by routine WiFi/4G/screen-off noise.
119
+ */
120
+ /** ICE healed back to `connected`/`completed`. */
121
+ onIceHealed() {
122
+ this.clearIceGrace();
123
+ }
124
+ /**
125
+ * ICE hit `disconnected` — TRANSIENT per spec, debounce before
126
+ * escalating. If the tab is hidden, JS timers are throttled and
127
+ * would fire unpredictably late, so defer the grace window to the
128
+ * next foreground frame.
129
+ */
130
+ onIceDisconnected() {
131
+ if (typeof document !== 'undefined' && document.hidden) {
132
+ this._armIceGraceOnVisibility();
133
+ }
134
+ else {
135
+ this._scheduleIceGrace(ICE_DISCONNECT_GRACE_MS, 'disconnected');
136
+ }
137
+ }
138
+ /**
139
+ * ICE hit `failed` — terminal per spec, but in practice we've seen
140
+ * `failed → connected` on rapid AP roams / BT route changes on iOS.
141
+ * Give the ICE agent a short window to surprise us before rejecting
142
+ * the session.
143
+ */
144
+ onIceFailed() {
145
+ this._scheduleIceGrace(ICE_FAILED_GRACE_MS, 'failed');
146
+ }
147
+ /**
148
+ * Cancel any pending ICE grace timer and visibility handler. Called
149
+ * on a healed `connected`/`completed` transition AND from the
150
+ * lifecycle teardown paths so a callback can't fire after the pc
151
+ * is closed.
152
+ */
153
+ clearIceGrace() {
154
+ if (this._iceGraceTimer !== null) {
155
+ clearTimeout(this._iceGraceTimer);
156
+ this._iceGraceTimer = null;
157
+ }
158
+ this._iceGraceReason = null;
159
+ if (this._pendingVisibilityHandler && typeof document !== 'undefined') {
160
+ document.removeEventListener('visibilitychange', this._pendingVisibilityHandler);
161
+ }
162
+ this._pendingVisibilityHandler = null;
163
+ }
164
+ /**
165
+ * Start a grace window. After `ms`, re-check the live ICE state:
166
+ * - If we healed back to `connected`/`completed`, the timer was
167
+ * already cancelled in the pc's state handler, so we never get
168
+ * here.
169
+ * - If we're still in the originally-observed bad state (or
170
+ * worse), surface the error and reject any pending session
171
+ * promise. The original code path is preserved verbatim so
172
+ * downstream consumers see the same `error` payload shape.
173
+ */
174
+ _scheduleIceGrace(ms, reason) {
175
+ // Coalesce: if a grace is already pending and the reason hasn't
176
+ // changed, keep the original timer so a flurry of identical
177
+ // transitions doesn't reset the clock. If the reason changed
178
+ // (typically `disconnected` → `failed`, but also the reverse on
179
+ // some Android WebViews), replace the timer with the new
180
+ // (reason, ms) pair — the latest signal wins.
181
+ if (this._iceGraceTimer !== null) {
182
+ if (this._iceGraceReason === reason)
183
+ return;
184
+ clearTimeout(this._iceGraceTimer);
185
+ }
186
+ this._iceGraceReason = reason;
187
+ this._iceGraceTimer = setTimeout(() => {
188
+ this._iceGraceTimer = null;
189
+ const r = this._iceGraceReason;
190
+ this._iceGraceReason = null;
191
+ const s = this._deps.iceState();
192
+ if (s === 'connected' || s === 'completed')
193
+ return; // healed
194
+ if (r === 'disconnected' && s === 'disconnected') {
195
+ if (this.maybeBeginRedial(`ICE stuck in 'disconnected' for > ${ms}ms`))
196
+ return;
197
+ this._emitError(new Error(`ICE stuck in 'disconnected' for > ${ms}ms`));
198
+ return;
199
+ }
200
+ if (r === 'failed' || s === 'failed') {
201
+ const err = new Error('ICE connection failed');
202
+ if (this._deps.rejectPendingSession(err)) {
203
+ // Mid-setup failure: keep the promise contract — the
204
+ // caller of startSession() owns the retry decision.
205
+ // When that caller is the redial loop itself, skip
206
+ // the fatal `error` emit: the loop retries and the
207
+ // app only sees `sessionReconnecting`.
208
+ if (!this._redialing)
209
+ this._emitError(err);
210
+ return;
211
+ }
212
+ if (this.maybeBeginRedial('ICE connection failed'))
213
+ return;
214
+ this._emitError(err);
215
+ }
216
+ }, ms);
217
+ }
218
+ /**
219
+ * `disconnected` while the tab is hidden. JS timers are throttled
220
+ * in background tabs (Chrome clamps to ~1 Hz, Safari can pause
221
+ * altogether), so a foreground grace timer would either miss the
222
+ * window or fire long after the connection healed. Wait for the
223
+ * tab to come back, then re-evaluate.
224
+ */
225
+ _armIceGraceOnVisibility() {
226
+ if (this._pendingVisibilityHandler)
227
+ return;
228
+ const deferredAt = Date.now();
229
+ const handler = () => {
230
+ if (typeof document !== 'undefined' && document.hidden)
231
+ return;
232
+ document.removeEventListener('visibilitychange', handler);
233
+ this._pendingVisibilityHandler = null;
234
+ if (!this._deps.hasPc())
235
+ return;
236
+ const s = this._deps.iceState();
237
+ if (s === 'connected' || s === 'completed')
238
+ return; // healed in bg
239
+ // Ceiling: if the user backgrounded past the daemon's
240
+ // ICE-consent freshness window the session is gone from
241
+ // the daemon's side regardless of what the pc reports
242
+ // locally. Running another foreground grace would tell
243
+ // the user "Reconnecting…" for a recovery that can never
244
+ // happen. Escalate immediately so the host renders the
245
+ // real "session expired" UX. See MAX_VISIBILITY_DEFER_MS.
246
+ if (Date.now() - deferredAt > MAX_VISIBILITY_DEFER_MS) {
247
+ const err = new Error('Session expired while tab was backgrounded');
248
+ if (this._deps.rejectPendingSession(err)) {
249
+ if (!this._redialing)
250
+ this._emitError(err);
251
+ return;
252
+ }
253
+ // The transport is unrecoverable (daemon dropped its side
254
+ // past the consent-freshness window) but the user is BACK
255
+ // and looking at the app — the perfect moment to re-dial
256
+ // rather than render "session expired".
257
+ if (this.maybeBeginRedial('Session expired while tab was backgrounded'))
258
+ return;
259
+ this._emitError(err);
260
+ return;
261
+ }
262
+ if (s === 'failed') {
263
+ this._scheduleIceGrace(ICE_FAILED_GRACE_MS, 'failed');
264
+ return;
265
+ }
266
+ // Still disconnected when we came back — give it a normal
267
+ // foreground grace window now that timers fire reliably.
268
+ this._scheduleIceGrace(ICE_DISCONNECT_GRACE_MS, 'disconnected');
269
+ };
270
+ document.addEventListener('visibilitychange', handler);
271
+ this._pendingVisibilityHandler = handler;
272
+ }
273
+ /**
274
+ * Install browser-level network listeners and forward them as
275
+ * public `networkOnline` / `networkOffline` / `networkChange`
276
+ * events on the instance. Idempotent: called from `startSession()`,
277
+ * removed by `uninstallNetworkListeners` on teardown. Reachable
278
+ * only when there's a live `window` (defensive guard for SSR /
279
+ * test environments).
280
+ *
281
+ * `online` / `offline` are semantically about CONNECTIVITY:
282
+ * "does the OS think we can reach the internet". They flip
283
+ * symmetrically.
284
+ *
285
+ * `connection.change` (NetworkInformation API, Chrome / Android
286
+ * WebView only) is semantically about the TRANSPORT: it fires
287
+ * on Wi-Fi → 4G swaps, AP roams, etc. without necessarily going
288
+ * through `offline`. We forward it as its own `networkChange`
289
+ * event rather than aliasing it onto `networkOnline`, so
290
+ * consumers don't have to guess whether they're seeing a real
291
+ * connectivity recovery or a silent transport swap.
292
+ */
293
+ installNetworkListeners() {
294
+ if (this._onlineHandler || typeof window === 'undefined')
295
+ return;
296
+ const onOnline = () => this._deps.emit('networkOnline', {});
297
+ const onOffline = () => this._deps.emit('networkOffline', {});
298
+ window.addEventListener('online', onOnline);
299
+ window.addEventListener('offline', onOffline);
300
+ this._onlineHandler = onOnline;
301
+ this._offlineHandler = onOffline;
302
+ const conn = navigator.connection;
303
+ if (conn && typeof conn.addEventListener === 'function') {
304
+ const onChange = () => this._deps.emit('networkChange', {
305
+ effectiveType: conn.effectiveType,
306
+ downlink: conn.downlink,
307
+ rtt: conn.rtt,
308
+ saveData: conn.saveData,
309
+ });
310
+ conn.addEventListener('change', onChange);
311
+ this._connectionChangeHandler = onChange;
312
+ }
313
+ }
314
+ /** Counterpart to `installNetworkListeners`. */
315
+ uninstallNetworkListeners() {
316
+ if (typeof window !== 'undefined') {
317
+ if (this._onlineHandler) {
318
+ window.removeEventListener('online', this._onlineHandler);
319
+ }
320
+ if (this._offlineHandler) {
321
+ window.removeEventListener('offline', this._offlineHandler);
322
+ }
323
+ }
324
+ const conn = navigator.connection;
325
+ if (conn && this._connectionChangeHandler && typeof conn.removeEventListener === 'function') {
326
+ conn.removeEventListener('change', this._connectionChangeHandler);
327
+ }
328
+ this._onlineHandler = null;
329
+ this._offlineHandler = null;
330
+ this._connectionChangeHandler = null;
331
+ }
332
+ /* ─── Automatic session re-dial ─────────────────────────────────── */
333
+ /**
334
+ * Escalation funnel for dead-transport signals. Returns `true` when
335
+ * the failure is being handled by a re-dial (callers should then skip
336
+ * their fatal `error` emit). Only fires for an ESTABLISHED session:
337
+ * mid-setup failures keep rejecting the startSession() promise so the
338
+ * original caller stays in charge of retries.
339
+ */
340
+ maybeBeginRedial(cause) {
341
+ if (!this._autoReconnect)
342
+ return false;
343
+ if (this._redialing)
344
+ return true; // already in progress
345
+ if (this._deps.isMidSetup())
346
+ return false;
347
+ const robotId = this._deps.selectedRobotId();
348
+ if (!robotId || !this._deps.hasPc())
349
+ return false;
350
+ this._redialing = true;
351
+ log.info(`auto-reconnect: ${cause} - re-dialing ${robotId}`);
352
+ void this._runRedialLoop(robotId, cause);
353
+ return true;
354
+ }
355
+ /**
356
+ * The re-dial loop. One teardown, then up to REDIAL_BACKOFF_MS.length
357
+ * attempts, each preceded by its backoff slot. Every attempt re-runs
358
+ * the normal startSession() flow (and connect() first if the SSE feed
359
+ * died with the network), so a success is indistinguishable from a
360
+ * fresh session: `streaming` re-fires, videoTrack re-attaches, the
361
+ * pose subscription is re-asserted by the session-ready path.
362
+ *
363
+ * Exits early — without emitting anything further — when the user
364
+ * calls stopSession()/disconnect() or dials another robot, all of
365
+ * which flip `_redialing` off via `cancelRedial`.
366
+ */
367
+ async _runRedialLoop(robotId, cause) {
368
+ const maxAttempts = REDIAL_BACKOFF_MS.length;
369
+ this._deps.teardownForRedial();
370
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
371
+ if (!this._redialing)
372
+ return;
373
+ this._deps.emit('sessionReconnecting', { attempt, maxAttempts, cause });
374
+ await this._redialSleep(REDIAL_BACKOFF_MS[attempt - 1]);
375
+ if (!this._redialing)
376
+ return;
377
+ try {
378
+ if (this._deps.isSignalingDown()) {
379
+ // The network drop also killed the signaling SSE feed —
380
+ // re-establish it before dialing.
381
+ await this._deps.reconnectSignaling();
382
+ }
383
+ if (!this._redialing)
384
+ return;
385
+ await withTimeout(this._deps.dial(robotId), REDIAL_DIAL_TIMEOUT_MS, 'auto-reconnect dial timed out');
386
+ if (!this._redialing)
387
+ return;
388
+ this._redialing = false;
389
+ log.info(`auto-reconnect: recovered on attempt ${attempt}`);
390
+ this._deps.emit('sessionReconnected', { attempt });
391
+ return;
392
+ }
393
+ catch (e) {
394
+ log.warn(`auto-reconnect attempt ${attempt}/${maxAttempts} failed:`, e);
395
+ // A failed attempt can leave a half-open pc (e.g. the dial
396
+ // timed out mid-ICE). Clean it so the next attempt starts
397
+ // from a blank slate; also frees the robot-side slot via
398
+ // endSession when one was allocated.
399
+ if (this._redialing)
400
+ this._deps.teardownForRedial();
401
+ }
402
+ }
403
+ if (!this._redialing)
404
+ return;
405
+ this._redialing = false;
406
+ const message = `Auto-reconnect gave up after ${maxAttempts} attempts (${cause})`;
407
+ log.warn(message);
408
+ this._deps.emit('sessionStopped', { reason: 'reconnect_failed', message });
409
+ this._emitError(new Error(message));
410
+ }
411
+ /**
412
+ * Cancellable backoff sleep — `cancelRedial` wakes it immediately.
413
+ * A timer firing after a cancel is harmless: the loop re-checks
414
+ * `_redialing` right after every sleep.
415
+ */
416
+ _redialSleep(ms) {
417
+ if (ms <= 0)
418
+ return Promise.resolve();
419
+ return new Promise((resolve) => {
420
+ this._redialWake = resolve;
421
+ setTimeout(() => {
422
+ if (this._redialWake === resolve)
423
+ this._redialWake = null;
424
+ resolve();
425
+ }, ms);
426
+ });
427
+ }
428
+ /**
429
+ * Abort a pending re-dial. Called from stopSession(), disconnect()
430
+ * and an external startSession() — every path where the user (or the
431
+ * consumer app) takes over the session lifecycle.
432
+ */
433
+ cancelRedial() {
434
+ if (!this._redialing && !this._redialWake)
435
+ return;
436
+ this._redialing = false;
437
+ if (this._redialWake) {
438
+ const wake = this._redialWake;
439
+ this._redialWake = null;
440
+ wake();
441
+ }
442
+ }
443
+ /* ─── Data-channel silence watchdog ─────────────────────────────── */
444
+ /** (Re)start the watchdog. Called when a session reaches ready. */
445
+ startDcWatchdog() {
446
+ this.stopDcWatchdog();
447
+ const now = Date.now();
448
+ this._lastDcInboundAt = now;
449
+ this._dcWatchdogLastTickAt = now;
450
+ this._dcSilenceNudged = false;
451
+ this._dcWatchdogId = setInterval(() => this._dcWatchdogTick(), DC_WATCHDOG_TICK_MS);
452
+ }
453
+ stopDcWatchdog() {
454
+ if (this._dcWatchdogId !== null) {
455
+ clearInterval(this._dcWatchdogId);
456
+ this._dcWatchdogId = null;
457
+ }
458
+ }
459
+ /**
460
+ * Liveness stamp: every inbound data-channel message — control
461
+ * replies, broadcasts, pose frames — proves the transport is alive.
462
+ */
463
+ stampDcInbound() {
464
+ this._lastDcInboundAt = Date.now();
465
+ this._dcSilenceNudged = false;
466
+ }
467
+ _dcWatchdogTick() {
468
+ const now = Date.now();
469
+ const tickGap = now - this._dcWatchdogLastTickAt;
470
+ this._dcWatchdogLastTickAt = now;
471
+ if (!this._deps.isStreaming())
472
+ return;
473
+ // A tick that arrives way late means the timer was throttled or
474
+ // suspended (hidden tab, iOS app switch). Whatever silence we
475
+ // measure against that stale baseline is meaningless — and the
476
+ // background-transport question is already owned by the
477
+ // visibility-deferred ICE grace. Re-baseline and judge again
478
+ // from the next healthy tick.
479
+ if (tickGap > DC_WATCHDOG_TICK_MS * 3) {
480
+ this._lastDcInboundAt = now;
481
+ this._dcSilenceNudged = false;
482
+ return;
483
+ }
484
+ if (typeof document !== 'undefined' && document.hidden) {
485
+ this._lastDcInboundAt = now;
486
+ this._dcSilenceNudged = false;
487
+ return;
488
+ }
489
+ const silence = now - this._lastDcInboundAt;
490
+ if (silence < DC_SILENCE_NUDGE_MS)
491
+ return;
492
+ if (!this._dcSilenceNudged) {
493
+ // One extra get_state so a legitimately quiet channel (pose
494
+ // stream just died, poll on hold) gets a chance to answer
495
+ // before we call the transport dead. Any inbound message
496
+ // clears the flag via `stampDcInbound`.
497
+ this._dcSilenceNudged = true;
498
+ this._deps.nudgeState();
499
+ return;
500
+ }
501
+ if (silence < DC_SILENCE_FATAL_MS)
502
+ return;
503
+ // Dead transport, and no redial to hand it to: emit once and
504
+ // stand down. The next startDcWatchdog() re-arms.
505
+ this._dcSilenceNudged = false;
506
+ const cause = `No data-channel traffic for ${silence}ms`;
507
+ if (this.maybeBeginRedial(cause))
508
+ return;
509
+ this.stopDcWatchdog();
510
+ this._emitError(new Error(cause));
511
+ }
512
+ }
513
+ /** Reject `p` with `label` if it hasn't settled within `ms`. */
514
+ function withTimeout(p, ms, label) {
515
+ return Promise.race([
516
+ p,
517
+ new Promise((_, rj) => setTimeout(() => rj(new Error(label)), ms)),
518
+ ]);
519
+ }
520
+ //# sourceMappingURL=session-supervisor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-supervisor.js","sourceRoot":"","sources":["../../lib/session-supervisor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Session-scoped persistence of the HF OAuth token.
3
+ *
4
+ * The token lives in `sessionStorage` (tab-scoped, dies with the tab),
5
+ * under keys shared with the host shell and the embed bootstrap. On top
6
+ * of the OAuth expiry, reads enforce a SLIDING IDLE WINDOW: a token
7
+ * whose last successful read is older than `TOKEN_MAX_IDLE_MS` is
8
+ * treated as absent and wiped. That property replaced the host's old
9
+ * "wipe on pagehide" hygiene, which fired on every reload / navigation
10
+ * (pagehide can't tell a refresh from a tab close) and forced a
11
+ * re-login on every F5. The idle window keeps the one attack the wipe
12
+ * actually covered - a tab resurrected days later via session restore
13
+ * gets its sessionStorage back - without punishing reloads.
14
+ *
15
+ * `consumeOAuthErrorParams` supports the silent sign-in flow
16
+ * (`login({ prompt: 'none' })`): a failed silent authorization comes
17
+ * back as `?error=login_required...` query params, which must be
18
+ * stripped from the URL before the app router or a copy-pasted link
19
+ * leaks them around.
20
+ */
21
+ /** Idle cutoff: a cached token unused for longer than this is dropped. */
22
+ export declare const TOKEN_MAX_IDLE_MS: number;
23
+ export interface StoredToken {
24
+ token: string;
25
+ username: string;
26
+ /** ISO date string of the OAuth expiry. */
27
+ expires: string;
28
+ }
29
+ /** Persist a token bundle and stamp its last-seen time. */
30
+ export declare function writeStoredToken(bundle: StoredToken): void;
31
+ /**
32
+ * Read the cached token if it is still usable, refreshing its
33
+ * last-seen stamp (sliding window). Returns `null` - and wipes the
34
+ * store - when the token is expired or idle for too long.
35
+ *
36
+ * A missing last-seen stamp (bundle written by an older SDK, or seeded
37
+ * by the embed's fragment-credentials path) is treated as fresh and
38
+ * stamped now, so a rollout never logs anyone out.
39
+ */
40
+ export declare function readUsableToken(now?: number): StoredToken | null;
41
+ /** Remove every token key from the store. */
42
+ export declare function clearStoredToken(): void;
43
+ /**
44
+ * Detect an OAuth error return (`?error=...` from a `prompt=none`
45
+ * silent authorization), strip the OAuth params from the URL, and
46
+ * return the error code. Returns `null` when the URL carries no OAuth
47
+ * error. Other query params are preserved.
48
+ */
49
+ export declare function consumeOAuthErrorParams(): string | null;
50
+ //# sourceMappingURL=token-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../lib/token-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAOH,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,QAAsB,CAAC;AAErD,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,2DAA2D;AAC3D,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAS1D;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAmB,GAAG,WAAW,GAAG,IAAI,CAyB5E;AAED,6CAA6C;AAC7C,wBAAgB,gBAAgB,IAAI,IAAI,CASvC;AAeD;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAavD"}