rnxsim 0.1.513 → 0.1.515
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/cli/cloud-client.ts +18 -8
- package/cli/commands/inspect/get-layout.ts +2 -1
- package/cli/outbound-endpoints.ts +1 -7
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +64 -1
- package/dist-lib/bridge-contract-input.mjs +63 -1
- package/dist-lib/bridge-contract.cjs +73 -3
- package/dist-lib/bridge-contract.mjs +69 -2
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +117 -22
- package/dist-lib/cloud-contract.mjs +104 -21
- package/dist-lib/cloud.cjs +121 -27
- package/dist-lib/cloud.mjs +121 -27
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +45 -20
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +62 -24
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +62 -24
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite.cjs +1 -1
- package/package.json +2 -1
- package/src/bridge-contract-input.ts +74 -0
- package/src/bridge-contract.ts +103 -0
- package/src/cloud-contract.ts +191 -19
- package/src/cloud.ts +104 -7
package/src/cloud.ts
CHANGED
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
isRnxCloudCommandType,
|
|
15
15
|
RNX_CLOUD_COMMAND_TYPES,
|
|
16
16
|
RNX_CLOUD_MAX_ARTIFACT_BYTES,
|
|
17
|
+
RNX_CLOUD_TOKEN_SCOPES,
|
|
18
|
+
rnxCloudCommandScope,
|
|
19
|
+
rnxCloudScopesAllowCommand,
|
|
17
20
|
type RnxCloudBoxCreateReceipt,
|
|
18
21
|
type RnxCloudClaim,
|
|
19
22
|
type RnxCloudCreateReceipt,
|
|
@@ -26,6 +29,15 @@ export type { RnxCloudClaim, RnxCloudCreateReceipt, RnxScreenCapture }
|
|
|
26
29
|
const DEFAULT_DEVICE = 'iphone-16'
|
|
27
30
|
const DEFAULT_WAIT_READY_MS = 20_000
|
|
28
31
|
|
|
32
|
+
// how many times a request goes back after a retryable refusal or a server
|
|
33
|
+
// error. the sim service passes the account's own 429 bodies through, so the
|
|
34
|
+
// code on the body is what decides: `rate_limited` carries a `retry-after`
|
|
35
|
+
// and is worth waiting for, while `concurrency_limit` refuses until the
|
|
36
|
+
// customer stops something and retrying only hammers a closed door.
|
|
37
|
+
// `early_access` and 402 never change on retry either.
|
|
38
|
+
const CLOUD_MAX_RETRIES = 2
|
|
39
|
+
const CLOUD_NEVER_RETRY_CODES = new Set(['concurrency_limit', 'early_access'])
|
|
40
|
+
|
|
29
41
|
export type RemoteCommand = {
|
|
30
42
|
type: (typeof RNX_CLOUD_COMMAND_TYPES)[number]
|
|
31
43
|
[key: string]: unknown
|
|
@@ -117,6 +129,86 @@ function responseError(status: number, text: string): Error {
|
|
|
117
129
|
)
|
|
118
130
|
}
|
|
119
131
|
|
|
132
|
+
function readCloudErrorCode(text: string): string | null {
|
|
133
|
+
// the account api answers `{ error: 'code', message }` and the sim service
|
|
134
|
+
// `{ error: { code, message } }`; both reach this client, so both read.
|
|
135
|
+
let parsed: unknown
|
|
136
|
+
try {
|
|
137
|
+
parsed = JSON.parse(text)
|
|
138
|
+
} catch {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
if (!isRecord(parsed)) return null
|
|
142
|
+
if (typeof parsed.error === 'string') return parsed.error
|
|
143
|
+
if (isRecord(parsed.error) && typeof parsed.error.code === 'string') {
|
|
144
|
+
return parsed.error.code
|
|
145
|
+
}
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function readRetryAfterMs(response: Response, text: string): number | null {
|
|
150
|
+
const header = response.headers.get('retry-after')
|
|
151
|
+
if (header !== null) {
|
|
152
|
+
const seconds = Number(header.trim())
|
|
153
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000
|
|
154
|
+
}
|
|
155
|
+
let parsed: unknown
|
|
156
|
+
try {
|
|
157
|
+
parsed = JSON.parse(text)
|
|
158
|
+
} catch {
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
if (
|
|
162
|
+
isRecord(parsed) &&
|
|
163
|
+
typeof parsed.retryAfterMs === 'number' &&
|
|
164
|
+
Number.isFinite(parsed.retryAfterMs) &&
|
|
165
|
+
parsed.retryAfterMs >= 0
|
|
166
|
+
) {
|
|
167
|
+
return parsed.retryAfterMs
|
|
168
|
+
}
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function sleep(ms: number): Promise<void> {
|
|
173
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// one fetch with the cloud retry policy: `rate_limited` waits out its
|
|
177
|
+
// `retry-after` and 5xx goes again at once, each at most twice, and nothing
|
|
178
|
+
// else is retried. the body is buffered only on a retryable status, so every
|
|
179
|
+
// other refusal reaches the caller on its original response.
|
|
180
|
+
async function fetchWithCloudRetry(
|
|
181
|
+
url: string,
|
|
182
|
+
init: RequestInit,
|
|
183
|
+
options?: { timeoutMs?: number },
|
|
184
|
+
): Promise<Response> {
|
|
185
|
+
let attempt = 0
|
|
186
|
+
for (;;) {
|
|
187
|
+
const response = await fetch(
|
|
188
|
+
url,
|
|
189
|
+
options?.timeoutMs === undefined
|
|
190
|
+
? init
|
|
191
|
+
: { ...init, signal: AbortSignal.timeout(options.timeoutMs) },
|
|
192
|
+
)
|
|
193
|
+
if (response.ok) return response
|
|
194
|
+
const retryableStatus = response.status === 429 || response.status >= 500
|
|
195
|
+
if (!retryableStatus || attempt >= CLOUD_MAX_RETRIES) return response
|
|
196
|
+
const text = await response.text()
|
|
197
|
+
const code = readCloudErrorCode(text)
|
|
198
|
+
const retryable =
|
|
199
|
+
response.status >= 500
|
|
200
|
+
? !(code !== null && CLOUD_NEVER_RETRY_CODES.has(code))
|
|
201
|
+
: code === 'rate_limited'
|
|
202
|
+
if (!retryable) {
|
|
203
|
+
return new Response(text, { status: response.status, headers: response.headers })
|
|
204
|
+
}
|
|
205
|
+
if (response.status === 429) {
|
|
206
|
+
await sleep(readRetryAfterMs(response, text) ?? 1000)
|
|
207
|
+
}
|
|
208
|
+
attempt += 1
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
120
212
|
function copyArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
121
213
|
const copy = new ArrayBuffer(bytes.byteLength)
|
|
122
214
|
new Uint8Array(copy).set(bytes)
|
|
@@ -249,6 +341,13 @@ function cloudCommand(command: { type: string; [key: string]: unknown }): Remote
|
|
|
249
341
|
if (!isRnxCloudCommandType(command.type)) {
|
|
250
342
|
throw new Error(`RNX Cloud does not support bridge command type ${command.type}`)
|
|
251
343
|
}
|
|
344
|
+
// the customer sdk never carries debug: evaluate/call/close belong to the
|
|
345
|
+
// account api key caller. refusing here names the scope before a request.
|
|
346
|
+
if (!rnxCloudScopesAllowCommand(RNX_CLOUD_TOKEN_SCOPES, command.type)) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`RNX Cloud command ${command.type} requires the ${rnxCloudCommandScope(command.type)} scope`,
|
|
349
|
+
)
|
|
350
|
+
}
|
|
252
351
|
return { ...clean, type: command.type }
|
|
253
352
|
}
|
|
254
353
|
|
|
@@ -325,7 +424,7 @@ export class RemoteSimulator {
|
|
|
325
424
|
}
|
|
326
425
|
let response: Response
|
|
327
426
|
try {
|
|
328
|
-
response = await
|
|
427
|
+
response = await fetchWithCloudRetry(
|
|
329
428
|
`${this.session.origin}/v1/sims/${encodeURIComponent(this.session.simId)}/commands`,
|
|
330
429
|
{
|
|
331
430
|
method: 'POST',
|
|
@@ -334,10 +433,8 @@ export class RemoteSimulator {
|
|
|
334
433
|
'content-type': 'application/json',
|
|
335
434
|
},
|
|
336
435
|
body: JSON.stringify(body),
|
|
337
|
-
...(options?.timeoutMs
|
|
338
|
-
? { signal: AbortSignal.timeout(options.timeoutMs) }
|
|
339
|
-
: {}),
|
|
340
436
|
},
|
|
437
|
+
options?.timeoutMs === undefined ? undefined : { timeoutMs: options.timeoutMs },
|
|
341
438
|
)
|
|
342
439
|
} catch (error) {
|
|
343
440
|
throw new Error(
|
|
@@ -365,7 +462,7 @@ export class RemoteSimulator {
|
|
|
365
462
|
}
|
|
366
463
|
|
|
367
464
|
async confirm(): Promise<RnxCloudClaim> {
|
|
368
|
-
const response = await
|
|
465
|
+
const response = await fetchWithCloudRetry(
|
|
369
466
|
`${this.session.origin}/v1/sims/${encodeURIComponent(this.session.simId)}/claim`,
|
|
370
467
|
{
|
|
371
468
|
method: 'POST',
|
|
@@ -393,7 +490,7 @@ export class RemoteSimulator {
|
|
|
393
490
|
}
|
|
394
491
|
|
|
395
492
|
async close(): Promise<void> {
|
|
396
|
-
const response = await
|
|
493
|
+
const response = await fetchWithCloudRetry(
|
|
397
494
|
`${this.session.origin}/v1/boxes/${encodeURIComponent(this.session.boxId)}`,
|
|
398
495
|
{
|
|
399
496
|
method: 'DELETE',
|
|
@@ -427,7 +524,7 @@ export async function createRemoteSimulator(
|
|
|
427
524
|
const device = input.device?.trim() || DEFAULT_DEVICE
|
|
428
525
|
let response: Response
|
|
429
526
|
try {
|
|
430
|
-
response = await
|
|
527
|
+
response = await fetchWithCloudRetry(`${origin}/v1/boxes`, {
|
|
431
528
|
method: 'POST',
|
|
432
529
|
headers: {
|
|
433
530
|
authorization: input.authorization,
|