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

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 (127) hide show
  1. package/APP_CREATION_GUIDE.md +102 -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-D6qt3s1W.js} +24927 -27489
  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/TopBar.d.ts +5 -6
  92. package/host/dist/components/TopBar.d.ts.map +1 -1
  93. package/host/dist/components/WelcomeBackOverlay.d.ts.map +1 -1
  94. package/host/dist/embed/index.d.ts +68 -5
  95. package/host/dist/embed/index.d.ts.map +1 -1
  96. package/host/dist/entry/auto.js +2 -2
  97. package/host/dist/entry/embed.d.ts +5 -4
  98. package/host/dist/entry/embed.d.ts.map +1 -1
  99. package/host/dist/entry/embed.js +5 -4
  100. package/host/dist/hooks/useHostBridge.d.ts +35 -1
  101. package/host/dist/hooks/useHostBridge.d.ts.map +1 -1
  102. package/host/dist/hooks/useLatestDaemonVersion.d.ts +7 -0
  103. package/host/dist/hooks/useLatestDaemonVersion.d.ts.map +1 -0
  104. package/host/dist/hooks/useOAuth.d.ts +18 -0
  105. package/host/dist/hooks/useOAuth.d.ts.map +1 -1
  106. package/host/dist/hooks/useRobots.d.ts +0 -2
  107. package/host/dist/hooks/useRobots.d.ts.map +1 -1
  108. package/host/dist/index.d.ts +2 -2
  109. package/host/dist/index.d.ts.map +1 -1
  110. package/host/dist/index.js +11 -10
  111. package/host/dist/lib/daemonRelease.d.ts +84 -0
  112. package/host/dist/lib/daemonRelease.d.ts.map +1 -0
  113. package/host/dist/lib/protocol.d.ts +141 -7
  114. package/host/dist/lib/protocol.d.ts.map +1 -1
  115. package/host/dist/lib/rebootWatch.d.ts +51 -0
  116. package/host/dist/lib/rebootWatch.d.ts.map +1 -0
  117. package/host/dist/lib/sdk-types.d.ts +1 -1
  118. package/host/dist/lib/sdk-types.d.ts.map +1 -1
  119. package/host/dist/lib/sdkStaleness.d.ts +78 -0
  120. package/host/dist/lib/sdkStaleness.d.ts.map +1 -0
  121. package/host/dist/lib/settings.d.ts +16 -5
  122. package/host/dist/lib/settings.d.ts.map +1 -1
  123. package/package.json +11 -4
  124. package/host/dist/chunks/index-lMs2sCXG.js +0 -439
  125. package/host/dist/chunks/reachy-mini-CdsdQLIU.js +0 -1523
  126. package/host/dist/lib/centralListener.d.ts +0 -73
  127. package/host/dist/lib/centralListener.d.ts.map +0 -1
