@limrun/api 0.39.1 → 0.39.3

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/src/ios-client.ts CHANGED
@@ -270,7 +270,17 @@ export type DetoxLaunchRuntime = {
270
270
 
271
271
  export type LaunchAppRuntime = DetoxLaunchRuntime;
272
272
 
273
- type StandardLaunchAppOptions = {
273
+ export type LaunchAppShutdownCallback = (exitCode: number, logs: string[]) => Promise<void>;
274
+
275
+ type LaunchAppShutdownOptions = {
276
+ /**
277
+ * Called when the launched app exits. The logs array contains one entry per
278
+ * stdout/stderr line produced by that execution, as reported by limulator.
279
+ */
280
+ onShutdown?: LaunchAppShutdownCallback;
281
+ };
282
+
283
+ type StandardLaunchAppOptions = LaunchAppShutdownOptions & {
274
284
  /**
275
285
  * Launch behavior when the app may already be running.
276
286
  * Defaults to `ForegroundIfRunning` server-side.
@@ -279,7 +289,7 @@ type StandardLaunchAppOptions = {
279
289
  runtime?: undefined;
280
290
  };
281
291
 
282
- type RuntimeLaunchAppOptions = {
292
+ type RuntimeLaunchAppOptions = LaunchAppShutdownOptions & {
283
293
  /** Runtime launches must relaunch so runtime injection is applied. */
284
294
  mode?: Extract<LaunchAppMode, 'RelaunchIfRunning'>;
285
295
  /** Optional app runtime to attach during launch. */
@@ -960,6 +970,9 @@ type ServerResponse = {
960
970
  exitCode?: number;
961
971
  // Log tail fields
962
972
  logs?: string;
973
+ // App shutdown fields
974
+ execId?: string;
975
+ logLineCount?: number;
963
976
  // App log streaming fields
964
977
  lines?: string[];
965
978
  // performActions batch result fields
@@ -1295,6 +1308,12 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1295
1308
  // Simctl uses streaming, so it needs separate handling
1296
1309
  const simctlExecutions: Map<string, SimctlExecution> = new Map();
1297
1310
 
1311
+ // Server notifications are keyed by notification type and that type's
1312
+ // identifier field. They intentionally survive transient WebSocket reconnects
1313
+ // and are one-shot once a matching notification is processed.
1314
+ type ServerNotificationHandler = (message: ServerResponse) => Promise<void>;
1315
+ const serverNotifications: Map<string, Map<string, ServerNotificationHandler>> = new Map();
1316
+
1298
1317
  const xcrunShimCleanups: Array<() => Promise<void>> = [];
1299
1318
  const httpProxyCleanups: Array<() => Promise<void>> = [];
1300
1319
 
@@ -1375,6 +1394,52 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1375
1394
  cleanupClientResources();
1376
1395
  };
1377
1396
 
1397
+ const registerServerNotification = (
1398
+ type: string,
1399
+ notificationId: string,
1400
+ handler: ServerNotificationHandler,
1401
+ ): void => {
1402
+ const handlers = serverNotifications.get(type) ?? new Map<string, ServerNotificationHandler>();
1403
+ handlers.set(notificationId, handler);
1404
+ serverNotifications.set(type, handlers);
1405
+ };
1406
+
1407
+ const takeServerNotification = (
1408
+ type: string,
1409
+ notificationId: string,
1410
+ ): ServerNotificationHandler | undefined => {
1411
+ const handlers = serverNotifications.get(type);
1412
+ if (!handlers) {
1413
+ return undefined;
1414
+ }
1415
+
1416
+ const handler = handlers.get(notificationId);
1417
+ if (!handler) {
1418
+ return undefined;
1419
+ }
1420
+
1421
+ handlers.delete(notificationId);
1422
+ if (handlers.size === 0) {
1423
+ serverNotifications.delete(type);
1424
+ }
1425
+ return handler;
1426
+ };
1427
+
1428
+ const deleteServerNotification = (type: string, notificationId: string): void => {
1429
+ const handlers = serverNotifications.get(type);
1430
+ if (!handlers) {
1431
+ return;
1432
+ }
1433
+ handlers.delete(notificationId);
1434
+ if (handlers.size === 0) {
1435
+ serverNotifications.delete(type);
1436
+ }
1437
+ };
1438
+
1439
+ const clearServerNotifications = (): void => {
1440
+ serverNotifications.clear();
1441
+ };
1442
+
1378
1443
  let pingInterval: NodeJS.Timeout | undefined;
1379
1444
  const keepAliveSessionId =
1380
1445
  Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
@@ -1392,12 +1457,14 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1392
1457
  if (isNonRetryableError(lastError ?? '')) {
1393
1458
  logger.error(`Skipping reconnection (non-retryable error): ${lastError}`);
1394
1459
  updateConnectionState('disconnected');
1460
+ clearServerNotifications();
1395
1461
  return;
1396
1462
  }
1397
1463
 
1398
1464
  if (reconnectAttempts >= maxReconnectAttempts) {
1399
1465
  logger.error(`Max reconnection attempts (${maxReconnectAttempts}) reached. Giving up.`);
1400
1466
  updateConnectionState('disconnected');
1467
+ clearServerNotifications();
1401
1468
  return;
1402
1469
  }
1403
1470
 
@@ -1427,6 +1494,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1427
1494
  return {
1428
1495
  type: request['type'],
1429
1496
  id: request['id'],
1497
+ execId: request['execId'],
1430
1498
  bundleId: request['bundleId'],
1431
1499
  mode: request['mode'],
1432
1500
  runtime: runtime ? { kind: runtime.kind, version: runtime.version } : undefined,
@@ -1471,6 +1539,45 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1471
1539
  });
1472
1540
  };
1473
1541
 
1542
+ const splitAppLogTail = (logs: string): string[] => {
1543
+ if (logs.length === 0) {
1544
+ return [];
1545
+ }
1546
+ return logs.split(/\r?\n/);
1547
+ };
1548
+
1549
+ const handleAppShutdown = (message: ServerResponse): boolean => {
1550
+ if (message.type !== 'appShutdown') {
1551
+ return false;
1552
+ }
1553
+
1554
+ const { execId, bundleId, exitCode, logLineCount } = message;
1555
+ if (
1556
+ typeof execId !== 'string' ||
1557
+ typeof bundleId !== 'string' ||
1558
+ typeof exitCode !== 'number' ||
1559
+ !Number.isInteger(exitCode) ||
1560
+ typeof logLineCount !== 'number' ||
1561
+ !Number.isInteger(logLineCount) ||
1562
+ logLineCount < 0
1563
+ ) {
1564
+ logger.warn('Received malformed appShutdown message:', message);
1565
+ return true;
1566
+ }
1567
+
1568
+ const notification = takeServerNotification('appShutdown', execId);
1569
+ if (!notification) {
1570
+ logger.debug(`Received appShutdown for unknown or already handled execId: ${execId}`);
1571
+ return true;
1572
+ }
1573
+
1574
+ void notification(message).catch((error) => {
1575
+ logger.error(`Error processing appShutdown notification for execId ${execId}:`, error);
1576
+ });
1577
+
1578
+ return true;
1579
+ };
1580
+
1474
1581
  // Response handlers - transform raw responses to typed results
1475
1582
  const responseHandlers: Record<string, (msg: ServerResponse) => unknown> = {
1476
1583
  screenshotResult: (msg) => ({
@@ -1545,6 +1652,10 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1545
1652
  return;
1546
1653
  }
1547
1654
 
1655
+ if (handleAppShutdown(message)) {
1656
+ return;
1657
+ }
1658
+
1548
1659
  // Handle simctl streaming separately (it uses multiple messages per request)
1549
1660
  if (message.type === 'simctlStream') {
1550
1661
  const execution = simctlExecutions.get(message.id);
@@ -1638,6 +1749,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1638
1749
  cleanup();
1639
1750
  updateConnectionState('disconnected');
1640
1751
  failPendingRequests('Non-retryable error');
1752
+ clearServerNotifications();
1641
1753
  logger.debug('Non-retryable error. Closing connection.');
1642
1754
  }
1643
1755
  });
@@ -1800,10 +1912,49 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
1800
1912
  );
