// "send to box": the running local app becomes a nano cloud simulator. // // one function serves both callers. the rail button in the shell reaches it // through the local daemon's `/__send-to-box` route, and `rnx box send` calls // it directly; both hand it a way to run a bridge command against the sim that // is running the app, and both get the same simulator id and URL back. import { isStorageSnapshot } from '../../sootsim-engine/src/preview/storage-snapshot-contract.ts' import { DEFAULT_DEVICE_BY_PLATFORM, getRuntimePlatform, listDeviceModels, } from '../../sootsim-engine/src/settings/devices.ts' import { toRNXProductionBundleUrl } from '../src/metro-production-bundle.ts' import { createCloudBox } from './cloud-client.ts' import type { StorageSnapshot } from '../../sootsim-engine/src/preview/storage-snapshot-contract.ts' import type { DeviceModel } from '../../sootsim-engine/src/settings/devices.ts' import type { BridgeCommandType } from '../src/bridge-contract.ts' export interface SendToBoxOptions { /** runs one bridge command against the sim running the app. */ send: (command: { type: BridgeCommandType; [key: string]: unknown }) => Promise authorization: string /** the account the box bills. an api key names its own, so it sends none. */ accountId: string | null /** the cloud device model. defaults to the device the sim is running on. */ device?: DeviceModel /** * carry the signed-in session into the box. on by default in the shell: the * box is the sender's own, and an app that boots signed out is not the app * they were looking at. */ carryAuth: boolean boxName?: string | null apiOrigin?: string simId?: string } export interface SendToBoxResult { boxId: string simId: string url: string carriedAuth: boolean warnings: string[] } function isRecord(value: unknown): value is Record { return !!value && typeof value === 'object' && !Array.isArray(value) } // the box opens on the device the sender was looking at. a remote simulator is // iOS only, so an Android sim sends to the default iPhone rather than refusing. function cloudDevice(capturedModel: unknown): DeviceModel { const captured = listDeviceModels().find((model) => model === capturedModel) return captured && getRuntimePlatform(captured) === 'ios' ? captured : DEFAULT_DEVICE_BY_PLATFORM.ios } export async function sendToBox(options: SendToBoxOptions): Promise { const captured: unknown = await options.send({ type: 'evaluate', simId: options.simId, code: '(typeof window.__sootsimCaptureBundle === "function") ? window.__sootsimCaptureBundle() : null', }) const bundleUrl = isRecord(captured) ? captured.bundleUrl : null if (!isRecord(captured) || typeof bundleUrl !== 'string' || !bundleUrl) { throw new Error( 'send to box needs a simulator running an app: this one has no bundle loaded', ) } // the snapshot is taken before the production bundle is fetched, so the box // gets the state the app was in when the button was pressed rather than the // state it drifted to while Metro rebuilt. const dumped: unknown = await options.send({ type: 'storageSnapshot', simId: options.simId, storageSnapshotOptions: { includeCredentials: options.carryAuth }, }) if (!isStorageSnapshot(dumped)) { throw new Error('the simulator returned a storage snapshot this build cannot read') } const storage: StorageSnapshot = dumped const created = await createCloudBox({ bundlePath: toRNXProductionBundleUrl(bundleUrl), device: options.device ?? cloudDevice(isRecord(captured.deviceSpec) ? captured.deviceSpec.model : null), authorization: options.authorization, accountId: options.accountId, apiOrigin: options.apiOrigin, boxName: options.boxName ?? null, storage, }) const simId = created.receipt.simulator.simId const url = `${created.session.apiOrigin.replace(/\/$/, '')}/sim/${simId}` // the local terminal is where an agent driving this checkout reads results, // so the id and the URL land there whichever caller asked for the send. console.log(` box simulator: ${simId}`) console.log(` ${url}`) for (const warning of created.warnings) console.warn(` warning: ${warning}`) return { boxId: created.receipt.boxId, simId, url, carriedAuth: options.carryAuth, warnings: created.warnings, } }