@@ -69,6 +69,7 @@ app's UI** and nothing else.
69
69
  4. [Standalone exit hooks (`installShutdownHandler`)](#144-standalone-exit-hooks-installshutdownhandler)
70
70
  5. [Daemon parity warning](#145-daemon-parity-warning)
71
71
  6. [Anti-patterns](#146-anti-patterns)
72
+ 7. [Backgrounded tabs: clock robot logic off a Web Worker](#147-backgrounded-tabs-clock-robot-logic-off-a-web-worker)
72
73
 
73
74
  ---
74
75
 
@@ -713,6 +714,7 @@ app_build_command: npm ci && npm run build # HF runs this on its builder
713
714
  app_file: dist/index.html # HF serves the build output as entry
714
715
  pinned: false
715
716
  hf_oauth: true
717
+ hf_oauth_expiration_minutes: 43200 # 30 days (max); default is only 8 h
716
718
  short_description: One-line description shown in the mobile catalog.
717
719
  tags:
718
720
  - reachy_mini
@@ -729,6 +731,21 @@ tags:
729
731
  build completes**. For a Vite-built SPA that's `dist/index.html`.
730
732
  - `hf_oauth: true` is what triggers `__OAUTH_CLIENT_ID__` substitution
731
733
  inside HTML files in the served output (post-build).
734
+ - `hf_oauth_expiration_minutes` sets the OAuth token lifetime. Strongly
735
+ recommended: the default (8 hours) forces a re-auth round trip several
736
+ times a day. The host's silent sign-in makes those round trips
737
+ invisible for logged-in users, but a long-lived token avoids them
738
+ entirely. Two properties bound the exposure of that long-lived token:
739
+ - **Scope.** Don't set `hf_oauth_scopes`; the token then carries only
740
+ `openid profile` - identity (username, avatar) and nothing else. It
741
+ cannot read private repos, write to the Hub, or call the Inference
742
+ API, so a leaked token identifies the user but grants no account
743
+ access.
744
+ - **Idle window.** The SDK stores the token in `sessionStorage`
745
+ (tab-scoped, dies with the tab) and additionally drops any token
746
+ unused for more than 24 h (`TOKEN_MAX_IDLE_MS` in the SDK's token
747
+ store). A tab resurrected days later via session restore re-auths;
748
+ a plain reload does not.
732
749
  - The **`reachy_mini_js_app` tag is mandatory** for mobile-catalog
733
750
  discovery. The catalog API filters on this exact string.
734
751
  - Apps in the `pollen-robotics/*` namespace are automatically tagged
@@ -1504,6 +1521,8 @@ before trusting the payload.
1504
1521
  | host → embed | `host:leaving` | Tear-down request with `timeoutMs` |
1505
1522
  | embed → host | `embed:request-leave` | App requests end-of-session |
1506
1523
  | embed → host | `embed:error` | Error report (`{ message, fatal, detail? }`) |
1524
+ | host → embed | `host:start-update` | Run the daemon's PyPI self-update (only the embed holds a data channel) |
1525
+ | embed → host | `embed:update-progress` | Install progress, plus `rebooting` when the restart kills the session |
1507
1526
 
1508
1527
  Intentionally **not** in the v1 protocol:
1509
1528
 
@@ -1531,6 +1550,31 @@ Intentionally **not** in the v1 protocol:
1531
1550
  - `host:init` may arrive twice (rare: bridge re-arm); the embed
1532
1551
  treats the latest as authoritative and re-applies theme / config.
1533
1552
 
1553
+ #### Daemon update gate (Mode A only)
1554
+
1555
+ The shell checks the robot's daemon version against the latest
1556
+ GitHub release and can interrupt the app. App authors don't wire
1557
+ anything up - `connectToHost()` reports the version for them - but
1558
+ should know the two outcomes exist:
1559
+
1560
+ - **Below `MIN_SUPPORTED_DAEMON_VERSION`** (`host/src/lib/daemonRelease.ts`):
1561
+ a full-screen block. Such a daemon predates the OTA command, so
1562
+ the only way out is the desktop app. The app never becomes
1563
+ interactive.
1564
+ - **Merely behind the latest release**: a dismissable card once the
1565
+ app is `live`. The app keeps running; the user may start an
1566
+ update, which reboots the robot and drops the session.
1567
+
1568
+ This is deliberately softer than the mobile app, which blocks on
1569
+ any version behind the latest. A Space is public: applying that
1570
+ rule here would make every daemon release a global kill switch for
1571
+ robots that were working a minute earlier.
1572
+
1573
+ Mode B needs none of this. The mobile app never mounts the shell -
1574
+ it points its iframe straight at `?embedded=1` and runs its own
1575
+ gate before an app can be opened - so the component simply doesn't
1576
+ exist in that path.
1577
+
1534
1578
  ### 13.7 Non-goals
1535
1579
 
1536
1580
  To stay simple and auditable, the host shell explicitly does NOT do:
@@ -1793,3 +1837,61 @@ strict mode, silently no-op otherwise).
1793
1837
  | `await safelyReturnToPose(...)` expecting the move to complete. | It resolves after **dispatch**, not after motion finishes. `await sleep(plan.duration * 1000)` or subscribe to `state` if you need to wait. |
1794
1838
  | Carry degrees through your motion code. | Convert at the UI boundary; speak radians + magic-mm everywhere below it. |
1795
1839
  | Pass `null` head / antennas / body_yaw to opt a channel out of a `gotoTarget`. | Use `PartialPose` and **omit** the channel. The SDK treats omission as "hold previous target". |
1840
+
1841
+ ### 14.7 Backgrounded tabs: clock robot logic off a Web Worker
1842
+
1843
+ When the user switches tabs (or the phone locks), the browser throttles
1844
+ your app hard: `requestAnimationFrame` **pauses entirely** and
1845
+ `setInterval`/`setTimeout` are clamped to **~1 tick per second**. If your
1846
+ pose streaming, audio pipeline, or stream-health watchdog is clocked by
1847
+ either of them, the robot freezes mid-motion and stalls go unnoticed until
1848
+ the tab comes back.
1849
+
1850
+ The fix is to split your loop in two:
1851
+
1852
+ - **Logic** (pose computation + `setTarget`, audio gain, health checks,
1853
+ reconnects): clock it from a **Web Worker**. Worker timers are *not*
1854
+ visibility-throttled, and the `message` events they post are delivered
1855
+ on the main thread even while the tab is hidden.
1856
+ - **Visuals** (DOM updates, canvas, meters): keep them on `rAF`. It pauses
1857
+ when hidden - which is exactly what you want for work nobody can see -
1858
+ and resumes on its own.
1859
+
1860
+ ```ts
1861
+ // pose-heartbeat.worker.ts - the whole file:
1862
+ const INTERVAL_MS = 25; // ~40 Hz; the main thread down-samples as needed
1863
+ setInterval(() => postMessage(0), INTERVAL_MS);
1864
+ ```
1865
+
1866
+ ```ts
1867
+ // embed.ts
1868
+ let worker: Worker | null = null;
1869
+ try {
1870
+ worker = new Worker(new URL("./pose-heartbeat.worker.ts", import.meta.url), { type: "module" });
1871
+ worker.onmessage = () => stepLogic(performance.now());
1872
+ } catch {
1873
+ // Workers unavailable (rare): degrade to a throttled interval.
1874
+ setInterval(() => stepLogic(performance.now()), 25);
1875
+ }
1876
+
1877
+ const renderVisuals = () => { /* DOM/canvas only - no robot commands */ };
1878
+ const rafLoop = () => { renderVisuals(); requestAnimationFrame(rafLoop); };
1879
+ requestAnimationFrame(rafLoop);
1880
+ ```
1881
+
1882
+ Two companion rules make this robust:
1883
+
1884
+ - **Clamp your `dt`.** After a long hidden stretch the first tick sees a
1885
+ huge time delta; clamp it (e.g. `Math.min(dt, 100)`) so filters and
1886
+ interpolators don't jump.
1887
+ - **Resync on `visibilitychange`.** When the tab returns, call
1888
+ `reachy.requestState()` and run one logic step immediately instead of
1889
+ waiting for the next scheduled tick, so the UI repaints from fresh state.
1890
+
1891
+ Remember to `worker.terminate()` in your `onLeave` cleanup. A complete
1892
+ reference implementation lives in the `reachy_mini_radio_js` app
1893
+ (`src/embed.ts` + `src/pose-heartbeat.worker.ts`), and the
1894
+ [`pollen-robotics/sdk-js-demo-app`](https://huggingface.co/spaces/pollen-robotics/sdk-js-demo-app)
1895
+ Space both applies the pattern to its pose editor and demos the throttling
1896
+ live: its **Background resilience** panel meters rAF vs `setInterval` vs a
1897
+ worker clock, with a recap of what each delivered while the tab was hidden.
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Long-lived SSE listener channel to Hugging Face central: a realtime
3
+ * view of the user's robot fleet WITHOUT claiming a peer slot. The
4
+ * canonical implementation shared by every first-party picker / scan
5
+ * screen (the per-app copies used to drift).
6
+ *
7
+ * Why not just use the `ReachyMini` SDK class? Its signaling stream
8
+ * registers a *peer* at central, and central enforces a 1:1
9
+ * `token → peer_id` mapping - so a fleet-watching UI that kept an SDK
10
+ * instance open would clobber the peer slot the next session's WebRTC
11
+ * handshake needs. This module registers with `roles: ["listener"]`
12
+ * only, and callers `close()` it before any SDK session starts on the
13
+ * same token.
14
+ *
15
+ * `fetch` + manual stream reader rather than the native `EventSource`
16
+ * so the HF token travels in the `Authorization` header instead of
17
+ * the URL (no secret in DevTools / proxy logs / browser history).
18
+ */
19
+ /**
20
+ * Shape of a producer entry in the SSE `list` frame and in
21
+ * `/api/robot-status`. Mirrors `get_producers_list` server-side.
22
+ * Kept loose so future fields appear without a wire bump.
23
+ */
24
+ export interface CentralStreamProducer {
25
+ id: string;
26
+ meta?: Record<string, unknown>;
27
+ busy?: boolean;
28
+ activeApp?: string | null;
29
+ }
30
+ export interface CentralPeerStatusChangedEvent {
31
+ peerId: string;
32
+ roles: string[];
33
+ meta?: Record<string, unknown>;
34
+ }
35
+ export interface CentralSessionStateChangedEvent {
36
+ peerId: string;
37
+ busy: boolean;
38
+ activeApp?: string | null;
39
+ meta?: Record<string, unknown>;
40
+ }
41
+ export interface OpenCentralListenerOpts {
42
+ token: string;
43
+ /** Central base URL, e.g. from the consumer's env/config. */
44
+ signalingUrl: string;
45
+ /**
46
+ * `setPeerStatus(listener)` payload's `meta.name`. Surfaces in
47
+ * central logs as the listener identity - pass something that
48
+ * identifies the surface (e.g. "Reachy Mini Host (picker)").
49
+ */
50
+ appName?: string;
51
+ /** First batch of producers received after each (re)connect. */
52
+ onList?: (producers: CentralStreamProducer[]) => void;
53
+ /** A robot came online / went offline (same-owner). */
54
+ onPeerStatusChanged?: (event: CentralPeerStatusChangedEvent) => void;
55
+ /** A robot transitioned busy ↔ free (same-owner). */
56
+ onSessionStateChanged?: (event: CentralSessionStateChangedEvent) => void;
57
+ /** SSE is open and `setPeerStatus(listener)` has been POSTed. */
58
+ onConnect?: () => void;
59
+ /** SSE dropped (network, central restart, scheduled reconnect). */
60
+ onDisconnect?: (reason: string) => void;
61
+ /** Non-fatal errors. Fatal auth errors end the stream and call
62
+ * onDisconnect with a meaningful reason instead. */
63
+ onError?: (err: Error) => void;
64
+ }
65
+ export interface CentralListenerHandle {
66
+ /** Tear the stream down. Idempotent. No further callbacks
67
+ * fire after this returns. */
68
+ close(): void;
69
+ }
70
+ export declare function openCentralListener(opts: OpenCentralListenerOpts): CentralListenerHandle;
71
+ //# sourceMappingURL=central-listener.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"central-listener.d.ts","sourceRoot":"","sources":["../../lib/central-listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAqCH;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,6BAA6B;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,+BAA+B;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,qBAAqB,EAAE,KAAK,IAAI,CAAC;IACtD,uDAAuD;IACvD,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,6BAA6B,KAAK,IAAI,CAAC;IACrE,qDAAqD;IACrD,qBAAqB,CAAC,EAAE,CAAC,KAAK,EAAE,+BAA+B,KAAK,IAAI,CAAC;IACzE,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,mEAAmE;IACnE,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;yDACqD;IACrD,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,qBAAqB;IAClC;mCAC+B;IAC/B,KAAK,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,mBAAmB,CAC/B,IAAI,EAAE,uBAAuB,GAC9B,qBAAqB,CA6OvB"}
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Long-lived SSE listener channel to Hugging Face central: a realtime
3
+ * view of the user's robot fleet WITHOUT claiming a peer slot. The
4
+ * canonical implementation shared by every first-party picker / scan
5
+ * screen (the per-app copies used to drift).
6
+ *
7
+ * Why not just use the `ReachyMini` SDK class? Its signaling stream
8
+ * registers a *peer* at central, and central enforces a 1:1
9
+ * `token → peer_id` mapping - so a fleet-watching UI that kept an SDK
10
+ * instance open would clobber the peer slot the next session's WebRTC
11
+ * handshake needs. This module registers with `roles: ["listener"]`
12
+ * only, and callers `close()` it before any SDK session starts on the
13
+ * same token.
14
+ *
15
+ * `fetch` + manual stream reader rather than the native `EventSource`
16
+ * so the HF token travels in the `Authorization` header instead of
17
+ * the URL (no secret in DevTools / proxy logs / browser history).
18
+ */
19
+ const INITIAL_BACKOFF_MS = 1_000;
20
+ const MAX_BACKOFF_MS = 30_000;
21
+ const DEFAULT_LISTENER_APP_NAME = 'Reachy Mini (listener)';
22
+ // Heartbeat: the server runs a TTL sweeper that evicts peers whose
23
+ // `last_seen` is older than `LEASE_SECONDS` (30 s by default; cf.
24
+ // `reachy_mini_central/app.py`). `last_seen` only refreshes on inbound
25
+ // `POST /send`, so a pure SSE listener that registers once and stays
26
+ // silent gets evicted silently after one lease window - the SSE stays
27
+ // open but every subsequent broadcast is dropped because the server's
28
+ // `broadcast_to_listeners` iterates `self.peers`, which no longer
29
+ // contains us. Symptom: the picker sees `busy=true` when a session
30
+ // starts (within the first 30 s) but never receives the matching
31
+ // `busy=false` because the listener slot has been reaped by then.
32
+ //
33
+ // Mirror the daemon's `_heartbeat_loop` (cf. `central_signaling_relay.py`):
34
+ // re-emit `setPeerStatus(listener)` periodically, at the cadence the
35
+ // welcome recommends (`recommended_heartbeat_interval_seconds`, with a
36
+ // `lease_seconds / 3` fallback).
37
+ const HEARTBEAT_DEFAULT_INTERVAL_MS = 10_000;
38
+ const negotiateHeartbeatIntervalMs = (welcomeMsg) => {
39
+ const raw = welcomeMsg['recommended_heartbeat_interval_seconds'];
40
+ if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {
41
+ return raw * 1000;
42
+ }
43
+ const lease = welcomeMsg['lease_seconds'];
44
+ if (typeof lease === 'number' && Number.isFinite(lease) && lease > 0) {
45
+ return (lease * 1000) / 3;
46
+ }
47
+ return HEARTBEAT_DEFAULT_INTERVAL_MS;
48
+ };
49
+ export function openCentralListener(opts) {
50
+ const { signalingUrl, token } = opts;
51
+ const appName = opts.appName ?? DEFAULT_LISTENER_APP_NAME;
52
+ let closed = false;
53
+ let abortController = null;
54
+ let reconnectTimer = null;
55
+ let heartbeatTimer = null;
56
+ let backoff = INITIAL_BACKOFF_MS;
57
+ const stopHeartbeat = () => {
58
+ if (heartbeatTimer !== null) {
59
+ clearInterval(heartbeatTimer);
60
+ heartbeatTimer = null;
61
+ }
62
+ };
63
+ const close = () => {
64
+ if (closed)
65
+ return;
66
+ closed = true;
67
+ stopHeartbeat();
68
+ if (reconnectTimer !== null) {
69
+ clearTimeout(reconnectTimer);
70
+ reconnectTimer = null;
71
+ }
72
+ if (abortController !== null) {
73
+ abortController.abort();
74
+ abortController = null;
75
+ }
76
+ };
77
+ const scheduleReconnect = (reason) => {
78
+ // The `reconnectTimer` check is a re-entrancy guard: stream EOF and
79
+ // an in-flight heartbeat's eviction branch can both land here in the
80
+ // same tick, and arming twice would spawn two concurrent runStream()
81
+ // loops (the abortController only tracks the latest).
82
+ if (closed || reconnectTimer !== null)
83
+ return;
84
+ // Stop the heartbeat eagerly: the SSE is dead, so any outstanding
85
+ // setPeerStatus POST would fail anyway and the next welcome will
86
+ // start a fresh interval with a freshly-negotiated cadence.
87
+ stopHeartbeat();
88
+ opts.onDisconnect?.(reason);
89
+ const jitter = backoff * (Math.random() - 0.5) * 0.5;
90
+ const delay = Math.min(MAX_BACKOFF_MS, Math.round(backoff + jitter));
91
+ reconnectTimer = setTimeout(() => {
92
+ reconnectTimer = null;
93
+ backoff = Math.min(MAX_BACKOFF_MS, backoff * 2);
94
+ void runStream();
95
+ }, delay);
96
+ };
97
+ /**
98
+ * POST `setPeerStatus(roles=['listener'])`.
99
+ *
100
+ * Used both as the one-shot post-welcome registration and as the
101
+ * recurring heartbeat re-emission. The wire payload is identical
102
+ * by design: central treats repeated setPeerStatus from the same
103
+ * peer as idempotent and uses the inbound POST to refresh
104
+ * `last_seen` (cf. `signaling.touch()` in central app.py).
105
+ *
106
+ * Returns the HTTP status code on a completed round-trip (so the
107
+ * caller can distinguish "peer evicted server-side" from "transient
108
+ * network blip"), or `null` on abort / network error.
109
+ */
110
+ const sendListenerRegistration = async () => {
111
+ try {
112
+ const resp = await fetch(`${signalingUrl}/send`, {
113
+ method: 'POST',
114
+ headers: {
115
+ 'Content-Type': 'application/json',
116
+ Authorization: `Bearer ${token}`,
117
+ },
118
+ body: JSON.stringify({
119
+ type: 'setPeerStatus',
120
+ roles: ['listener'],
121
+ meta: { name: appName },
122
+ }),
123
+ signal: abortController?.signal,
124
+ });
125
+ if (!resp.ok) {
126
+ opts.onError?.(new Error(`setPeerStatus(listener) returned HTTP ${resp.status}`));
127
+ }
128
+ return resp.status;
129
+ }
130
+ catch (err) {
131
+ if (err instanceof DOMException && err.name === 'AbortError')
132
+ return null;
133
+ opts.onError?.(err instanceof Error ? err : new Error(String(err)));
134
+ return null;
135
+ }
136
+ };
137
+ const startHeartbeat = (intervalMs) => {
138
+ stopHeartbeat();
139
+ if (closed)
140
+ return;
141
+ heartbeatTimer = setInterval(() => {
142
+ if (closed)
143
+ return;
144
+ void (async () => {
145
+ const status = await sendListenerRegistration();
146
+ if (closed)
147
+ return;
148
+ // Server-side eviction: `disconnect_peer` (TTL sweeper or
149
+ // explicit) clears `token_to_peer`, so the next POST /send
150
+ // returns HTTP 400 ("Connect to /events first"). The SSE is
151
+ // still half-open from our side - keepalive pings keep it
152
+ // looking alive on the wire even though the central no longer
153
+ // routes broadcasts to us. Abort the dead stream AND schedule
154
+ // the reconnect ourselves: `runStream()` deliberately swallows
155
+ // aborts (they normally mean `close()`), so it would never
156
+ // reach its own `scheduleReconnect()` on this path. The
157
+ // reconnect mints a fresh peer and replays `welcome` + `list`.
158
+ if (status !== null && status >= 400 && status < 500) {
159
+ if (abortController !== null) {
160
+ abortController.abort();
161
+ }
162
+ scheduleReconnect(`listener evicted (HTTP ${status})`);
163
+ }
164
+ })();
165
+ }, intervalMs);
166
+ };
167
+ const handleMessage = (msg) => {
168
+ switch (msg.type) {
169
+ case 'welcome':
170
+ backoff = INITIAL_BACKOFF_MS;
171
+ // Register (central requires the SSE channel be established
172
+ // before it accepts a /send for this peer), THEN report
173
+ // connected - so `onConnect` consumers can immediately issue
174
+ // REST calls without racing the listener slot creation.
175
+ void (async () => {
176
+ await sendListenerRegistration();
177
+ if (closed)
178
+ return;
179
+ startHeartbeat(negotiateHeartbeatIntervalMs(msg));
180
+ opts.onConnect?.();
181
+ })();
182
+ return;
183
+ // Frames come from the first-party central, so they are cast, not
184
+ // field-validated — same trust level as the SDK's own signaling
185
+ // parser.
186
+ case 'list':
187
+ opts.onList?.(Array.isArray(msg.producers)
188
+ ? msg.producers
189
+ : []);
190
+ return;
191
+ case 'peerStatusChanged':
192
+ opts.onPeerStatusChanged?.(msg);
193
+ return;
194
+ case 'sessionStateChanged':
195
+ opts.onSessionStateChanged?.(msg);
196
+ return;
197
+ // Other types (`peer`, `startSession`, `endSession`, `ping`)
198
+ // only fire when this peer becomes consumer / producer. As
199
+ // a pure listener we ignore them.
200
+ default:
201
+ return;
202
+ }
203
+ };
204
+ const runStream = async () => {
205
+ if (closed)
206
+ return;
207
+ abortController = new AbortController();
208
+ const signal = abortController.signal;
209
+ let resp;
210
+ try {
211
+ resp = await fetch(`${signalingUrl}/events`, {
212
+ method: 'GET',
213
+ headers: { Authorization: `Bearer ${token}` },
214
+ signal,
215
+ });
216
+ }
217
+ catch (err) {
218
+ if (closed)
219
+ return;
220
+ const reason = err instanceof Error ? err.message : String(err);
221
+ scheduleReconnect(`fetch failed: ${reason}`);
222
+ return;
223
+ }
224
+ if (resp.status === 401 || resp.status === 403) {
225
+ opts.onError?.(new Error(`Hugging Face central rejected the token (HTTP ${resp.status})`));
226
+ close();
227
+ opts.onDisconnect?.('auth_rejected');
228
+ return;
229
+ }
230
+ if (!resp.ok || resp.body === null) {
231
+ scheduleReconnect(`HTTP ${resp.status}`);
232
+ return;
233
+ }
234
+ const reader = resp.body.getReader();
235
+ const decoder = new TextDecoder();
236
+ let buffer = '';
237
+ try {
238
+ while (true) {
239
+ const { done, value } = await reader.read();
240
+ if (done)
241
+ break;
242
+ buffer += decoder.decode(value, { stream: true });
243
+ const lines = buffer.split('\n');
244
+ buffer = lines.pop() ?? '';
245
+ for (const line of lines) {
246
+ if (!line.startsWith('data:'))
247
+ continue;
248
+ const payload = line.slice(5).trim();
249
+ if (payload.length === 0)
250
+ continue;
251
+ try {
252
+ handleMessage(JSON.parse(payload));
253
+ }
254
+ catch (err) {
255
+ opts.onError?.(err instanceof Error ? err : new Error(String(err)));
256
+ }
257
+ }
258
+ }
259
+ // `signal.aborted` covers both `close()` and the heartbeat's
260
+ // eviction path (which schedules its own reconnect); some fetch
261
+ // implementations surface an abort as a clean EOF rather than an
262
+ // AbortError, so check it here too to avoid double-scheduling.
263
+ if (closed || signal.aborted)
264
+ return;
265
+ scheduleReconnect('stream ended');
266
+ }
267
+ catch (err) {
268
+ if (signal.aborted || closed)
269
+ return;
270
+ const reason = err instanceof Error ? err.message : String(err);
271
+ scheduleReconnect(`read failed: ${reason}`);
272
+ }
273
+ };
274
+ void runStream();
275
+ return { close };
276
+ }
277
+ //# sourceMappingURL=central-listener.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"central-listener.js","sourceRoot":"","sources":["../../lib/central-listener.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=central-listener.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"central-listener.test.d.ts","sourceRoot":"","sources":["../../lib/central-listener.test.ts"],"names":[],"mappings":""}