craft-native 0.0.276 → 0.0.290

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 CHANGED
@@ -668,3 +668,19 @@ Browser audio assigns one owner per sound ID. A newer play or stop invalidates
668
668
  pending media resolution for that ID; stopping all sounds, changing accounts,
669
669
  aborting a call or disposing the host also releases active audio. Superseded
670
670
  play calls reject with `AbortError`.
671
+
672
+ ### Packaging sandboxed macOS apps
673
+
674
+ Pass `macos.appBundlePath` to package a complete STX app without discarding its
675
+ resources. Distribution options also apply to prebuilt bundles: `buildNumber`,
676
+ `provisioningProfile`, `privacyManifest`, and the icon are embedded before signing.
677
+ Craft signs nested Mach-O code before the main bundle and verifies the result.
678
+ Use `helperEntitlements` for a compiled backend's runtime permissions; App Store
679
+ helpers inherit the main sandbox by default. `signingKeychain` selects the
680
+ keychain for both application and installer identities.
681
+
682
+ Craft's bundled-backend host reads `backend: { executable: 'stx-backend' }` from
683
+ `Contents/Resources/craft.json`. The native app remains the main process, starts
684
+ the helper with `--serve --managed`, and consumes its authenticated loopback URL.
685
+ The helper's standard input is a lifetime lease: it must stop when that pipe
686
+ closes. `craft --capabilities` advertises `bundleBackend` on macOS.
@@ -10,6 +10,10 @@ export interface AndroidEmulatorOptions {
10
10
  acceptLicenses?: boolean;
11
11
  /** Defaults to 10 minutes; hardware is capped there, software cold provisioning at 20 minutes. */
12
12
  bootTimeoutMs?: number;
13
+ /** Requested guest RAM, 1536–8192 MB (Android emulator limits); defaults to 2048. */
14
+ memoryMb?: number;
15
+ /** Requested guest CPU count, 1–8; defaults to 2. */
16
+ virtualCpus?: number;
13
17
  env?: Record<string, string | undefined>;
14
18
  }
