@alivelabs/expo-orchestrator-react-client 0.2.0 → 0.2.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,WAAW,EACX,cAAc,EACd,sBAAsB,EAEvB,MAAM,YAAY,CAAC;AAIpB,KAAK,QAAQ,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;AAGxE,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAEpC,cAAM,iBAAiB,CAAC,MAAM,SAAS,QAAQ;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwD;IAElF,EAAE,CAAC,CAAC,SAAS,MAAM,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IASxF,GAAG,CAAC,CAAC,SAAS,MAAM,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;IAInF,IAAI,CAAC,CAAC,SAAS,MAAM,MAAM,GAAG,MAAM,EAClC,KAAK,EAAE,CAAC,EACR,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,SAAS,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GACtD,IAAI;IAQP,kBAAkB,IAAI,IAAI;CAG3B;AAUD,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,YAAa,SAAQ,iBAAiB,CAAC,cAAc,CAAC;IACjE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,SAAS,CAAS;gBAEd,EAAE,OAAiC,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,mBAAmB;YAU7E,SAAS;IAgBvB,UAAU,IAAI,OAAO,CAAC,aAAa,CAAC;IAIpC,OAAO,IAAI,OAAO,CAAC,WAAW,CAAC;IAI/B,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAU3D,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC;IAc/C,OAAO,KAAK,KAAK,GAKhB;IAED,OAAO,IAAI,IAAI;IAMf,OAAO,CAAC,UAAU;IA6DlB,OAAO,CAAC,iBAAiB;IASzB,UAAU,IAAI,IAAI;CAYnB"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,WAAW,EACX,cAAc,EACd,sBAAsB,EAEvB,MAAM,YAAY,CAAC;AAIpB,KAAK,QAAQ,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;AAGxE,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAEpC,cAAM,iBAAiB,CAAC,MAAM,SAAS,QAAQ;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwD;IAElF,EAAE,CAAC,CAAC,SAAS,MAAM,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IASxF,GAAG,CAAC,CAAC,SAAS,MAAM,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;IAInF,IAAI,CAAC,CAAC,SAAS,MAAM,MAAM,GAAG,MAAM,EAClC,KAAK,EAAE,CAAC,EACR,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,SAAS,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GACtD,IAAI;IAQP,kBAAkB,IAAI,IAAI;CAG3B;AAkBD,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,YAAa,SAAQ,iBAAiB,CAAC,cAAc,CAAC;IACjE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,SAAS,CAAS;gBAEd,EAAE,OAAiC,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,mBAAmB;YAU7E,SAAS;IAgBvB,UAAU,IAAI,OAAO,CAAC,aAAa,CAAC;IAIpC,OAAO,IAAI,OAAO,CAAC,WAAW,CAAC;IAI/B,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAU3D,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC;IAc/C,OAAO,KAAK,KAAK,GAKhB;IAED,OAAO,IAAI,IAAI;IAMf,OAAO,CAAC,UAAU;IAwElB,OAAO,CAAC,iBAAiB;IASzB,UAAU,IAAI,IAAI;CAYnB"}
package/dist/client.js CHANGED
@@ -29,6 +29,13 @@ class TypedEventEmitter {
29
29
  const INITIAL_DELAY_MS = 500;
30
30
  const MAX_DELAY_MS = 30000;
31
31
  const MAX_ATTEMPTS = 10;
32
+ // Close codes the server sends on purpose — reconnecting can't recover these,
33
+ // so we stop and (where useful) surface why. A `null` message closes quietly.
34
+ const TERMINAL_CLOSE_CODES = {
35
+ 1000: null, // normal closure — the session ended
36
+ 1008: "Unauthorized — the token was rejected for this session.",
37
+ 4404: "This session isn't live. Live video and input are only available while a build is running.",
38
+ };
32
39
  export class ExpoCiClient extends TypedEventEmitter {
33
40
  constructor({ baseUrl = "http://localhost:3000", sessionId, apiToken }) {
34
41
  super();
@@ -138,12 +145,24 @@ export class ExpoCiClient extends TypedEventEmitter {
138
145
  break;
139
146
  }
140
147
  });
141
- socket.addEventListener("close", () => {
142
- if (this.destroyed) {
143
- this.emit("close");
148
+ socket.addEventListener("close", (event) => {
149
+ this.emit("close");
150
+ if (this.destroyed)
151
+ return;
152
+ // A deliberate server close (unauthorized, session not live, ended) won't
153
+ // recover on retry — stop, and surface the reason to the UI.
154
+ if (Object.hasOwn(TERMINAL_CLOSE_CODES, event.code)) {
155
+ const message = TERMINAL_CLOSE_CODES[event.code];
156
+ if (message) {
157
+ this.emit("error", {
158
+ type: "error",
159
+ sessionId: this.sessionId,
160
+ timestamp: new Date().toISOString(),
161
+ data: { message },
162
+ });
163
+ }
144
164
  return;
145
165
  }
146
- this.emit("close");
147
166
  this.scheduleReconnect();
148
167
  });
149
168
  socket.addEventListener("error", () => {
package/dist/decoder.d.ts CHANGED
@@ -15,11 +15,14 @@ export declare class SimulatorDecoder {
15
15
  private ctx;
16
16
  private size;
17
17
  private onResizeCb;
18
+ private onErrorCb;
18
19
  private nextTimestamp;
19
20
  private awaitingKeyframe;
20
21
  private disposed;
21
22
  setCanvas(canvas: HTMLCanvasElement | null): void;
22
23
  onResize(cb: ((size: FrameSize) => void) | null): void;
24
+ onError(cb: ((message: string) => void) | null): void;
25
+ private reportError;
23
26
  get frameSize(): FrameSize | null;
24
27
  push(chunk: ArrayBuffer): void;
25
28
  dispose(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"decoder.d.ts","sourceRoot":"","sources":["../src/decoder.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,qBAAa,gBAAgB;IAC3B,+EAA+E;IAC/E,MAAM,CAAC,WAAW,IAAI,OAAO;IAI7B,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,GAAG,CAAyC;IACpD,OAAO,CAAC,IAAI,CAA0B;IACtC,OAAO,CAAC,UAAU,CAA4C;IAG9D,OAAO,CAAC,aAAa,CAAK;IAG1B,OAAO,CAAC,gBAAgB,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAS;IAEzB,SAAS,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,GAAG,IAAI;IASjD,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAItD,IAAI,SAAS,IAAI,SAAS,GAAG,IAAI,CAEhC;IAED,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAqB9B,OAAO,IAAI,IAAI;IAUf,OAAO,CAAC,SAAS;IAsBjB,OAAO,CAAC,MAAM;IAsBd,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,SAAS;IAiBjB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,YAAY;CAWrB"}
1
+ {"version":3,"file":"decoder.d.ts","sourceRoot":"","sources":["../src/decoder.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,qBAAa,gBAAgB;IAC3B,+EAA+E;IAC/E,MAAM,CAAC,WAAW,IAAI,OAAO;IAI7B,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,GAAG,CAAyC;IACpD,OAAO,CAAC,IAAI,CAA0B;IACtC,OAAO,CAAC,UAAU,CAA4C;IAC9D,OAAO,CAAC,SAAS,CAA4C;IAG7D,OAAO,CAAC,aAAa,CAAK;IAG1B,OAAO,CAAC,gBAAgB,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAS;IAEzB,SAAS,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,GAAG,IAAI;IASjD,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAItD,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAIrD,OAAO,CAAC,WAAW;IAMnB,IAAI,SAAS,IAAI,SAAS,GAAG,IAAI,CAEhC;IAED,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAqB9B,OAAO,IAAI,IAAI;IAWf,OAAO,CAAC,SAAS;IAmBjB,OAAO,CAAC,MAAM;IAuBd,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,SAAS;IAiBjB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,YAAY;CAWrB"}
package/dist/decoder.js CHANGED
@@ -21,6 +21,7 @@ export class SimulatorDecoder {
21
21
  this.ctx = null;
22
22
  this.size = null;
23
23
  this.onResizeCb = null;
24
+ this.onErrorCb = null;
24
25
  // Monotonic, strictly-increasing microsecond timestamps. The values are
25
26
  // arbitrary (we render immediately) but EncodedVideoChunk requires them.
26
27
  this.nextTimestamp = 0;
@@ -44,6 +45,14 @@ export class SimulatorDecoder {
44
45
  onResize(cb) {
45
46
  this.onResizeCb = cb;
46
47
  }
48
+ onError(cb) {
49
+ this.onErrorCb = cb;
50
+ }
51
+ reportError(message, err) {
52
+ // Surface decoder problems instead of silently showing a black canvas.
53
+ console.warn(`[expo-orchestrator] ${message}`, err ?? "");
54
+ this.onErrorCb?.(message);
55
+ }
47
56
  get frameSize() {
48
57
  return this.size;
49
58
  }
@@ -74,6 +83,7 @@ export class SimulatorDecoder {
74
83
  this.canvas = null;
75
84
  this.ctx = null;
76
85
  this.onResizeCb = null;
86
+ this.onErrorCb = null;
77
87
  }
78
88
  // ── internals ────────────────────────────────────────────────────────────
79
89
  configure(description) {
@@ -84,16 +94,13 @@ export class SimulatorDecoder {
84
94
  output: (frame) => this.paintFrame(frame),
85
95
  error: (err) => this.onDecoderError(err),
86
96
  });
97
+ const codec = codecFromAvcC(description);
87
98
  try {
88
- decoder.configure({
89
- codec: codecFromAvcC(description),
90
- description,
91
- optimizeForLatency: true,
92
- });
99
+ decoder.configure({ codec, description, optimizeForLatency: true });
93
100
  }
94
- catch {
95
- // Unsupported profile/level on this platform — leave video unsupported.
101
+ catch (err) {
96
102
  decoder.close();
103
+ this.reportError(`failed to configure the video decoder for codec ${codec}`, err);
97
104
  return;
98
105
  }
99
106
  this.decoder = decoder;
@@ -116,9 +123,10 @@ export class SimulatorDecoder {
116
123
  data,
117
124
  }));
118
125
  }
119
- catch {
126
+ catch (err) {
120
127
  // A bad/out-of-order chunk — wait for the next keyframe to resync.
121
128
  this.awaitingKeyframe = true;
129
+ this.reportError(`failed to decode a ${type} frame`, err);
122
130
  }
123
131
  }
124
132
  paintFrame(frame) {
@@ -159,9 +167,10 @@ export class SimulatorDecoder {
159
167
  }
160
168
  this.onResizeCb?.(this.size);
161
169
  }
162
- onDecoderError(_err) {
170
+ onDecoderError(err) {
163
171
  // Reset and wait for the next server-forced keyframe to recover.
164
172
  this.awaitingKeyframe = true;
173
+ this.reportError("video decoder error", err);
165
174
  }
166
175
  closeDecoder() {
167
176
  const decoder = this.decoder;
@@ -1 +1 @@
1
- {"version":3,"file":"useExpoCiSession.d.ts","sourceRoot":"","sources":["../src/useExpoCiSession.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAKV,uBAAuB,EACvB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAIpB,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,SAAS,EACT,QAAQ,EACR,WAAkB,EAClB,OAA0B,GAC3B,EAAE,uBAAuB,GAAG,sBAAsB,CA8HlD"}
1
+ {"version":3,"file":"useExpoCiSession.d.ts","sourceRoot":"","sources":["../src/useExpoCiSession.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAKV,uBAAuB,EACvB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAIpB,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,SAAS,EACT,QAAQ,EACR,WAAkB,EAClB,OAA0B,GAC3B,EAAE,uBAAuB,GAAG,sBAAsB,CAmIlD"}
@@ -12,14 +12,16 @@ export function useExpoCiSession({ baseUrl, sessionId, apiToken, autoConnect = t
12
12
  // We keep the client in a ref so reconnect() can call client.connect()
13
13
  // without causing a re-render or stale closure issues.
14
14
  const clientRef = useRef(null);
15
- // One decoder per hook instance; lives across reconnects within a session.
15
+ // The decoder is created/torn down inside the connect effect so its lifecycle
16
+ // is paired with the socket (StrictMode-safe). The canvas node is held in its
17
+ // own ref so a freshly-created decoder can be (re)wired to it immediately.
16
18
  const decoderRef = useRef(null);
17
- if (decoderRef.current === null) {
18
- decoderRef.current = new SimulatorDecoder();
19
- decoderRef.current.onResize(setFrameSize);
20
- }
21
- // Stable callback to wire the rendering <canvas> into the decoder.
19
+ const canvasNodeRef = useRef(null);
20
+ // Stable callback to wire the rendering <canvas> into the decoder. The ref
21
+ // callback fires during commit (before the effect creates the decoder), so we
22
+ // stash the node and also push it to the decoder if one already exists.
22
23
  const attachCanvas = useCallback((canvas) => {
24
+ canvasNodeRef.current = canvas;
23
25
  decoderRef.current?.setCanvas(canvas);
24
26
  }, []);
25
27
  // Stable reconnect callback
@@ -34,14 +36,15 @@ export function useExpoCiSession({ baseUrl, sessionId, apiToken, autoConnect = t
34
36
  }
35
37
  return client.sendInput(input);
36
38
  }, []);
37
- // Tear the decoder down only on unmount — it is reused across reconnects.
38
- useEffect(() => {
39
- return () => {
40
- decoderRef.current?.dispose();
41
- decoderRef.current = null;
42
- };
43
- }, []);
44
39
  useEffect(() => {
40
+ // Create the decoder here (not in render) so create + dispose are paired
41
+ // with this effect — StrictMode's mount/unmount/mount can't leave the active
42
+ // decoder canvas-less. Wire the current canvas immediately.
43
+ const decoder = new SimulatorDecoder();
44
+ decoder.onResize(setFrameSize);
45
+ decoder.onError(setError);
46
+ decoder.setCanvas(canvasNodeRef.current);
47
+ decoderRef.current = decoder;
45
48
  const client = new ExpoCiClient({ baseUrl, sessionId, apiToken });
46
49
  clientRef.current = client;
47
50
  // Fetch initial session status
@@ -76,7 +79,7 @@ export function useExpoCiSession({ baseUrl, sessionId, apiToken, autoConnect = t
76
79
  });
77
80
  });
78
81
  const offFrame = client.on("video-chunk", (chunk) => {
79
- decoderRef.current?.push(chunk);
82
+ decoder.push(chunk);
80
83
  });
81
84
  const offStatus = client.on("status", (msg) => {
82
85
  setStatus(msg.data.status);
@@ -96,6 +99,8 @@ export function useExpoCiSession({ baseUrl, sessionId, apiToken, autoConnect = t
96
99
  offError();
97
100
  client.disconnect();
98
101
  clientRef.current = null;
102
+ decoder.dispose();
103
+ decoderRef.current = null;
99
104
  };
100
105
  // eslint-disable-next-line react-hooks/exhaustive-deps
101
106
  }, [baseUrl, sessionId, apiToken, autoConnect, maxLogs]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alivelabs/expo-orchestrator-react-client",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "React client for Expo CI Orchestrator — streaming logs, live simulator video, and interactive controls.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  "typecheck": "tsc --noEmit"
31
31
  },
32
32
  "dependencies": {
33
- "@alivelabs/expo-orchestrator-schemas": "^0.2.0"
33
+ "@alivelabs/expo-orchestrator-schemas": "^0.2.1"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": ">=18.0.0",