1801
1913
  }
1802
1914
  const mode = launchOptions.runtime ? 'RelaunchIfRunning' : launchOptions.mode;
1915
+ const onShutdown = launchOptions.onShutdown;
1916
+ const execId = onShutdown ? generateId() : undefined;
1917
+ if (execId && onShutdown) {
1918
+ registerServerNotification('appShutdown', execId, async (message) => {
1919
+ const { bundleId, exitCode, logLineCount } = message;
1920
+ if (
1921
+ typeof bundleId !== 'string' ||
1922
+ typeof exitCode !== 'number' ||
1923
+ !Number.isInteger(exitCode) ||
1924
+ typeof logLineCount !== 'number' ||
1925
+ !Number.isInteger(logLineCount) ||
1926
+ logLineCount < 0
1927
+ ) {
1928
+ logger.warn(`Received malformed appShutdown payload for execId ${execId}:`, message);
1929
+ return;
1930
+ }
1931
+
1932
+ let logs: string[] = [];
1933
+ try {
1934
+ logs = splitAppLogTail(
1935
+ await sendRequest<string>('appLogTail', { bundleId, lines: logLineCount }),
1936
+ );
1937
+ } catch (error) {
1938
+ logger.error(`Failed to fetch app logs for shutdown execId ${execId}:`, error);
1939
+ }
1940
+
1941
+ try {
1942
+ await onShutdown(exitCode, logs);
1943
+ } catch (error) {
1944
+ logger.error(`Error in onShutdown callback for execId ${execId}:`, error);
1945
+ }
1946
+ });
1947
+ }
1803
1948
  return sendRequest<void>('launchApp', {
1804
1949
  bundleId,
1805
1950
  mode,
1806
1951
  runtime: launchOptions.runtime,
1952
+ execId,
1953
+ }).catch((error) => {
1954
+ if (execId) {
1955
+ deleteServerNotification('appShutdown', execId);
1956
+ }
1957
+ throw error;
1807
1958
  });
