@phone-use/sdk 0.3.1 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phone-use/sdk",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Typed mobile-device SDK \u2014 Device, backends, errors, actions",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -163,6 +163,48 @@ export class CloudSandboxBackend extends BaseDeviceBackend {
163
163
  }
164
164
 
165
165
  /** Upload a zipped .app bundle (base64) and install it on the sandbox device. */
166
+ /**
167
+ * Upload a zipped .app as a raw stream. Prefer this over {@link installApp}:
168
+ * base64 inside a JSON body inflates the payload by a third, which caps real
169
+ * apps near 144MB. Pass a `Bun.file(...)` (or any Blob) and the bytes go to the
170
+ * wire without being buffered in memory.
171
+ */
172
+ async installAppStream(zip: Blob): Promise<{ installed?: string }> {
173
+ const response = await this.fetchImpl(`${this.endpoint}/app`, {
174
+ method: 'POST',
175
+ headers: {
176
+ authorization: `Bearer ${this.token}`,
177
+ 'content-type': 'application/zip',
178
+ },
179
+ body: zip,
180
+ });
181
+ // A worker older than this SDK has no /app route. Fall back to the base64
182
+ // RPC rather than failing: a client should keep working against a fleet
183
+ // that has not been redeployed yet. The old ceiling (~144MB of zip) still
184
+ // applies on that path, and an app above it will say so.
185
+ if (response.status === 404) {
186
+ const bytes = await zip.arrayBuffer();
187
+ return this.installApp(Buffer.from(bytes).toString('base64'));
188
+ }
189
+ const body = (await response.json().catch(() => ({}))) as
190
+ | SandboxRpcResponse
191
+ | { error?: string };
192
+ if (!response.ok || !('ok' in body)) {
193
+ const message =
194
+ ('error' in body && typeof body.error === 'string' ? body.error : undefined) ??
195
+ `HTTP ${response.status}`;
196
+ throw new PhoneUseError(message, { code: 'UNKNOWN', retryable: false });
197
+ }
198
+ if (!body.ok) {
199
+ throw new PhoneUseError(body.error.message, {
200
+ code: normalizeErrorCode(body.error.code),
201
+ retryable: body.error.retryable ?? false,
202
+ });
203
+ }
204
+ return body.result as { installed?: string };
205
+ }
206
+
207
+ /** @deprecated Base64-in-JSON; use {@link installAppStream}. */
166
208
  installApp(base64Zip: string): Promise<{ installed?: string }> {
167
209
  return this.rpc('installApp', base64Zip);
168
210
  }
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * deliberately not re-exported here.
8
8
  */
9
9
  /** The published package version (kept in sync with package.json by the release flow). */
10
- export const VERSION = '0.3.1';
10
+ export const VERSION = '0.4.0';
11
11
 
12
12
  export {
13
13
  type Action,
package/src/observe.ts CHANGED
@@ -880,8 +880,11 @@ export class DeviceCore {
880
880
  await this.press(el.ref);
881
881
  return true;
882
882
  } catch (error) {
883
- if (!/off-?screen/i.test(describeError(error))) throw error;
884
- // straddled the edge fall through to a nudge scroll and retry
883
+ // Straddled the edge, or sitting under a sheet/toast/overlay. Both are
884
+ // positional and both are what the nudge scroll below is for, so retry
885
+ // rather than throw — real apps stack overlays over their own chrome,
886
+ // and a covered element used to abort an entire crawl.
887
+ if (!/off-?screen|covered/i.test(describeError(error))) throw error;
885
888
  }
886
889
  }
887
890
  // Known position → scroll toward it. Not in the realized tree at all →