@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.
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +23 -4
- package/dist/decoder.d.ts +3 -0
- package/dist/decoder.d.ts.map +1 -1
- package/dist/decoder.js +18 -9
- package/dist/useExpoCiSession.d.ts.map +1 -1
- package/dist/useExpoCiSession.js +19 -14
- package/package.json +2 -2
package/dist/client.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
143
|
-
|
|
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;
|
package/dist/decoder.d.ts.map
CHANGED
|
@@ -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;
|
|
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(
|
|
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,
|
|
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"}
|
package/dist/useExpoCiSession.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
33
|
+
"@alivelabs/expo-orchestrator-schemas": "^0.2.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"react": ">=18.0.0",
|