15
19
  export interface AndroidEmulatorSession {
@@ -198,6 +198,12 @@ async function createAndroidEmulator(options) {
198
198
  const maximumBootTimeoutMs = options.acceleration === "software" ? 1200000 : 600000;
199
199
  if (!Number.isSafeInteger(bootTimeoutMs) || bootTimeoutMs < 1000 || bootTimeoutMs > maximumBootTimeoutMs)
200
200
  throw new Error(`Android bootTimeoutMs must be an integer between 1000 and ${maximumBootTimeoutMs} milliseconds`);
201
+ const memoryMb = options.memoryMb ?? 2048;
202
+ if (!Number.isSafeInteger(memoryMb) || memoryMb < 1536 || memoryMb > 8192)
203
+ throw new Error("Android memoryMb must be an integer between 1536 and 8192 megabytes");
204
+ const virtualCpus = options.virtualCpus ?? 2;
205
+ if (!Number.isSafeInteger(virtualCpus) || virtualCpus < 1 || virtualCpus > 8)
206
+ throw new Error("Android virtualCpus must be an integer between 1 and 8");
201
207
  const port = options.port ?? 5558;
202
208
  if (!Number.isInteger(port) || port < 5554 || port > 5682 || port % 2 !== 0)
203
209
  throw new Error("Android console ports must be even and between 5554 and 5682");
@@ -299,7 +305,7 @@ async function createAndroidEmulator(options) {
299
305
  const hostCpus = cpus();
300
306
  cpuStart = hostCpus.reduce((total, cpu) => total + Object.values(cpu.times).reduce((sum, value) => sum + value, 0), 0);
301
307
  idleStart = hostCpus.reduce((total, cpu) => total + cpu.times.idle, 0);
302
- child = Bun.spawn([executable, "-avd", name, "-port", String(port), "-no-window", "-no-snapshot", "-no-audio", "-no-boot-anim", "-gpu", "software", "-memory", "2048", "-cores", "2", "-accel", options.acceleration === "software" ? "off" : "on"], { env, stdout: log, stderr: log });
308
+ child = Bun.spawn([executable, "-avd", name, "-port", String(port), "-no-window", "-no-snapshot", "-no-audio", "-no-boot-anim", "-gpu", "software", "-memory", String(memoryMb), "-cores", String(virtualCpus), "-accel", options.acceleration === "software" ? "off" : "on"], { env, stdout: log, stderr: log });
303
309
  identity = {
304
310
  image: options.image,
305
311
  acceleration: options.acceleration,
@@ -307,8 +313,8 @@ async function createAndroidEmulator(options) {
307
313
  bootTimeoutMs,
308
314
  bootStartedAt: new Date(bootStartedAt).toISOString(),
309
315
  host: { cpuModel: hostCpus[0]?.model, logicalCpus: hostCpus.length, availableParallelism: availableParallelism(), memoryBytes: totalmem() },
310
- virtualCpus: 2,
311
- memoryMb: 2048,
316
+ virtualCpus,
317
+ memoryMb,
312
318
  serial,
313
319
  avdDirectory: env.ANDROID_AVD_HOME,
314
320
  emulatorPid: child.pid
@@ -335,11 +341,13 @@ async function createAndroidEmulator(options) {
335
341
  const packages = await run([adb, "-s", serial, "shell", "pm", "path", "android"], undefined, probeTimeout(), true);
336
342
  const playStore = requiresPlayStore ? await run([adb, "-s", serial, "shell", "pm", "path", "com.android.vending"], undefined, probeTimeout(), true) : undefined;
337
343
  const systemUi = await run([adb, "-s", serial, "shell", "pidof", "com.android.systemui"], undefined, probeTimeout(), true);
338
- const events = await run([adb, "-s", serial, "logcat", "-b", "events", "-d", "-t", "1"], undefined, probeTimeout(), true);
344
+ const events = await run([adb, "-s", serial, "logcat", "-b", "events", "-d", "-v", "threadtime"], undefined, probeTimeout(), true);
339
345
  const home = await run([adb, "-s", serial, "shell", "cmd", "package", "resolve-activity", "--brief", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.HOME"], undefined, probeTimeout(), true);
340
346
  const homeActivity = home.stdout.trim().split(/\r?\n/).at(-1) ?? "";
341
- const temporaryHome = homeActivity.startsWith("com.android.sdksetup/") || /^com\.android\.settings\/(?:\.|com\.android\.settings\.)FallbackHome$/.test(homeActivity) || /^android\/.*(?:Resolver|Chooser)Activity$/.test(homeActivity);
342
- booted = settings.code === 0 && packages.code === 0 && systemUi.code === 0 && events.code === 0 && /^package:\/\S+/m.test(packages.stdout) && /^\d+(?:\s+\d+)*$/.test(systemUi.stdout.trim()) && (!requiresPlayStore || playStore?.code === 0 && /^package:\/\S+/m.test(playStore.stdout)) && home.code === 0 && /^[\w.]+\/[\w.$]+$/.test(homeActivity) && !temporaryHome;
347
+ const temporaryHome = homeActivity.startsWith("com.android.sdksetup/") || homeActivity.startsWith("com.google.android.googlesdksetup/") || /^com\.android\.settings\/(?:\.|com\.android\.settings\.)FallbackHome$/.test(homeActivity) || /^android\/.*(?:Resolver|Chooser)Activity$/.test(homeActivity);
348
+ const windows = await run([adb, "-s", serial, "shell", "dumpsys", "window"], undefined, probeTimeout(), true);
349
+ const focusedPackage = windows.stdout.match(/^\s*mCurrentFocus\s*=\s*Window\{[^}\r\n]*\bu\d+\s+([\w.]+)\//m)?.[1];
350
+ booted = settings.code === 0 && packages.code === 0 && systemUi.code === 0 && events.code === 0 && /^package:\/\S+/m.test(packages.stdout) && /^\d+(?:\s+\d+)*$/.test(systemUi.stdout.trim()) && (!requiresPlayStore || playStore?.code === 0 && /^package:\/\S+/m.test(playStore.stdout)) && home.code === 0 && /^[\w.]+\/[\w.$]+$/.test(homeActivity) && !temporaryHome && windows.code === 0 && focusedPackage === homeActivity.split("/")[0];
343
351
  }
344
352
  }
345
353
  const checkedAt = Date.now();
@@ -845,6 +853,7 @@ function writeNativeShellSources(output, packagePath, packageName, config) {
845
853
  };
846
854
  write("CraftBluetooth.kt.template", "CraftBluetooth.kt");
847
855
  write("CraftNativeShell.kt.template", "CraftNativeShell.kt");
856
+ write("CraftSnapshotFile.kt.template", "CraftSnapshotFile.kt");
848
857
  write("CraftNativeImageCache.kt.template", "CraftNativeImageCache.kt");
849
858
  write("CraftNativeHaptics.kt.template", "CraftNativeHaptics.kt");
850
859
  write("CraftNativeBackground.kt.template", "CraftNativeBackground.kt");
@@ -119,6 +119,10 @@ class CraftNativeMutationDocument {
119
119
  // be reconciled so the host can move its existing view.
120
120
  requiresFullRender = true
121
121
  }
122
+ "replaceChildren" -> {
123
+ val parentId = replaceChildren(operation, candidateNodes)
124
+ if (parentId !in affectedNodeIds) affectedNodeIds.add(parentId)
125
+ }
122
126
  "removeNode" -> {
123
127
  val parent = candidateNodes[operation.optString("id")]?.parent
124
128
  if (parent == null) requiresFullRender = true
@@ -257,6 +261,37 @@ class CraftNativeMutationDocument {
257
261
  return previousParentId
258
262
  }
259
263
 
264
+ /** Replaces a container's ordered child snapshot in one atomic mutation. */
265
+ private fun replaceChildren(operation: JSONObject, nodes: MutableMap<String, Node>): String {
266
+ val parentId = requiredId(operation, "parentId", "replaceChildren")
267
+ val parent = nodes[parentId] ?: fail("UNKNOWN_PARENT", "node $parentId does not exist")
268
+ if (!acceptsChildren(parent.type)) fail("INVALID_PARENT", "node $parentId cannot contain child nodes")
269
+ val values = operation.optJSONArray("children")
270
+ ?: fail("INVALID_CHILDREN", "replaceChildren.children must be an array")
271
+ val childIds = mutableListOf<String>()
272
+ for (index in 0 until values.length()) {
273
+ val childId = values.opt(index) as? String
274
+ if (childId.isNullOrBlank()) {
275
+ fail("INVALID_CHILDREN", "replaceChildren.children must contain non-empty ids")
276
+ }
277
+ if (childId in childIds) fail("DUPLICATE_CHILD", "child $childId appears more than once")
278
+ val child = nodes[childId] ?: fail("UNKNOWN_NODE", "node $childId does not exist")
279
+ if (childId == parentId || parentId in descendants(childId, nodes)) {
280
+ fail("CYCLE", "attaching node $childId would create a cycle")
281
+ }
282
+ if (child.parent != null && child.parent != parentId) {
283
+ fail("NODE_ATTACHED", "node $childId already has a parent")
284
+ }
285
+ childIds.add(childId)
286
+ }
287
+ parent.children.filterNot { it in childIds }.forEach { childId ->
288
+ (descendants(childId, nodes) + childId).forEach { nodes.remove(it) }
289
+ }
290
+ parent.children = childIds
291
+ childIds.forEach { nodes[it]?.parent = parentId }
292
+ return parentId
293
+ }
294
+
260
295
  private fun removeNode(
261
296
  operation: JSONObject,
262
297
  nodes: MutableMap<String, Node>,
@@ -270,7 +270,12 @@ internal object CraftNativeStorage {
270
270
 
271
271
  }
272
272
 
273
- internal class CraftNativeStores private constructor(context: Context, private val secureEnabled: Boolean) {
273
+ internal class CraftNativeStores private constructor(
274
+ context: Context,
275
+ private val secureEnabled: Boolean,
276
+ private val snapshotFiles: CraftSnapshotFile,
277
+ ) {
278
+ private constructor(context: Context, secureEnabled: Boolean) : this(context, secureEnabled, CraftSnapshotFile())
274
279
  private val appContext = context.applicationContext
275
280
  private val storage: SharedPreferences = appContext.getSharedPreferences("craft-native-storage", Context.MODE_PRIVATE)
276
281
  private val snapshotsDir = File(appContext.filesDir, "craft-snapshots").apply { mkdirs() }
@@ -355,20 +360,22 @@ internal class CraftNativeStores private constructor(context: Context, private v
355
360
  fun setSnapshot(name: String, json: String?, from: String?, done: (Boolean) -> Unit) {
356
361
  writer.execute {
357
362
  val file = snapshotFile(name)
363
+ val previous = snapshotCache[name]
358
364
  val ok = runCatching {
359
365
  if (json == null) {
360
366
  !file.exists() || file.delete()
361
367
  } else {
362
- val temporary = File(file.parentFile, "${file.name}.tmp")
363
- temporary.writeText(json)
364
- temporary.renameTo(file) || run { file.writeText(json); temporary.delete(); true }
368
+ snapshotFiles.set(file, json)
369
+ true
365
370
  }
366
371
  }.getOrDefault(false)
367
372
  if (ok) {
368
373
  if (json == null) snapshotCache.remove(name) else snapshotCache[name] = json
369
374
  }
370
375
  main.post {
371
- if (ok) changed("snapshots", name, json, null)
376
+ // Isolates optimistically display their write. Restore the
377
+ // confirmed value before rejecting an unsuccessful checkpoint.
378
+ changed("snapshots", name, if (ok) json else previous, null)
372
379
  done(ok)
373
380
  }
374
381
  }
@@ -0,0 +1,26 @@
1
+ package {{PACKAGE_NAME}}
2
+
3
+ import java.io.File
4
+ import java.io.FileOutputStream
5
+ import java.nio.file.Files
6
+ import java.nio.file.StandardCopyOption
7
+ import java.util.UUID
8
+
9
+ /** One checkpoint replacement. Filesystem failures must leave the previous checkpoint intact. */
10
+ internal class CraftSnapshotFile(
11
+ private val write: (File, ByteArray) -> Unit = { file, bytes ->
12
+ FileOutputStream(file).use { stream -> stream.write(bytes); stream.fd.sync() }
13
+ },
14
+ private val replace: (File, File) -> Unit = { source, destination ->
15
+ Files.move(source.toPath(), destination.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING)
16
+ },
17
+ ) {
18
+ fun set(destination: File, json: String) {
19
+ val temporary = File(destination.parentFile, ".craft-snapshot-${UUID.randomUUID()}.tmp")
20
+ val bytes = json.toByteArray(Charsets.UTF_8)
21
+ try {
22
+ write(temporary, bytes)
23
+ replace(temporary, destination)
24
+ } finally { temporary.delete() }
25
+ }
26
+ }
@@ -4661,6 +4661,7 @@ class MainActivity : AppCompatActivity() {
4661
4661
  capabilities: ${capabilityNames()},
4662
4662
  initialAppState: ${JSONObject.quote(nativeAppState)},
4663
4663
  colorScheme: ${JSONObject.quote(colorScheme())},
4664
+ viewportWidth: ${resources.configuration.screenWidthDp},
4664
4665
  appearance: ${appearance()},
4665
4666
  appURL: ${JSONObject.quote(config.optString("devServerURL"))},
4666
4667
  appVersion: ${JSONObject.quote(appVersion().first)},
@@ -4909,6 +4910,21 @@ class MainActivity : AppCompatActivity() {
4909
4910
  override fun onConfigurationChanged(newConfig: Configuration) {
4910
4911
  super.onConfigurationChanged(newConfig)
4911
4912
  publishAppearance()
4913
+ publishViewport(newConfig)
4914
+ }
4915
+
4916
+ /// The width the screens last heard, in dp (a web page's CSS pixels).
4917
+ private var reportedViewportWidth = -1
4918
+
4919
+ /// Rotation, split screen or a resized window: the bundle's width classes
4920
+ /// (`md:`, `lg:`) follow the window's new width.
4921
+ private fun publishViewport(config: Configuration) {
4922
+ val width = config.screenWidthDp
4923
+ if (width <= 0 || width == reportedViewportWidth) return
4924
+ reportedViewportWidth = width
4925
+ if (!::root.isInitialized) return
4926
+ val payload = JSONObject().put("width", width).put("height", config.screenHeightDp)
4927
+ allScreens().forEach { screen -> if (!screen.headless) send(screen, "VIEWPORT", payload) }
4912
4928
  }
4913
4929
 
4914
4930
  private fun publishAppearance() {
@@ -17,8 +17,8 @@ export type { HttpClientOptions, RequestOptions, HttpResponse, WebSocketOptions
17
17
  export { crypto, uuid, randomString, hmac, timingSafeEqual, hashPassword, verifyPassword } from './crypto.js';
18
18
  export { env, getPlatform, isDesktop, isMobile, isCraft, getSystemInfo, exec, spawn, ChildProcess, cwd, homeDir, tempDir, exit, argv, open } from './process.js';
19
19
  export type { Platform, SystemInfo, ExecOptions, ExecResult, SpawnOptions } from './process.js';
20
- export { device, haptics, permissions, camera, biometrics, secureStorage, storage, location, share, lifecycle, notifications, notifications as notification } from './mobile.js';
21
- export type { DeviceInfo, DeviceCapabilities, HapticStyle, HapticNotificationType, PermissionType, PermissionStatus, CameraOptions as MobileCameraOptions, PhotoResult, BiometricType, Location, LocationOptions, ShareOptions, AppState, NotificationOptions as MobileNotificationOptions } from './mobile.js';
20
+ export { device, haptics, permissions, camera, files, biometrics, secureStorage, storage, location, share, lifecycle, notifications, notifications as notification } from './mobile.js';
21
+ export type { DeviceInfo, DeviceCapabilities, HapticStyle, HapticNotificationType, PermissionType, PermissionStatus, CameraOptions as MobileCameraOptions, PhotoResult, FileInfo, PickFileOptions, BiometricType, Location, LocationOptions, ShareOptions, AppState, NotificationOptions as MobileNotificationOptions } from './mobile.js';
22
22
  export { carplay, appClips, liveActivities, sharePlay, storeKit, appIntents, tipKit, focusFilters } from './ios-advanced.js';
23
23
  export type { CarPlayTemplateType, CarPlayListItem, CarPlayGridItem, CarPlayTemplate, AppClipInvocation, LiveActivityContentState, LiveActivityAttributes, LiveActivityConfig, LiveActivityHandle, SharePlaySessionState, SharePlayParticipant, SharePlayActivity, ProductType, Product, Transaction, IntentParameterType, IntentParameter, AppIntent, TipDisplayFrequency, Tip, FocusStatus, FocusFilter } from './ios-advanced.js';
24
24
  export { materialYou, photoPicker, workManager, foregroundService, predictiveBack, appLanguage, widgets as androidWidgets, playBilling } from './android-advanced.js';
@@ -314,6 +314,30 @@ export interface PhotoResult {
314
314
  /** MIME type */
315
315
  mimeType: string;
316
316
  }
317
+ /** A file copied into the app-owned native file area. */
318
+ export interface FileInfo {
319
+ /** App-owned path suitable for `fetch` FormData and sharing. */
320
+ path: string;
321
+ /** Original (sanitized) file name. */
322
+ name: string;
323
+ /** Preferred MIME type, or `application/octet-stream` when unknown. */
324
+ type: string;
325
+ /** Number of bytes in the copied file. */
326
+ size: number;
327
+ }
328
+ /** General document picker options. */
329
+ export interface PickFileOptions {
330
+ /** MIME types, UTIs, or filename extensions (for example `pdf` or `public.data`). */
331
+ contentTypes?: string[];
332
+ /** Maximum number of documents; native hosts clamp this to 1–20. */
333
+ maxSelections?: number;
334
+ /** Maximum size of each document in bytes; native hosts default to 512 MiB. */
335
+ maxBytes?: number;
336
+ }
337
+ /** General document picker API. */
338
+ export declare const files: {
339
+ pickFile(options?: PickFileOptions): Promise<FileInfo[]>;
340
+ };
317
341
  /**
318
342
  * Camera and image picker API.
319
343
  *
@@ -1835,6 +1859,7 @@ declare const mobile: {
1835
1859
  haptics: typeof haptics;
1836
1860
  permissions: typeof permissions;
1837
1861
  camera: typeof camera;
1862
+ files: typeof files;
1838
1863
  biometrics: typeof biometrics;
1839
1864
  secureStorage: typeof secureStorage;
1840
1865
  location: typeof location;
@@ -17,8 +17,10 @@ export interface BrowserStorageOptions {
17
17
  * or in another tab: called once the new account's storage is in place.
18
18
  */
19
19
  sessionChanged?: (token: string | null, previous: string | null) => void;
20
- /** A host lock primitive; browsers use their native Web Locks implementation. */
21
- lock?: (<T>(name: string, operation: () => T | Promise<T>) => Promise<T>) | null;
20
+ /** A host lock primitive; honor signal cancellation while queued, as native Web Locks do. */
21
+ lock?: (<T>(name: string, operation: () => T | Promise<T>, options?: {
22
+ signal?: AbortSignal;
23
+ }) => Promise<T>) | null;
22
24
  }
23
25
  export interface BrowserStorage {
24
26
  atomicStorage: boolean;
@@ -37,7 +39,7 @@ export interface BrowserStorage {
37
39
  keys: () => string[];
38
40
  };
39
41
  };
40
- call: (module: string, method: string, args: unknown[]) => Promise<unknown>;
42
+ call: (module: string, method: string, args: unknown[], signal?: AbortSignal) => Promise<unknown>;
41
43
  accountChanged: () => Promise<void>;
42
44
  scope: () => string;
43
45
  }
package/dist/browser.d.ts CHANGED
@@ -18,6 +18,7 @@ export interface BrowserPlatformOptions {
18
18
  export interface BrowserPlatform {
19
19
  craft: Record<string, unknown>;
20
20
  capabilities: string[];
21
+ /** Queued storage calls abort without writing; cancellation cannot undo an already committed synchronous write. */
21
22
  call: (module: string, method: string, args: unknown[], signal: AbortSignal) => Promise<unknown>;
22
23
  navigate: (path: string, replace?: boolean) => void;
23
24
  emit: (listener: (type: string, payload: unknown) => void) => () => void;
package/dist/browser.js CHANGED
@@ -63,12 +63,35 @@ function secureUUID() {
63
63
  }
64
64
 
65
65
  // src/browser/storage.ts
66
+ async function waitForAccount(operation, signal) {
67
+ if (!signal)
68
+ return operation;
69
+ let abort;
70
+ try {
71
+ return await Promise.race([operation, new Promise((resolve, reject) => {
72
+ abort = () => reject(signal.reason);
73
+ if (signal.aborted)
74
+ abort();
75
+ else
76
+ signal.addEventListener("abort", abort, { once: true });
77
+ })]);
78
+ } finally {
79
+ signal.removeEventListener("abort", abort);
80
+ }
81
+ }
66
82
  async function createBrowserStorage(options) {
67
83
  const { storage, namespace, tokenKey, changed } = options;
68
84
  const sessionTokenKey = options.sessionTokenKey || "auth.token";
69
85
  const prefix = `${namespace}:`;
70
- const lock = options.lock === null ? undefined : options.lock ?? (typeof navigator !== "undefined" && typeof navigator.locks?.request === "function" ? (name, operation) => navigator.locks.request(name, operation) : undefined);
71
- const exclusive = (operation) => lock ? lock(`${namespace}:storage`, operation) : Promise.resolve().then(operation);
86
+ const lock = options.lock === null ? undefined : options.lock ?? (typeof navigator !== "undefined" && typeof navigator.locks?.request === "function" ? (name, operation, settings) => navigator.locks.request(name, settings ?? {}, operation) : undefined);
87
+ const exclusive = (operation, signal) => {
88
+ const guarded = () => {
89
+ signal?.throwIfAborted();
90
+ return operation();
91
+ };
92
+ signal?.throwIfAborted();
93
+ return lock ? lock(`${namespace}:storage`, guarded, { signal }) : Promise.resolve().then(guarded);
94
+ };
72
95
  const scopeChanged = () => Object.assign(new Error("The storage account changed"), { code: "STORAGE_SCOPE_CHANGED" });
73
96
  const pending = new Map;
74
97
  let token = storage.getItem(tokenKey);
@@ -215,10 +238,11 @@ async function createBrowserStorage(options) {
215
238
  keys: () => available() ? keys(key("snapshot:")).map((name) => name.slice(key("snapshot:").length)) : []
216
239
  }
217
240
  },
218
- async call(module, method, args) {
241
+ async call(module, method, args, signal) {
242
+ signal?.throwIfAborted();
219
243
  const captured = capture();
220
244
  const wasAvailable = available();
221
- await accountChanged();
245
+ await waitForAccount(accountChanged(), signal);
222
246
  const name = String(args[0] ?? "");
223
247
  if (module === "Storage") {
224
248
  if (!wasAvailable)
@@ -259,7 +283,7 @@ async function createBrowserStorage(options) {
259
283
  return true;
260
284
  }
261
285
  throw Object.assign(new Error(`Unsupported browser storage operation ${method}`), { code: "UNSUPPORTED" });
262
- });
286
+ }, signal);
263
287
  }
264
288
  if (module === "SecureStorage") {
265
289
  if (!wasAvailable)
@@ -284,7 +308,7 @@ async function createBrowserStorage(options) {
284
308
  return true;
285
309
  }
286
310
  throw Object.assign(new Error(`Unsupported browser storage operation ${module}.${method}`), { code: "UNSUPPORTED" });
287
- });
311
+ }, signal);
288
312
  if (method === "clear" || name === sessionTokenKey && (method === "set" || method === "remove"))
289
313
  await accountChanged();
290
314
  return result;
@@ -608,7 +632,7 @@ async function createBrowserPlatform(options = {}) {
608
632
  const first = object(args[0]);
609
633
  if (module === "Storage" || module === "SecureStorage") {
610
634
  const before = persistence.craft.secureStorage.getSync(sessionTokenKey);
611
- const answer = await persistence.call(module, method, args);
635
+ const answer = await persistence.call(module, method, args, signal);
612
636
  if (before !== persistence.craft.secureStorage.getSync(sessionTokenKey))
613
637
  await resetAccount();
614
638
  return answer;