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 +16 -0
- package/dist/android/src/emulator.d.ts +4 -0
- package/dist/android/src/index.js +15 -6
- package/dist/android/templates/CraftNativeMutation.kt.template +35 -0
- package/dist/android/templates/CraftNativeShell.kt.template +12 -5
- package/dist/android/templates/CraftSnapshotFile.kt.template +26 -0
- package/dist/android/templates/MainActivityNative.kt.template +16 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/mobile.d.ts +25 -0
- package/dist/browser/storage.d.ts +5 -3
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +31 -7
- package/dist/cli.js +131 -28
- package/dist/index.cjs +177 -27
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +181 -28
- package/dist/ios/src/index.js +7 -3
- package/dist/ios/templates/CraftApp.swift +44 -5
- package/dist/ios/templates/CraftHybrid.swift +21 -0
- package/dist/ios/templates/CraftNativeActions.swift +6 -0
- package/dist/ios/templates/CraftNativeFiles.swift +136 -0
- package/dist/ios/templates/CraftNativeMutation.swift +51 -0
- package/dist/ios/templates/CraftNativeScreen.swift +116 -8
- package/dist/ios/templates/CraftWatchApp.swift.template +303 -9
- package/dist/ios/templates/WatchExtension.Info.plist.template +1 -0
- package/dist/macos/templates/CraftNativeMutation.swift +51 -0
- package/dist/mobile.js +50 -0
- package/dist/package.d.ts +29 -3
- package/dist/types.d.ts +32 -0
- package/package.json +1 -1
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",
|
|
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
|
|
311
|
-
memoryMb
|
|
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", "-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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() {
|
package/dist/api/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/api/mobile.d.ts
CHANGED
|
@@ -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;
|
|
21
|
-
lock?: (<T>(name: string, operation: () => T | Promise<T
|
|
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) =>
|
|
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;
|