@glassly/engine 0.3.8-dev.980 → 0.3.8-dev.982

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.
@@ -489,6 +489,11 @@ class LocalDisplayManager {
489
489
  }
490
490
  }
491
491
 
492
+ /** A frame for native to keep warm, never shown (SceneRenderer.prefetchScene). */
493
+ public prefetch(packageName: string, view: "main" | "dashboard", elements: SceneElementInput[]): boolean {
494
+ return sceneRenderer.prefetchScene(packageName, view, elements)
495
+ }
496
+
492
497
  /**
493
498
  * The overlay slot only; bypasses arbitration and main ownership state.
494
499
  * Frames from an owner other than the slot's are dropped. Returns the scene
@@ -1495,6 +1495,9 @@ class LocalMiniappRuntime {
1495
1495
  case MiniappRequestType.SHOW_VIEW:
1496
1496
  this.handleShowView(packageName, payload, requestId)
1497
1497
  break
1498
+ case MiniappRequestType.PREFETCH:
1499
+ this.handlePrefetch(packageName, payload, requestId)
1500
+ break
1498
1501
  case MiniappRequestType.SETTINGS_GET:
1499
1502
  this.handleSettingsGet(packageName, payload, requestId)
1500
1503
  break
@@ -2651,6 +2654,27 @@ class LocalMiniappRuntime {
2651
2654
  )
2652
2655
  }
2653
2656
 
2657
+ /**
2658
+ * display.prefetch: the active dashboard hands over the frame its view would
2659
+ * show if opened now. Native keeps its fonts and images on the glasses while
2660
+ * nothing is on screen; nothing is displayed or retained here.
2661
+ */
2662
+ private handlePrefetch(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
2663
+ if (payload.view !== "dashboard" || !Array.isArray(payload.elements)) {
2664
+ this.sendResult(packageName, requestId, false, undefined, {
2665
+ code: MiniappErrorCode.INVALID_ARGUMENT,
2666
+ message: 'prefetch takes the "dashboard" view and an elements array',
2667
+ })
2668
+ return
2669
+ }
2670
+ if (!this.connectedApps.has(packageName) || !isDashboardApp(packageName) || !isOverlayApp(packageName)) {
2671
+ this.sendResult(packageName, requestId, true, {status: "unsupported", reason: "not the active dashboard"})
2672
+ return
2673
+ }
2674
+ const sent = localDisplayManager.prefetch(packageName, "dashboard", payload.elements as NonNullable<DisplayPayload["scene"]>)
2675
+ this.sendResult(packageName, requestId, true, sent ? {status: "queued"} : {status: "unsupported", reason: "no scene display"})
2676
+ }
2677
+
2654
2678
  /**
2655
2679
  * Reject the request with PERMISSION_NOT_DECLARED unless the caller's
2656
2680
  * manifest declares `permission`. System apps pass without declaring it.
@@ -283,6 +283,34 @@ class SceneRenderer {
283
283
  }
284
284
  }
285
285
 
286
+ /**
287
+ * Hand native the frame `elements` would make for (app, view) without
288
+ * showing it or touching the retained scene: drivers with an on-glass asset
289
+ * cache upload its fonts and images while the panel is idle. Every element
290
+ * is "created" and the frame carries seq 0 (no presentation report). Empty
291
+ * elements drop it. False when there is nothing to send it to.
292
+ */
293
+ public prefetchScene(appId: string, view: "main" | "dashboard", elements: SceneElementInput[]): boolean {
294
+ const caps = this.currentCapabilities()
295
+ if (!caps?.canPosition) return false
296
+ const processed = processScene(resolveAnchors(elements, caps.width, caps.height), caps, displayProcessor.getSceneProfile())
297
+ let synthetic = 0
298
+ const {elements: created} = diffScene([], processed.elements, () => `~prefetch${synthetic++}`)
299
+ const frame: SceneFrame = {
300
+ appId,
301
+ view,
302
+ sceneEpoch: 0,
303
+ seq: 0,
304
+ elements: created,
305
+ removed: [],
306
+ canvas: {width: caps.width, height: caps.height},
307
+ }
308
+ void Promise.resolve(BluetoothSdk.displayEvent({view, scene: frame, prefetch: true})).catch((err) => {
309
+ console.error(`${LOG_TAG}: native prefetch failed:`, err)
310
+ })
311
+ return true
312
+ }
313
+
286
314
  /**
287
315
  * Re-emit an app's retained scene (reconnect / arbitration restore). Replay
288
316
  * is always create-based — the device rebuilds from scratch (spec §4).
@@ -388,17 +388,6 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
388
388
  // owns the settings store).
389
389
  useSettingsStore.getState().setSetting(SETTINGS.has_ever_activated_app.key, true)
390
390
 
391
- // If the tapped app is already running, this is a re-open: the nav/foreground
392
- // above already drove the animation, and there's nothing to spawn. Short-
393
- // circuit before the spawn pipeline. A promoted app keeps its context but
394
- // still takes the foreground below.
395
- if (app.running && !app.widgetOnly) {
396
- saveLastOpenTime(packageName)
397
- // The running background only knows the source of its spawn.
398
- if (app.local) miniappLauncher.notifyLaunch(packageName, opts?.launchSource ?? "phone")
399
- return true
400
- }
401
-
402
391
  // Only-one rules: one standard app in the foreground, one dashboard
403
392
  // drawing the overlay. Starting another stops the one running. Widget-only
404
393
  // peers hold no foreground, so they stay.
@@ -411,6 +400,17 @@ export const useAppStatusStore = create<AppStatusState>((set, get) => ({
411
400
  }
412
401
  }
413
402
 
403
+ // If the tapped app is already running, this is a re-open: the nav/foreground
404
+ // above already drove the animation, and there's nothing to spawn. Short-
405
+ // circuit before the spawn pipeline. A promoted app keeps its context but
406
+ // still takes the foreground below.
407
+ if (app.running && !app.widgetOnly) {
408
+ saveLastOpenTime(packageName)
409
+ // The running background only knows the source of its spawn.
410
+ if (app.local) miniappLauncher.notifyLaunch(packageName, opts?.launchSource ?? "phone")
411
+ return true
412
+ }
413
+
414
414
  set((s) => ({
415
415
  apps: s.apps.map((a) => (a.packageName === packageName ? {...a, running: true, loading: false} : a)),
416
416
  }))