1808
1959
  };
1809
1960
 
@@ -2268,6 +2419,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
2268
2419
  cleanup();
2269
2420
  updateConnectionState('disconnected');
2270
2421
  failPendingRequests('Intentional disconnect');
2422
+ clearServerNotifications();
2271
2423
  logger.debug('Intentionally disconnected from WebSocket.');
2272
2424
  };
2273
2425
 
@@ -52,6 +52,12 @@ export type SyncOptions = {
52
52
  };
53
53
 
54
54
  export type SyncResult = {
55
+ /**
56
+ * Number of bytes transmitted to the server for this sync (full uploads plus
57
+ * delta patches, before transport compression). In watch mode, this reflects
58
+ * the initial sync only.
59
+ */
60
+ bytesSent?: number;
55
61
  /** Present only when watch=true; call to stop watching */
56
62
  stopWatching?: () => Promise<void>;
57
63
  };
@@ -628,10 +634,14 @@ export class XcodeInstances extends GeneratedXcodeInstances {
628
634
  };
629
635
 
630
636
  const result = await syncFolderImpl(localCodePath, codeSyncOpts);
637
+ const out: SyncResult = {};
638
+ if (result.bytesSent !== undefined) {
639
+ out.bytesSent = result.bytesSent;
640
+ }
631
641
  if (result.stopWatching) {
632
- return { stopWatching: result.stopWatching };
642
+ out.stopWatching = result.stopWatching;
633
643
  }
634
- return {};
644
+ return out;
635
645
  },
636
646
 
637
647
  xcodebuild(settings?: XcodeProjectConfig, options?: XcodeBuildOptions): ExecChildProcess {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.39.1'; // x-release-please-version
1
+ export const VERSION = '0.39.3'; // x-release-please-version
package/version.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.39.1";
1
+ export declare const VERSION = "0.39.3";
2
2
  //# sourceMappingURL=version.d.mts.map
package/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.39.1";
1
+ export declare const VERSION = "0.39.3";
2
2
  //# sourceMappingURL=version.d.ts.map
package/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
- exports.VERSION = '0.39.1'; // x-release-please-version
4
+ exports.VERSION = '0.39.3'; // x-release-please-version
5
5
  //# sourceMappingURL=version.js.map
package/version.mjs CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = '0.39.1'; // x-release-please-version
1
+ export const VERSION = '0.39.3'; // x-release-please-version
2
2
  //# sourceMappingURL=version.mjs.map