@pollen-robotics/reachy-mini-sdk 1.10.0-dev.0.main.828a720 → 1.10.0-dev.0.main.8e84768

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 (87) hide show
  1. package/APP_CREATION_GUIDE.md +55 -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 +160 -91
  39. package/dist/lib/reachy-mini.d.ts.map +1 -1
  40. package/dist/lib/reachy-mini.js +556 -612
  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 +237 -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/chunks/{index-lMs2sCXG.js → index-rW7LY0k3.js} +209 -118
  76. package/host/dist/chunks/reachy-mini-DHkS9x-z.js +2145 -0
  77. package/host/dist/embed/index.d.ts +60 -0
  78. package/host/dist/embed/index.d.ts.map +1 -1
  79. package/host/dist/entry/auto.js +1 -1
  80. package/host/dist/entry/embed.d.ts +5 -4
  81. package/host/dist/entry/embed.d.ts.map +1 -1
  82. package/host/dist/entry/embed.js +5 -4
  83. package/host/dist/index.d.ts +2 -2
  84. package/host/dist/index.d.ts.map +1 -1
  85. package/host/dist/index.js +11 -10
  86. package/package.json +9 -2
  87. package/host/dist/chunks/reachy-mini-DwiuAr8T.js +0 -1590
@@ -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
 
@@ -1793,3 +1794,57 @@ strict mode, silently no-op otherwise).
1793
1794
  | `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
1795
  | Carry degrees through your motion code. | Convert at the UI boundary; speak radians + magic-mm everywhere below it. |
1795
1796
  | 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". |
1797
+
1798
+ ### 14.7 Backgrounded tabs: clock robot logic off a Web Worker
1799
+
1800
+ When the user switches tabs (or the phone locks), the browser throttles
1801
+ your app hard: `requestAnimationFrame` **pauses entirely** and
1802
+ `setInterval`/`setTimeout` are clamped to **~1 tick per second**. If your
1803
+ pose streaming, audio pipeline, or stream-health watchdog is clocked by
1804
+ either of them, the robot freezes mid-motion and stalls go unnoticed until
1805
+ the tab comes back.
1806
+
1807
+ The fix is to split your loop in two:
1808
+
1809
+ - **Logic** (pose computation + `setTarget`, audio gain, health checks,
1810
+ reconnects): clock it from a **Web Worker**. Worker timers are *not*
1811
+ visibility-throttled, and the `message` events they post are delivered
1812
+ on the main thread even while the tab is hidden.
1813
+ - **Visuals** (DOM updates, canvas, meters): keep them on `rAF`. It pauses
1814
+ when hidden - which is exactly what you want for work nobody can see -
1815
+ and resumes on its own.
1816
+
1817
+ ```ts
1818
+ // pose-heartbeat.worker.ts - the whole file:
1819
+ const INTERVAL_MS = 25; // ~40 Hz; the main thread down-samples as needed
1820
+ setInterval(() => postMessage(0), INTERVAL_MS);
1821
+ ```
1822
+
1823
+ ```ts
1824
+ // embed.ts
1825
+ let worker: Worker | null = null;
1826
+ try {
1827
+ worker = new Worker(new URL("./pose-heartbeat.worker.ts", import.meta.url), { type: "module" });
1828
+ worker.onmessage = () => stepLogic(performance.now());
1829
+ } catch {
1830
+ // Workers unavailable (rare): degrade to a throttled interval.
1831
+ setInterval(() => stepLogic(performance.now()), 25);
1832
+ }
1833
+
1834
+ const renderVisuals = () => { /* DOM/canvas only - no robot commands */ };
1835
+ const rafLoop = () => { renderVisuals(); requestAnimationFrame(rafLoop); };
1836
+ requestAnimationFrame(rafLoop);
1837
+ ```
1838
+
1839
+ Two companion rules make this robust:
1840
+
1841
+ - **Clamp your `dt`.** After a long hidden stretch the first tick sees a
1842
+ huge time delta; clamp it (e.g. `Math.min(dt, 100)`) so filters and
1843
+ interpolators don't jump.
1844
+ - **Resync on `visibilitychange`.** When the tab returns, call
1845
+ `reachy.requestState()` and run one logic step immediately instead of
1846
+ waiting for the next scheduled tick, so the UI repaints from fresh state.
1847
+
1848
+ Remember to `worker.terminate()` in your `onLeave` cleanup. A complete
1849
+ reference implementation lives in the `reachy_mini_radio_js` app
1850
+ (`src/embed.ts` + `src/pose-heartbeat.worker.ts`).
@@ -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":""}