serve-sim 0.1.44-beta.99.3 → 0.1.44

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.
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serve-sim",
3
- "version": "0.1.44-beta.99.3",
3
+ "version": "0.1.44",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Evan Bacon",
@@ -46,10 +46,8 @@
46
46
  "Sources/SimAXSettings/build.sh",
47
47
  "dist/native/serve-sim-native.node",
48
48
  "Package.swift",
49
- "Sources/SimNative/sim-module.swift",
50
- "Sources/SimNative/sim-capture.swift",
51
- "Sources/SimNative/build.sh",
52
- "Sources/SimStreamHelper/*.swift"
49
+ "Sources/SimNative/*.swift",
50
+ "Sources/SimNative/build.sh"
53
51
  ],
54
52
  "exports": {
55
53
  "./middleware": {
@@ -76,11 +74,10 @@
76
74
  "commander": "^14.0.1",
77
75
  "debug": "^4.4.3",
78
76
  "lucide-react": "^1.20.0",
79
- "node-swift": "1.4.0",
77
+ "node-swift": "1.5.1",
80
78
  "preact": "^10.29.1",
81
79
  "react": "^19.0.0",
82
80
  "react-dom": "^19.0.0",
83
- "serve-sim-client": "workspace:*",
84
81
  "tailwindcss": "^4.1.7",
85
82
  "typescript": "^5.7.0"
86
83
  },
package/src/middleware.ts CHANGED
@@ -302,6 +302,29 @@ function queryDevice(rawUrl: string): string | null {
302
302
  return new URLSearchParams(rawUrl.slice(qIndex + 1)).get("device");
303
303
  }
304
304
 
305
+ /**
306
+ * Parse `/grid/api` pagination params. `limit` absent → return the whole list
307
+ * (back-compat for embedded mounts that expect every device in one response).
308
+ * The full DeviceKit `chrome` descriptor is only resolved for the returned
309
+ * page, so a remote viewer over a tunnel fetches a small first page instead of
310
+ * the whole simulator catalog (~150KB) up front.
311
+ */
312
+ export function parseGridPaging(rawUrl: string): { limit: number | null; offset: number } {
313
+ const qIndex = rawUrl.indexOf("?");
314
+ if (qIndex === -1) return { limit: null, offset: 0 };
315
+ const params = new URLSearchParams(rawUrl.slice(qIndex + 1));
316
+ const rawLimit = params.get("limit");
317
+ const rawOffset = params.get("offset");
318
+ // Clamp to sane bounds; ignore non-numeric/negative input rather than erroring.
319
+ const limit =
320
+ rawLimit == null || !/^\d+$/.test(rawLimit)
321
+ ? null
322
+ : Math.min(Math.max(Number(rawLimit), 1), 1000);
323
+ const offset =
324
+ rawOffset == null || !/^\d+$/.test(rawOffset) ? 0 : Math.max(Number(rawOffset), 0);
325
+ return { limit, offset };
326
+ }
327
+
305
328
  function hostForRequest(req: SimReq): string | undefined {
306
329
  const host = req.headers?.host;
307
330
  if (host) return host;
@@ -1263,32 +1286,14 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
1263
1286
  const states = readServeSimStates();
1264
1287
  const helperByUdid = new Map(states.map((s) => [s.device, s] as const));
1265
1288
  const sims = listAllSimulators();
1266
- const devices = sims.map((d) => {
1267
- const helper = helperByUdid.get(d.udid);
1268
- const remoteHelper = helper ? rewriteStateForRequestHost(helper, hostForRequest(req), base, httpProtocolForRequest(req), proxyHelpers) : null;
1269
- return {
1270
- device: d.udid,
1271
- name: d.name,
1272
- runtime: d.runtime,
1273
- state: d.state,
1274
- chrome: resolveDeviceKitChrome(d),
1275
- placeholderAsset: resolveDevicePlaceholderAsset(d),
1276
- helper: remoteHelper
1277
- ? {
1278
- port: remoteHelper.port,
1279
- url: remoteHelper.url,
1280
- streamUrl: remoteHelper.streamUrl,
1281
- wsUrl: remoteHelper.wsUrl,
1282
- }
1283
- : null,
1284
- };
1285
- });
1286
1289
  // Order mirrors Xcode's Devices window: the devices the user is actually
1287
1290
  // using float to the top — streaming first, then booted, then the
1288
1291
  // simulator they last opened in Simulator.app — and everything else falls
1289
1292
  // back to a stable family / newest-OS / name grouping. This surfaces the
1290
1293
  // handful of relevant devices instead of burying them in an alphabetical
1291
- // wall of near-identical names.
1294
+ // wall of near-identical names. Sort on the cheap metadata BEFORE
1295
+ // resolving the DeviceKit chrome descriptor, so pagination resolves chrome
1296
+ // only for the page actually returned.
1292
1297
  const preferredUdid = getPreferredDeviceUdid();
1293
1298
  const familyRank = (name: string): number => {
1294
1299
  if (/iphone/i.test(name)) return 0;
@@ -1298,9 +1303,18 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
1298
1303
  if (/vision|reality/i.test(name)) return 4;
1299
1304
  return 5;
1300
1305
  };
1301
- // Lower is higher in the list: streaming > booted > last-opened > rest.
1302
- const stateRank = (x: typeof devices[number]) =>
1303
- x.helper ? 0 : x.state === "Booted" ? 1 : x.device === preferredUdid ? 2 : 3;
1306
+ // Lower is higher in the list: streaming > selected > booted > last-opened
1307
+ // > rest. The active `?device=` selection is ranked near the top so it's
1308
+ // always inside the first page — otherwise a paginated client that selected
1309
+ // a shut-down device deep in the catalog would get no chrome/placeholder
1310
+ // for the view it's actually showing.
1311
+ const stateRank = (d: (typeof sims)[number]) => {
1312
+ if (helperByUdid.has(d.udid)) return 0;
1313
+ if (selectedDevice && d.udid === selectedDevice) return 1;
1314
+ if (d.state === "Booted") return 2;
1315
+ if (d.udid === preferredUdid) return 3;
1316
+ return 4;
1317
+ };
1304
1318
  // Newest runtime first, so "iPhone 17 Pro (27.0)" sorts above its 26.x twins.
1305
1319
  const runtimeRank = (runtime: string): number => {
1306
1320
  const m = runtime.match(/-(\d+)-(\d+)/);
@@ -1308,17 +1322,43 @@ export function simMiddleware(options?: SimMiddlewareOptions): SimMiddleware {
1308
1322
  const minor = m ? Number(m[2]) : 0;
1309
1323
  return -(major * 1000 + minor);
1310
1324
  };
1311
- devices.sort((a, b) =>
1325
+ sims.sort((a, b) =>
1312
1326
  stateRank(a) - stateRank(b) ||
1313
1327
  familyRank(a.name) - familyRank(b.name) ||
1314
1328
  a.name.localeCompare(b.name) ||
1315
1329
  runtimeRank(a.runtime) - runtimeRank(b.runtime),
1316
1330
  );
1331
+
1332
+ const total = sims.length;
1333
+ const { limit, offset } = parseGridPaging(rawUrl);
1334
+ const page = limit == null ? sims : sims.slice(offset, offset + limit);
1335
+ const devices = page.map((d) => {
1336
+ const helper = helperByUdid.get(d.udid);
1337
+ const remoteHelper = helper ? rewriteStateForRequestHost(helper, hostForRequest(req), base, httpProtocolForRequest(req), proxyHelpers) : null;
1338
+ return {
1339
+ device: d.udid,
1340
+ name: d.name,
1341
+ runtime: d.runtime,
1342
+ state: d.state,
1343
+ chrome: resolveDeviceKitChrome(d),
1344
+ placeholderAsset: resolveDevicePlaceholderAsset(d),
1345
+ helper: remoteHelper
1346
+ ? {
1347
+ port: remoteHelper.port,
1348
+ url: remoteHelper.url,
1349
+ streamUrl: remoteHelper.streamUrl,
1350
+ wsUrl: remoteHelper.wsUrl,
1351
+ }
1352
+ : null,
1353
+ };
1354
+ });
1317
1355
  res.writeHead(200, {
1318
1356
  "Content-Type": "application/json",
1319
1357
  "Cache-Control": "no-store",
1320
1358
  });
1321
- res.end(JSON.stringify({ devices }));
1359
+ // `total` lets the client show "X of Y" and know when to stop paging;
1360
+ // older clients that read only `devices` are unaffected.
1361
+ res.end(JSON.stringify({ devices, total, offset: limit == null ? 0 : offset, limit: limit ?? total }));
1322
1362
  return;
1323
1363
  }
1324
1364