@glassly/engine 0.3.7 → 0.3.8-dev.977
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 +4 -0
- package/build/engine.d.ts +8 -2
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +7 -4
- package/build/engine.js.map +1 -1
- package/build/facades/backup.d.ts +14 -0
- package/build/facades/backup.d.ts.map +1 -0
- package/build/facades/backup.js +22 -0
- package/build/facades/backup.js.map +1 -0
- package/build/facades/glasses.d.ts +4 -4
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +4 -4
- package/build/facades/glasses.js.map +1 -1
- package/build/generated/releaseMetadata.js +4 -4
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/build/services/AppRegistry.d.ts +2 -1
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +38 -8
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/AppStateBackup.d.ts +98 -0
- package/build/services/AppStateBackup.d.ts.map +1 -0
- package/build/services/AppStateBackup.js +224 -0
- package/build/services/AppStateBackup.js.map +1 -0
- package/build/services/BlobStore.d.ts.map +1 -1
- package/build/services/BlobStore.js +5 -12
- package/build/services/BlobStore.js.map +1 -1
- package/build/services/CloudClientService.d.ts +1 -0
- package/build/services/CloudClientService.d.ts.map +1 -1
- package/build/services/CloudClientService.js +34 -2
- package/build/services/CloudClientService.js.map +1 -1
- package/build/services/DisplayProcessor.d.ts +2 -0
- package/build/services/DisplayProcessor.d.ts.map +1 -1
- package/build/services/DisplayProcessor.js +15 -1
- package/build/services/DisplayProcessor.js.map +1 -1
- package/build/services/GlassesTouchInput.d.ts.map +1 -1
- package/build/services/GlassesTouchInput.js +4 -2
- package/build/services/GlassesTouchInput.js.map +1 -1
- package/build/services/JSCrashloopReportService.d.ts.map +1 -1
- package/build/services/JSCrashloopReportService.js +5 -0
- package/build/services/JSCrashloopReportService.js.map +1 -1
- package/build/services/JSRouter.d.ts +11 -3
- package/build/services/JSRouter.d.ts.map +1 -1
- package/build/services/JSRouter.js +36 -6
- package/build/services/JSRouter.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +13 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +54 -20
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MiniappEngine.d.ts +9 -0
- package/build/services/MiniappEngine.d.ts.map +1 -1
- package/build/services/MiniappEngine.js +60 -27
- package/build/services/MiniappEngine.js.map +1 -1
- package/build/services/MiniappLauncher.d.ts +2 -0
- package/build/services/MiniappLauncher.d.ts.map +1 -1
- package/build/services/MiniappLauncher.js +6 -0
- package/build/services/MiniappLauncher.js.map +1 -1
- package/build/services/NavigationService.d.ts +1 -0
- package/build/services/NavigationService.d.ts.map +1 -1
- package/build/services/NavigationService.js +20 -6
- package/build/services/NavigationService.js.map +1 -1
- package/build/services/PhoneCalendarService.d.ts.map +1 -1
- package/build/services/PhoneCalendarService.js +8 -0
- package/build/services/PhoneCalendarService.js.map +1 -1
- package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
- package/build/services/PhoneNotificationsSync.js +7 -3
- package/build/services/PhoneNotificationsSync.js.map +1 -1
- package/build/services/SceneRenderer.js +1 -1
- package/build/services/SceneRenderer.js.map +1 -1
- package/build/services/appStateBackupFile.d.ts +32 -0
- package/build/services/appStateBackupFile.d.ts.map +1 -0
- package/build/services/appStateBackupFile.js +135 -0
- package/build/services/appStateBackupFile.js.map +1 -0
- package/build/services/controllers.d.ts.map +1 -1
- package/build/services/controllers.js +11 -3
- package/build/services/controllers.js.map +1 -1
- package/build/services/effectiveCapabilities.js +3 -0
- package/build/services/effectiveCapabilities.js.map +1 -1
- package/build/services/pages/pageEligibility.d.ts.map +1 -1
- package/build/services/pages/pageEligibility.js +8 -1
- package/build/services/pages/pageEligibility.js.map +1 -1
- package/build/services/settingsBridge.d.ts.map +1 -1
- package/build/services/settingsBridge.js +1 -0
- package/build/services/settingsBridge.js.map +1 -1
- package/build/services/{appleWatch/AppleWatchLink.d.ts → watch/WatchLink.d.ts} +8 -7
- package/build/services/watch/WatchLink.d.ts.map +1 -0
- package/build/services/{appleWatch/AppleWatchLink.js → watch/WatchLink.js} +14 -15
- package/build/services/watch/WatchLink.js.map +1 -0
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +14 -2
- package/build/stores/apps.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +53 -3
- package/build/stores/settings.js.map +1 -1
- package/build/types/capabilities/even-realities-g1.d.ts.map +1 -1
- package/build/types/capabilities/even-realities-g1.js +3 -0
- package/build/types/capabilities/even-realities-g1.js.map +1 -1
- package/build/types/capabilities/memomind-one.d.ts +3 -0
- package/build/types/capabilities/memomind-one.d.ts.map +1 -0
- package/build/types/capabilities/memomind-one.js +41 -0
- package/build/types/capabilities/memomind-one.js.map +1 -0
- package/build/types/enums.d.ts +6 -2
- package/build/types/enums.d.ts.map +1 -1
- package/build/types/enums.js +4 -0
- package/build/types/enums.js.map +1 -1
- package/build/types/hardware.d.ts +3 -1
- package/build/types/hardware.d.ts.map +1 -1
- package/build/types/hardware.js +3 -1
- package/build/types/hardware.js.map +1 -1
- package/build/utils/display/index.d.ts +2 -1
- package/build/utils/display/index.d.ts.map +1 -1
- package/build/utils/display/index.js +3 -2
- package/build/utils/display/index.js.map +1 -1
- package/build/utils/display/profiles/index.d.ts +2 -1
- package/build/utils/display/profiles/index.d.ts.map +1 -1
- package/build/utils/display/profiles/index.js +3 -2
- package/build/utils/display/profiles/index.js.map +1 -1
- package/build/utils/display/profiles/rayneo-io.d.ts +1 -1
- package/build/utils/display/profiles/rayneo-io.d.ts.map +1 -1
- package/build/utils/display/profiles/rayneo-io.js +1 -1
- package/build/utils/display/profiles/rayneo-io.js.map +1 -1
- package/build/utils/display/scene/index.d.ts +1 -1
- package/build/utils/display/scene/index.d.ts.map +1 -1
- package/build/utils/display/scene/index.js +1 -1
- package/build/utils/display/scene/index.js.map +1 -1
- package/build/utils/display/scene/process.d.ts +6 -0
- package/build/utils/display/scene/process.d.ts.map +1 -1
- package/build/utils/display/scene/process.js +22 -7
- package/build/utils/display/scene/process.js.map +1 -1
- package/build/utils/miniappGlobals.d.ts +13 -17
- package/build/utils/miniappGlobals.d.ts.map +1 -1
- package/build/utils/miniappGlobals.js +24 -56
- package/build/utils/miniappGlobals.js.map +1 -1
- package/package.json +7 -7
- package/src/engine.ts +7 -4
- package/src/facades/backup.ts +28 -0
- package/src/facades/glasses.ts +5 -5
- package/src/generated/releaseMetadata.ts +4 -4
- package/src/index.ts +1 -0
- package/src/services/AppRegistry.ts +36 -9
- package/src/services/AppStateBackup.ts +283 -0
- package/src/services/BlobStore.ts +5 -11
- package/src/services/CloudClientService.ts +35 -4
- package/src/services/DisplayProcessor.ts +20 -8
- package/src/services/GlassesTouchInput.ts +6 -2
- package/src/services/JSCrashloopReportService.ts +5 -0
- package/src/services/JSRouter.ts +37 -6
- package/src/services/LocalMiniappRuntime.ts +55 -20
- package/src/services/MiniappEngine.ts +62 -27
- package/src/services/MiniappLauncher.ts +7 -0
- package/src/services/NavigationService.ts +28 -8
- package/src/services/PhoneCalendarService.ts +7 -0
- package/src/services/PhoneNotificationsSync.ts +8 -3
- package/src/services/SceneRenderer.ts +1 -1
- package/src/services/appStateBackupFile.ts +171 -0
- package/src/services/controllers.ts +12 -3
- package/src/services/effectiveCapabilities.ts +3 -0
- package/src/services/pages/pageEligibility.ts +9 -1
- package/src/services/settingsBridge.ts +1 -0
- package/src/services/{appleWatch/AppleWatchLink.ts → watch/WatchLink.ts} +20 -20
- package/src/stores/apps.ts +13 -2
- package/src/stores/settings.ts +60 -7
- package/src/types/capabilities/even-realities-g1.ts +4 -0
- package/src/types/capabilities/memomind-one.ts +42 -0
- package/src/types/enums.ts +4 -0
- package/src/types/hardware.ts +14 -1
- package/src/utils/display/index.ts +4 -2
- package/src/utils/display/profiles/index.ts +9 -2
- package/src/utils/display/profiles/rayneo-io.ts +6 -1
- package/src/utils/display/scene/index.ts +1 -1
- package/src/utils/display/scene/process.ts +22 -7
- package/src/utils/miniappGlobals.ts +32 -64
- package/build/services/appleWatch/AppleWatchLink.d.ts.map +0 -1
- package/build/services/appleWatch/AppleWatchLink.js.map +0 -1
package/src/services/JSRouter.ts
CHANGED
|
@@ -146,6 +146,9 @@ export class JSRouter {
|
|
|
146
146
|
*/
|
|
147
147
|
uiRouter: UIRouter | null = null
|
|
148
148
|
|
|
149
|
+
/** Teardowns in progress, so a stop and a racing start or stop see the same one. */
|
|
150
|
+
private readonly unregistering = new Map<string, Promise<void>>()
|
|
151
|
+
|
|
149
152
|
constructor(
|
|
150
153
|
private readonly runtime: LocalMiniappRuntime,
|
|
151
154
|
private readonly crust: JSCrustBinding,
|
|
@@ -351,12 +354,26 @@ export class JSRouter {
|
|
|
351
354
|
}
|
|
352
355
|
|
|
353
356
|
/**
|
|
354
|
-
* Tear down a JSContext and unregister from the runtime.
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
+
* Tear down a JSContext and unregister from the runtime. The session's
|
|
358
|
+
* `onBeforeDisconnect` handlers get a short grace window first, so a
|
|
359
|
+
* miniapp can clear what it drew and end its own work while its messages
|
|
360
|
+
* still reach the host. Concurrent calls share one teardown.
|
|
357
361
|
*/
|
|
358
|
-
|
|
359
|
-
if (!this.registered.has(packageName)) return
|
|
362
|
+
unregister(packageName: string): Promise<void> {
|
|
363
|
+
if (!this.registered.has(packageName)) return Promise.resolve()
|
|
364
|
+
const inFlight = this.unregistering.get(packageName)
|
|
365
|
+
if (inFlight) return inFlight
|
|
366
|
+
const teardown = this.teardown(packageName).finally(() => this.unregistering.delete(packageName))
|
|
367
|
+
this.unregistering.set(packageName, teardown)
|
|
368
|
+
return teardown
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Resolves once an in-flight {@link unregister} of this package has finished. */
|
|
372
|
+
async whenUnregistered(packageName: string): Promise<void> {
|
|
373
|
+
await this.unregistering.get(packageName)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private async teardown(packageName: string): Promise<void> {
|
|
360
377
|
// Cancel any pending crash-respawn before tearing down — we don't
|
|
361
378
|
// want a stale timer to re-spawn a miniapp the user just disabled.
|
|
362
379
|
const pending = this.respawnTimers.get(packageName)
|
|
@@ -364,6 +381,7 @@ export class JSRouter {
|
|
|
364
381
|
BgTimer.clearTimeout(pending)
|
|
365
382
|
this.respawnTimers.delete(packageName)
|
|
366
383
|
}
|
|
384
|
+
await this.runtime.notifyWillDisconnect(packageName, "stopped")
|
|
367
385
|
this.spawnCache.delete(packageName)
|
|
368
386
|
this.crashController?.onKill(packageName)
|
|
369
387
|
// User-initiated close/disable: end any active nav trip so the glasses
|
|
@@ -382,6 +400,14 @@ export class JSRouter {
|
|
|
382
400
|
}
|
|
383
401
|
|
|
384
402
|
/** List packages the router has registered. */
|
|
403
|
+
/** The user opened a running miniapp again; the source rides the next respawn too. */
|
|
404
|
+
notifyLaunch(packageName: string, launchSource: MiniappLaunchSource): void {
|
|
405
|
+
if (!this.registered.has(packageName)) return
|
|
406
|
+
const cached = this.spawnCache.get(packageName)
|
|
407
|
+
if (cached) cached.launchSource = launchSource
|
|
408
|
+
this.runtime.sendLaunchChange(packageName, launchSource)
|
|
409
|
+
}
|
|
410
|
+
|
|
385
411
|
registeredPackages(): string[] {
|
|
386
412
|
return Array.from(this.registered)
|
|
387
413
|
}
|
|
@@ -483,13 +509,18 @@ export class JSRouter {
|
|
|
483
509
|
if (iface === "__error") {
|
|
484
510
|
const payload = this.tryParseArgs(msg.argsJson)
|
|
485
511
|
this.logger.error(`[${packageName}] ${method}`, payload)
|
|
512
|
+
// Crashes reach the `glassly dev` terminal too, not only console calls.
|
|
513
|
+
const summary = this.summarizeReason(payload, method)
|
|
514
|
+
const stack = (payload as {stack?: unknown} | null)?.stack
|
|
515
|
+
const errorArgs = typeof stack === "string" && stack ? [summary, stack] : [summary]
|
|
516
|
+
devServerBridge.forwardLog(packageName, "error", errorArgs, Date.now(), "background")
|
|
486
517
|
if (this.crashController) {
|
|
487
518
|
// Only treat "exception" + "unhandledRejection" + "uncaught" as
|
|
488
519
|
// crash signals. `console.error` calls also flow through the
|
|
489
520
|
// log path — those don't end the JSContext.
|
|
490
521
|
const isCrash = method === "exception" || method === "unhandledRejection" || method === "uncaught"
|
|
491
522
|
if (isCrash) {
|
|
492
|
-
this.handleCrash(packageName,
|
|
523
|
+
this.handleCrash(packageName, summary)
|
|
493
524
|
}
|
|
494
525
|
}
|
|
495
526
|
return
|
|
@@ -304,6 +304,20 @@ function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T | nul
|
|
|
304
304
|
})
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
/** Bounds the cold-GPS wait for a one-shot poll; on a stall, any older fix beats an error. */
|
|
308
|
+
const FRESH_LOCATION_TIMEOUT_MS = 8_000
|
|
309
|
+
|
|
310
|
+
async function freshLocationOrStale(): Promise<Location.LocationObject> {
|
|
311
|
+
const fresh = await withTimeout(
|
|
312
|
+
Location.getCurrentPositionAsync({accuracy: Location.Accuracy.Low}),
|
|
313
|
+
FRESH_LOCATION_TIMEOUT_MS,
|
|
314
|
+
)
|
|
315
|
+
if (fresh) return fresh
|
|
316
|
+
const stale = await Location.getLastKnownPositionAsync()
|
|
317
|
+
if (stale) return stale
|
|
318
|
+
throw new Error("No location fix available")
|
|
319
|
+
}
|
|
320
|
+
|
|
307
321
|
const LOG_TAG = "LOCAL_MINIAPP"
|
|
308
322
|
const DIAGNOSTIC_MAX_LIST_ITEMS = 100
|
|
309
323
|
const DIAGNOSTIC_MAX_STRING_LENGTH = 512
|
|
@@ -326,6 +340,8 @@ const SYSTEM_MINIAPP_PACKAGES = new Set([
|
|
|
326
340
|
"com.glassly.miniappdev",
|
|
327
341
|
])
|
|
328
342
|
const PING_INTERVAL_MS = 5_000
|
|
343
|
+
/** How long a stopping miniapp's `beforeDisconnect` handlers get before teardown. */
|
|
344
|
+
const WILL_DISCONNECT_GRACE_MS = 100
|
|
329
345
|
// Min gap between "transcription limit reached" notices on the glasses. A
|
|
330
346
|
// debounce, not a cooldown: the cloud re-announces the block per refused
|
|
331
347
|
// subscribe, and miniapps retry in bursts — this only coalesces those bursts
|
|
@@ -1235,21 +1251,26 @@ class LocalMiniappRuntime {
|
|
|
1235
1251
|
* undeliverable anyway.
|
|
1236
1252
|
*/
|
|
1237
1253
|
public async gracefullyUnregisterApp(packageName: string, reason = "unregistering"): Promise<void> {
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
console.log(`${LOG_TAG}: gracefullyUnregisterApp(${packageName}) — sending WILL_DISCONNECT`)
|
|
1242
|
-
this.sendToMiniapp(packageName, {
|
|
1243
|
-
type: MiniappResponseType.WILL_DISCONNECT,
|
|
1244
|
-
reason,
|
|
1245
|
-
})
|
|
1246
|
-
// 50 ms: imperceptible to the user, plenty of time for the SDK's
|
|
1247
|
-
// synchronous `beforeDisconnect` handlers to flush a final
|
|
1248
|
-
// sendOneShot through the still-open transport.
|
|
1249
|
-
await new Promise<void>((resolve) => BgTimer.setTimeout(resolve, 50))
|
|
1254
|
+
if (!this.connectedApps.has(packageName)) return
|
|
1255
|
+
await this.notifyWillDisconnect(packageName, reason)
|
|
1250
1256
|
this.unregisterApp(packageName)
|
|
1251
1257
|
}
|
|
1252
1258
|
|
|
1259
|
+
/**
|
|
1260
|
+
* Send `WILL_DISCONNECT` and wait out the grace window, so the session's
|
|
1261
|
+
* `beforeDisconnect` handlers run (Navigation clears its HUD and ends its
|
|
1262
|
+
* trip) and their last messages land before teardown. No-op unless the
|
|
1263
|
+
* app finished its handshake.
|
|
1264
|
+
*/
|
|
1265
|
+
public async notifyWillDisconnect(packageName: string, reason: string): Promise<void> {
|
|
1266
|
+
if (!this.connectedApps.has(packageName) || !this.handshookApps.has(packageName)) return
|
|
1267
|
+
console.log(`${LOG_TAG}: ${packageName} — sending WILL_DISCONNECT (${reason})`)
|
|
1268
|
+
this.sendToMiniapp(packageName, {type: MiniappResponseType.WILL_DISCONNECT, reason})
|
|
1269
|
+
// The message and the replies it triggers each cross a JSContext hop;
|
|
1270
|
+
// short enough not to slow an app switch noticeably.
|
|
1271
|
+
await new Promise<void>((resolve) => BgTimer.setTimeout(resolve, WILL_DISCONNECT_GRACE_MS))
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1253
1274
|
public unregisterApp(packageName: string, opts?: {endNavigation?: boolean}): void {
|
|
1254
1275
|
console.log(`${LOG_TAG}: unregisterApp(${packageName})`)
|
|
1255
1276
|
const releasedMicGateOverride = micStateCoordinator.clearMiniappGateOverrides(packageName)
|
|
@@ -3450,7 +3471,7 @@ class LocalMiniappRuntime {
|
|
|
3450
3471
|
const locationStartedAt = Date.now()
|
|
3451
3472
|
const cached = await Location.getLastKnownPositionAsync({maxAge: 60_000})
|
|
3452
3473
|
console.log(`${LOG_TAG}: location poll cache hit=${cached !== null} elapsed=${Date.now() - locationStartedAt}ms`)
|
|
3453
|
-
const location = cached ?? (await
|
|
3474
|
+
const location = cached ?? (await freshLocationOrStale())
|
|
3454
3475
|
console.log(`${LOG_TAG}: location poll resolved elapsed=${Date.now() - locationStartedAt}ms`)
|
|
3455
3476
|
|
|
3456
3477
|
this.sendResult(packageName, requestId, true, {
|
|
@@ -6071,13 +6092,16 @@ class LocalMiniappRuntime {
|
|
|
6071
6092
|
private async startInteropApp(packageName: string): Promise<boolean> {
|
|
6072
6093
|
const app = this.interopApps().find((a) => a.packageName === packageName)
|
|
6073
6094
|
if (!app) return false
|
|
6074
|
-
//
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
}
|
|
6080
|
-
|
|
6095
|
+
// session.miniapps.start comes from the glasses (the dashboard's Apps menu,
|
|
6096
|
+
// an Assist voice command), unlike a headless action wake, which carries no source.
|
|
6097
|
+
// The full start, as a phone open runs it: the standard peer stops so this
|
|
6098
|
+
// app owns the display, and an app already running hears the new launch.
|
|
6099
|
+
const launchSource = "glasses" as const
|
|
6100
|
+
const started = await useAppStatusStore.getState().start(app, {skipNavigation: true, launchSource})
|
|
6101
|
+
if (!started) return false
|
|
6102
|
+
// The caller may message the app next; start() resolves at spawn.
|
|
6103
|
+
if (app.local) await miniappLauncher.ensureConnected(packageName, undefined, {launchSource})
|
|
6104
|
+
return true
|
|
6081
6105
|
}
|
|
6082
6106
|
|
|
6083
6107
|
private stopInteropApp(packageName: string): Promise<void> {
|
|
@@ -6851,6 +6875,17 @@ class LocalMiniappRuntime {
|
|
|
6851
6875
|
this.sendToMiniapp(packageName, {type: MiniappResponseType.LINK, params})
|
|
6852
6876
|
}
|
|
6853
6877
|
|
|
6878
|
+
/**
|
|
6879
|
+
* Tell a running miniapp the user opened it again and from where. The
|
|
6880
|
+
* CONNECT_ACK only carries the source of the spawn, which is stale for a
|
|
6881
|
+
* background app that keeps running between opens.
|
|
6882
|
+
*/
|
|
6883
|
+
public sendLaunchChange(packageName: string, launchSource: MiniappLaunchSource): void {
|
|
6884
|
+
const existing = this.connectedApps.get(packageName)
|
|
6885
|
+
if (existing) existing.launchSource = launchSource
|
|
6886
|
+
this.sendToMiniapp(packageName, {type: MiniappResponseType.LAUNCH_CHANGE, launchSource})
|
|
6887
|
+
}
|
|
6888
|
+
|
|
6854
6889
|
/**
|
|
6855
6890
|
* Send a VISIBILITY_CHANGE push to a miniapp.
|
|
6856
6891
|
*/
|
|
@@ -29,42 +29,77 @@ export interface MiniappEngine {
|
|
|
29
29
|
|
|
30
30
|
let engine: MiniappEngine | null = null
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Kill and re-spawn a miniapp's background JSContext from its installed bundle
|
|
34
|
+
* (dev: the dev server's fresh build). Resolves false when it isn't installed
|
|
35
|
+
* or the spawn failed.
|
|
36
|
+
*/
|
|
37
|
+
async function respawnBackground(router: JSRouter, uiRouter: UIRouter, packageName: string): Promise<boolean> {
|
|
38
|
+
// Re-resolve the freshly built bundle (dev: HTTP off the dev server;
|
|
39
|
+
// released: file:// snapshot) via the launcher's shared recipe. Carries
|
|
40
|
+
// the declared permissions + manifest across the respawn — omitting them
|
|
41
|
+
// would respawn the JSContext with no permissions, so SUBSCRIBE gates and
|
|
42
|
+
// per-call dispatch checks would start rejecting after a background
|
|
43
|
+
// hot-reload.
|
|
44
|
+
const resolved = await miniappLauncher.resolveBundle(packageName)
|
|
45
|
+
if (!resolved) {
|
|
46
|
+
console.warn(`JSRuntime: respawn-bg could not resolve bundle for ${packageName}`)
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
// Force a respawn (kill + spawn) rather than launcher.ensureRunning,
|
|
50
|
+
// which is idempotent and would no-op an already-registered context.
|
|
51
|
+
await router.unregister(packageName)
|
|
52
|
+
const ok = await router.spawnAndRegister(packageName, resolved.bgSource, {
|
|
53
|
+
permissions: resolved.declaredPermissions,
|
|
54
|
+
installedManifest: resolved.installedManifest,
|
|
55
|
+
})
|
|
56
|
+
if (!ok) {
|
|
57
|
+
console.warn(`JSRuntime: respawn-bg failed for ${packageName}`)
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
// The respawned JSContext is a fresh MiniappSession with ui.bound false.
|
|
61
|
+
// The mounted WebView won't re-fire glassly.ready() (it's latched), so
|
|
62
|
+
// re-announce it so RPC replies (glassly.request) reach the UI again
|
|
63
|
+
// instead of being dropped while unbound.
|
|
64
|
+
uiRouter.notifyReopen(packageName)
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
32
68
|
function wireDevServerRespawnBackground(router: JSRouter, uiRouter: UIRouter): void {
|
|
33
69
|
devServerBridge.onRespawnBackground(async (packageName) => {
|
|
34
70
|
try {
|
|
35
|
-
|
|
36
|
-
// released: file:// snapshot) via the launcher's shared recipe. Carries
|
|
37
|
-
// the declared permissions + manifest across the respawn — omitting them
|
|
38
|
-
// would respawn the JSContext with no permissions, so SUBSCRIBE gates and
|
|
39
|
-
// per-call dispatch checks would start rejecting after a background
|
|
40
|
-
// hot-reload.
|
|
41
|
-
const resolved = await miniappLauncher.resolveBundle(packageName)
|
|
42
|
-
if (!resolved) {
|
|
43
|
-
console.warn(`JSRuntime: respawn-bg could not resolve bundle for ${packageName}`)
|
|
44
|
-
return
|
|
45
|
-
}
|
|
46
|
-
// Force a respawn (kill + spawn) rather than launcher.ensureRunning,
|
|
47
|
-
// which is idempotent and would no-op an already-registered context.
|
|
48
|
-
await router.unregister(packageName)
|
|
49
|
-
const ok = await router.spawnAndRegister(packageName, resolved.bgSource, {
|
|
50
|
-
permissions: resolved.declaredPermissions,
|
|
51
|
-
installedManifest: resolved.installedManifest,
|
|
52
|
-
})
|
|
53
|
-
if (!ok) {
|
|
54
|
-
console.warn(`JSRuntime: respawn-bg failed for ${packageName}`)
|
|
55
|
-
return
|
|
56
|
-
}
|
|
57
|
-
// The respawned JSContext is a fresh MiniappSession with ui.bound false.
|
|
58
|
-
// The mounted WebView won't re-fire glassly.ready() (it's latched), so
|
|
59
|
-
// re-announce it so RPC replies (glassly.request) reach the UI again
|
|
60
|
-
// instead of being dropped while unbound.
|
|
61
|
-
uiRouter.notifyReopen(packageName)
|
|
71
|
+
await respawnBackground(router, uiRouter, packageName)
|
|
62
72
|
} catch (e) {
|
|
63
73
|
console.warn(`JSRuntime: respawn-bg threw for ${packageName}:`, e)
|
|
64
74
|
}
|
|
65
75
|
})
|
|
66
76
|
}
|
|
67
77
|
|
|
78
|
+
/** Packages with a live background JSContext. */
|
|
79
|
+
export function runningMiniappPackages(): string[] {
|
|
80
|
+
return engine?.router.registeredPackages() ?? []
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Tear down a miniapp's background JSContext; a no-op when it isn't running. */
|
|
84
|
+
export async function stopMiniapp(packageName: string): Promise<void> {
|
|
85
|
+
await engine?.router.unregister(packageName)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Start (or restart) a miniapp's background so it reads its storage afresh (an
|
|
90
|
+
* app state restore). Resolves false when the engine isn't up or the spawn failed.
|
|
91
|
+
*/
|
|
92
|
+
export async function respawnMiniapp(packageName: string): Promise<boolean> {
|
|
93
|
+
const current = engine
|
|
94
|
+
if (!current) return false
|
|
95
|
+
try {
|
|
96
|
+
return await respawnBackground(current.router, current.uiRouter, packageName)
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.warn(`JSRuntime: respawn threw for ${packageName}:`, e)
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
68
103
|
/**
|
|
69
104
|
* Construct (once) and return the JSRuntime engine singletons. Starts the router's
|
|
70
105
|
* message pump on first call. Subsequent calls return the same instances.
|
|
@@ -91,6 +91,11 @@ class MiniappLauncher {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/** True iff the package's background JS context is currently spawned. */
|
|
94
|
+
/** Tell an already-running miniapp the user opened it again and from where. */
|
|
95
|
+
notifyLaunch(packageName: string, launchSource: MiniappLaunchSource): void {
|
|
96
|
+
this.deps?.router.notifyLaunch(packageName, launchSource)
|
|
97
|
+
}
|
|
98
|
+
|
|
94
99
|
isRunning(packageName: string): boolean {
|
|
95
100
|
return this.deps?.router.registeredPackages().includes(packageName) ?? false
|
|
96
101
|
}
|
|
@@ -228,6 +233,8 @@ class MiniappLauncher {
|
|
|
228
233
|
*/
|
|
229
234
|
async ensureRunning(packageName: string, hints?: LaunchHints): Promise<LaunchResult> {
|
|
230
235
|
const router = this.requireRouter()
|
|
236
|
+
// A context still tearing down from a stop is not one to reuse.
|
|
237
|
+
await router.whenUnregistered(packageName)
|
|
231
238
|
|
|
232
239
|
// Already spawned: best-effort resolve for the UI entry, never throw —
|
|
233
240
|
// a headless caller that doesn't need UI gets {null, null} fast even if
|
|
@@ -16,6 +16,7 @@ import CrustModule from "@glassly/crust"
|
|
|
16
16
|
import {resolveMapboxAccessToken, resolveMapboxRenderToken, userMapboxApiKey} from "./MapboxTokenService"
|
|
17
17
|
import {decodePolyline, parseDurationSeconds} from "./navigation/routesApiCodec"
|
|
18
18
|
import {resolveStepRoads} from "./navigation/roadNameResolver"
|
|
19
|
+
import {BgTimer} from "../utils/timers"
|
|
19
20
|
|
|
20
21
|
const LOG_TAG = "NAV_SERVICE"
|
|
21
22
|
|
|
@@ -394,7 +395,7 @@ class NavigationService {
|
|
|
394
395
|
sessionToken: string,
|
|
395
396
|
): Promise<{
|
|
396
397
|
ok: boolean
|
|
397
|
-
suggestions?: Array<{placeId: string; mainText: string; secondaryText: string}>
|
|
398
|
+
suggestions?: Array<{placeId: string; mainText: string; secondaryText: string; distanceMeters?: number}>
|
|
398
399
|
error?: string
|
|
399
400
|
}> {
|
|
400
401
|
return placeAutocompleteViaSearchBox(query, near, sessionToken)
|
|
@@ -506,6 +507,19 @@ export default navigationService
|
|
|
506
507
|
const MAPBOX_DIRECTIONS_BASE = "https://api.mapbox.com/directions/v5/mapbox"
|
|
507
508
|
const MAPBOX_GEOCODE_REVERSE = "https://api.mapbox.com/search/geocode/v6/reverse"
|
|
508
509
|
const MAPBOX_SEARCHBOX_SUGGEST = "https://api.mapbox.com/search/searchbox/v1/suggest"
|
|
510
|
+
|
|
511
|
+
/** Place search sits in front of a voice command; a stalled call must fail over, not hang. */
|
|
512
|
+
const PLACE_SEARCH_TIMEOUT_MS = 6_000
|
|
513
|
+
|
|
514
|
+
async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
|
|
515
|
+
const controller = new AbortController()
|
|
516
|
+
const timer = BgTimer.setTimeout(() => controller.abort(), timeoutMs)
|
|
517
|
+
try {
|
|
518
|
+
return await fetch(url, {method: "GET", signal: controller.signal})
|
|
519
|
+
} finally {
|
|
520
|
+
BgTimer.clearTimeout(timer)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
509
523
|
const MAPBOX_SEARCHBOX_RETRIEVE = "https://api.mapbox.com/search/searchbox/v1/retrieve"
|
|
510
524
|
|
|
511
525
|
/**
|
|
@@ -818,7 +832,7 @@ async function placeAutocompleteViaSearchBox(
|
|
|
818
832
|
sessionToken: string,
|
|
819
833
|
): Promise<{
|
|
820
834
|
ok: boolean
|
|
821
|
-
suggestions?: Array<{placeId: string; mainText: string; secondaryText: string}>
|
|
835
|
+
suggestions?: Array<{placeId: string; mainText: string; secondaryText: string; distanceMeters?: number}>
|
|
822
836
|
error?: string
|
|
823
837
|
}> {
|
|
824
838
|
if (!query.trim()) return {ok: true, suggestions: []}
|
|
@@ -837,11 +851,14 @@ async function placeAutocompleteViaSearchBox(
|
|
|
837
851
|
types: "poi,address,place,street",
|
|
838
852
|
limit: "10",
|
|
839
853
|
})
|
|
840
|
-
// Mapbox wants lng,lat for proximity bias.
|
|
841
|
-
if (near)
|
|
854
|
+
// Mapbox wants lng,lat for proximity bias; `origin` adds per-suggestion distance.
|
|
855
|
+
if (near) {
|
|
856
|
+
params.set("proximity", `${near.lng},${near.lat}`)
|
|
857
|
+
params.set("origin", `${near.lng},${near.lat}`)
|
|
858
|
+
}
|
|
842
859
|
|
|
843
860
|
try {
|
|
844
|
-
const res = await
|
|
861
|
+
const res = await fetchWithTimeout(`${MAPBOX_SEARCHBOX_SUGGEST}?${params.toString()}`, PLACE_SEARCH_TIMEOUT_MS)
|
|
845
862
|
if (!res.ok) return {ok: false, error: `Search Box suggest ${res.status}`}
|
|
846
863
|
const json = (await res.json()) as {
|
|
847
864
|
suggestions?: Array<{
|
|
@@ -849,6 +866,7 @@ async function placeAutocompleteViaSearchBox(
|
|
|
849
866
|
name?: string
|
|
850
867
|
place_formatted?: string
|
|
851
868
|
full_address?: string
|
|
869
|
+
distance?: number
|
|
852
870
|
}>
|
|
853
871
|
}
|
|
854
872
|
const suggestions = (json.suggestions ?? [])
|
|
@@ -857,6 +875,7 @@ async function placeAutocompleteViaSearchBox(
|
|
|
857
875
|
placeId: s.mapbox_id,
|
|
858
876
|
mainText: s.name ?? "",
|
|
859
877
|
secondaryText: s.place_formatted ?? s.full_address ?? "",
|
|
878
|
+
...(typeof s.distance === "number" ? {distanceMeters: s.distance} : {}),
|
|
860
879
|
}))
|
|
861
880
|
return {ok: true, suggestions}
|
|
862
881
|
} catch (err) {
|
|
@@ -890,9 +909,10 @@ async function placeDetailsViaSearchBox(
|
|
|
890
909
|
}
|
|
891
910
|
const params = new URLSearchParams({access_token: token, session_token: sessionToken})
|
|
892
911
|
try {
|
|
893
|
-
const res = await
|
|
894
|
-
|
|
895
|
-
|
|
912
|
+
const res = await fetchWithTimeout(
|
|
913
|
+
`${MAPBOX_SEARCHBOX_RETRIEVE}/${encodeURIComponent(placeId)}?${params.toString()}`,
|
|
914
|
+
PLACE_SEARCH_TIMEOUT_MS,
|
|
915
|
+
)
|
|
896
916
|
if (!res.ok) return {ok: false, error: `Search Box retrieve ${res.status}`}
|
|
897
917
|
const json = (await res.json()) as {
|
|
898
918
|
features?: Array<{
|
|
@@ -49,9 +49,16 @@ export async function listPhoneCalendarEvents(request: CalendarListRequest): Pro
|
|
|
49
49
|
if (calendars.length === 0) return {events: [], truncated: false}
|
|
50
50
|
|
|
51
51
|
const rawEvents = await Calendar.listEvents(calendars, startsAt, endsAt)
|
|
52
|
+
const seen = new Set<string>()
|
|
52
53
|
const events = rawEvents
|
|
53
54
|
.map(normalizeCalendarEvent)
|
|
54
55
|
.filter((event) => calendarEventOverlapsWindow(event, startsAt, endsAt))
|
|
56
|
+
.filter((event) => {
|
|
57
|
+
const key = `${event.title}\n${event.startsAt}\n${event.endsAt}\n${event.allDay}`
|
|
58
|
+
if (seen.has(key)) return false
|
|
59
|
+
seen.add(key)
|
|
60
|
+
return true
|
|
61
|
+
})
|
|
55
62
|
.sort((a, b) => Date.parse(a.startsAt) - Date.parse(b.startsAt) || Date.parse(a.endsAt) - Date.parse(b.endsAt))
|
|
56
63
|
|
|
57
64
|
return {
|
|
@@ -91,6 +91,11 @@ export function nativeNotificationsSupported(): boolean {
|
|
|
91
91
|
return getModelCapabilities(model).hasNativeNotifications === true
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
function notificationRelaySupported(): boolean {
|
|
95
|
+
const model = useGlassesStore.getState().deviceModel as DeviceTypes
|
|
96
|
+
return Platform.OS === "ios" && getModelCapabilities(model).hasNotificationRelay === true
|
|
97
|
+
}
|
|
98
|
+
|
|
94
99
|
/** Popup durations the firmware accepts, or an empty list when it takes any. */
|
|
95
100
|
export function nativeNotificationPopupSeconds(): number[] {
|
|
96
101
|
const model = useGlassesStore.getState().deviceModel as DeviceTypes
|
|
@@ -141,10 +146,10 @@ export function currentPresentationOwner(hostActive: boolean): PresentationOwner
|
|
|
141
146
|
*/
|
|
142
147
|
export function captureAuthorization(): "granted" | "denied" | "unknown" | "not_needed" {
|
|
143
148
|
if (Platform.OS !== "ios") return "not_needed"
|
|
144
|
-
if (!nativeNotificationsSupported()) return "not_needed"
|
|
149
|
+
if (!nativeNotificationsSupported() && !notificationRelaySupported()) return "not_needed"
|
|
145
150
|
const glasses = useGlassesStore.getState()
|
|
146
151
|
if (!isGlassesConnected(glasses.connection)) return "unknown"
|
|
147
|
-
return glasses.ancsAuthorized ? "granted" : "denied"
|
|
152
|
+
return glasses.ancsAuthorized ? "granted" : notificationRelaySupported() ? "unknown" : "denied"
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
/**
|
|
@@ -178,7 +183,7 @@ function pushListenerConfig(): void {
|
|
|
178
183
|
}
|
|
179
184
|
|
|
180
185
|
function pushGlassesConfig(): void {
|
|
181
|
-
if (!nativeNotificationsSupported()) return
|
|
186
|
+
if (!nativeNotificationsSupported() && !notificationRelaySupported()) return
|
|
182
187
|
if (!isGlassesConnected(useGlassesStore.getState().connection)) return
|
|
183
188
|
void Promise.resolve(BluetoothSdk.setGlassesNotificationConfig(desiredGlassesNotificationConfig())).catch(
|
|
184
189
|
(err: unknown) =>
|
|
@@ -250,7 +250,7 @@ class SceneRenderer {
|
|
|
250
250
|
return {kind: "legacy", layout, degraded, dropped}
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
const processed = processScene(elements, caps, displayProcessor.
|
|
253
|
+
const processed = processScene(elements, caps, displayProcessor.getSceneProfile())
|
|
254
254
|
// A stale baseline means the glasses were cleared since the last commit —
|
|
255
255
|
// diff from empty so everything repaints (the retained frame stays valid
|
|
256
256
|
// as the replay source only).
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App state backup files: the share-sheet export and file-picker restore
|
|
3
|
+
* behind Settings > Profile > Backup. What goes in the file is decided in
|
|
4
|
+
* AppStateBackup.ts; this side reads and writes the live stores.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {MemoryImportResult} from "@glassly/miniapp"
|
|
8
|
+
import {File, Paths} from "expo-file-system"
|
|
9
|
+
import {Platform} from "react-native"
|
|
10
|
+
import Share from "react-native-share"
|
|
11
|
+
|
|
12
|
+
import {SETTINGS, useSettingsStore} from "../stores/settings"
|
|
13
|
+
import {useAppStatusStore} from "../stores/apps"
|
|
14
|
+
import {storage} from "../utils/storage/storage"
|
|
15
|
+
import appRegistry, {saveAlwaysOnOverride, saveWidgetExpandChoice} from "./AppRegistry"
|
|
16
|
+
import {
|
|
17
|
+
AppStateBackupError,
|
|
18
|
+
collectAppStateBackup,
|
|
19
|
+
miniappPrefs,
|
|
20
|
+
miniappsWithStorage,
|
|
21
|
+
parseAppStateBackup,
|
|
22
|
+
restorableSettings,
|
|
23
|
+
restoreMiniappStorage,
|
|
24
|
+
type AppStateBackupBackend,
|
|
25
|
+
} from "./AppStateBackup"
|
|
26
|
+
import {cloudClientService} from "./CloudClientService"
|
|
27
|
+
import {memoryStore} from "./memoryStoreInstance"
|
|
28
|
+
import {respawnMiniapp, runningMiniappPackages, stopMiniapp} from "./MiniappEngine"
|
|
29
|
+
|
|
30
|
+
export interface AppStateExportResult {
|
|
31
|
+
success: boolean
|
|
32
|
+
cancelled?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AppStateRestoreResult {
|
|
36
|
+
/** Settings written. */
|
|
37
|
+
settings: number
|
|
38
|
+
/** Miniapps whose storage was restored. */
|
|
39
|
+
miniapps: number
|
|
40
|
+
memory: MemoryImportResult | null
|
|
41
|
+
/** Installed when the backup was made but not on this phone now. */
|
|
42
|
+
missing: string[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AppStateRestoreOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Install what the backup had installed and this phone lacks. Runs before
|
|
48
|
+
* the restored miniapps start again, so the dashboard comes back with every
|
|
49
|
+
* widget provider its saved layout names: it drops unknown widgets from the
|
|
50
|
+
* layout the next time it saves it.
|
|
51
|
+
*/
|
|
52
|
+
reinstall?: (packageNames: string[]) => Promise<void>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const PICK_MIME_TYPES =
|
|
56
|
+
Platform.OS === "android" ? ["application/json", "application/octet-stream"] : ["application/json"]
|
|
57
|
+
|
|
58
|
+
const backend: AppStateBackupBackend = {
|
|
59
|
+
get(key) {
|
|
60
|
+
const result = storage.load<unknown>(key)
|
|
61
|
+
return result.is_ok() ? result.value : null
|
|
62
|
+
},
|
|
63
|
+
set(key, value) {
|
|
64
|
+
const result = storage.save(key, value)
|
|
65
|
+
if (result.is_error()) throw result.error
|
|
66
|
+
},
|
|
67
|
+
keys: () => storage.getAllKeys(),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function exportAppStateFile(): Promise<AppStateExportResult> {
|
|
71
|
+
const userId = await cloudClientService.resolveUserId()
|
|
72
|
+
const installed: Array<{packageName: string; version: string; source?: string}> = []
|
|
73
|
+
for (const app of await appRegistry.getInstalledMiniapps()) {
|
|
74
|
+
const version = await appRegistry.getActiveVersion(app.packageName)
|
|
75
|
+
if (!version) continue
|
|
76
|
+
installed.push({
|
|
77
|
+
packageName: app.packageName,
|
|
78
|
+
version,
|
|
79
|
+
source: appRegistry.getReleaseIdentity(app.packageName, version)?.source,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
const backup = collectAppStateBackup({
|
|
83
|
+
backend,
|
|
84
|
+
userId,
|
|
85
|
+
settings: Object.values(SETTINGS),
|
|
86
|
+
installed,
|
|
87
|
+
memory: await memoryStore.exportBackup(),
|
|
88
|
+
now: Date.now(),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const filename = `glassly-backup-${localDate(backup.exportedAt)}.json`
|
|
92
|
+
const file = new File(Paths.cache, filename)
|
|
93
|
+
file.writeSync(JSON.stringify(backup))
|
|
94
|
+
try {
|
|
95
|
+
await Share.open({url: file.uri, type: "application/json", filename, title: "Glassly backup"})
|
|
96
|
+
return {success: true}
|
|
97
|
+
} catch (error: any) {
|
|
98
|
+
// react-native-share rejects when the user dismisses the sheet.
|
|
99
|
+
if (error?.message?.includes("User did not share")) return {success: false, cancelled: true}
|
|
100
|
+
throw error
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Resolves null when the user cancels the picker. */
|
|
105
|
+
export async function importAppStateFile(opts: AppStateRestoreOptions = {}): Promise<AppStateRestoreResult | null> {
|
|
106
|
+
const picked = await File.pickFileAsync({mimeTypes: PICK_MIME_TYPES})
|
|
107
|
+
if (picked.canceled) return null
|
|
108
|
+
let data: unknown
|
|
109
|
+
try {
|
|
110
|
+
data = JSON.parse(await picked.result.text())
|
|
111
|
+
} catch {
|
|
112
|
+
throw new AppStateBackupError("Not a Glassly app state backup")
|
|
113
|
+
}
|
|
114
|
+
const backup = parseAppStateBackup(data)
|
|
115
|
+
const userId = await cloudClientService.resolveUserId()
|
|
116
|
+
|
|
117
|
+
// First, so a memory section this build rejects fails the restore before anything is written.
|
|
118
|
+
const memory = backup.memory ? await memoryStore.importBackup(backup.memory) : null
|
|
119
|
+
|
|
120
|
+
const {plain, indexed} = restorableSettings(backup, Object.values(SETTINGS))
|
|
121
|
+
const written = await useSettingsStore.getState().setManyLocally(plain)
|
|
122
|
+
if (written.is_error()) throw written.error
|
|
123
|
+
// setManyLocally only takes registry keys; `key:index` values go to storage and the store directly.
|
|
124
|
+
for (const [key, value] of Object.entries(indexed)) backend.set(key, value)
|
|
125
|
+
if (Object.keys(indexed).length > 0) {
|
|
126
|
+
useSettingsStore.setState((state) => ({settings: {...state.settings, ...indexed}}))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// A running miniapp holds its old values in memory and could write them back over the
|
|
130
|
+
// restore, so the ones getting storage stop now and start again at the end.
|
|
131
|
+
const running = new Set(runningMiniappPackages())
|
|
132
|
+
const restarting = miniappsWithStorage(backup).filter((packageName) => running.has(packageName))
|
|
133
|
+
for (const packageName of restarting) await stopMiniapp(packageName)
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const restored = restoreMiniappStorage(backend, userId, backup)
|
|
137
|
+
|
|
138
|
+
const apps = useAppStatusStore.getState()
|
|
139
|
+
for (const [packageName, record] of Object.entries(backup.miniapps)) {
|
|
140
|
+
const prefs = miniappPrefs(record)
|
|
141
|
+
if (prefs.hidden !== undefined) apps.setHiddenStatus(packageName, prefs.hidden)
|
|
142
|
+
if (prefs.alwaysOn !== undefined) saveAlwaysOnOverride(packageName, prefs.alwaysOn)
|
|
143
|
+
for (const [widgetId, expand] of Object.entries(prefs.widgetExpand)) {
|
|
144
|
+
saveWidgetExpandChoice(packageName, widgetId, expand)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
appRegistry.markRefreshNeeded()
|
|
148
|
+
await apps.refresh()
|
|
149
|
+
|
|
150
|
+
const installedNow = new Set((await appRegistry.getInstalledMiniapps()).map((app) => app.packageName))
|
|
151
|
+
const missing = Object.entries(backup.miniapps)
|
|
152
|
+
.filter(([packageName, record]) => record.installed && !installedNow.has(packageName))
|
|
153
|
+
.map(([packageName]) => packageName)
|
|
154
|
+
if (missing.length > 0) await opts.reinstall?.(missing)
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
settings: Object.keys(plain).length + Object.keys(indexed).length,
|
|
158
|
+
miniapps: restored.packages.length,
|
|
159
|
+
memory,
|
|
160
|
+
missing,
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
for (const packageName of restarting) await respawnMiniapp(packageName)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function localDate(ms: number): string {
|
|
168
|
+
const d = new Date(ms)
|
|
169
|
+
const pad = (n: number) => String(n).padStart(2, "0")
|
|
170
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
171
|
+
}
|