@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/README.md +59 -2
- package/dist/index.d.ts +269 -4
- package/dist/index.js +628 -45
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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 (
|
|
65
|
-
|
|
408
|
+
if (merged.action !== undefined && merged.retryable === undefined) {
|
|
409
|
+
merged.retryable = response.status === 408 || response.status === 429 || response.status >= 500;
|
|
66
410
|
}
|
|
67
|
-
return
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
528
|
+
throw await apiError(response);
|
|
156
529
|
return (await response.json());
|
|
157
530
|
}
|
|
158
|
-
async requestBytes(path) {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
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
|
|
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;
|