@run-cloud/sdk 0.12.0 → 0.13.0

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/index.js CHANGED
@@ -1,5 +1,242 @@
1
1
  import WebSocket from "ws";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
+ export const SIMULATOR_KEYS = [
4
+ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
5
+ "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
6
+ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
7
+ "enter", "escape", "backspace", "tab", "space", "minus", "equal",
8
+ "bracketLeft", "bracketRight", "backslash", "semicolon", "quote", "backquote",
9
+ "comma", "period", "slash", "capsLock",
10
+ "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12",
11
+ "printScreen", "scrollLock", "pause", "insert", "home", "pageUp", "delete", "end",
12
+ "pageDown", "arrowRight", "arrowLeft", "arrowDown", "arrowUp", "numLock",
13
+ "numpadDivide", "numpadMultiply", "numpadSubtract", "numpadAdd", "numpadEnter",
14
+ "numpad0", "numpad1", "numpad2", "numpad3", "numpad4", "numpad5", "numpad6",
15
+ "numpad7", "numpad8", "numpad9", "numpadDecimal",
16
+ ];
17
+ export const SIMULATOR_BUTTONS = [
18
+ "home",
19
+ "back",
20
+ "appSwitcher",
21
+ "recents",
22
+ "power",
23
+ "volumeUp",
24
+ "volumeDown",
25
+ "menu",
26
+ "sideButton",
27
+ "actionButton",
28
+ "digitalCrown",
29
+ ];
30
+ export const SIMULATOR_RENDER_DEBUG_OPTIONS = [
31
+ "colorBlendedLayers",
32
+ "colorCopiedImages",
33
+ "colorMisalignedImages",
34
+ "colorOffscreenRendered",
35
+ "slowAnimations",
36
+ ];
37
+ export const SIMULATOR_INTERACTION_ERROR_CODES = [
38
+ "invalid_interaction",
39
+ "active_session_not_found",
40
+ "simulator_capacity_unavailable",
41
+ "unsupported_action",
42
+ "duplicate_request",
43
+ "interaction_cancelled",
44
+ "interaction_timeout",
45
+ "interaction_transport_error",
46
+ "interaction_invalid_response",
47
+ "interaction_failed",
48
+ ];
49
+ export const SIMULATOR_SCREENSHOT_ERROR_CODES = [
50
+ "invalid_screenshot_request",
51
+ "active_session_not_found",
52
+ "simulator_capacity_unavailable",
53
+ "screenshot_cancelled",
54
+ "screenshot_timeout",
55
+ "screenshot_transport_error",
56
+ "screenshot_invalid_response",
57
+ "screenshot_failed",
58
+ ];
59
+ export const DEFAULT_SIMULATOR_INTERACTION_TIMEOUT_MS = 15_000;
60
+ const SIMULATOR_RESPONSE_MARGIN_MS = 250;
61
+ const REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
62
+ function assertNormalizedPoint(point, name) {
63
+ if (!Number.isFinite(point.x)
64
+ || point.x < 0
65
+ || point.x > 1
66
+ || !Number.isFinite(point.y)
67
+ || point.y < 0
68
+ || point.y > 1)
69
+ throw new RangeError(`${name} coordinates must be finite numbers from 0 to 1`);
70
+ }
71
+ function assertIntegerRange(value, min, max, name) {
72
+ if (!Number.isInteger(value) || value < min || value > max) {
73
+ throw new RangeError(`${name} must be an integer from ${min} to ${max}`);
74
+ }
75
+ }
76
+ export function validateSimulatorInteraction(interaction, timeoutMs = DEFAULT_SIMULATOR_INTERACTION_TIMEOUT_MS) {
77
+ assertIntegerRange(timeoutMs, 100, 60_000, "timeoutMs");
78
+ switch (interaction.action) {
79
+ case "tap":
80
+ assertNormalizedPoint(interaction, "tap");
81
+ return;
82
+ case "swipe":
83
+ assertNormalizedPoint(interaction.from, "swipe from");
84
+ assertNormalizedPoint(interaction.to, "swipe to");
85
+ if (interaction.durationMs !== undefined) {
86
+ assertIntegerRange(interaction.durationMs, 50, 30_000, "swipe durationMs");
87
+ }
88
+ return;
89
+ case "gesture": {
90
+ if (interaction.steps.length < 2 || interaction.steps.length > 1_000) {
91
+ throw new RangeError("gesture steps must contain from 2 to 1000 entries");
92
+ }
93
+ let pointCount;
94
+ let delayTotal = 0;
95
+ for (const [index, step] of interaction.steps.entries()) {
96
+ const expectedPhase = index === 0
97
+ ? "begin"
98
+ : index === interaction.steps.length - 1
99
+ ? "end"
100
+ : "move";
101
+ if (step.phase !== expectedPhase) {
102
+ throw new TypeError(`gesture step ${index} phase must be ${expectedPhase}`);
103
+ }
104
+ if ((index === 0) !== (step.phase === "begin")) {
105
+ throw new TypeError("gesture must start with begin and contain no later begin phase");
106
+ }
107
+ if ((index === interaction.steps.length - 1) !== (step.phase === "end")) {
108
+ throw new TypeError("gesture must end with end and contain no earlier end phase");
109
+ }
110
+ if (step.points.length !== 1 && step.points.length !== 2) {
111
+ throw new RangeError("each gesture step must contain one or two points");
112
+ }
113
+ if (pointCount === undefined)
114
+ pointCount = step.points.length;
115
+ else if (pointCount !== step.points.length) {
116
+ throw new TypeError("every gesture step must use the same number of points");
117
+ }
118
+ for (const point of step.points)
119
+ assertNormalizedPoint(point, "gesture");
120
+ if (step.delayMs !== undefined) {
121
+ assertIntegerRange(step.delayMs, 0, 5_000, "gesture delayMs");
122
+ if (index === interaction.steps.length - 1 && step.delayMs !== 0) {
123
+ throw new RangeError("final gesture end step delayMs must be 0 because there is no next step");
124
+ }
125
+ delayTotal += step.delayMs;
126
+ }
127
+ }
128
+ if (delayTotal > timeoutMs)
129
+ throw new RangeError("gesture delays cannot exceed timeoutMs");
130
+ return;
131
+ }
132
+ case "typeText":
133
+ if (interaction.text.length < 1 || interaction.text.length > 10_000) {
134
+ throw new RangeError("text must contain from 1 to 10000 characters");
135
+ }
136
+ if (!/^[\x09\x0a\x20-\x7e]+$/.test(interaction.text)) {
137
+ throw new TypeError("text must contain only US ASCII characters, tabs, and line feeds");
138
+ }
139
+ return;
140
+ case "pressKey":
141
+ if (!SIMULATOR_KEYS.includes(interaction.key)) {
142
+ throw new TypeError(`unknown simulator key: ${interaction.key}`);
143
+ }
144
+ if (interaction.durationMs !== undefined) {
145
+ assertIntegerRange(interaction.durationMs, 20, 30_000, "key durationMs");
146
+ }
147
+ if (interaction.modifiers) {
148
+ const known = new Set(["shift", "control", "alt", "meta"]);
149
+ if (interaction.modifiers.some((modifier) => !known.has(modifier))) {
150
+ throw new TypeError("key modifiers must be shift, control, alt, or meta");
151
+ }
152
+ if (new Set(interaction.modifiers).size !== interaction.modifiers.length) {
153
+ throw new TypeError("key modifiers must be unique");
154
+ }
155
+ }
156
+ return;
157
+ case "pressButton":
158
+ if (!SIMULATOR_BUTTONS.includes(interaction.button)) {
159
+ throw new TypeError(`unknown simulator button: ${interaction.button}`);
160
+ }
161
+ if (interaction.durationMs !== undefined) {
162
+ assertIntegerRange(interaction.durationMs, 20, 30_000, "button durationMs");
163
+ }
164
+ return;
165
+ case "rotate":
166
+ if (!["portrait", "portrait_upside_down", "landscape_left", "landscape_right"].includes(interaction.orientation)) {
167
+ throw new TypeError(`unknown simulator orientation: ${interaction.orientation}`);
168
+ }
169
+ return;
170
+ case "reload":
171
+ case "toggleSoftwareKeyboard":
172
+ case "simulateMemoryWarning":
173
+ return;
174
+ case "scroll":
175
+ if (!Number.isFinite(interaction.deltaX)
176
+ || interaction.deltaX < -1
177
+ || interaction.deltaX > 1
178
+ || !Number.isFinite(interaction.deltaY)
179
+ || interaction.deltaY < -1
180
+ || interaction.deltaY > 1)
181
+ throw new RangeError("scroll deltaX and deltaY must be finite numbers from -1 to 1");
182
+ if (interaction.deltaX === 0 && interaction.deltaY === 0) {
183
+ throw new RangeError("at least one scroll delta must be nonzero");
184
+ }
185
+ if ((interaction.x === undefined) !== (interaction.y === undefined)) {
186
+ throw new TypeError("scroll x and y must be provided together");
187
+ }
188
+ if (interaction.x !== undefined && interaction.y !== undefined) {
189
+ assertNormalizedPoint({ x: interaction.x, y: interaction.y }, "scroll anchor");
190
+ }
191
+ return;
192
+ case "rotateDigitalCrown":
193
+ if (!Number.isFinite(interaction.delta) || interaction.delta === 0 || Math.abs(interaction.delta) > 10_000) {
194
+ throw new RangeError("digital crown delta must be nonzero and between -10000 and 10000");
195
+ }
196
+ return;
197
+ case "setRenderDebug":
198
+ if (!SIMULATOR_RENDER_DEBUG_OPTIONS.includes(interaction.option)) {
199
+ throw new TypeError(`unknown render debug option: ${interaction.option}`);
200
+ }
201
+ if (typeof interaction.enabled !== "boolean") {
202
+ throw new TypeError("render debug enabled must be a boolean");
203
+ }
204
+ return;
205
+ }
206
+ }
207
+ function requestAbort(options) {
208
+ const { signal, timeoutMs } = options;
209
+ if (timeoutMs === undefined && !signal) {
210
+ return { signal: undefined, timedOut: () => false, cleanup: () => undefined };
211
+ }
212
+ if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
213
+ throw new RangeError("timeoutMs must be a positive finite number");
214
+ }
215
+ const controller = new AbortController();
216
+ let didTimeOut = false;
217
+ const abortFromCaller = () => {
218
+ controller.abort(signal?.reason ?? new DOMException("The operation was aborted", "AbortError"));
219
+ };
220
+ if (signal?.aborted)
221
+ abortFromCaller();
222
+ else
223
+ signal?.addEventListener("abort", abortFromCaller, { once: true });
224
+ const timer = timeoutMs === undefined
225
+ ? undefined
226
+ : setTimeout(() => {
227
+ didTimeOut = true;
228
+ controller.abort(new DOMException(options.timeoutMessage ?? "The operation timed out", "TimeoutError"));
229
+ }, timeoutMs);
230
+ return {
231
+ signal: controller.signal,
232
+ timedOut: () => didTimeOut,
233
+ cleanup: () => {
234
+ if (timer !== undefined)
235
+ clearTimeout(timer);
236
+ signal?.removeEventListener("abort", abortFromCaller);
237
+ },
238
+ };
239
+ }
3
240
  const FILE_CHUNK_BYTES = 512 * 1024;
