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.
Files changed (60) hide show
  1. package/cli/cloud-client.ts +18 -8
  2. package/cli/commands/inspect/get-layout.ts +2 -1
  3. package/cli/outbound-endpoints.ts +1 -7
  4. package/dist-lib/agent-daemon-client.cjs +1 -1
  5. package/dist-lib/agent-events.cjs +1 -1
  6. package/dist-lib/agent-identity.cjs +1 -1
  7. package/dist-lib/agent-sessions.cjs +1 -1
  8. package/dist-lib/attached-projects.cjs +1 -1
  9. package/dist-lib/auth/shared-session.cjs +1 -1
  10. package/dist-lib/backend-origin.cjs +1 -1
  11. package/dist-lib/beta.cjs +1 -1
  12. package/dist-lib/beta.mjs +1 -1
  13. package/dist-lib/bridge-constants.cjs +1 -1
  14. package/dist-lib/bridge-contract-input.cjs +64 -1
  15. package/dist-lib/bridge-contract-input.mjs +63 -1
  16. package/dist-lib/bridge-contract.cjs +73 -3
  17. package/dist-lib/bridge-contract.mjs +69 -2
  18. package/dist-lib/capture-contract.cjs +1 -1
  19. package/dist-lib/capture-contract.mjs +1 -1
  20. package/dist-lib/cli-constants.cjs +1 -1
  21. package/dist-lib/cloud-contract.cjs +117 -22
  22. package/dist-lib/cloud-contract.mjs +104 -21
  23. package/dist-lib/cloud.cjs +121 -27
  24. package/dist-lib/cloud.mjs +121 -27
  25. package/dist-lib/config.cjs +1 -1
  26. package/dist-lib/detox/index.cjs +1 -1
  27. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  28. package/dist-lib/home-paths.cjs +1 -1
  29. package/dist-lib/host/bridge-host.cjs +45 -20
  30. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  31. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  32. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  33. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  34. package/dist-lib/host/websocket-proxy.cjs +1 -1
  35. package/dist-lib/index.cjs +62 -24
  36. package/dist-lib/jump-to-source-babel.cjs +1 -1
  37. package/dist-lib/jump-to-source-native.cjs +1 -1
  38. package/dist-lib/menu.cjs +1 -1
  39. package/dist-lib/menu.mjs +1 -1
  40. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  41. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  42. package/dist-lib/metro-production-bundle.cjs +1 -1
  43. package/dist-lib/metro-production-bundle.mjs +1 -1
  44. package/dist-lib/metro.cjs +1 -1
  45. package/dist-lib/profiles.cjs +1 -1
  46. package/dist-lib/public-brand.cjs +1 -1
  47. package/dist-lib/react-native-host-modules.cjs +1 -1
  48. package/dist-lib/react-native-host-modules.mjs +1 -1
  49. package/dist-lib/render-mode.cjs +1 -1
  50. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  51. package/dist-lib/sdk.cjs +1 -1
  52. package/dist-lib/sdk.mjs +1 -1
  53. package/dist-lib/skills.cjs +62 -24
  54. package/dist-lib/swift.cjs +1 -1
  55. package/dist-lib/vite.cjs +1 -1
  56. package/package.json +2 -1
  57. package/src/bridge-contract-input.ts +74 -0
  58. package/src/bridge-contract.ts +103 -0
  59. package/src/cloud-contract.ts +191 -19
  60. 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 fetch(
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 fetch(
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 fetch(
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 fetch(`${origin}/v1/boxes`, {
527
+ response = await fetchWithCloudRetry(`${origin}/v1/boxes`, {
431
528
  method: 'POST',
432
529
  headers: {
433
530
  authorization: input.authorization,