serve-sim 0.1.35 → 0.1.36
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/bin/serve-sim-bin +0 -0
- package/dist/middleware.js +23 -23
- package/dist/serve-sim.js +54 -54
- package/package.json +1 -1
- package/src/ax.ts +41 -4
- package/src/middleware.ts +62 -8
package/package.json
CHANGED
package/src/ax.ts
CHANGED
|
@@ -159,6 +159,7 @@ function sseMessage(payload: unknown) {
|
|
|
159
159
|
interface AxStreamer {
|
|
160
160
|
addClient(res: { write(chunk: string): void }): () => void;
|
|
161
161
|
setPort(port: number): void;
|
|
162
|
+
dispose(): void;
|
|
162
163
|
}
|
|
163
164
|
|
|
164
165
|
function createAxStreamer({ port }: { port: number }): AxStreamer {
|
|
@@ -168,15 +169,16 @@ function createAxStreamer({ port }: { port: number }): AxStreamer {
|
|
|
168
169
|
let pollIntervalMs = POLL_INTERVAL_MS;
|
|
169
170
|
let polling = false;
|
|
170
171
|
let currentPort = port;
|
|
172
|
+
let disposed = false;
|
|
171
173
|
|
|
172
174
|
const schedule = () => {
|
|
173
|
-
if (clients.size === 0 || timer) return;
|
|
175
|
+
if (disposed || clients.size === 0 || timer) return;
|
|
174
176
|
timer = setTimeout(poll, pollIntervalMs);
|
|
175
177
|
};
|
|
176
178
|
|
|
177
179
|
const poll = async () => {
|
|
178
180
|
timer = null;
|
|
179
|
-
if (polling || clients.size === 0) {
|
|
181
|
+
if (disposed || polling || clients.size === 0) {
|
|
180
182
|
schedule();
|
|
181
183
|
return;
|
|
182
184
|
}
|
|
@@ -206,7 +208,7 @@ function createAxStreamer({ port }: { port: number }): AxStreamer {
|
|
|
206
208
|
|
|
207
209
|
return {
|
|
208
210
|
setPort(nextPort: number) {
|
|
209
|
-
if (nextPort === currentPort) return;
|
|
211
|
+
if (disposed || nextPort === currentPort) return;
|
|
210
212
|
currentPort = nextPort;
|
|
211
213
|
latestMessage = null;
|
|
212
214
|
// Avoid sitting on the unavailable-backoff interval (15s) when the
|
|
@@ -219,6 +221,7 @@ function createAxStreamer({ port }: { port: number }): AxStreamer {
|
|
|
219
221
|
void poll();
|
|
220
222
|
},
|
|
221
223
|
addClient(res) {
|
|
224
|
+
if (disposed) return () => {};
|
|
222
225
|
clients.add(res);
|
|
223
226
|
if (latestMessage) res.write(latestMessage);
|
|
224
227
|
void poll();
|
|
@@ -230,10 +233,26 @@ function createAxStreamer({ port }: { port: number }): AxStreamer {
|
|
|
230
233
|
}
|
|
231
234
|
};
|
|
232
235
|
},
|
|
236
|
+
dispose() {
|
|
237
|
+
if (disposed) return;
|
|
238
|
+
disposed = true;
|
|
239
|
+
if (timer) {
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
timer = null;
|
|
242
|
+
}
|
|
243
|
+
clients.clear();
|
|
244
|
+
latestMessage = null;
|
|
245
|
+
},
|
|
233
246
|
};
|
|
234
247
|
}
|
|
235
248
|
|
|
236
|
-
export
|
|
249
|
+
export interface AxStreamerCache {
|
|
250
|
+
get(udid: string, port: number): AxStreamer;
|
|
251
|
+
prune(activeUdids: Iterable<string>): void;
|
|
252
|
+
size(): number;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function createAxStreamerCache(): AxStreamerCache {
|
|
237
256
|
const streamers = new Map<string, AxStreamer>();
|
|
238
257
|
|
|
239
258
|
return {
|
|
@@ -253,5 +272,23 @@ export function createAxStreamerCache() {
|
|
|
253
272
|
streamers.set(udid, streamer);
|
|
254
273
|
return streamer;
|
|
255
274
|
},
|
|
275
|
+
/**
|
|
276
|
+
* Drop streamers for simulators no longer present in `activeUdids`.
|
|
277
|
+
* Without this, the cache grew append-only across a server's lifetime
|
|
278
|
+
* as devices were booted/erased/reset, each entry holding a poll
|
|
279
|
+
* timer, last-snapshot buffer, and SSE client set.
|
|
280
|
+
*/
|
|
281
|
+
prune(activeUdids) {
|
|
282
|
+
const active = activeUdids instanceof Set ? activeUdids : new Set(activeUdids);
|
|
283
|
+
for (const [udid, streamer] of streamers) {
|
|
284
|
+
if (!active.has(udid)) {
|
|
285
|
+
streamer.dispose();
|
|
286
|
+
streamers.delete(udid);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
size() {
|
|
291
|
+
return streamers.size;
|
|
292
|
+
},
|
|
256
293
|
};
|
|
257
294
|
}
|
package/src/middleware.ts
CHANGED
|
@@ -83,6 +83,11 @@ export interface ServeSimState {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
const axStreamerCache = createAxStreamerCache();
|
|
86
|
+
|
|
87
|
+
// Hard cap on the SSE line-assembly buffer for child-process stdout.
|
|
88
|
+
// A malformed log entry without a newline can't grow this beyond 1 MB;
|
|
89
|
+
// the partial line is dropped rather than retained indefinitely.
|
|
90
|
+
const SSE_LINE_BUFFER_LIMIT = 1024 * 1024;
|
|
86
91
|
let inspectWebKitBridge: Promise<WebKitBridge> | null = null;
|
|
87
92
|
|
|
88
93
|
// Known bundle IDs that are always React Native shells (used as a fallback
|
|
@@ -257,6 +262,38 @@ function endpoint(base: string, path: string, device: string): string {
|
|
|
257
262
|
return `${value}?device=${encodeURIComponent(device)}`;
|
|
258
263
|
}
|
|
259
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Rewrite the helper URLs in a state so they point at the hostname the request
|
|
267
|
+
* came in on. The helper binds on `*:<port>`, so once the host portion matches
|
|
268
|
+
* the dev-server origin, a remote viewer (LAN, or tunnel exposing the helper
|
|
269
|
+
* port under the same hostname) can reach the stream. Loopback callers get
|
|
270
|
+
* the state untouched.
|
|
271
|
+
*/
|
|
272
|
+
export function rewriteStateForRequestHost(
|
|
273
|
+
state: ServeSimState,
|
|
274
|
+
hostHeader: string | undefined,
|
|
275
|
+
): ServeSimState {
|
|
276
|
+
if (!hostHeader) return state;
|
|
277
|
+
let hostname: string;
|
|
278
|
+
try {
|
|
279
|
+
hostname = new URL(`http://${hostHeader}`).hostname;
|
|
280
|
+
} catch {
|
|
281
|
+
return state;
|
|
282
|
+
}
|
|
283
|
+
// `URL.hostname` keeps brackets around IPv6 literals, so the IPv6 loopback
|
|
284
|
+
// comparison is against the bracketed form rather than `::1`.
|
|
285
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]") {
|
|
286
|
+
return state;
|
|
287
|
+
}
|
|
288
|
+
const rewrite = (s: string) => s.replace("127.0.0.1", hostname);
|
|
289
|
+
return {
|
|
290
|
+
...state,
|
|
291
|
+
url: rewrite(state.url),
|
|
292
|
+
streamUrl: rewrite(state.streamUrl),
|
|
293
|
+
wsUrl: rewrite(state.wsUrl),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
260
297
|
export function previewConfigForState(
|
|
261
298
|
state: ServeSimState,
|
|
262
299
|
base: string,
|
|
@@ -725,7 +762,8 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
725
762
|
}
|
|
726
763
|
|
|
727
764
|
if (state) {
|
|
728
|
-
const
|
|
765
|
+
const remoteState = rewriteStateForRequestHost(state, req.headers?.host);
|
|
766
|
+
const config = JSON.stringify(previewConfigForState(remoteState, base, serveSimBinPath(), execToken));
|
|
729
767
|
const configScript = `<script>window.__SIM_PREVIEW__=${config}</script>`;
|
|
730
768
|
html = html.replace("<!--__SIM_PREVIEW_CONFIG__-->", configScript);
|
|
731
769
|
}
|
|
@@ -755,17 +793,18 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
755
793
|
const sims = listAllSimulators();
|
|
756
794
|
const devices = sims.map((d) => {
|
|
757
795
|
const helper = helperByUdid.get(d.udid);
|
|
796
|
+
const remoteHelper = helper ? rewriteStateForRequestHost(helper, req.headers?.host) : null;
|
|
758
797
|
return {
|
|
759
798
|
device: d.udid,
|
|
760
799
|
name: d.name,
|
|
761
800
|
runtime: d.runtime,
|
|
762
801
|
state: d.state,
|
|
763
|
-
helper:
|
|
802
|
+
helper: remoteHelper
|
|
764
803
|
? {
|
|
765
|
-
port:
|
|
766
|
-
url:
|
|
767
|
-
streamUrl:
|
|
768
|
-
wsUrl:
|
|
804
|
+
port: remoteHelper.port,
|
|
805
|
+
url: remoteHelper.url,
|
|
806
|
+
streamUrl: remoteHelper.streamUrl,
|
|
807
|
+
wsUrl: remoteHelper.wsUrl,
|
|
769
808
|
}
|
|
770
809
|
: null,
|
|
771
810
|
};
|
|
@@ -1003,7 +1042,8 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
1003
1042
|
"Content-Type": "application/json",
|
|
1004
1043
|
"Cache-Control": "no-store",
|
|
1005
1044
|
});
|
|
1006
|
-
|
|
1045
|
+
const remoteState = state ? rewriteStateForRequestHost(state, req.headers?.host) : null;
|
|
1046
|
+
res.end(JSON.stringify(remoteState ? previewConfigForState(remoteState, base, serveSimBinPath(), execToken) : null));
|
|
1007
1047
|
return;
|
|
1008
1048
|
}
|
|
1009
1049
|
|
|
@@ -1023,6 +1063,7 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
1023
1063
|
"X-Accel-Buffering": "no",
|
|
1024
1064
|
});
|
|
1025
1065
|
res.write(":\n\n");
|
|
1066
|
+
axStreamerCache.prune(states.map((s) => s.device));
|
|
1026
1067
|
const ax = axStreamerCache.get(state.device, state.port);
|
|
1027
1068
|
const removeClient = ax.addClient(res);
|
|
1028
1069
|
req.on("close", removeClient);
|
|
@@ -1138,10 +1179,17 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
1138
1179
|
buf = buf.slice(nl + 1);
|
|
1139
1180
|
if (line) res.write("data: " + line + "\n\n");
|
|
1140
1181
|
}
|
|
1182
|
+
// Drop a runaway partial line so a malformed/never-terminated
|
|
1183
|
+
// log entry can't grow `buf` without bound.
|
|
1184
|
+
if (buf.length > SSE_LINE_BUFFER_LIMIT) buf = "";
|
|
1141
1185
|
});
|
|
1142
1186
|
|
|
1187
|
+
child.on("error", () => { try { res.end(); } catch {} });
|
|
1143
1188
|
child.on("close", () => res.end());
|
|
1144
|
-
req.on("close", () =>
|
|
1189
|
+
req.on("close", () => {
|
|
1190
|
+
child.stdout?.destroy();
|
|
1191
|
+
child.kill();
|
|
1192
|
+
});
|
|
1145
1193
|
return;
|
|
1146
1194
|
}
|
|
1147
1195
|
|
|
@@ -1225,11 +1273,17 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
|
|
|
1225
1273
|
if (!event) continue;
|
|
1226
1274
|
emitApp(event.bundleId, event.pid);
|
|
1227
1275
|
}
|
|
1276
|
+
if (buf.length > SSE_LINE_BUFFER_LIMIT) buf = "";
|
|
1228
1277
|
});
|
|
1229
1278
|
|
|
1279
|
+
child.on("error", () => {
|
|
1280
|
+
closed = true;
|
|
1281
|
+
try { res.end(); } catch {}
|
|
1282
|
+
});
|
|
1230
1283
|
child.on("close", () => res.end());
|
|
1231
1284
|
req.on("close", () => {
|
|
1232
1285
|
closed = true;
|
|
1286
|
+
child.stdout?.destroy();
|
|
1233
1287
|
child.kill();
|
|
1234
1288
|
});
|
|
1235
1289
|
return;
|