@glassly/engine 0.3.5-beta.710 → 0.3.5-beta.760
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/build/engine.d.ts +4 -1
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +5 -1
- package/build/engine.js.map +1 -1
- package/build/facades/ai.d.ts +3 -3
- package/build/facades/ai.d.ts.map +1 -1
- package/build/facades/ai.js +3 -3
- package/build/facades/ai.js.map +1 -1
- package/build/facades/glasses.d.ts +5 -1
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +5 -0
- package/build/facades/glasses.js.map +1 -1
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/GestureDotsService.d.ts.map +1 -1
- package/build/services/GestureDotsService.js +2 -1
- package/build/services/GestureDotsService.js.map +1 -1
- package/build/services/GlassesTouchInput.d.ts +6 -4
- package/build/services/GlassesTouchInput.d.ts.map +1 -1
- package/build/services/GlassesTouchInput.js +76 -37
- package/build/services/GlassesTouchInput.js.map +1 -1
- package/build/services/LocalDisplayManager.d.ts +12 -4
- package/build/services/LocalDisplayManager.d.ts.map +1 -1
- package/build/services/LocalDisplayManager.js +21 -7
- package/build/services/LocalDisplayManager.js.map +1 -1
- package/build/services/LocalLlmModelManager.d.ts +3 -1
- package/build/services/LocalLlmModelManager.d.ts.map +1 -1
- package/build/services/LocalLlmModelManager.js +13 -5
- package/build/services/LocalLlmModelManager.js.map +1 -1
- package/build/services/LocalLlmPrompt.d.ts +10 -9
- package/build/services/LocalLlmPrompt.d.ts.map +1 -1
- package/build/services/LocalLlmPrompt.js +43 -32
- package/build/services/LocalLlmPrompt.js.map +1 -1
- package/build/services/LocalLlmProvider.d.ts.map +1 -1
- package/build/services/LocalLlmProvider.js +39 -20
- package/build/services/LocalLlmProvider.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +22 -4
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +108 -134
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MiniappExitGesture.d.ts +6 -3
- package/build/services/MiniappExitGesture.d.ts.map +1 -1
- package/build/services/MiniappExitGesture.js +4 -4
- package/build/services/MiniappExitGesture.js.map +1 -1
- package/build/services/MiniappWidgetMode.d.ts +6 -4
- package/build/services/MiniappWidgetMode.d.ts.map +1 -1
- package/build/services/MiniappWidgetMode.js +7 -6
- package/build/services/MiniappWidgetMode.js.map +1 -1
- package/build/services/SceneRenderer.d.ts.map +1 -1
- package/build/services/SceneRenderer.js +3 -1
- package/build/services/SceneRenderer.js.map +1 -1
- package/build/services/actionInvoke.d.ts +37 -0
- package/build/services/actionInvoke.d.ts.map +1 -0
- package/build/services/actionInvoke.js +89 -0
- package/build/services/actionInvoke.js.map +1 -0
- package/build/services/appleWatch/AppleWatchLink.d.ts +42 -0
- package/build/services/appleWatch/AppleWatchLink.d.ts.map +1 -0
- package/build/services/appleWatch/AppleWatchLink.js +259 -0
- package/build/services/appleWatch/AppleWatchLink.js.map +1 -0
- package/build/services/controllers.d.ts.map +1 -1
- package/build/services/controllers.js +16 -0
- package/build/services/controllers.js.map +1 -1
- package/build/services/settingsBridge.d.ts +4 -4
- package/build/services/settingsBridge.d.ts.map +1 -1
- package/build/services/settingsBridge.js +9 -4
- package/build/services/settingsBridge.js.map +1 -1
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +13 -11
- package/build/stores/apps.js.map +1 -1
- package/build/stores/bluetoothSettingKeys.d.ts +2 -0
- package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
- package/build/stores/bluetoothSettingKeys.js +17 -0
- package/build/stores/bluetoothSettingKeys.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +32 -2
- package/build/stores/settings.js.map +1 -1
- package/build/types/capabilities/rayneo-io.d.ts.map +1 -1
- package/build/types/capabilities/rayneo-io.js +2 -0
- package/build/types/capabilities/rayneo-io.js.map +1 -1
- package/build/types/enums.d.ts +2 -1
- package/build/types/enums.d.ts.map +1 -1
- package/build/types/enums.js +1 -0
- package/build/types/enums.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.map +1 -1
- package/build/utils/display/scene/process.d.ts.map +1 -1
- package/build/utils/display/scene/process.js +20 -1
- package/build/utils/display/scene/process.js.map +1 -1
- package/build/utils/display/scene/types.d.ts +1 -1
- package/build/utils/display/scene/types.d.ts.map +1 -1
- package/build/utils/display/scene/types.js.map +1 -1
- package/package.json +7 -7
- package/src/engine.ts +5 -1
- package/src/facades/ai.ts +3 -3
- package/src/facades/glasses.ts +7 -0
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/services/GestureDotsService.ts +2 -1
- package/src/services/GlassesTouchInput.ts +81 -34
- package/src/services/LocalDisplayManager.ts +22 -8
- package/src/services/LocalLlmModelManager.ts +16 -6
- package/src/services/LocalLlmPrompt.ts +56 -37
- package/src/services/LocalLlmProvider.ts +49 -22
- package/src/services/LocalMiniappRuntime.ts +140 -134
- package/src/services/MiniappExitGesture.ts +8 -5
- package/src/services/MiniappWidgetMode.ts +9 -7
- package/src/services/SceneRenderer.ts +3 -1
- package/src/services/actionInvoke.ts +105 -0
- package/src/services/appleWatch/AppleWatchLink.ts +297 -0
- package/src/services/controllers.ts +17 -0
- package/src/services/settingsBridge.ts +9 -4
- package/src/stores/apps.ts +12 -10
- package/src/stores/bluetoothSettingKeys.ts +20 -0
- package/src/stores/settings.ts +34 -2
- package/src/types/capabilities/rayneo-io.ts +2 -0
- package/src/types/enums.ts +1 -0
- package/src/utils/display/scene/index.ts +1 -0
- package/src/utils/display/scene/process.ts +17 -1
- package/src/utils/display/scene/types.ts +1 -0
|
@@ -162,6 +162,13 @@ import {invokeScanQrSeam} from "../runtime/scanQrSeam"
|
|
|
162
162
|
import {invokeWebLoginSeam} from "../runtime/webLoginSeam"
|
|
163
163
|
import {normalizeStreamAudioConfig, normalizeStreamVideoConfig} from "../runtime/streamConfig"
|
|
164
164
|
import {visibleActionRows} from "./manifestActions"
|
|
165
|
+
import {
|
|
166
|
+
actionFailure,
|
|
167
|
+
appDeclaresAction,
|
|
168
|
+
checkActionAccess,
|
|
169
|
+
PendingActionCalls,
|
|
170
|
+
type ActionOutcome,
|
|
171
|
+
} from "./actionInvoke"
|
|
165
172
|
import {toLanguageHint} from "@glassly/cloud-protocol/languages"
|
|
166
173
|
import {buildMiniappManifestSnapshot, type MiniappRuntimeDiagnosticSnapshot} from "../utils/miniappDiagnostics"
|
|
167
174
|
import ttsModelManager from "./TTSModelManager"
|
|
@@ -641,6 +648,10 @@ class LocalMiniappRuntime {
|
|
|
641
648
|
private connectedApps: Map<string, ConnectedMiniapp> = new Map()
|
|
642
649
|
/** Claimed gestures (CLAIM_GESTURES): a stack per gesture, the most recent claimer holding it. */
|
|
643
650
|
private gestureClaims: GestureClaimState = emptyGestureClaims()
|
|
651
|
+
// The last main-view frame each app asked to render. Widget mode refuses
|
|
652
|
+
// frames and demotion clears them, and an app that has drawn once may not
|
|
653
|
+
// draw again on its own, so promotion replays this.
|
|
654
|
+
private retainedRenders = new Map<string, Record<string, unknown>>()
|
|
644
655
|
|
|
645
656
|
/**
|
|
646
657
|
* Packages that have completed their CONNECT handshake (sent
|
|
@@ -705,7 +716,13 @@ class LocalMiniappRuntime {
|
|
|
705
716
|
private localSecret = `miniapp_${Date.now()}_${Math.random().toString(36).slice(2, 14)}`
|
|
706
717
|
private usedTokens = new Set<string>()
|
|
707
718
|
|
|
708
|
-
private constructor() {
|
|
719
|
+
private constructor() {
|
|
720
|
+
miniappWidgetMode.subscribe((packageName, widgetOnly) => {
|
|
721
|
+
// Promotion happens inside the apps store's start(); the core-app slot
|
|
722
|
+
// settles with that store write, so replay once it has.
|
|
723
|
+
if (!widgetOnly) queueMicrotask(() => this.replayRetainedRender(packageName))
|
|
724
|
+
})
|
|
725
|
+
}
|
|
709
726
|
|
|
710
727
|
/** Snapshot of miniapps actively subscribed to hardware button events. */
|
|
711
728
|
public getButtonPressSubscribers(): string[] {
|
|
@@ -1122,7 +1139,11 @@ class LocalMiniappRuntime {
|
|
|
1122
1139
|
// window ends early once this app pushes its first display (see
|
|
1123
1140
|
// LocalDisplayManager.request), or after ~1.5s. Fires once per spawn —
|
|
1124
1141
|
// including a crash-respawn, since each spawn is a fresh "app is starting".
|
|
1125
|
-
|
|
1142
|
+
// A spawn for a dashboard widget is not the wearer starting the app: it draws
|
|
1143
|
+
// nothing of its own, so it shows no boot text either.
|
|
1144
|
+
if (!miniappWidgetMode.isWidgetOnly(packageName)) {
|
|
1145
|
+
localDisplayManager.onMount(packageName, installedManifest?.name ?? packageName)
|
|
1146
|
+
}
|
|
1126
1147
|
}
|
|
1127
1148
|
|
|
1128
1149
|
/**
|
|
@@ -1244,6 +1265,7 @@ class LocalMiniappRuntime {
|
|
|
1244
1265
|
}
|
|
1245
1266
|
// Its gesture claims go back to whoever claimed them before.
|
|
1246
1267
|
this.applyGestureClaimChanges(dropGestureClaims(this.gestureClaims, packageName))
|
|
1268
|
+
this.retainedRenders.delete(packageName)
|
|
1247
1269
|
|
|
1248
1270
|
// Warm-up is a request-owned camera lease. Cancel it even if the app record was already
|
|
1249
1271
|
// removed so a close racing the camera-open promise cannot leave the sensor running.
|
|
@@ -2489,11 +2511,6 @@ class LocalMiniappRuntime {
|
|
|
2489
2511
|
this.sendResult(packageName, requestId, true, {status: "blocked", reason: "app stopped"})
|
|
2490
2512
|
return
|
|
2491
2513
|
}
|
|
2492
|
-
// A widget-only app draws through its widget alone.
|
|
2493
|
-
if (miniappWidgetMode.isWidgetOnly(packageName)) {
|
|
2494
|
-
this.sendResult(packageName, requestId, true, {status: "blocked", reason: "widget mode"})
|
|
2495
|
-
return
|
|
2496
|
-
}
|
|
2497
2514
|
if (!Array.isArray(payload.elements)) {
|
|
2498
2515
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
2499
2516
|
code: MiniappErrorCode.INTERNAL,
|
|
@@ -2501,6 +2518,15 @@ class LocalMiniappRuntime {
|
|
|
2501
2518
|
})
|
|
2502
2519
|
return
|
|
2503
2520
|
}
|
|
2521
|
+
if (payload.view !== "dashboard") {
|
|
2522
|
+
if (payload.elements.length === 0) this.retainedRenders.delete(packageName)
|
|
2523
|
+
else this.retainedRenders.set(packageName, payload)
|
|
2524
|
+
}
|
|
2525
|
+
// A widget-only app draws through its widget alone; the frame waits for promotion.
|
|
2526
|
+
if (miniappWidgetMode.isWidgetOnly(packageName)) {
|
|
2527
|
+
this.sendResult(packageName, requestId, true, {status: "blocked", reason: "widget mode"})
|
|
2528
|
+
return
|
|
2529
|
+
}
|
|
2504
2530
|
// The dashboard view is the overlay app's alone; anyone else's frame
|
|
2505
2531
|
// is dropped, reported rather than silently lost.
|
|
2506
2532
|
if (payload.view === "dashboard" && !isOverlayApp(packageName)) {
|
|
@@ -2530,6 +2556,13 @@ class LocalMiniappRuntime {
|
|
|
2530
2556
|
}
|
|
2531
2557
|
}
|
|
2532
2558
|
|
|
2559
|
+
/** Put a promoted app's last main-view frame on the glasses; widget mode had refused it. */
|
|
2560
|
+
private replayRetainedRender(packageName: string): void {
|
|
2561
|
+
const payload = this.retainedRenders.get(packageName)
|
|
2562
|
+
if (!payload || !this.connectedApps.has(packageName) || miniappWidgetMode.isWidgetOnly(packageName)) return
|
|
2563
|
+
this.handleRender(packageName, payload)
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2533
2566
|
/**
|
|
2534
2567
|
* display.showView: the active dashboard takes or releases the overlay
|
|
2535
2568
|
* slot. Refused (NOT_PERMITTED) for an app that is not a dashboard;
|
|
@@ -5995,10 +6028,7 @@ class LocalMiniappRuntime {
|
|
|
5995
6028
|
private static readonly ACTION_PAYLOAD_CAP = 256 * 1024
|
|
5996
6029
|
|
|
5997
6030
|
/** Outstanding action invocations, keyed by host-generated callId. */
|
|
5998
|
-
private actionCalls = new
|
|
5999
|
-
string,
|
|
6000
|
-
{callerPackageName: string; targetPackageName: string; callerRequestId?: string; timer: number}
|
|
6001
|
-
>()
|
|
6031
|
+
private actionCalls = new PendingActionCalls(BgTimer, (value) => this.actionPayloadTooLarge(value))
|
|
6002
6032
|
private actionCallSeq = 0
|
|
6003
6033
|
|
|
6004
6034
|
private interopApps(): ClientApp[] {
|
|
@@ -6381,10 +6411,12 @@ class LocalMiniappRuntime {
|
|
|
6381
6411
|
}
|
|
6382
6412
|
const provider = app.packageName
|
|
6383
6413
|
// Record the placement before the wake so the spawn lands in widget mode
|
|
6384
|
-
// and an unplace racing it cannot stop the provider early.
|
|
6414
|
+
// and an unplace racing it cannot stop the provider early. A provider the
|
|
6415
|
+
// store already runs (a wearer's launch still spawning, say) stays a
|
|
6416
|
+
// normal run; the registry alone lags that launch.
|
|
6385
6417
|
this.widgetPlacements = placeWidget(this.widgetPlacements, {hostPackageName, providerPackageName: provider, widgetId, box})
|
|
6386
6418
|
miniappWidgetMode.setPlaced(provider, true)
|
|
6387
|
-
if (!miniappRunningRegistry.has(provider)) miniappWidgetMode.setWidgetOnly(provider, true)
|
|
6419
|
+
if (!app.running && !miniappRunningRegistry.has(provider)) miniappWidgetMode.setWidgetOnly(provider, true)
|
|
6388
6420
|
try {
|
|
6389
6421
|
await this.wakeInteropApp(provider)
|
|
6390
6422
|
} catch (e) {
|
|
@@ -6568,111 +6600,118 @@ class LocalMiniappRuntime {
|
|
|
6568
6600
|
}
|
|
6569
6601
|
}
|
|
6570
6602
|
|
|
6603
|
+
/** Floored at 6s: a freshly-woken target buffers an undelivered ACTION_CALL for 5s (HANDLER_WAIT_MS in @glassly/miniapp's actions module) + 1s for the reply. Keep in sync. */
|
|
6604
|
+
private static clampActionTimeout(raw: unknown): number {
|
|
6605
|
+
return Math.min(Math.max(Number(raw) || 30_000, 6_000), 120_000)
|
|
6606
|
+
}
|
|
6607
|
+
|
|
6608
|
+
/** Caller identity the host presents for host-initiated actions (a system caller). */
|
|
6609
|
+
private static readonly HOST_ACTION_CALLER = "host"
|
|
6610
|
+
|
|
6611
|
+
/** Installed, compatible, and declares the action — the same gates `invokeActionFromHost` applies up front. */
|
|
6612
|
+
public hostActionAvailable(targetPackageName: string, actionId: string): boolean {
|
|
6613
|
+
return appDeclaresAction(
|
|
6614
|
+
this.interopApps().find((a) => a.packageName === targetPackageName),
|
|
6615
|
+
actionId,
|
|
6616
|
+
)
|
|
6617
|
+
}
|
|
6618
|
+
|
|
6571
6619
|
/**
|
|
6572
|
-
*
|
|
6573
|
-
* and
|
|
6620
|
+
* Host-initiated action call (e.g. the watch remote): same gates, headless
|
|
6621
|
+
* wake and ACTION_CALL/ACTION_RESULT correlation as a miniapp's
|
|
6622
|
+
* session.actions.invoke, with the host as a system caller.
|
|
6623
|
+
* Rejects with an Error whose `code` is a MiniappErrorCode.
|
|
6574
6624
|
*/
|
|
6625
|
+
public async invokeActionFromHost(
|
|
6626
|
+
targetPackageName: string,
|
|
6627
|
+
actionId: string,
|
|
6628
|
+
params: Record<string, unknown> = {},
|
|
6629
|
+
timeoutMs?: number,
|
|
6630
|
+
): Promise<unknown> {
|
|
6631
|
+
const outcome = await this.runActionInvoke(
|
|
6632
|
+
LocalMiniappRuntime.HOST_ACTION_CALLER,
|
|
6633
|
+
true,
|
|
6634
|
+
targetPackageName,
|
|
6635
|
+
actionId,
|
|
6636
|
+
params,
|
|
6637
|
+
LocalMiniappRuntime.clampActionTimeout(timeoutMs),
|
|
6638
|
+
)
|
|
6639
|
+
if (outcome.ok) return outcome.data
|
|
6640
|
+
throw Object.assign(new Error(outcome.error.message), {code: outcome.error.code})
|
|
6641
|
+
}
|
|
6642
|
+
|
|
6643
|
+
/** session.actions.invoke → run the call, then relay the outcome to the caller's pending request. */
|
|
6575
6644
|
private async handleActionInvoke(
|
|
6576
6645
|
callerPackageName: string,
|
|
6577
6646
|
payload: Record<string, unknown>,
|
|
6578
6647
|
requestId?: string,
|
|
6579
6648
|
): Promise<void> {
|
|
6580
|
-
const
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6649
|
+
const outcome = await this.runActionInvoke(
|
|
6650
|
+
callerPackageName,
|
|
6651
|
+
this.isSystemPackage(callerPackageName),
|
|
6652
|
+
payload.targetPackageName as string | undefined,
|
|
6653
|
+
payload.actionId as string | undefined,
|
|
6654
|
+
(payload.params as Record<string, unknown> | undefined) ?? {},
|
|
6655
|
+
LocalMiniappRuntime.clampActionTimeout(payload.timeoutMs),
|
|
6656
|
+
)
|
|
6657
|
+
if (outcome.ok) this.sendResult(callerPackageName, requestId, true, outcome.data)
|
|
6658
|
+
else this.sendResult(callerPackageName, requestId, false, undefined, outcome.error)
|
|
6659
|
+
}
|
|
6660
|
+
|
|
6661
|
+
/**
|
|
6662
|
+
* Gate, wake the target headlessly, deliver an ACTION_CALL, and resolve when
|
|
6663
|
+
* its ACTION_RESULT (or the timeout) arrives. Never rejects.
|
|
6664
|
+
*/
|
|
6665
|
+
private async runActionInvoke(
|
|
6666
|
+
callerPackageName: string,
|
|
6667
|
+
callerIsSystem: boolean,
|
|
6668
|
+
target: string | undefined,
|
|
6669
|
+
actionId: string | undefined,
|
|
6670
|
+
params: Record<string, unknown>,
|
|
6671
|
+
timeoutMs: number,
|
|
6672
|
+
): Promise<ActionOutcome> {
|
|
6673
|
+
const fail = actionFailure
|
|
6591
6674
|
|
|
6592
6675
|
if (this.actionPayloadTooLarge(params)) {
|
|
6593
|
-
|
|
6594
|
-
code: MiniappErrorCode.PAYLOAD_TOO_LARGE,
|
|
6595
|
-
message: "action params exceeded the 256 KB cap",
|
|
6596
|
-
})
|
|
6597
|
-
return
|
|
6676
|
+
return fail(MiniappErrorCode.PAYLOAD_TOO_LARGE, "action params exceeded the 256 KB cap")
|
|
6598
6677
|
}
|
|
6599
6678
|
|
|
6600
6679
|
const app = target ? this.interopApps().find((a) => a.packageName === target) : undefined
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
packageName: target,
|
|
6623
|
-
reason: `${target} is not compatible with the connected glasses`,
|
|
6624
|
-
metadata: {missingRequired: app.compatibility.missingRequired ?? [], via: "miniapps.invoke"},
|
|
6625
|
-
timestamp: Date.now(),
|
|
6626
|
-
})
|
|
6627
|
-
return
|
|
6628
|
-
}
|
|
6629
|
-
// Declared-action gate: the target must declare the action in its manifest.
|
|
6630
|
-
const declared = (app.actions ?? []).find((a) => a.id === actionId)
|
|
6631
|
-
if (!declared) {
|
|
6632
|
-
this.sendResult(callerPackageName, requestId, false, undefined, {
|
|
6633
|
-
code: MiniappErrorCode.ACTION_NOT_FOUND,
|
|
6634
|
-
message: `${target} does not declare action "${actionId}"`,
|
|
6635
|
-
})
|
|
6636
|
-
return
|
|
6637
|
-
}
|
|
6638
|
-
// Visibility gate: declared actions are public by default; ones marked
|
|
6639
|
-
// `visibility: "system"` stay restricted to system/dev callers.
|
|
6640
|
-
if (declared.visibility === "system" && !this.isSystemPackage(callerPackageName)) {
|
|
6641
|
-
this.auditInterop({
|
|
6642
|
-
caller: callerPackageName,
|
|
6643
|
-
op: "invoke",
|
|
6644
|
-
target,
|
|
6645
|
-
actionId,
|
|
6646
|
-
ok: false,
|
|
6647
|
-
errorCode: MiniappErrorCode.NOT_PERMITTED,
|
|
6648
|
-
})
|
|
6649
|
-
this.sendResult(callerPackageName, requestId, false, undefined, {
|
|
6650
|
-
code: MiniappErrorCode.NOT_PERMITTED,
|
|
6651
|
-
message: `Action "${actionId}" on ${target} is restricted to system apps`,
|
|
6652
|
-
})
|
|
6653
|
-
return
|
|
6680
|
+
const denied = checkActionAccess(app, target, actionId, callerIsSystem)
|
|
6681
|
+
if (denied) {
|
|
6682
|
+
if (denied.code === MiniappErrorCode.APP_NOT_COMPATIBLE && target && app) {
|
|
6683
|
+
islandNotifications.emit({
|
|
6684
|
+
kind: "version_incompatible",
|
|
6685
|
+
packageName: target,
|
|
6686
|
+
reason: denied.message,
|
|
6687
|
+
metadata: {missingRequired: app.compatibility?.missingRequired ?? [], via: "miniapps.invoke"},
|
|
6688
|
+
timestamp: Date.now(),
|
|
6689
|
+
})
|
|
6690
|
+
} else if (denied.code === MiniappErrorCode.NOT_PERMITTED) {
|
|
6691
|
+
this.auditInterop({
|
|
6692
|
+
caller: callerPackageName,
|
|
6693
|
+
op: "invoke",
|
|
6694
|
+
target,
|
|
6695
|
+
actionId,
|
|
6696
|
+
ok: false,
|
|
6697
|
+
errorCode: MiniappErrorCode.NOT_PERMITTED,
|
|
6698
|
+
})
|
|
6699
|
+
}
|
|
6700
|
+
return {ok: false, error: denied}
|
|
6654
6701
|
}
|
|
6702
|
+
if (!target || !actionId) return fail(MiniappErrorCode.INTERNAL, "unreachable")
|
|
6655
6703
|
|
|
6656
6704
|
// Headless wake + wait for CONNECT (idempotent / fast if already connected).
|
|
6657
6705
|
try {
|
|
6658
6706
|
await this.wakeInteropApp(target)
|
|
6659
6707
|
} catch (e) {
|
|
6660
|
-
|
|
6661
|
-
code: MiniappErrorCode.WAKE_FAILED,
|
|
6662
|
-
message: (e as Error)?.message ?? `failed to wake ${target}`,
|
|
6663
|
-
})
|
|
6664
|
-
return
|
|
6708
|
+
return fail(MiniappErrorCode.WAKE_FAILED, (e as Error)?.message ?? `failed to wake ${target}`)
|
|
6665
6709
|
}
|
|
6666
6710
|
|
|
6667
6711
|
// The wake waited for CONNECT, but the target could have dropped in the gap
|
|
6668
6712
|
// before delivery — fail fast rather than arming a timer for a call that
|
|
6669
|
-
// sendToMiniapp would silently drop
|
|
6670
|
-
// full invoke timeout for an ACTION_CALL that was never delivered).
|
|
6713
|
+
// sendToMiniapp would silently drop.
|
|
6671
6714
|
if (!this.connectedApps.has(target)) {
|
|
6672
|
-
this.sendResult(callerPackageName, requestId, false, undefined, {
|
|
6673
|
-
code: MiniappErrorCode.WAKE_FAILED,
|
|
6674
|
-
message: `${target} disconnected before the action could be delivered`,
|
|
6675
|
-
})
|
|
6676
6715
|
this.auditInterop({
|
|
6677
6716
|
caller: callerPackageName,
|
|
6678
6717
|
op: "invoke",
|
|
@@ -6681,19 +6720,12 @@ class LocalMiniappRuntime {
|
|
|
6681
6720
|
ok: false,
|
|
6682
6721
|
errorCode: MiniappErrorCode.WAKE_FAILED,
|
|
6683
6722
|
})
|
|
6684
|
-
return
|
|
6723
|
+
return fail(MiniappErrorCode.WAKE_FAILED, `${target} disconnected before the action could be delivered`)
|
|
6685
6724
|
}
|
|
6686
6725
|
|
|
6687
|
-
// Deliver the call and arm the handler timeout; ACTION_RESULT
|
|
6726
|
+
// Deliver the call and arm the handler timeout; ACTION_RESULT settles it.
|
|
6688
6727
|
const callId = `act-${Date.now().toString(36)}-${this.actionCallSeq++}`
|
|
6689
|
-
const
|
|
6690
|
-
this.actionCalls.delete(callId)
|
|
6691
|
-
this.sendResult(callerPackageName, requestId, false, undefined, {
|
|
6692
|
-
code: MiniappErrorCode.ACTION_TIMEOUT,
|
|
6693
|
-
message: `action "${actionId}" timed out after ${timeoutMs}ms`,
|
|
6694
|
-
})
|
|
6695
|
-
}, timeoutMs)
|
|
6696
|
-
this.actionCalls.set(callId, {callerPackageName, targetPackageName: target, callerRequestId: requestId, timer})
|
|
6728
|
+
const outcome = this.actionCalls.register(callId, target, actionId, timeoutMs)
|
|
6697
6729
|
this.sendToMiniapp(target, {
|
|
6698
6730
|
type: MiniappResponseType.ACTION_CALL,
|
|
6699
6731
|
callId,
|
|
@@ -6702,38 +6734,12 @@ class LocalMiniappRuntime {
|
|
|
6702
6734
|
callerPackageName,
|
|
6703
6735
|
})
|
|
6704
6736
|
this.auditInterop({caller: callerPackageName, op: "invoke", target, actionId, ok: true})
|
|
6737
|
+
return outcome
|
|
6705
6738
|
}
|
|
6706
6739
|
|
|
6707
|
-
/** Target → host:
|
|
6740
|
+
/** Target → host: settle the pending invoke for this ACTION_RESULT. */
|
|
6708
6741
|
private handleActionResult(targetPackageName: string, payload: Record<string, unknown>): void {
|
|
6709
|
-
|
|
6710
|
-
if (!callId) return
|
|
6711
|
-
const pending = this.actionCalls.get(callId)
|
|
6712
|
-
if (!pending) return
|
|
6713
|
-
// Only the invoked target may resolve this call. callIds are guessable
|
|
6714
|
-
// (timestamp + seq), so a different connected miniapp must not be able to
|
|
6715
|
-
// spoof a result/error for someone else's invoke.
|
|
6716
|
-
if (pending.targetPackageName !== targetPackageName) return
|
|
6717
|
-
this.actionCalls.delete(callId)
|
|
6718
|
-
BgTimer.clearTimeout(pending.timer)
|
|
6719
|
-
|
|
6720
|
-
if (payload.ok === true) {
|
|
6721
|
-
const result = payload.result
|
|
6722
|
-
if (this.actionPayloadTooLarge(result)) {
|
|
6723
|
-
this.sendResult(pending.callerPackageName, pending.callerRequestId, false, undefined, {
|
|
6724
|
-
code: MiniappErrorCode.PAYLOAD_TOO_LARGE,
|
|
6725
|
-
message: "action result exceeded the 256 KB cap",
|
|
6726
|
-
})
|
|
6727
|
-
return
|
|
6728
|
-
}
|
|
6729
|
-
this.sendResult(pending.callerPackageName, pending.callerRequestId, true, result ?? null)
|
|
6730
|
-
} else {
|
|
6731
|
-
const error = (payload.error as ({code: string; message: string} & Record<string, unknown>) | undefined) ?? {
|
|
6732
|
-
code: MiniappErrorCode.INTERNAL,
|
|
6733
|
-
message: "action handler error",
|
|
6734
|
-
}
|
|
6735
|
-
this.sendResult(pending.callerPackageName, pending.callerRequestId, false, undefined, error)
|
|
6736
|
-
}
|
|
6742
|
+
this.actionCalls.handleResult(targetPackageName, payload)
|
|
6737
6743
|
}
|
|
6738
6744
|
|
|
6739
6745
|
private sendResult(
|
|
@@ -4,6 +4,8 @@ interface ExitGestureApp {
|
|
|
4
4
|
packageName: string
|
|
5
5
|
name: string
|
|
6
6
|
running: boolean
|
|
7
|
+
/** Running only for a placed widget: the wearer is not in it, so there is nothing to leave. */
|
|
8
|
+
widgetOnly?: boolean
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
interface ExitGestureState {
|
|
@@ -15,7 +17,8 @@ interface ExitGestureState {
|
|
|
15
17
|
|
|
16
18
|
export interface ExitGestureDependencies {
|
|
17
19
|
getState: () => ExitGestureState
|
|
18
|
-
|
|
20
|
+
/** The miniapp a double tap leaves right now, or null when the tap is the system's. */
|
|
21
|
+
getTargetPackageName: () => string | null
|
|
19
22
|
beginExitNotice: (packageName: string, text: string) => boolean
|
|
20
23
|
finishExitNotice: (packageName: string) => void
|
|
21
24
|
cancelExitNotice: (packageName: string) => void
|
|
@@ -29,16 +32,16 @@ export class MiniappExitGesture {
|
|
|
29
32
|
|
|
30
33
|
public constructor(private readonly deps: ExitGestureDependencies) {}
|
|
31
34
|
|
|
32
|
-
/** Returns true when the system gesture was consumed by a
|
|
33
|
-
public
|
|
35
|
+
/** Returns true when the system gesture was consumed by a miniapp exit. */
|
|
36
|
+
public tryExitMiniapp(): boolean {
|
|
34
37
|
// Swallow repeat taps while the notice is already counting down.
|
|
35
38
|
if (this.pending) return true
|
|
36
39
|
|
|
37
|
-
const packageName = this.deps.
|
|
40
|
+
const packageName = this.deps.getTargetPackageName()
|
|
38
41
|
if (!packageName) return false
|
|
39
42
|
|
|
40
43
|
const app = this.deps.getState().apps.find((candidate) => candidate.packageName === packageName)
|
|
41
|
-
if (!app?.running) return false
|
|
44
|
+
if (!app?.running || app.widgetOnly) return false
|
|
42
45
|
|
|
43
46
|
const name = app.name.trim() || packageName
|
|
44
47
|
if (!this.deps.beginExitNotice(packageName, `Exiting ${name}…`)) return false
|
|
@@ -2,25 +2,27 @@
|
|
|
2
2
|
* Widget mode: a miniapp whose background context runs only because a host
|
|
3
3
|
* (the dashboard) placed one of its widgets. It is running, but it has no
|
|
4
4
|
* card in the app switcher, draws nothing of its own on the glasses and
|
|
5
|
-
* hears no touch input; the Widgets pill lists it.
|
|
6
|
-
*
|
|
7
|
-
* tray demotes it back here instead of
|
|
5
|
+
* hears no touch input; the Widgets pill lists it. Foregrounding the app on
|
|
6
|
+
* the phone promotes it to a normal run (the store's start/setForeground);
|
|
7
|
+
* closing a placed app from the tray demotes it back here instead of
|
|
8
|
+
* stopping it.
|
|
8
9
|
*
|
|
9
10
|
* The runtime is the writer (placements come and go through it); the apps
|
|
10
11
|
* store projects `widgetOnly` from here beside `running`.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
/** Called with the app whose mode changed and whether it is now widget-only. */
|
|
15
|
+
type Listener = (packageName: string, widgetOnly: boolean) => void
|
|
14
16
|
|
|
15
17
|
const widgetOnly = new Set<string>()
|
|
16
18
|
/** Providers with at least one widget placed by some host. */
|
|
17
19
|
const placed = new Set<string>()
|
|
18
20
|
const listeners = new Set<Listener>()
|
|
19
21
|
|
|
20
|
-
function notify(): void {
|
|
22
|
+
function notify(packageName: string, on: boolean): void {
|
|
21
23
|
for (const fn of listeners) {
|
|
22
24
|
try {
|
|
23
|
-
fn()
|
|
25
|
+
fn(packageName, on)
|
|
24
26
|
} catch (e) {
|
|
25
27
|
console.warn("MiniappWidgetMode: listener threw", e)
|
|
26
28
|
}
|
|
@@ -32,7 +34,7 @@ export const miniappWidgetMode = {
|
|
|
32
34
|
setWidgetOnly(packageName: string, on: boolean): void {
|
|
33
35
|
const changed = on ? !widgetOnly.has(packageName) : widgetOnly.delete(packageName)
|
|
34
36
|
if (on) widgetOnly.add(packageName)
|
|
35
|
-
if (changed) notify()
|
|
37
|
+
if (changed) notify(packageName, on)
|
|
36
38
|
},
|
|
37
39
|
|
|
38
40
|
isWidgetOnly(packageName: string): boolean {
|
|
@@ -66,7 +66,9 @@ const MAX_SETTLE_MS = 255 * 33
|
|
|
66
66
|
function settleTimeOf(elements: FrameElement[]): number {
|
|
67
67
|
let ms = 0
|
|
68
68
|
for (const el of elements) {
|
|
69
|
-
|
|
69
|
+
// Entering and leaving elements tween too, from or to their offset.
|
|
70
|
+
const edge = el.transition?.exit !== undefined || (el.change === "created" && el.transition?.enter !== undefined)
|
|
71
|
+
if (el.change !== "updated" && el.change !== "moved" && !edge) continue
|
|
70
72
|
const t = el.transition?.durationMs ?? 0
|
|
71
73
|
// Rotation has its own millisecond clock, beyond the geometry frame limit.
|
|
72
74
|
const duration = Math.min(t, el.rotation !== undefined ? 65535 : MAX_SETTLE_MS)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {MiniappErrorCode} from "@glassly/miniapp"
|
|
2
|
+
|
|
3
|
+
import type {ClientApp} from "../types/applet"
|
|
4
|
+
|
|
5
|
+
export type ActionError = {code: string; message: string} & Record<string, unknown>
|
|
6
|
+
export type ActionOutcome = {ok: true; data: unknown} | {ok: false; error: ActionError}
|
|
7
|
+
|
|
8
|
+
export const actionFailure = (code: string, message: string): ActionOutcome => ({ok: false, error: {code, message}})
|
|
9
|
+
|
|
10
|
+
/** Installed, compatible, and declares the action. */
|
|
11
|
+
export function appDeclaresAction(app: ClientApp | undefined, actionId: string): boolean {
|
|
12
|
+
if (!app) return false
|
|
13
|
+
if (app.compatibility && app.compatibility.isCompatible === false) return false
|
|
14
|
+
return (app.actions ?? []).some((a) => a.id === actionId)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Gates shared by miniapp->miniapp invoke and host invoke. Declared actions are
|
|
19
|
+
* public by default; `visibility: "system"` ones need a system caller.
|
|
20
|
+
*/
|
|
21
|
+
export function checkActionAccess(
|
|
22
|
+
app: ClientApp | undefined,
|
|
23
|
+
target: string | undefined,
|
|
24
|
+
actionId: string | undefined,
|
|
25
|
+
callerIsSystem: boolean,
|
|
26
|
+
): ActionError | null {
|
|
27
|
+
if (!target || !app) {
|
|
28
|
+
return {
|
|
29
|
+
code: MiniappErrorCode.APP_NOT_FOUND,
|
|
30
|
+
message: `Miniapp not found: ${target ?? "(missing targetPackageName)"}`,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (!actionId) return {code: MiniappErrorCode.ACTION_NOT_FOUND, message: "invoke requires an actionId"}
|
|
34
|
+
if (app.compatibility && app.compatibility.isCompatible === false) {
|
|
35
|
+
return {
|
|
36
|
+
code: MiniappErrorCode.APP_NOT_COMPATIBLE,
|
|
37
|
+
message: `${target} is not compatible with the connected glasses`,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const declared = (app.actions ?? []).find((a) => a.id === actionId)
|
|
41
|
+
if (!declared) {
|
|
42
|
+
return {code: MiniappErrorCode.ACTION_NOT_FOUND, message: `${target} does not declare action "${actionId}"`}
|
|
43
|
+
}
|
|
44
|
+
if (declared.visibility === "system" && !callerIsSystem) {
|
|
45
|
+
return {
|
|
46
|
+
code: MiniappErrorCode.NOT_PERMITTED,
|
|
47
|
+
message: `Action "${actionId}" on ${target} is restricted to system apps`,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** BgTimer's shape; passed in so this module stays loadable without the native timer. */
|
|
54
|
+
export interface ActionTimers {
|
|
55
|
+
setTimeout: (cb: () => void, ms: number) => number
|
|
56
|
+
clearTimeout: (id: number) => void
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Outstanding ACTION_CALLs awaiting an ACTION_RESULT, keyed by host-generated callId. */
|
|
60
|
+
export class PendingActionCalls {
|
|
61
|
+
private readonly calls = new Map<string, {target: string; settle: (o: ActionOutcome) => void; timer: number}>()
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
private readonly timers: ActionTimers,
|
|
65
|
+
private readonly payloadTooLarge: (value: unknown) => boolean,
|
|
66
|
+
) {}
|
|
67
|
+
|
|
68
|
+
/** Register a call; resolves with its outcome, or ACTION_TIMEOUT. */
|
|
69
|
+
register(callId: string, target: string, actionId: string, timeoutMs: number): Promise<ActionOutcome> {
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
const timer = this.timers.setTimeout(() => {
|
|
72
|
+
this.calls.delete(callId)
|
|
73
|
+
resolve(actionFailure(MiniappErrorCode.ACTION_TIMEOUT, `action "${actionId}" timed out after ${timeoutMs}ms`))
|
|
74
|
+
}, timeoutMs)
|
|
75
|
+
this.calls.set(callId, {target, settle: resolve, timer})
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Settle from a target's ACTION_RESULT payload. Only the invoked target may resolve a call. */
|
|
80
|
+
handleResult(targetPackageName: string, payload: Record<string, unknown>): void {
|
|
81
|
+
const callId = payload.callId as string | undefined
|
|
82
|
+
if (!callId) return
|
|
83
|
+
const pending = this.calls.get(callId)
|
|
84
|
+
// callIds are guessable (timestamp + seq): a different miniapp must not spoof someone else's result.
|
|
85
|
+
if (!pending || pending.target !== targetPackageName) return
|
|
86
|
+
this.calls.delete(callId)
|
|
87
|
+
this.timers.clearTimeout(pending.timer)
|
|
88
|
+
|
|
89
|
+
if (payload.ok === true) {
|
|
90
|
+
if (this.payloadTooLarge(payload.result)) {
|
|
91
|
+
pending.settle(actionFailure(MiniappErrorCode.PAYLOAD_TOO_LARGE, "action result exceeded the 256 KB cap"))
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
pending.settle({ok: true, data: payload.result ?? null})
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
pending.settle({
|
|
98
|
+
ok: false,
|
|
99
|
+
error: (payload.error as ActionError | undefined) ?? {
|
|
100
|
+
code: MiniappErrorCode.INTERNAL,
|
|
101
|
+
message: "action handler error",
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|