4
241
  const MAX_CONCURRENT_ASSET_UPLOADS = 6;
5
242
  const MAX_ASSET_UPLOAD_ATTEMPTS = 3;
@@ -28,10 +265,33 @@ function normalizeSandbox(raw) {
28
265
  export class RunCloudError extends Error {
29
266
  status;
30
267
  detail;
31
- constructor(status, detail) {
268
+ code;
269
+ retryable;
270
+ details;
271
+ requestId;
272
+ sessionId;
273
+ platform;
274
+ action;
275
+ interactionStatus;
276
+ acceptedAt;
277
+ completedAt;
278
+ durationMs;
279
+ constructor(status, detail, options = {}) {
32
280
  super(`run.cloud API ${status}: ${detail}`);
33
281
  this.status = status;
34
282
  this.detail = detail;
283
+ this.name = "RunCloudError";
284
+ this.code = options.code;
285
+ this.retryable = options.retryable;
286
+ this.details = options.details;
287
+ this.requestId = options.requestId;
288
+ this.sessionId = options.sessionId;
289
+ this.platform = options.platform;
290
+ this.action = options.action;
291
+ this.interactionStatus = options.interactionStatus;
292
+ this.acceptedAt = options.acceptedAt;
293
+ this.completedAt = options.completedAt;
294
+ this.durationMs = options.durationMs;
35
295
  }
36
296
  }
37
297
  function env(name) {
@@ -47,24 +307,108 @@ function apiKeyFromEnv() {
47
307
  function apiUrlFromEnv() {
48
308
  return (env("RUN_CLOUD_API_URL") ?? "https://api.run.cloud").replace(/\/+$/, "");
49
309
  }
50
- async function errorDetail(response) {
310
+ function optionalString(value) {
311
+ return typeof value === "string" ? value : undefined;
312
+ }
313
+ function optionalFiniteNumber(value) {
314
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
315
+ }
316
+ const simulatorInteractionErrorCodeSet = new Set(SIMULATOR_INTERACTION_ERROR_CODES);
317
+ const simulatorScreenshotErrorCodeSet = new Set(SIMULATOR_SCREENSHOT_ERROR_CODES);
318
+ function normalizedInteractionErrorCode(value) {
319
+ if (typeof value === "string" && simulatorInteractionErrorCodeSet.has(value)) {
320
+ return value;
321
+ }
322
+ switch (value) {
323
+ case "timeout":
324
+ case "timed_out":
325
+ case "interaction_transport_timeout":
326
+ return "interaction_timeout";
327
+ case "cancelled":
328
+ return "interaction_cancelled";
329
+ case "invalid_request":
330
+ return "invalid_interaction";
331
+ case "simulator_session_ended":
332
+ return "active_session_not_found";
333
+ case "unsupported_on_platform":
334
+ return "unsupported_action";
335
+ case "invalid_host_response":
336
+ case "invalid_interaction_result":
337
+ case "interaction_platform_mismatch":
338
+ return "interaction_invalid_response";
339
+ default:
340
+ return "interaction_failed";
341
+ }
342
+ }
343
+ function normalizedScreenshotErrorCode(value) {
344
+ return typeof value === "string" && simulatorScreenshotErrorCodeSet.has(value)
345
+ ? value
346
+ : "screenshot_failed";
347
+ }
348
+ async function parseApiError(response) {
51
349
  const text = await response.text();
52
350
  try {
53
351
  const parsed = JSON.parse(text);
54
- for (const value of [parsed.detail, parsed.error, parsed.message]) {
55
- if (typeof value === "string")
56
- return value;
57
- if (value !== undefined)
58
- return JSON.stringify(value);
59
- }
352
+ const nested = typeof parsed.error === "object" && parsed.error !== null
353
+ ? parsed.error
354
+ : undefined;
355
+ const message = nested?.message ?? parsed.detail ?? parsed.error ?? parsed.message;
356
+ const detail = typeof message === "string"
357
+ ? message
358
+ : message !== undefined
359
+ ? JSON.stringify(message)
360
+ : response.statusText;
361
+ const interactionStatus = parsed.status === "failed"
362
+ || parsed.status === "cancelled"
363
+ || parsed.status === "timed_out"
364
+ ? parsed.status
365
+ : undefined;
366
+ const platform = parsed.platform === "ios" || parsed.platform === "android"
367
+ ? parsed.platform
368
+ : undefined;
369
+ const retryable = nested?.retryable ?? parsed.retryable;
370
+ const action = optionalString(parsed.action);
371
+ const rawCode = nested?.code ?? parsed.code;
372
+ const code = action === "screenshot"
373
+ ? normalizedScreenshotErrorCode(rawCode)
374
+ : action !== undefined || interactionStatus !== undefined
375
+ ? normalizedInteractionErrorCode(rawCode)
376
+ : optionalString(rawCode);
377
+ return {
378
+ detail,
379
+ code,
380
+ retryable: typeof retryable === "boolean" ? retryable : undefined,
381
+ details: nested?.details ?? parsed.details,
382
+ requestId: optionalString(parsed.requestId),
383
+ sessionId: optionalString(parsed.sessionId),
384
+ platform,
385
+ action,
386
+ interactionStatus,
387
+ acceptedAt: optionalString(parsed.acceptedAt),
388
+ completedAt: optionalString(parsed.completedAt),
389
+ durationMs: optionalFiniteNumber(parsed.durationMs),
390
+ };
60
391
  }
61
392
  catch {
62
- // plain text
393
+ if (/<(?:!doctype|html)\b/i.test(text)) {
394
+ return {
395
+ detail: "the API edge returned an HTML error page; retry the request or use the streaming/direct-transfer API",
396
+ };
397
+ }
398
+ return { detail: text || response.statusText };
399
+ }
400
+ }
401
+ async function apiError(response, fallback = {}) {
402
+ const { detail, ...options } = await parseApiError(response);
403
+ const merged = { ...fallback };
404
+ for (const [key, value] of Object.entries(options)) {
405
+ if (value !== undefined)
406
+ Object.assign(merged, { [key]: value });
63
407
  }
64
- if (/<(?:!doctype|html)\b/i.test(text)) {
65
- return "the API edge returned an HTML error page; retry the request or use the streaming/direct-transfer API";
408
+ if (merged.action !== undefined && merged.retryable === undefined) {
409
+ merged.retryable = response.status === 408 || response.status === 429 || response.status >= 500;
66
410
  }
67
- return text || response.statusText;
411
+ return new RunCloudError(response.status, detail, merged);
68
412
  }
69
413
  async function md5Hex(blob) {
70
414
  const hash = createHash("md5");
@@ -126,24 +470,53 @@ export class Client {
126
470
  meteringStatus: account.meteringStatus ?? account.run_cloud_metering_status,
127
471
  };
128
472
  }
129
- async request(method, path, body, headers) {
130
- const response = await this.fetchImpl(`${this.apiUrl}${path}`, {
131
- method,
132
- headers: {
133
- Authorization: `Bearer ${this.apiKey}`,
134
- ...(body === undefined ? {} : { "Content-Type": "application/json" }),
135
- ...headers,
136
- },
137
- body: body === undefined ? undefined : JSON.stringify(body),
138
- });
139
- if (!response.ok)
140
- throw new RunCloudError(response.status, await errorDetail(response));
141
- if (response.status === 204)
142
- return undefined;
143
- const contentType = response.headers.get("content-type") ?? "";
144
- return contentType.includes("application/json")
145
- ? (await response.json())
146
- : (await response.text());
473
+ async request(method, path, body, headers, abortOptions = {}) {
474
+ const operation = requestAbort(abortOptions);
475
+ try {
476
+ const response = await this.fetchImpl(`${this.apiUrl}${path}`, {
477
+ method,
478
+ headers: {
479
+ Authorization: `Bearer ${this.apiKey}`,
480
+ ...(body === undefined ? {} : { "Content-Type": "application/json" }),
481
+ ...headers,
482
+ },
483
+ body: body === undefined ? undefined : JSON.stringify(body),
484
+ signal: operation.signal,
485
+ });
486
+ if (!response.ok)
487
+ throw await apiError(response, abortOptions.errorContext);
488
+ if (response.status === 204)
489
+ return undefined;
490
+ const contentType = response.headers.get("content-type") ?? "";
491
+ return contentType.includes("application/json")
492
+ ? (await response.json())
493
+ : (await response.text());
494
+ }
495
+ catch (error) {
496
+ if (operation.timedOut()) {
497
+ throw new RunCloudError(0, abortOptions.timeoutMessage ?? "the request timed out", {
498
+ ...abortOptions.timeoutContext,
499
+ code: abortOptions.timeoutCode ?? "interaction_timeout",
500
+ retryable: true,
501
+ interactionStatus: "timed_out",
502
+ });
503
+ }
504
+ if (abortOptions.errorContext
505
+ && !abortOptions.signal?.aborted
506
+ && error instanceof TypeError) {
507
+ throw new RunCloudError(0, abortOptions.transportErrorMessage
508
+ ?? "simulator interaction could not reach the run.cloud API", {
509
+ ...abortOptions.errorContext,
510
+ code: abortOptions.transportErrorCode ?? "interaction_transport_error",
511
+ retryable: true,
512
+ interactionStatus: "failed",
513
+ });
514
+ }
515
+ throw error;
516
+ }
517
+ finally {
518
+ operation.cleanup();
519
+ }
147
520
  }
148
521
  async uploadForm(path, form) {
149
522
  const response = await this.fetchImpl(`${this.apiUrl}${path}`, {
@@ -152,17 +525,48 @@ export class Client {
152
525
  body: form,
153
526
  });
154
527
  if (!response.ok)
155
- throw new RunCloudError(response.status, await errorDetail(response));
528
+ throw await apiError(response);
156
529
  return (await response.json());
157
530
  }
158
- async requestBytes(path) {
159
- const response = await this.fetchImpl(`${this.apiUrl}${path}`, {
160
- method: "GET",
161
- headers: { Authorization: `Bearer ${this.apiKey}` },
162
- });
163
- if (!response.ok)
164
- throw new RunCloudError(response.status, await errorDetail(response));
165
- return new Uint8Array(await response.arrayBuffer());
531
+ async requestBytes(path, abortOptions = {}) {
532
+ const operation = requestAbort(abortOptions);
533
+ try {
534
+ const response = await this.fetchImpl(`${this.apiUrl}${path}`, {
535
+ method: "GET",
536
+ headers: {
537
+ Authorization: `Bearer ${this.apiKey}`,
538
+ ...abortOptions.headers,
539
+ },
540
+ signal: operation.signal,
541
+ });
542
+ if (!response.ok)
543
+ throw await apiError(response, abortOptions.errorContext);
544
+ return new Uint8Array(await response.arrayBuffer());
545
+ }
546
+ catch (error) {
547
+ if (operation.timedOut()) {
548
+ throw new RunCloudError(0, abortOptions.timeoutMessage ?? "the request timed out", {
549
+ ...abortOptions.timeoutContext,
550
+ code: abortOptions.timeoutCode ?? "screenshot_timeout",
551
+ retryable: true,
552
+ interactionStatus: "timed_out",
553
+ });
554
+ }
555
+ if (abortOptions.errorContext
556
+ && !abortOptions.signal?.aborted
557
+ && error instanceof TypeError) {
558
+ throw new RunCloudError(0, abortOptions.transportErrorMessage ?? "simulator screenshot could not reach the run.cloud API", {
559
+ ...abortOptions.errorContext,
560
+ code: abortOptions.transportErrorCode ?? "screenshot_transport_error",
561
+ retryable: true,
562
+ interactionStatus: "failed",
563
+ });
564
+ }
565
+ throw error;
566
+ }
567
+ finally {
568
+ operation.cleanup();
569
+ }
166
570
  }
167
571
  async *requestTextStream(path, signal) {
168
572
  const response = await this.fetchImpl(`${this.apiUrl}${path}`, {
@@ -174,7 +578,7 @@ export class Client {
174
578
  signal,
175
579
  });
176
580
  if (!response.ok)
177
- throw new RunCloudError(response.status, await errorDetail(response));
581
+ throw await apiError(response);
178
582
  if (!response.body)
179
583
  throw new RunCloudError(0, "simulator log stream returned no body");
180
584
  const contentType = response.headers.get("content-type") ?? "";
@@ -205,7 +609,7 @@ export class Client {
205
609
  async putSigned(url, body, headers) {
206
610
  const response = await this.fetchImpl(url, { method: "PUT", headers, body });
207
611
  if (!response.ok)
208
- throw new RunCloudError(response.status, await errorDetail(response));
612
+ throw await apiError(response);
209
613
  }
210
614
  async execStream(path, request, callbacks = {}) {
211
615
  const url = new URL(`${this.apiUrl}${path}`);
@@ -306,10 +710,13 @@ export class SimulatorClient {
306
710
  this.platform = platform;
307
711
  }
308
712
  async create(options = {}) {
309
- const { tags, labels, ...rest } = options;
713
+ const { tags, labels, inactivityTimeout, ...rest } = options;
310
714
  const resolved = tags ?? labels;
311
715
  return await this.client.request("POST", `/run-cloud/${this.platform}`, {
312
716
  ...rest,
717
+ ...(inactivityTimeout === undefined
718
+ ? {}
719
+ : { inactivityTimeout: inactivityTimeout ?? "none" }),
313
720
  ...(resolved ? { tags: resolved } : {}),
314
721
  });
315
722
  }
@@ -333,6 +740,125 @@ export class SimulatorClient {
333
740
  async openUrl(id, url) {
334
741
  return await this.client.request("POST", `/run-cloud/${this.platform}/${encodeURIComponent(id)}/open-url`, { url });
335
742
  }
743
+ async interact(id, interaction, options = {}) {
744
+ const timeoutMs = options.timeoutMs ?? DEFAULT_SIMULATOR_INTERACTION_TIMEOUT_MS;
745
+ const requestId = options.requestId ?? `interaction_${randomUUID()}`;
746
+ if (!REQUEST_ID_PATTERN.test(requestId)) {
747
+ throw new TypeError("requestId must match /^[A-Za-z0-9._:-]{1,128}$/");
748
+ }
749
+ validateSimulatorInteraction(interaction, timeoutMs);
750
+ return await this.client.request("POST", `/run-cloud/${this.platform}/${encodeURIComponent(id)}/interactions`, { ...interaction, requestId, timeoutMs }, undefined, {
751
+ signal: options.signal,
752
+ timeoutMs: timeoutMs + SIMULATOR_RESPONSE_MARGIN_MS,
753
+ timeoutMessage: `simulator ${interaction.action} did not complete within ${timeoutMs} ms`,
754
+ timeoutCode: "interaction_timeout",
755
+ transportErrorCode: "interaction_transport_error",
756
+ timeoutContext: {
757
+ requestId,
758
+ sessionId: id,
759
+ platform: this.platform,
760
+ action: interaction.action,
761
+ },
762
+ errorContext: {
763
+ requestId,
764
+ sessionId: id,
765
+ platform: this.platform,
766
+ action: interaction.action,
767
+ code: "interaction_failed",
768
+ interactionStatus: "failed",
769
+ },
770
+ });
771
+ }
772
+ async tap(id, point, options = {}) {
773
+ return await this.interact(id, { action: "tap", ...point }, options);
774
+ }
775
+ async swipe(id, from, to, options = {}) {
776
+ const { durationMs, ...requestOptions } = options;
777
+ return await this.interact(id, { action: "swipe", from, to, ...(durationMs === undefined ? {} : { durationMs }) }, requestOptions);
778
+ }
779
+ async gesture(id, steps, options = {}) {
780
+ return await this.interact(id, { action: "gesture", steps }, options);
781
+ }
782
+ async typeText(id, text, options = {}) {
783
+ return await this.interact(id, { action: "typeText", text }, options);
784
+ }
785
+ async pressKey(id, key, options = {}) {
786
+ const { modifiers, durationMs, ...requestOptions } = options;
787
+ return await this.interact(id, {
788
+ action: "pressKey",
789
+ key,
790
+ ...(modifiers?.length ? { modifiers } : {}),
791
+ ...(durationMs === undefined ? {} : { durationMs }),
792
+ }, requestOptions);
793
+ }
794
+ async pressButton(id, button, options = {}) {
795
+ const { durationMs, ...requestOptions } = options;
796
+ return await this.interact(id, {
797
+ action: "pressButton",
798
+ button,
799
+ ...(durationMs === undefined ? {} : { durationMs }),
800
+ }, requestOptions);
801
+ }
802
+ async rotate(id, orientation, options = {}) {
803
+ return await this.interact(id, { action: "rotate", orientation }, options);
804
+ }
805
+ async reload(id, options = {}) {
806
+ return await this.interact(id, { action: "reload" }, options);
807
+ }
808
+ async scroll(id, input, options = {}) {
809
+ return await this.interact(id, { action: "scroll", ...input }, options);
810
+ }
811
+ async toggleSoftwareKeyboard(id, options = {}) {
812
+ return await this.interact(id, { action: "toggleSoftwareKeyboard" }, options);
813
+ }
814
+ async simulateMemoryWarning(id, options = {}) {
815
+ return await this.interact(id, { action: "simulateMemoryWarning" }, options);
816
+ }
817
+ async rotateDigitalCrown(id, delta, options = {}) {
818
+ return await this.interact(id, { action: "rotateDigitalCrown", delta }, options);
819
+ }
820
+ async setRenderDebug(id, option, enabled, options = {}) {
821
+ return await this.interact(id, { action: "setRenderDebug", option, enabled }, options);
822
+ }
823
+ async screenshot(id, options = {}) {
824
+ const timeoutMs = options.timeoutMs ?? DEFAULT_SIMULATOR_INTERACTION_TIMEOUT_MS;
825
+ const requestId = options.requestId ?? `screenshot_${randomUUID()}`;
826
+ if (!REQUEST_ID_PATTERN.test(requestId)) {
827
+ throw new TypeError("requestId must match /^[A-Za-z0-9._:-]{1,128}$/");
828
+ }
829
+ assertIntegerRange(timeoutMs, 100, 60_000, "timeoutMs");
830
+ const context = {
831
+ requestId,
832
+ sessionId: id,
833
+ platform: this.platform,
834
+ action: "screenshot",
835
+ };
836
+ const bytes = await this.client.requestBytes(`/run-cloud/${this.platform}/${encodeURIComponent(id)}/screenshot`, {
837
+ signal: options.signal,
838
+ timeoutMs: timeoutMs + SIMULATOR_RESPONSE_MARGIN_MS,
839
+ timeoutMessage: `simulator screenshot did not complete within ${timeoutMs} ms`,
840
+ timeoutCode: "screenshot_timeout",
841
+ transportErrorCode: "screenshot_transport_error",
842
+ transportErrorMessage: "simulator screenshot could not reach the run.cloud API",
843
+ headers: { "X-Run-Cloud-Request-ID": requestId },
844
+ timeoutContext: context,
845
+ errorContext: {
846
+ ...context,
847
+ code: "screenshot_failed",
848
+ interactionStatus: "failed",
849
+ },
850
+ });
851
+ const pngSignature = [137, 80, 78, 71, 13, 10, 26, 10];
852
+ if (pngSignature.some((byte, index) => bytes[index] !== byte)) {
853
+ throw new RunCloudError(502, "simulator screenshot response was not a valid PNG image", {
854
+ ...context,
855
+ code: "screenshot_invalid_response",
856
+ retryable: true,
857
+ interactionStatus: "failed",
858
+ });
859
+ }
860
+ return bytes;
861
+ }
336
862
  async logs(id, options = {}) {
337
863
  const tail = options.tail ?? 200;
338
864
  if (!Number.isInteger(tail) || tail < 1 || tail > 1_000) {
@@ -382,9 +908,6 @@ export class IosSimulatorClient extends SimulatorClient {
382
908
  constructor(client) {
383
909
  super(client, "ios");
384
910
  }
385
- async screenshot(id) {
386
- return await this.client.requestBytes(`/run-cloud/ios/${encodeURIComponent(id)}/screenshot`);
387
- }
388
911
  async uploadVideo(id, video, options = {}) {
389
912
  const contentType = video.type.toLowerCase();
390
913
  if (contentType !== "video/mp4" && contentType !== "video/quicktime") {
@@ -463,6 +986,66 @@ export class SimulatorsClient {
463
986
  async openUrl(id, url, options = {}) {
464
987
  return await new SimulatorClient(this.client, options.platform ?? "ios").openUrl(id, url);
465
988
  }
989
+ async interact(id, interaction, options = {}) {
990
+ const { platform = "ios", ...requestOptions } = options;
991
+ return await new SimulatorClient(this.client, platform).interact(id, interaction, requestOptions);
992
+ }
993
+ async tap(id, point, options = {}) {
994
+ const { platform = "ios", ...requestOptions } = options;
995
+ return await new SimulatorClient(this.client, platform).tap(id, point, requestOptions);
996
+ }
997
+ async swipe(id, from, to, options = {}) {
998
+ const { platform = "ios", ...requestOptions } = options;
999
+ return await new SimulatorClient(this.client, platform).swipe(id, from, to, requestOptions);
1000
+ }
1001
+ async gesture(id, steps, options = {}) {
1002
+ const { platform = "ios", ...requestOptions } = options;
1003
+ return await new SimulatorClient(this.client, platform).gesture(id, steps, requestOptions);
1004
+ }
1005
+ async typeText(id, text, options = {}) {
1006
+ const { platform = "ios", ...requestOptions } = options;
1007
+ return await new SimulatorClient(this.client, platform).typeText(id, text, requestOptions);
1008
+ }
1009
+ async pressKey(id, key, options = {}) {
1010
+ const { platform = "ios", ...requestOptions } = options;
1011
+ return await new SimulatorClient(this.client, platform).pressKey(id, key, requestOptions);
1012
+ }
1013
+ async pressButton(id, button, options = {}) {
1014
+ const { platform = "ios", ...requestOptions } = options;
1015
+ return await new SimulatorClient(this.client, platform).pressButton(id, button, requestOptions);
1016
+ }
1017
+ async rotate(id, orientation, options = {}) {
1018
+ const { platform = "ios", ...requestOptions } = options;
1019
+ return await new SimulatorClient(this.client, platform).rotate(id, orientation, requestOptions);
1020
+ }
1021
+ async reload(id, options = {}) {
1022
+ const { platform = "ios", ...requestOptions } = options;
1023
+ return await new SimulatorClient(this.client, platform).reload(id, requestOptions);
1024
+ }
1025
+ async scroll(id, input, options = {}) {
1026
+ const { platform = "ios", ...requestOptions } = options;
1027
+ return await new SimulatorClient(this.client, platform).scroll(id, input, requestOptions);
1028
+ }
1029
+ async toggleSoftwareKeyboard(id, options = {}) {
1030
+ const { platform = "ios", ...requestOptions } = options;
1031
+ return await new SimulatorClient(this.client, platform).toggleSoftwareKeyboard(id, requestOptions);
1032
+ }
1033
+ async simulateMemoryWarning(id, options = {}) {
1034
+ const { platform = "ios", ...requestOptions } = options;
1035
+ return await new SimulatorClient(this.client, platform).simulateMemoryWarning(id, requestOptions);
1036
+ }
1037
+ async rotateDigitalCrown(id, delta, options = {}) {
1038
+ const { platform = "ios", ...requestOptions } = options;
1039
+ return await new SimulatorClient(this.client, platform).rotateDigitalCrown(id, delta, requestOptions);
1040
+ }
1041
+ async setRenderDebug(id, option, enabled, options = {}) {
1042
+ const { platform = "ios", ...requestOptions } = options;
1043
+ return await new SimulatorClient(this.client, platform).setRenderDebug(id, option, enabled, requestOptions);
1044
+ }
1045
+ async screenshot(id, options = {}) {
1046
+ const { platform = "ios", ...requestOptions } = options;
1047
+ return await new SimulatorClient(this.client, platform).screenshot(id, requestOptions);
1048
+ }
466
1049
  }
467
1050
  export class SandboxClient {
468
1051
  client;