@glassly/engine 0.3.7 → 0.3.8-dev.1000
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 +10 -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/ai.d.ts +3 -0
- package/build/facades/ai.d.ts.map +1 -1
- package/build/facades/ai.js +3 -0
- package/build/facades/ai.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 +2 -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/DisplayDiagnostics.d.ts.map +1 -1
- package/build/services/DisplayDiagnostics.js +4 -2
- package/build/services/DisplayDiagnostics.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/LocalDisplayManager.d.ts.map +1 -1
- package/build/services/LocalDisplayManager.js +5 -2
- package/build/services/LocalDisplayManager.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 +61 -22
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
- package/build/services/LocalSttFallbackCoordinator.js +2 -0
- package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
- package/build/services/LocalTranslationService.d.ts +2 -0
- package/build/services/LocalTranslationService.d.ts.map +1 -1
- package/build/services/LocalTranslationService.js +22 -0
- package/build/services/LocalTranslationService.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/OfflineActivity.d.ts +54 -0
- package/build/services/OfflineActivity.d.ts.map +1 -0
- package/build/services/OfflineActivity.js +86 -0
- package/build/services/OfflineActivity.js.map +1 -0
- 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/STTModelManager.d.ts +1 -0
- package/build/services/STTModelManager.d.ts.map +1 -1
- package/build/services/STTModelManager.js +6 -0
- package/build/services/STTModelManager.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 +12 -3
- package/build/services/controllers.js.map +1 -1
- package/build/services/effectiveCapabilities.js +6 -1
- 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 +22 -10
- package/build/stores/apps.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +63 -6
- 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 +42 -0
- package/build/types/capabilities/memomind-one.js.map +1 -0
- package/build/types/capabilities/rayneo-io.d.ts.map +1 -1
- package/build/types/capabilities/rayneo-io.js +3 -2
- package/build/types/capabilities/rayneo-io.js.map +1 -1
- package/build/types/enums.d.ts +7 -2
- package/build/types/enums.d.ts.map +1 -1
- package/build/types/enums.js +5 -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/ai.ts +4 -0
- 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 +9 -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/DisplayDiagnostics.ts +4 -2
- 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/LocalDisplayManager.ts +5 -2
- package/src/services/LocalMiniappRuntime.ts +61 -22
- package/src/services/LocalSttFallbackCoordinator.ts +4 -0
- package/src/services/LocalTranslationService.ts +20 -0
- package/src/services/MiniappEngine.ts +62 -27
- package/src/services/MiniappLauncher.ts +7 -0
- package/src/services/NavigationService.ts +28 -8
- package/src/services/OfflineActivity.ts +131 -0
- package/src/services/PhoneCalendarService.ts +7 -0
- package/src/services/PhoneNotificationsSync.ts +8 -3
- package/src/services/STTModelManager.ts +6 -0
- package/src/services/SceneRenderer.ts +1 -1
- package/src/services/appStateBackupFile.ts +171 -0
- package/src/services/controllers.ts +13 -3
- package/src/services/effectiveCapabilities.ts +6 -1
- 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 +22 -11
- package/src/stores/settings.ts +70 -10
- package/src/types/capabilities/even-realities-g1.ts +4 -0
- package/src/types/capabilities/memomind-one.ts +43 -0
- package/src/types/capabilities/rayneo-io.ts +3 -2
- package/src/types/enums.ts +5 -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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalSttFallbackCoordinator.d.ts","sourceRoot":"","sources":["../../src/services/LocalSttFallbackCoordinator.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"LocalSttFallbackCoordinator.d.ts","sourceRoot":"","sources":["../../src/services/LocalSttFallbackCoordinator.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;GAeG;AACH;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAC9B,aAAa,GACb,oBAAoB,GACpB,gBAAgB,GAChB,WAAW,GACX,sBAAsB,CAAA;AAE1B,cAAM,2BAA2B;IAC/B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA6B;IAEpD,OAAO,CAAC,4BAA4B,CAAQ;IAC5C,OAAO,CAAC,yBAAyB,CAAQ;IACzC,oFAAoF;IACpF,OAAO,CAAC,uBAAuB,CAAQ;IACvC,OAAO,CAAC,cAAc,CAAsB;IAC5C;;;;;OAKG;IACH,OAAO,CAAC,cAAc,CAAO;IAC7B,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,oBAAoB,CAAQ;IACpC;;;;;;OAMG;IACH,OAAO,CAAC,mBAAmB,CAAQ;IACnC,OAAO,CAAC,mBAAmB,CAAO;IAClC;;;;;OAKG;IACH,OAAO,CAAC,eAAe,CAAQ;IAE/B,OAAO;YAYO,oBAAoB;IAQlC;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;IAajC,MAAM,CAAC,WAAW,IAAI,2BAA2B;IAOjD,QAAQ,IAAI,OAAO;IAInB,iBAAiB,IAAI,MAAM,GAAG,IAAI;IAIlC,mEAAmE;IACnE,SAAS,IAAI,sBAAsB,GAAG,IAAI;IAI1C,oBAAoB,CAClB,gBAAgB,EAAE,OAAO,EACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,aAAa,UAAQ,EACrB,eAAe,UAAO,GACrB,IAAI;IAWP;;;;;;OAMG;IACH,iBAAiB,IAAI,IAAI;IAEzB;;;;OAIG;IACG,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9D,iBAAiB,IAAI,OAAO;IAI5B,uEAAuE;IACvE,yBAAyB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAQnE;2CACuC;IACvC,OAAO,CAAC,SAAS;YAWH,SAAS;IAmBvB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,aAAa;YAWP,aAAa;YASb,gBAAgB;IAqB9B,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,GAAG;CAGZ;AAED,QAAA,MAAM,2BAA2B,6BAA4C,CAAA;AAC7E,eAAe,2BAA2B,CAAA"}
|
|
@@ -2,6 +2,7 @@ import BluetoothSdk from "@glassly/bluetooth-sdk/internal";
|
|
|
2
2
|
import { ISLAND_SETTINGS_KEYS } from "../runtime/config";
|
|
3
3
|
import { useSettingsStore } from "../stores/settings";
|
|
4
4
|
import { cloudClientService } from "./CloudClientService";
|
|
5
|
+
import offlineActivity from "./OfflineActivity";
|
|
5
6
|
import sttModelManager from "./STTModelManager";
|
|
6
7
|
class LocalSttFallbackCoordinator {
|
|
7
8
|
static instance;
|
|
@@ -180,6 +181,7 @@ class LocalSttFallbackCoordinator {
|
|
|
180
181
|
}
|
|
181
182
|
publishReason() {
|
|
182
183
|
const reason = this.localActive ? this.currentReason() : "";
|
|
184
|
+
offlineActivity.setTranscription(this.localActive ? { reason: this.currentReason(), model: sttModelManager.currentModelLabel() } : null);
|
|
183
185
|
const store = useSettingsStore.getState();
|
|
184
186
|
if (store.getSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason) !== reason) {
|
|
185
187
|
store.setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason, reason);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalSttFallbackCoordinator.js","sourceRoot":"","sources":["../../src/services/LocalSttFallbackCoordinator.ts"],"names":[],"mappings":"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","sourcesContent":["import BluetoothSdk from \"@glassly/bluetooth-sdk/internal\"\nimport {ISLAND_SETTINGS_KEYS} from \"../runtime/config\"\nimport {useSettingsStore} from \"../stores/settings\"\nimport {cloudClientService} from \"./CloudClientService\"\nimport sttModelManager from \"./STTModelManager\"\n\n/**\n * Decides whether on-device Sherpa STT should produce transcripts for local\n * miniapps, instead of (or in addition to) cloud transcription. Local STT is\n * active when the cloud is unavailable (disconnected or refusing STT for plan\n * quota) and some subscriber accepts fallback text, or any subscription\n * requires `forceLocal`.\n *\n * Activation flips `localSttFallbackActive` in the settings store, which\n * the bluetooth-sdk native side reads to decide whether to feed PCM into\n * the SherpaOnnxTranscriber. Local transcripts then flow back through the\n * `local_transcription` Bridge event; the host wires that event to\n * `localMiniappRuntime.forwardEvent`, with this coordinator providing the\n * subscription-active gate so we never publish transcripts to miniapps that\n * aren't asking for them. LocalMiniappRuntime filters mixed local/cloud\n * delivery per miniapp.\n */\n/**\n * Why local STT is currently active, mirrored into the\n * `local_stt_fallback_reason` setting (\"\" while inactive) so UI can show it.\n * When several causes hold at once, the primary-path outage wins over\n * `force_local` (a forced subscription would keep local STT on either way).\n */\nexport type LocalSttFallbackReason =\n | \"force_local\"\n | \"cloud_disconnected\"\n | \"cloud_disabled\"\n | \"stt_quota\"\n | \"provider_unreachable\"\n\nclass LocalSttFallbackCoordinator {\n private static instance: LocalSttFallbackCoordinator\n\n private hasTranscriptionSubscription = false\n private hasForceLocalSubscription = false\n /** Some default-route subscriber takes on-device text while the primary is down. */\n private hasFallbackSubscription = false\n private activeLanguage: string | null = null\n /**\n * Default to \"cloud is up\" so we never accidentally activate local STT before\n * the cloud client has had a chance to start. We attach lazily on the first\n * subscription/reconcile pass; once attached, this field reflects the real\n * cloud-client runtime status.\n */\n private cloudConnected = true\n private localActive = false\n private starting: Promise<void> | null = null\n private cloudAdapterAttached = false\n /**\n * Soniox-direct (user API key) state, pushed by SonioxDirectService. While\n * enabled, IT is the primary transcription path — cloud connectivity is\n * irrelevant and sherpa engages on Soniox health instead. Defaults healthy\n * for the same reason cloudConnected defaults true: never activate local STT\n * before the primary has had a chance to come up.\n */\n private sonioxDirectEnabled = false\n private sonioxDirectHealthy = true\n /**\n * Cloud STT refused for plan quota (`stream.quota` blocked=true). The cloud\n * connection stays up, so this is tracked separately from `cloudConnected`:\n * for transcription purposes the cloud path is down, and sherpa takes over\n * if its model is on-device. Soniox-direct is unmetered and unaffected.\n */\n private sttQuotaBlocked = false\n\n private constructor() {\n // Reset the persisted mirror flag on boot — the in-memory state in this\n // coordinator is the source of truth, and a stale \"true\" left from the\n // previous session would cause native to feed Sherpa before any miniapp\n // registered a subscription.\n useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive, false)\n useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason, \"\")\n sttModelManager.onModelReady(() => {\n void this.retryAfterModelReady()\n })\n }\n\n private async retryAfterModelReady(): Promise<void> {\n // A model can finish downloading while the initial availability check is\n // still pending. Wait for that attempt before retrying so readiness is\n // never lost and native restarts cannot overlap.\n await this.starting\n await this.reconcile()\n }\n\n /**\n * Lazy-attach the engine cloud connection listener. The coordinator is\n * constructed at module load, before engine.start() brings up the cloud\n * client, so we defer until the first reconcile.\n */\n private attachCloudAdapterIfReady(): void {\n if (this.cloudAdapterAttached) return\n this.cloudConnected = cloudClientService.isConnected()\n cloudClientService.onStatusChanged((status) => {\n const connected = status.status === \"connected\"\n if (this.cloudConnected === connected) return\n this.log(`cloud connection -> ${connected ? \"up\" : \"down\"}`)\n this.cloudConnected = connected\n void this.reconcile()\n })\n this.cloudAdapterAttached = true\n }\n\n static getInstance(): LocalSttFallbackCoordinator {\n if (!LocalSttFallbackCoordinator.instance) {\n LocalSttFallbackCoordinator.instance = new LocalSttFallbackCoordinator()\n }\n return LocalSttFallbackCoordinator.instance\n }\n\n isActive(): boolean {\n return this.localActive\n }\n\n getActiveLanguage(): string | null {\n return this.activeLanguage\n }\n\n /** The cause of the current activation, or null while inactive. */\n getReason(): LocalSttFallbackReason | null {\n return this.localActive ? this.currentReason() : null\n }\n\n onSubscriptionChange(\n hasTranscription: boolean,\n language: string | null,\n hasForceLocal = false,\n acceptsFallback = true,\n ): void {\n this.log(\n `onSubscriptionChange(hasTx=${hasTranscription}, lang=${language}, forceLocal=${hasForceLocal}, fallback=${acceptsFallback})`,\n )\n this.hasTranscriptionSubscription = hasTranscription\n this.hasForceLocalSubscription = hasTranscription && hasForceLocal\n this.hasFallbackSubscription = hasTranscription && acceptsFallback\n this.activeLanguage = hasTranscription ? language : null\n void this.reconcile()\n }\n\n /**\n * Informational hook, retained for API stability. No longer called: the\n * authoritative switching signal is the cloud-client adapter (local miniapps\n * are powered only by V2). v1 cloud transcripts deliberately do NOT drive this\n * coordinator anymore. Kept for future hysteresis if connection-state alone\n * proves too coarse.\n */\n onCloudTranscript(): void {}\n\n /**\n * Pushed when the cloud announces/clears an STT quota block. Awaitable so\n * the caller can inspect `isActive()` afterwards to phrase its user notice\n * (\"continuing on-device\" vs \"transcription paused\").\n */\n async onSttQuotaBlockedChange(blocked: boolean): Promise<void> {\n if (this.sttQuotaBlocked === blocked) return\n this.log(`stt quota -> ${blocked ? \"blocked\" : \"restored\"}`)\n this.sttQuotaBlocked = blocked\n await this.reconcile()\n }\n\n isSttQuotaBlocked(): boolean {\n return this.sttQuotaBlocked\n }\n\n /** Pushed by SonioxDirectService on every enable/health transition. */\n onSonioxDirectStateChange(enabled: boolean, healthy: boolean): void {\n if (this.sonioxDirectEnabled === enabled && this.sonioxDirectHealthy === healthy) return\n this.log(`soniox-direct -> enabled=${enabled} healthy=${healthy}`)\n this.sonioxDirectEnabled = enabled\n this.sonioxDirectHealthy = healthy\n void this.reconcile()\n }\n\n /** Whether the primary transcription path (Soniox-direct when a user key is\n * set, the cloud otherwise) is up. */\n private primaryUp(): boolean {\n // Cloud turned off in Privacy settings: the cloud path is not merely down,\n // it is never coming back, so on-device STT is the primary. Checked ahead\n // of `cloudConnected` because that field defaults to true (so local STT\n // doesn't pre-empt cloud at boot) and would otherwise report a path that\n // will never connect as up. Soniox-direct is a user-supplied third-party\n // key, not Glassly cloud, so it stays available.\n if (!this.sonioxDirectEnabled && cloudClientService.isCloudDisabled()) return false\n return this.sonioxDirectEnabled ? this.sonioxDirectHealthy : this.cloudConnected && !this.sttQuotaBlocked\n }\n\n private async reconcile(): Promise<void> {\n this.attachCloudAdapterIfReady()\n const shouldBeActive = this.shouldUseLocalStt()\n if (shouldBeActive && !this.localActive) {\n if (!this.starting) {\n this.starting = this.startLocalStt().finally(() => {\n this.starting = null\n })\n }\n await this.starting\n } else if (!shouldBeActive && this.localActive) {\n this.stopLocalStt(this.hasTranscriptionSubscription ? \"primary transcription recovered\" : \"subscription gone\")\n } else if (shouldBeActive && this.localActive) {\n // Still active, but the cause may have shifted (e.g. quota block landing\n // while already offline for a disconnect) — keep the mirror current.\n this.publishReason()\n }\n }\n\n /**\n * Ranks the concurrent causes: a dead-forever cloud path first, then the\n * primary provider's own failure, then transient outages, with `force_local`\n * only when the primary is actually up. Mirrors `primaryUp()`.\n */\n private currentReason(): LocalSttFallbackReason {\n if (!this.sonioxDirectEnabled && cloudClientService.isCloudDisabled()) return \"cloud_disabled\"\n if (this.sonioxDirectEnabled && !this.sonioxDirectHealthy) return \"provider_unreachable\"\n if (!this.sonioxDirectEnabled && !this.cloudConnected) return \"cloud_disconnected\"\n if (!this.sonioxDirectEnabled && this.sttQuotaBlocked) return \"stt_quota\"\n return \"force_local\"\n }\n\n private publishReason(): void {\n const reason = this.localActive ? this.currentReason() : \"\"\n const store = useSettingsStore.getState()\n if (store.getSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason) !== reason) {\n store.setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason, reason)\n }\n }\n\n private async startLocalStt(): Promise<void> {\n this.log(\"starting local stt\")\n try {\n await this.activateLocalStt()\n } catch (err) {\n this.log(`local stt startup failed: ${err}`)\n }\n }\n\n private async activateLocalStt(): Promise<void> {\n const modelAvailable = await sttModelManager.isModelAvailable()\n if (!modelAvailable) {\n this.log(\"local stt model is not available yet — skipping activation\")\n return\n }\n if (!this.shouldUseLocalStt()) {\n this.log(\"local stt activation skipped: routing changed before start completed\")\n return\n }\n // Native rejects unless the recognizer and its processing loop are ready.\n await BluetoothSdk.restartTranscriber()\n if (!this.shouldUseLocalStt()) {\n this.log(\"local stt activation skipped: routing changed during transcriber restart\")\n return\n }\n useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive, true)\n this.localActive = true\n this.publishReason()\n }\n\n private stopLocalStt(reason: string): void {\n this.log(`stopping local stt: ${reason}`)\n useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive, false)\n this.localActive = false\n this.publishReason()\n }\n\n private shouldUseLocalStt(): boolean {\n return (\n this.hasTranscriptionSubscription &&\n (this.hasForceLocalSubscription || (this.hasFallbackSubscription && !this.primaryUp()))\n )\n }\n\n private log(msg: string): void {\n console.log(`[LocalSttFallback] ${msg}`)\n }\n}\n\nconst localSttFallbackCoordinator = LocalSttFallbackCoordinator.getInstance()\nexport default localSttFallbackCoordinator\n"]}
|
|
1
|
+
{"version":3,"file":"LocalSttFallbackCoordinator.js","sourceRoot":"","sources":["../../src/services/LocalSttFallbackCoordinator.ts"],"names":[],"mappings":"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","sourcesContent":["import BluetoothSdk from \"@glassly/bluetooth-sdk/internal\"\nimport {ISLAND_SETTINGS_KEYS} from \"../runtime/config\"\nimport {useSettingsStore} from \"../stores/settings\"\nimport {cloudClientService} from \"./CloudClientService\"\nimport offlineActivity from \"./OfflineActivity\"\nimport sttModelManager from \"./STTModelManager\"\n\n/**\n * Decides whether on-device Sherpa STT should produce transcripts for local\n * miniapps, instead of (or in addition to) cloud transcription. Local STT is\n * active when the cloud is unavailable (disconnected or refusing STT for plan\n * quota) and some subscriber accepts fallback text, or any subscription\n * requires `forceLocal`.\n *\n * Activation flips `localSttFallbackActive` in the settings store, which\n * the bluetooth-sdk native side reads to decide whether to feed PCM into\n * the SherpaOnnxTranscriber. Local transcripts then flow back through the\n * `local_transcription` Bridge event; the host wires that event to\n * `localMiniappRuntime.forwardEvent`, with this coordinator providing the\n * subscription-active gate so we never publish transcripts to miniapps that\n * aren't asking for them. LocalMiniappRuntime filters mixed local/cloud\n * delivery per miniapp.\n */\n/**\n * Why local STT is currently active, mirrored into the\n * `local_stt_fallback_reason` setting (\"\" while inactive) so UI can show it.\n * When several causes hold at once, the primary-path outage wins over\n * `force_local` (a forced subscription would keep local STT on either way).\n */\nexport type LocalSttFallbackReason =\n | \"force_local\"\n | \"cloud_disconnected\"\n | \"cloud_disabled\"\n | \"stt_quota\"\n | \"provider_unreachable\"\n\nclass LocalSttFallbackCoordinator {\n private static instance: LocalSttFallbackCoordinator\n\n private hasTranscriptionSubscription = false\n private hasForceLocalSubscription = false\n /** Some default-route subscriber takes on-device text while the primary is down. */\n private hasFallbackSubscription = false\n private activeLanguage: string | null = null\n /**\n * Default to \"cloud is up\" so we never accidentally activate local STT before\n * the cloud client has had a chance to start. We attach lazily on the first\n * subscription/reconcile pass; once attached, this field reflects the real\n * cloud-client runtime status.\n */\n private cloudConnected = true\n private localActive = false\n private starting: Promise<void> | null = null\n private cloudAdapterAttached = false\n /**\n * Soniox-direct (user API key) state, pushed by SonioxDirectService. While\n * enabled, IT is the primary transcription path — cloud connectivity is\n * irrelevant and sherpa engages on Soniox health instead. Defaults healthy\n * for the same reason cloudConnected defaults true: never activate local STT\n * before the primary has had a chance to come up.\n */\n private sonioxDirectEnabled = false\n private sonioxDirectHealthy = true\n /**\n * Cloud STT refused for plan quota (`stream.quota` blocked=true). The cloud\n * connection stays up, so this is tracked separately from `cloudConnected`:\n * for transcription purposes the cloud path is down, and sherpa takes over\n * if its model is on-device. Soniox-direct is unmetered and unaffected.\n */\n private sttQuotaBlocked = false\n\n private constructor() {\n // Reset the persisted mirror flag on boot — the in-memory state in this\n // coordinator is the source of truth, and a stale \"true\" left from the\n // previous session would cause native to feed Sherpa before any miniapp\n // registered a subscription.\n useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive, false)\n useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason, \"\")\n sttModelManager.onModelReady(() => {\n void this.retryAfterModelReady()\n })\n }\n\n private async retryAfterModelReady(): Promise<void> {\n // A model can finish downloading while the initial availability check is\n // still pending. Wait for that attempt before retrying so readiness is\n // never lost and native restarts cannot overlap.\n await this.starting\n await this.reconcile()\n }\n\n /**\n * Lazy-attach the engine cloud connection listener. The coordinator is\n * constructed at module load, before engine.start() brings up the cloud\n * client, so we defer until the first reconcile.\n */\n private attachCloudAdapterIfReady(): void {\n if (this.cloudAdapterAttached) return\n this.cloudConnected = cloudClientService.isConnected()\n cloudClientService.onStatusChanged((status) => {\n const connected = status.status === \"connected\"\n if (this.cloudConnected === connected) return\n this.log(`cloud connection -> ${connected ? \"up\" : \"down\"}`)\n this.cloudConnected = connected\n void this.reconcile()\n })\n this.cloudAdapterAttached = true\n }\n\n static getInstance(): LocalSttFallbackCoordinator {\n if (!LocalSttFallbackCoordinator.instance) {\n LocalSttFallbackCoordinator.instance = new LocalSttFallbackCoordinator()\n }\n return LocalSttFallbackCoordinator.instance\n }\n\n isActive(): boolean {\n return this.localActive\n }\n\n getActiveLanguage(): string | null {\n return this.activeLanguage\n }\n\n /** The cause of the current activation, or null while inactive. */\n getReason(): LocalSttFallbackReason | null {\n return this.localActive ? this.currentReason() : null\n }\n\n onSubscriptionChange(\n hasTranscription: boolean,\n language: string | null,\n hasForceLocal = false,\n acceptsFallback = true,\n ): void {\n this.log(\n `onSubscriptionChange(hasTx=${hasTranscription}, lang=${language}, forceLocal=${hasForceLocal}, fallback=${acceptsFallback})`,\n )\n this.hasTranscriptionSubscription = hasTranscription\n this.hasForceLocalSubscription = hasTranscription && hasForceLocal\n this.hasFallbackSubscription = hasTranscription && acceptsFallback\n this.activeLanguage = hasTranscription ? language : null\n void this.reconcile()\n }\n\n /**\n * Informational hook, retained for API stability. No longer called: the\n * authoritative switching signal is the cloud-client adapter (local miniapps\n * are powered only by V2). v1 cloud transcripts deliberately do NOT drive this\n * coordinator anymore. Kept for future hysteresis if connection-state alone\n * proves too coarse.\n */\n onCloudTranscript(): void {}\n\n /**\n * Pushed when the cloud announces/clears an STT quota block. Awaitable so\n * the caller can inspect `isActive()` afterwards to phrase its user notice\n * (\"continuing on-device\" vs \"transcription paused\").\n */\n async onSttQuotaBlockedChange(blocked: boolean): Promise<void> {\n if (this.sttQuotaBlocked === blocked) return\n this.log(`stt quota -> ${blocked ? \"blocked\" : \"restored\"}`)\n this.sttQuotaBlocked = blocked\n await this.reconcile()\n }\n\n isSttQuotaBlocked(): boolean {\n return this.sttQuotaBlocked\n }\n\n /** Pushed by SonioxDirectService on every enable/health transition. */\n onSonioxDirectStateChange(enabled: boolean, healthy: boolean): void {\n if (this.sonioxDirectEnabled === enabled && this.sonioxDirectHealthy === healthy) return\n this.log(`soniox-direct -> enabled=${enabled} healthy=${healthy}`)\n this.sonioxDirectEnabled = enabled\n this.sonioxDirectHealthy = healthy\n void this.reconcile()\n }\n\n /** Whether the primary transcription path (Soniox-direct when a user key is\n * set, the cloud otherwise) is up. */\n private primaryUp(): boolean {\n // Cloud turned off in Privacy settings: the cloud path is not merely down,\n // it is never coming back, so on-device STT is the primary. Checked ahead\n // of `cloudConnected` because that field defaults to true (so local STT\n // doesn't pre-empt cloud at boot) and would otherwise report a path that\n // will never connect as up. Soniox-direct is a user-supplied third-party\n // key, not Glassly cloud, so it stays available.\n if (!this.sonioxDirectEnabled && cloudClientService.isCloudDisabled()) return false\n return this.sonioxDirectEnabled ? this.sonioxDirectHealthy : this.cloudConnected && !this.sttQuotaBlocked\n }\n\n private async reconcile(): Promise<void> {\n this.attachCloudAdapterIfReady()\n const shouldBeActive = this.shouldUseLocalStt()\n if (shouldBeActive && !this.localActive) {\n if (!this.starting) {\n this.starting = this.startLocalStt().finally(() => {\n this.starting = null\n })\n }\n await this.starting\n } else if (!shouldBeActive && this.localActive) {\n this.stopLocalStt(this.hasTranscriptionSubscription ? \"primary transcription recovered\" : \"subscription gone\")\n } else if (shouldBeActive && this.localActive) {\n // Still active, but the cause may have shifted (e.g. quota block landing\n // while already offline for a disconnect) — keep the mirror current.\n this.publishReason()\n }\n }\n\n /**\n * Ranks the concurrent causes: a dead-forever cloud path first, then the\n * primary provider's own failure, then transient outages, with `force_local`\n * only when the primary is actually up. Mirrors `primaryUp()`.\n */\n private currentReason(): LocalSttFallbackReason {\n if (!this.sonioxDirectEnabled && cloudClientService.isCloudDisabled()) return \"cloud_disabled\"\n if (this.sonioxDirectEnabled && !this.sonioxDirectHealthy) return \"provider_unreachable\"\n if (!this.sonioxDirectEnabled && !this.cloudConnected) return \"cloud_disconnected\"\n if (!this.sonioxDirectEnabled && this.sttQuotaBlocked) return \"stt_quota\"\n return \"force_local\"\n }\n\n private publishReason(): void {\n const reason = this.localActive ? this.currentReason() : \"\"\n offlineActivity.setTranscription(\n this.localActive ? {reason: this.currentReason(), model: sttModelManager.currentModelLabel()} : null,\n )\n const store = useSettingsStore.getState()\n if (store.getSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason) !== reason) {\n store.setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackReason, reason)\n }\n }\n\n private async startLocalStt(): Promise<void> {\n this.log(\"starting local stt\")\n try {\n await this.activateLocalStt()\n } catch (err) {\n this.log(`local stt startup failed: ${err}`)\n }\n }\n\n private async activateLocalStt(): Promise<void> {\n const modelAvailable = await sttModelManager.isModelAvailable()\n if (!modelAvailable) {\n this.log(\"local stt model is not available yet — skipping activation\")\n return\n }\n if (!this.shouldUseLocalStt()) {\n this.log(\"local stt activation skipped: routing changed before start completed\")\n return\n }\n // Native rejects unless the recognizer and its processing loop are ready.\n await BluetoothSdk.restartTranscriber()\n if (!this.shouldUseLocalStt()) {\n this.log(\"local stt activation skipped: routing changed during transcriber restart\")\n return\n }\n useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive, true)\n this.localActive = true\n this.publishReason()\n }\n\n private stopLocalStt(reason: string): void {\n this.log(`stopping local stt: ${reason}`)\n useSettingsStore.getState().setSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive, false)\n this.localActive = false\n this.publishReason()\n }\n\n private shouldUseLocalStt(): boolean {\n return (\n this.hasTranscriptionSubscription &&\n (this.hasForceLocalSubscription || (this.hasFallbackSubscription && !this.primaryUp()))\n )\n }\n\n private log(msg: string): void {\n console.log(`[LocalSttFallback] ${msg}`)\n }\n}\n\nconst localSttFallbackCoordinator = LocalSttFallbackCoordinator.getInstance()\nexport default localSttFallbackCoordinator\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ServedTranslationPair } from "./OfflineActivity";
|
|
1
2
|
/** One concrete-target `translation:<source>:<target>` stream and who listens to it. */
|
|
2
3
|
export interface LocalTranslationSubscription {
|
|
3
4
|
/** Subscribed source: a language code, or `*` / `auto` for any. */
|
|
@@ -39,6 +40,7 @@ export declare class LocalTranslationService {
|
|
|
39
40
|
private utteranceCounter;
|
|
40
41
|
attach(host: Host): void;
|
|
41
42
|
setSubscriptions(subscriptions: LocalTranslationSubscription[]): void;
|
|
43
|
+
servedPairs(): ServedTranslationPair[];
|
|
42
44
|
onLocalTranscript(transcript: LocalTranscript, sourceLanguage: string): void;
|
|
43
45
|
private wantedTargets;
|
|
44
46
|
private enqueue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalTranslationService.d.ts","sourceRoot":"","sources":["../../src/services/LocalTranslationService.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"LocalTranslationService.d.ts","sourceRoot":"","sources":["../../src/services/LocalTranslationService.ts"],"names":[],"mappings":"AAAA,OAAwB,EAAC,KAAK,qBAAqB,EAAC,MAAM,mBAAmB,CAAA;AAG7E,wFAAwF;AACxF,MAAM,WAAW,4BAA4B;IAC3C,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,KAAK,EAAE,OAAO,CAAA;IACd,iEAAiE;IACjE,UAAU,EAAE,OAAO,CAAA;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,UAAU,IAAI;IACZ,mEAAmE;IACnE,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IAChE,8DAA8D;IAC9D,SAAS,EAAE,MAAM,OAAO,CAAA;IACxB,wDAAwD;IACxD,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;CAC7D;AAwBD;;;;;;;;GAQG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,aAAa,CAAqC;IAC1D,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,IAAI,CAAoB;IAChC,OAAO,CAAC,gBAAgB,CAAI;IAE5B,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAKxB,gBAAgB,CAAC,aAAa,EAAE,4BAA4B,EAAE,GAAG,IAAI;IASrE,WAAW,IAAI,qBAAqB,EAAE;IAiBtC,iBAAiB,CAAC,UAAU,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI;IAkB5E,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,OAAO;YAoBD,KAAK;IAoBnB,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,IAAI;CAgBb;AAED,QAAA,MAAM,uBAAuB,yBAAgC,CAAA;AAC7D,eAAe,uBAAuB,CAAA"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import offlineActivity from "./OfflineActivity";
|
|
1
2
|
import { offlineTranslationManager } from "./OfflineTranslationManager";
|
|
2
3
|
/** Interims are translated at most this often; finals always are. */
|
|
3
4
|
const INTERIM_MIN_INTERVAL_MS = 300;
|
|
@@ -19,6 +20,7 @@ export class LocalTranslationService {
|
|
|
19
20
|
utteranceCounter = 0;
|
|
20
21
|
attach(host) {
|
|
21
22
|
this.host = host;
|
|
23
|
+
offlineActivity.setTranslationSource(() => this.servedPairs());
|
|
22
24
|
}
|
|
23
25
|
setSubscriptions(subscriptions) {
|
|
24
26
|
this.subscriptions = subscriptions;
|
|
@@ -27,6 +29,26 @@ export class LocalTranslationService {
|
|
|
27
29
|
if (!live.has(key.substring(key.indexOf(">") + 1)))
|
|
28
30
|
this.lanes.delete(key);
|
|
29
31
|
}
|
|
32
|
+
offlineActivity.refresh();
|
|
33
|
+
}
|
|
34
|
+
servedPairs() {
|
|
35
|
+
if (!this.host || !offlineTranslationManager.isSupported())
|
|
36
|
+
return [];
|
|
37
|
+
const pairs = new Map();
|
|
38
|
+
let primaryUp = null;
|
|
39
|
+
for (const sub of this.subscriptions) {
|
|
40
|
+
if (baseLanguage(sub.source) === baseLanguage(sub.target))
|
|
41
|
+
continue;
|
|
42
|
+
if (!sub.forceLocal) {
|
|
43
|
+
primaryUp ??= this.host.primaryUp();
|
|
44
|
+
if (primaryUp || !sub.cloud)
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const key = `${sub.source}>${sub.target}`;
|
|
48
|
+
const existing = pairs.get(key);
|
|
49
|
+
pairs.set(key, { source: sub.source, target: sub.target, forceLocal: sub.forceLocal || !!existing?.forceLocal });
|
|
50
|
+
}
|
|
51
|
+
return [...pairs.values()];
|
|
30
52
|
}
|
|
31
53
|
onLocalTranscript(transcript, sourceLanguage) {
|
|
32
54
|
const text = transcript.text?.trim();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalTranslationService.js","sourceRoot":"","sources":["../../src/services/LocalTranslationService.ts"],"names":[],"mappings":"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","sourcesContent":["import {offlineTranslationManager} from \"./OfflineTranslationManager\"\n\n/** One concrete-target `translation:<source>:<target>` stream and who listens to it. */\nexport interface LocalTranslationSubscription {\n /** Subscribed source: a language code, or `*` / `auto` for any. */\n source: string\n target: string\n /** At least one listener on the default (cloud) route. */\n cloud: boolean\n /** At least one listener that requires on-device translation. */\n forceLocal: boolean\n}\n\nexport interface LocalTranscript {\n text: string\n isFinal?: boolean\n speakerId?: number | string\n startTime?: number\n endTime?: number\n}\n\ninterface Host {\n /** Fan a `translation:<source>:<target>` event out to miniapps. */\n deliver: (stream: string, data: Record<string, unknown>) => void\n /** Whether the primary (cloud / Soniox-direct) path is up. */\n primaryUp: () => boolean\n /** A wanted pair has no language pack on the device. */\n onPairUnavailable?: (source: string, target: string) => void\n}\n\ninterface Job {\n text: string\n isFinal: boolean\n utteranceId: string\n transcript: LocalTranscript\n}\n\ninterface Lane {\n busy: boolean\n finals: Job[]\n pendingInterim: Job | null\n lastInterimAt: number\n retryAt: number\n reportedUnavailable: boolean\n}\n\n/** Interims are translated at most this often; finals always are. */\nconst INTERIM_MIN_INTERVAL_MS = 300\nconst FAILURE_BACKOFF_MS = 5000\n\nconst baseLanguage = (code: string): string => code.split(\"-\")[0].toLowerCase()\n\n/**\n * Turns on-device transcripts into on-device translations. Apple's Translation\n * framework has no streaming API, so each update is a whole `translate` call:\n * one runs at a time per language pair, a newer interim replaces a waiting\n * one, and finals are never dropped.\n *\n * A pair is translated here when a subscriber forces local, or when the\n * primary path is down and would otherwise leave default subscribers silent.\n */\nexport class LocalTranslationService {\n private subscriptions: LocalTranslationSubscription[] = []\n private lanes = new Map<string, Lane>()\n private host: Host | null = null\n private utteranceCounter = 0\n\n attach(host: Host): void {\n this.host = host\n }\n\n setSubscriptions(subscriptions: LocalTranslationSubscription[]): void {\n this.subscriptions = subscriptions\n const live = new Set(subscriptions.map((s) => s.target))\n for (const key of this.lanes.keys()) {\n if (!live.has(key.substring(key.indexOf(\">\") + 1))) this.lanes.delete(key)\n }\n }\n\n onLocalTranscript(transcript: LocalTranscript, sourceLanguage: string): void {\n const text = transcript.text?.trim()\n if (!text || !this.host || !offlineTranslationManager.isSupported()) return\n const isFinal = transcript.isFinal === true\n const utteranceId = `local-${this.utteranceCounter}`\n if (isFinal) this.utteranceCounter++\n\n for (const target of this.wantedTargets(sourceLanguage)) {\n const job: Job = {text, isFinal, utteranceId, transcript}\n if (baseLanguage(target) === baseLanguage(sourceLanguage)) {\n // Same-language passthrough, as the cloud emits it: no originalText.\n this.emit(sourceLanguage, target, job, text, undefined)\n continue\n }\n this.enqueue(sourceLanguage, target, job)\n }\n }\n\n private wantedTargets(sourceLanguage: string): Set<string> {\n const targets = new Set<string>()\n let primaryUp: boolean | null = null\n for (const sub of this.subscriptions) {\n const anySource = sub.source === \"*\" || sub.source === \"auto\"\n if (!anySource && baseLanguage(sub.source) !== baseLanguage(sourceLanguage)) continue\n if (!sub.forceLocal) {\n primaryUp ??= this.host!.primaryUp()\n if (primaryUp || !sub.cloud) continue\n }\n targets.add(sub.target)\n }\n return targets\n }\n\n private enqueue(source: string, target: string, job: Job): void {\n const key = `${source}>${target}`\n let lane = this.lanes.get(key)\n if (!lane) {\n lane = {busy: false, finals: [], pendingInterim: null, lastInterimAt: 0, retryAt: 0, reportedUnavailable: false}\n this.lanes.set(key, lane)\n }\n const now = Date.now()\n if (now < lane.retryAt) return\n if (job.isFinal) {\n lane.pendingInterim = null\n lane.finals.push(job)\n } else {\n if (now - lane.lastInterimAt < INTERIM_MIN_INTERVAL_MS) return\n lane.lastInterimAt = now\n lane.pendingInterim = job\n }\n if (!lane.busy) void this.drain(source, target, lane)\n }\n\n private async drain(source: string, target: string, lane: Lane): Promise<void> {\n lane.busy = true\n try {\n for (;;) {\n const job = lane.finals.shift() ?? lane.pendingInterim\n if (!job) break\n if (!job.isFinal) lane.pendingInterim = null\n try {\n const translated = await offlineTranslationManager.translate(job.text, source, target)\n this.emit(source, target, job, translated, job.text)\n } catch (err) {\n this.handleFailure(source, target, lane, err)\n break\n }\n }\n } finally {\n lane.busy = false\n }\n }\n\n private handleFailure(source: string, target: string, lane: Lane, err: unknown): void {\n const message = (err as Error)?.message ?? String(err)\n console.warn(`[LocalTranslation] ${source}>${target} failed: ${message}`)\n lane.finals = []\n lane.pendingInterim = null\n lane.retryAt = Date.now() + FAILURE_BACKOFF_MS\n if (message.includes(\"not_installed\") && !lane.reportedUnavailable) {\n lane.reportedUnavailable = true\n this.host?.onPairUnavailable?.(source, target)\n }\n }\n\n private emit(source: string, target: string, job: Job, text: string, originalText: string | undefined): void {\n this.host?.deliver(`translation:${source}:${target}`, {\n type: \"translation\",\n text,\n originalText,\n isFinal: job.isFinal,\n utteranceId: job.utteranceId,\n startTime: job.transcript.startTime,\n endTime: job.transcript.endTime,\n speakerId: job.transcript.speakerId,\n transcribeLanguage: source,\n translateLanguage: target,\n didTranslate: originalText !== undefined,\n provider: \"apple-translation\",\n })\n }\n}\n\nconst localTranslationService = new LocalTranslationService()\nexport default localTranslationService\n"]}
|
|
1
|
+
{"version":3,"file":"LocalTranslationService.js","sourceRoot":"","sources":["../../src/services/LocalTranslationService.ts"],"names":[],"mappings":"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","sourcesContent":["import offlineActivity, {type ServedTranslationPair} from \"./OfflineActivity\"\nimport {offlineTranslationManager} from \"./OfflineTranslationManager\"\n\n/** One concrete-target `translation:<source>:<target>` stream and who listens to it. */\nexport interface LocalTranslationSubscription {\n /** Subscribed source: a language code, or `*` / `auto` for any. */\n source: string\n target: string\n /** At least one listener on the default (cloud) route. */\n cloud: boolean\n /** At least one listener that requires on-device translation. */\n forceLocal: boolean\n}\n\nexport interface LocalTranscript {\n text: string\n isFinal?: boolean\n speakerId?: number | string\n startTime?: number\n endTime?: number\n}\n\ninterface Host {\n /** Fan a `translation:<source>:<target>` event out to miniapps. */\n deliver: (stream: string, data: Record<string, unknown>) => void\n /** Whether the primary (cloud / Soniox-direct) path is up. */\n primaryUp: () => boolean\n /** A wanted pair has no language pack on the device. */\n onPairUnavailable?: (source: string, target: string) => void\n}\n\ninterface Job {\n text: string\n isFinal: boolean\n utteranceId: string\n transcript: LocalTranscript\n}\n\ninterface Lane {\n busy: boolean\n finals: Job[]\n pendingInterim: Job | null\n lastInterimAt: number\n retryAt: number\n reportedUnavailable: boolean\n}\n\n/** Interims are translated at most this often; finals always are. */\nconst INTERIM_MIN_INTERVAL_MS = 300\nconst FAILURE_BACKOFF_MS = 5000\n\nconst baseLanguage = (code: string): string => code.split(\"-\")[0].toLowerCase()\n\n/**\n * Turns on-device transcripts into on-device translations. Apple's Translation\n * framework has no streaming API, so each update is a whole `translate` call:\n * one runs at a time per language pair, a newer interim replaces a waiting\n * one, and finals are never dropped.\n *\n * A pair is translated here when a subscriber forces local, or when the\n * primary path is down and would otherwise leave default subscribers silent.\n */\nexport class LocalTranslationService {\n private subscriptions: LocalTranslationSubscription[] = []\n private lanes = new Map<string, Lane>()\n private host: Host | null = null\n private utteranceCounter = 0\n\n attach(host: Host): void {\n this.host = host\n offlineActivity.setTranslationSource(() => this.servedPairs())\n }\n\n setSubscriptions(subscriptions: LocalTranslationSubscription[]): void {\n this.subscriptions = subscriptions\n const live = new Set(subscriptions.map((s) => s.target))\n for (const key of this.lanes.keys()) {\n if (!live.has(key.substring(key.indexOf(\">\") + 1))) this.lanes.delete(key)\n }\n offlineActivity.refresh()\n }\n\n servedPairs(): ServedTranslationPair[] {\n if (!this.host || !offlineTranslationManager.isSupported()) return []\n const pairs = new Map<string, ServedTranslationPair>()\n let primaryUp: boolean | null = null\n for (const sub of this.subscriptions) {\n if (baseLanguage(sub.source) === baseLanguage(sub.target)) continue\n if (!sub.forceLocal) {\n primaryUp ??= this.host.primaryUp()\n if (primaryUp || !sub.cloud) continue\n }\n const key = `${sub.source}>${sub.target}`\n const existing = pairs.get(key)\n pairs.set(key, {source: sub.source, target: sub.target, forceLocal: sub.forceLocal || !!existing?.forceLocal})\n }\n return [...pairs.values()]\n }\n\n onLocalTranscript(transcript: LocalTranscript, sourceLanguage: string): void {\n const text = transcript.text?.trim()\n if (!text || !this.host || !offlineTranslationManager.isSupported()) return\n const isFinal = transcript.isFinal === true\n const utteranceId = `local-${this.utteranceCounter}`\n if (isFinal) this.utteranceCounter++\n\n for (const target of this.wantedTargets(sourceLanguage)) {\n const job: Job = {text, isFinal, utteranceId, transcript}\n if (baseLanguage(target) === baseLanguage(sourceLanguage)) {\n // Same-language passthrough, as the cloud emits it: no originalText.\n this.emit(sourceLanguage, target, job, text, undefined)\n continue\n }\n this.enqueue(sourceLanguage, target, job)\n }\n }\n\n private wantedTargets(sourceLanguage: string): Set<string> {\n const targets = new Set<string>()\n let primaryUp: boolean | null = null\n for (const sub of this.subscriptions) {\n const anySource = sub.source === \"*\" || sub.source === \"auto\"\n if (!anySource && baseLanguage(sub.source) !== baseLanguage(sourceLanguage)) continue\n if (!sub.forceLocal) {\n primaryUp ??= this.host!.primaryUp()\n if (primaryUp || !sub.cloud) continue\n }\n targets.add(sub.target)\n }\n return targets\n }\n\n private enqueue(source: string, target: string, job: Job): void {\n const key = `${source}>${target}`\n let lane = this.lanes.get(key)\n if (!lane) {\n lane = {busy: false, finals: [], pendingInterim: null, lastInterimAt: 0, retryAt: 0, reportedUnavailable: false}\n this.lanes.set(key, lane)\n }\n const now = Date.now()\n if (now < lane.retryAt) return\n if (job.isFinal) {\n lane.pendingInterim = null\n lane.finals.push(job)\n } else {\n if (now - lane.lastInterimAt < INTERIM_MIN_INTERVAL_MS) return\n lane.lastInterimAt = now\n lane.pendingInterim = job\n }\n if (!lane.busy) void this.drain(source, target, lane)\n }\n\n private async drain(source: string, target: string, lane: Lane): Promise<void> {\n lane.busy = true\n try {\n for (;;) {\n const job = lane.finals.shift() ?? lane.pendingInterim\n if (!job) break\n if (!job.isFinal) lane.pendingInterim = null\n try {\n const translated = await offlineTranslationManager.translate(job.text, source, target)\n this.emit(source, target, job, translated, job.text)\n } catch (err) {\n this.handleFailure(source, target, lane, err)\n break\n }\n }\n } finally {\n lane.busy = false\n }\n }\n\n private handleFailure(source: string, target: string, lane: Lane, err: unknown): void {\n const message = (err as Error)?.message ?? String(err)\n console.warn(`[LocalTranslation] ${source}>${target} failed: ${message}`)\n lane.finals = []\n lane.pendingInterim = null\n lane.retryAt = Date.now() + FAILURE_BACKOFF_MS\n if (message.includes(\"not_installed\") && !lane.reportedUnavailable) {\n lane.reportedUnavailable = true\n this.host?.onPairUnavailable?.(source, target)\n }\n }\n\n private emit(source: string, target: string, job: Job, text: string, originalText: string | undefined): void {\n this.host?.deliver(`translation:${source}:${target}`, {\n type: \"translation\",\n text,\n originalText,\n isFinal: job.isFinal,\n utteranceId: job.utteranceId,\n startTime: job.transcript.startTime,\n endTime: job.transcript.endTime,\n speakerId: job.transcript.speakerId,\n transcribeLanguage: source,\n translateLanguage: target,\n didTranslate: originalText !== undefined,\n provider: \"apple-translation\",\n })\n }\n}\n\nconst localTranslationService = new LocalTranslationService()\nexport default localTranslationService\n"]}
|
|
@@ -19,6 +19,15 @@ export interface MiniappEngine {
|
|
|
19
19
|
uiRouter: UIRouter;
|
|
20
20
|
crashController: JSCrashController;
|
|
21
21
|
}
|
|
22
|
+
/** Packages with a live background JSContext. */
|
|
23
|
+
export declare function runningMiniappPackages(): string[];
|
|
24
|
+
/** Tear down a miniapp's background JSContext; a no-op when it isn't running. */
|
|
25
|
+
export declare function stopMiniapp(packageName: string): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Start (or restart) a miniapp's background so it reads its storage afresh (an
|
|
28
|
+
* app state restore). Resolves false when the engine isn't up or the spawn failed.
|
|
29
|
+
*/
|
|
30
|
+
export declare function respawnMiniapp(packageName: string): Promise<boolean>;
|
|
22
31
|
/**
|
|
23
32
|
* Construct (once) and return the JSRuntime engine singletons. Starts the router's
|
|
24
33
|
* message pump on first call. Subsequent calls return the same instances.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MiniappEngine.d.ts","sourceRoot":"","sources":["../../src/services/MiniappEngine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAA;AACrD,OAAO,EAAC,QAAQ,EAAsB,MAAM,YAAY,CAAA;AACxD,OAAO,EAAC,QAAQ,EAAC,MAAM,YAAY,CAAA;AAEnC,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,QAAQ,CAAA;IAChB,QAAQ,EAAE,QAAQ,CAAA;IAClB,eAAe,EAAE,iBAAiB,CAAA;CACnC;
|
|
1
|
+
{"version":3,"file":"MiniappEngine.d.ts","sourceRoot":"","sources":["../../src/services/MiniappEngine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAA;AACrD,OAAO,EAAC,QAAQ,EAAsB,MAAM,YAAY,CAAA;AACxD,OAAO,EAAC,QAAQ,EAAC,MAAM,YAAY,CAAA;AAEnC,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,QAAQ,CAAA;IAChB,QAAQ,EAAE,QAAQ,CAAA;IAClB,eAAe,EAAE,iBAAiB,CAAA;CACnC;AAkDD,iDAAiD;AACjD,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAEjD;AAED,iFAAiF;AACjF,wBAAsB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpE;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAS1E;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,aAAa,CAyCnD;AAED,+EAA+E;AAC/E,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAsBvD;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,IAAI,CAEvD"}
|
|
@@ -19,42 +19,75 @@ import { JSCrashController } from "./JSCrashController";
|
|
|
19
19
|
import { JSRouter } from "./JSRouter";
|
|
20
20
|
import { UIRouter } from "./UIRouter";
|
|
21
21
|
let engine = null;
|
|
22
|
+
/**
|
|
23
|
+
* Kill and re-spawn a miniapp's background JSContext from its installed bundle
|
|
24
|
+
* (dev: the dev server's fresh build). Resolves false when it isn't installed
|
|
25
|
+
* or the spawn failed.
|
|
26
|
+
*/
|
|
27
|
+
async function respawnBackground(router, uiRouter, packageName) {
|
|
28
|
+
// Re-resolve the freshly built bundle (dev: HTTP off the dev server;
|
|
29
|
+
// released: file:// snapshot) via the launcher's shared recipe. Carries
|
|
30
|
+
// the declared permissions + manifest across the respawn — omitting them
|
|
31
|
+
// would respawn the JSContext with no permissions, so SUBSCRIBE gates and
|
|
32
|
+
// per-call dispatch checks would start rejecting after a background
|
|
33
|
+
// hot-reload.
|
|
34
|
+
const resolved = await miniappLauncher.resolveBundle(packageName);
|
|
35
|
+
if (!resolved) {
|
|
36
|
+
console.warn(`JSRuntime: respawn-bg could not resolve bundle for ${packageName}`);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
// Force a respawn (kill + spawn) rather than launcher.ensureRunning,
|
|
40
|
+
// which is idempotent and would no-op an already-registered context.
|
|
41
|
+
await router.unregister(packageName);
|
|
42
|
+
const ok = await router.spawnAndRegister(packageName, resolved.bgSource, {
|
|
43
|
+
permissions: resolved.declaredPermissions,
|
|
44
|
+
installedManifest: resolved.installedManifest,
|
|
45
|
+
});
|
|
46
|
+
if (!ok) {
|
|
47
|
+
console.warn(`JSRuntime: respawn-bg failed for ${packageName}`);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
// The respawned JSContext is a fresh MiniappSession with ui.bound false.
|
|
51
|
+
// The mounted WebView won't re-fire glassly.ready() (it's latched), so
|
|
52
|
+
// re-announce it so RPC replies (glassly.request) reach the UI again
|
|
53
|
+
// instead of being dropped while unbound.
|
|
54
|
+
uiRouter.notifyReopen(packageName);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
22
57
|
function wireDevServerRespawnBackground(router, uiRouter) {
|
|
23
58
|
devServerBridge.onRespawnBackground(async (packageName) => {
|
|
24
59
|
try {
|
|
25
|
-
|
|
26
|
-
// released: file:// snapshot) via the launcher's shared recipe. Carries
|
|
27
|
-
// the declared permissions + manifest across the respawn — omitting them
|
|
28
|
-
// would respawn the JSContext with no permissions, so SUBSCRIBE gates and
|
|
29
|
-
// per-call dispatch checks would start rejecting after a background
|
|
30
|
-
// hot-reload.
|
|
31
|
-
const resolved = await miniappLauncher.resolveBundle(packageName);
|
|
32
|
-
if (!resolved) {
|
|
33
|
-
console.warn(`JSRuntime: respawn-bg could not resolve bundle for ${packageName}`);
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
// Force a respawn (kill + spawn) rather than launcher.ensureRunning,
|
|
37
|
-
// which is idempotent and would no-op an already-registered context.
|
|
38
|
-
await router.unregister(packageName);
|
|
39
|
-
const ok = await router.spawnAndRegister(packageName, resolved.bgSource, {
|
|
40
|
-
permissions: resolved.declaredPermissions,
|
|
41
|
-
installedManifest: resolved.installedManifest,
|
|
42
|
-
});
|
|
43
|
-
if (!ok) {
|
|
44
|
-
console.warn(`JSRuntime: respawn-bg failed for ${packageName}`);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
// The respawned JSContext is a fresh MiniappSession with ui.bound false.
|
|
48
|
-
// The mounted WebView won't re-fire glassly.ready() (it's latched), so
|
|
49
|
-
// re-announce it so RPC replies (glassly.request) reach the UI again
|
|
50
|
-
// instead of being dropped while unbound.
|
|
51
|
-
uiRouter.notifyReopen(packageName);
|
|
60
|
+
await respawnBackground(router, uiRouter, packageName);
|
|
52
61
|
}
|
|
53
62
|
catch (e) {
|
|
54
63
|
console.warn(`JSRuntime: respawn-bg threw for ${packageName}:`, e);
|
|
55
64
|
}
|
|
56
65
|
});
|
|
57
66
|
}
|
|
67
|
+
/** Packages with a live background JSContext. */
|
|
68
|
+
export function runningMiniappPackages() {
|
|
69
|
+
return engine?.router.registeredPackages() ?? [];
|
|
70
|
+
}
|
|
71
|
+
/** Tear down a miniapp's background JSContext; a no-op when it isn't running. */
|
|
72
|
+
export async function stopMiniapp(packageName) {
|
|
73
|
+
await engine?.router.unregister(packageName);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Start (or restart) a miniapp's background so it reads its storage afresh (an
|
|
77
|
+
* app state restore). Resolves false when the engine isn't up or the spawn failed.
|
|
78
|
+
*/
|
|
79
|
+
export async function respawnMiniapp(packageName) {
|
|
80
|
+
const current = engine;
|
|
81
|
+
if (!current)
|
|
82
|
+
return false;
|
|
83
|
+
try {
|
|
84
|
+
return await respawnBackground(current.router, current.uiRouter, packageName);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
console.warn(`JSRuntime: respawn threw for ${packageName}:`, e);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
58
91
|
/**
|
|
59
92
|
* Construct (once) and return the JSRuntime engine singletons. Starts the router's
|
|
60
93
|
* message pump on first call. Subsequent calls return the same instances.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MiniappEngine.js","sourceRoot":"","sources":["../../src/services/MiniappEngine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * MiniappEngine — constructs the JSRuntime router singletons (crash controller,\n * UI router, JS router), binds them to the native Crust module + the launcher,\n * wires the dev-server background-respawn signal, and starts the message pump.\n *\n * Engine owns this so a bare OEM's `engine.start()` brings up the local-miniapp\n * engine with no host construction step. Host-only concerns that remain outside\n * engine, such as Glassly-app crashloop telemetry, are attached by the host after\n * this returns via `router.onCrashloop` / `router.onRestartToast`.\n *\n * Idempotent — `ensureMiniappEngine()` returns the same singletons on every call,\n * so it's safe to call from both `engine.start()` and the host bootstrap.\n */\n\nimport CrustModule from \"@glassly/crust\"\n\nimport devServerBridge from \"./DevServerBridge\"\nimport localMiniappRuntime from \"./LocalMiniappRuntime\"\nimport {miniappLauncher} from \"./MiniappLauncher\"\nimport {JSCrashController} from \"./JSCrashController\"\nimport {JSRouter, type JSCrustBinding} from \"./JSRouter\"\nimport {UIRouter} from \"./UIRouter\"\n\nexport interface MiniappEngine {\n router: JSRouter\n uiRouter: UIRouter\n crashController: JSCrashController\n}\n\nlet engine: MiniappEngine | null = null\n\nfunction wireDevServerRespawnBackground(router: JSRouter, uiRouter: UIRouter): void {\n devServerBridge.onRespawnBackground(async (packageName) => {\n try {\n // Re-resolve the freshly built bundle (dev: HTTP off the dev server;\n // released: file:// snapshot) via the launcher's shared recipe. Carries\n // the declared permissions + manifest across the respawn — omitting them\n // would respawn the JSContext with no permissions, so SUBSCRIBE gates and\n // per-call dispatch checks would start rejecting after a background\n // hot-reload.\n const resolved = await miniappLauncher.resolveBundle(packageName)\n if (!resolved) {\n console.warn(`JSRuntime: respawn-bg could not resolve bundle for ${packageName}`)\n return\n }\n // Force a respawn (kill + spawn) rather than launcher.ensureRunning,\n // which is idempotent and would no-op an already-registered context.\n await router.unregister(packageName)\n const ok = await router.spawnAndRegister(packageName, resolved.bgSource, {\n permissions: resolved.declaredPermissions,\n installedManifest: resolved.installedManifest,\n })\n if (!ok) {\n console.warn(`JSRuntime: respawn-bg failed for ${packageName}`)\n return\n }\n // The respawned JSContext is a fresh MiniappSession with ui.bound false.\n // The mounted WebView won't re-fire glassly.ready() (it's latched), so\n // re-announce it so RPC replies (glassly.request) reach the UI again\n // instead of being dropped while unbound.\n uiRouter.notifyReopen(packageName)\n } catch (e) {\n console.warn(`JSRuntime: respawn-bg threw for ${packageName}:`, e)\n }\n })\n}\n\n/**\n * Construct (once) and return the JSRuntime engine singletons. Starts the router's\n * message pump on first call. Subsequent calls return the same instances.\n */\nexport function ensureMiniappEngine(): MiniappEngine {\n if (engine) {\n wireDevServerRespawnBackground(engine.router, engine.uiRouter)\n engine.router.start()\n return engine\n }\n\n // The Crust native module's published TS typing doesn't include the new\n // js* functions until expo prebuild regenerates it; cast to the binding\n // shapes the routers expect.\n const crust = CrustModule as unknown as JSCrustBinding\n\n const crashController = new JSCrashController({\n maxRetries: 3,\n })\n const uiRouter = new UIRouter({\n jsDispatchToJs: (packageName: string, envelope: Record<string, unknown>) =>\n (\n CrustModule as unknown as {\n jsDispatchToJs: (p: string, e: Record<string, unknown>) => Promise<void>\n }\n ).jsDispatchToJs(packageName, envelope),\n })\n const router = new JSRouter(localMiniappRuntime, crust)\n router.crashController = crashController\n router.uiRouter = uiRouter\n\n // Hand the router to the engine MiniappLauncher so headless launch/teardown\n // (apps.ts start/stop, the action broker, the WebView mount path) all spawn\n // through one place.\n miniappLauncher.configure({router})\n\n // Wire up the dev server's \"respawn-bg\" signal so a touch under\n // src/background/ kills + re-spawns the JSContext with the latest bundle.\n // The WebView reload path stays separate (devServerBridge.onReload).\n wireDevServerRespawnBackground(router, uiRouter)\n\n router.start()\n\n engine = {router, uiRouter, crashController}\n return engine\n}\n\n/** Stop the JSRuntime message pump and tear down router-managed JSContexts. */\nexport async function stopMiniappEngine(): Promise<void> {\n const current = engine\n if (!current) return\n // Null the singleton BEFORE the async teardown below: a concurrent\n // ensureMiniappEngine() must construct fresh instead of re-starting (and\n // re-subscribing) the router that is mid-teardown, which would leak a zombie\n // Crust subscription once this function finishes.\n engine = null\n\n devServerBridge.clearRespawnBackgroundHandler()\n current.router.stop()\n\n const packages = current.router.registeredPackages()\n await Promise.all(\n packages.map(async (packageName) => {\n try {\n await current.router.unregister(packageName)\n } catch (err) {\n console.warn(`JSRuntime: failed to unregister ${packageName} during stop:`, err)\n }\n }),\n )\n}\n\n/** Returns the engine singletons if already constructed, else null. */\nexport function getMiniappEngine(): MiniappEngine | null {\n return engine\n}\n"]}
|
|
1
|
+
{"version":3,"file":"MiniappEngine.js","sourceRoot":"","sources":["../../src/services/MiniappEngine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,WAAW,MAAM,gBAAgB,CAAA;AAExC,OAAO,eAAe,MAAM,mBAAmB,CAAA;AAC/C,OAAO,mBAAmB,MAAM,uBAAuB,CAAA;AACvD,OAAO,EAAC,eAAe,EAAC,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAA;AACrD,OAAO,EAAC,QAAQ,EAAsB,MAAM,YAAY,CAAA;AACxD,OAAO,EAAC,QAAQ,EAAC,MAAM,YAAY,CAAA;AAQnC,IAAI,MAAM,GAAyB,IAAI,CAAA;AAEvC;;;;GAIG;AACH,KAAK,UAAU,iBAAiB,CAAC,MAAgB,EAAE,QAAkB,EAAE,WAAmB;IACxF,qEAAqE;IACrE,wEAAwE;IACxE,yEAAyE;IACzE,0EAA0E;IAC1E,oEAAoE;IACpE,cAAc;IACd,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,aAAa,CAAC,WAAW,CAAC,CAAA;IACjE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,CAAC,IAAI,CAAC,sDAAsD,WAAW,EAAE,CAAC,CAAA;QACjF,OAAO,KAAK,CAAA;IACd,CAAC;IACD,qEAAqE;IACrE,qEAAqE;IACrE,MAAM,MAAM,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;IACpC,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,WAAW,EAAE,QAAQ,CAAC,QAAQ,EAAE;QACvE,WAAW,EAAE,QAAQ,CAAC,mBAAmB;QACzC,iBAAiB,EAAE,QAAQ,CAAC,iBAAiB;KAC9C,CAAC,CAAA;IACF,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,OAAO,CAAC,IAAI,CAAC,oCAAoC,WAAW,EAAE,CAAC,CAAA;QAC/D,OAAO,KAAK,CAAA;IACd,CAAC;IACD,yEAAyE;IACzE,uEAAuE;IACvE,qEAAqE;IACrE,0CAA0C;IAC1C,QAAQ,CAAC,YAAY,CAAC,WAAW,CAAC,CAAA;IAClC,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,8BAA8B,CAAC,MAAgB,EAAE,QAAkB;IAC1E,eAAe,CAAC,mBAAmB,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE;QACxD,IAAI,CAAC;YACH,MAAM,iBAAiB,CAAC,MAAM,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAA;QACxD,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,mCAAmC,WAAW,GAAG,EAAE,CAAC,CAAC,CAAA;QACpE,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,sBAAsB;IACpC,OAAO,MAAM,EAAE,MAAM,CAAC,kBAAkB,EAAE,IAAI,EAAE,CAAA;AAClD,CAAC;AAED,iFAAiF;AACjF,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,WAAmB;IACnD,MAAM,MAAM,EAAE,MAAM,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;AAC9C,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,WAAmB;IACtD,MAAM,OAAO,GAAG,MAAM,CAAA;IACtB,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAA;IAC1B,IAAI,CAAC;QACH,OAAO,MAAM,iBAAiB,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAA;IAC/E,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,IAAI,CAAC,gCAAgC,WAAW,GAAG,EAAE,CAAC,CAAC,CAAA;QAC/D,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB;IACjC,IAAI,MAAM,EAAE,CAAC;QACX,8BAA8B,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAA;QAC9D,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAA;QACrB,OAAO,MAAM,CAAA;IACf,CAAC;IAED,wEAAwE;IACxE,wEAAwE;IACxE,6BAA6B;IAC7B,MAAM,KAAK,GAAG,WAAwC,CAAA;IAEtD,MAAM,eAAe,GAAG,IAAI,iBAAiB,CAAC;QAC5C,UAAU,EAAE,CAAC;KACd,CAAC,CAAA;IACF,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC;QAC5B,cAAc,EAAE,CAAC,WAAmB,EAAE,QAAiC,EAAE,EAAE,CAEvE,WAGD,CAAC,cAAc,CAAC,WAAW,EAAE,QAAQ,CAAC;KAC1C,CAAC,CAAA;IACF,MAAM,MAAM,GAAG,IAAI,QAAQ,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAA;IACvD,MAAM,CAAC,eAAe,GAAG,eAAe,CAAA;IACxC,MAAM,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAE1B,4EAA4E;IAC5E,4EAA4E;IAC5E,qBAAqB;IACrB,eAAe,CAAC,SAAS,CAAC,EAAC,MAAM,EAAC,CAAC,CAAA;IAEnC,gEAAgE;IAChE,0EAA0E;IAC1E,qEAAqE;IACrE,8BAA8B,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IAEhD,MAAM,CAAC,KAAK,EAAE,CAAA;IAEd,MAAM,GAAG,EAAC,MAAM,EAAE,QAAQ,EAAE,eAAe,EAAC,CAAA;IAC5C,OAAO,MAAM,CAAA;AACf,CAAC;AAED,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,MAAM,OAAO,GAAG,MAAM,CAAA;IACtB,IAAI,CAAC,OAAO;QAAE,OAAM;IACpB,mEAAmE;IACnE,yEAAyE;IACzE,6EAA6E;IAC7E,kDAAkD;IAClD,MAAM,GAAG,IAAI,CAAA;IAEb,eAAe,CAAC,6BAA6B,EAAE,CAAA;IAC/C,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAA;IAErB,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,kBAAkB,EAAE,CAAA;IACpD,MAAM,OAAO,CAAC,GAAG,CACf,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE;QACjC,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;QAC9C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,mCAAmC,WAAW,eAAe,EAAE,GAAG,CAAC,CAAA;QAClF,CAAC;IACH,CAAC,CAAC,CACH,CAAA;AACH,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,gBAAgB;IAC9B,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["/**\n * MiniappEngine — constructs the JSRuntime router singletons (crash controller,\n * UI router, JS router), binds them to the native Crust module + the launcher,\n * wires the dev-server background-respawn signal, and starts the message pump.\n *\n * Engine owns this so a bare OEM's `engine.start()` brings up the local-miniapp\n * engine with no host construction step. Host-only concerns that remain outside\n * engine, such as Glassly-app crashloop telemetry, are attached by the host after\n * this returns via `router.onCrashloop` / `router.onRestartToast`.\n *\n * Idempotent — `ensureMiniappEngine()` returns the same singletons on every call,\n * so it's safe to call from both `engine.start()` and the host bootstrap.\n */\n\nimport CrustModule from \"@glassly/crust\"\n\nimport devServerBridge from \"./DevServerBridge\"\nimport localMiniappRuntime from \"./LocalMiniappRuntime\"\nimport {miniappLauncher} from \"./MiniappLauncher\"\nimport {JSCrashController} from \"./JSCrashController\"\nimport {JSRouter, type JSCrustBinding} from \"./JSRouter\"\nimport {UIRouter} from \"./UIRouter\"\n\nexport interface MiniappEngine {\n router: JSRouter\n uiRouter: UIRouter\n crashController: JSCrashController\n}\n\nlet engine: MiniappEngine | null = null\n\n/**\n * Kill and re-spawn a miniapp's background JSContext from its installed bundle\n * (dev: the dev server's fresh build). Resolves false when it isn't installed\n * or the spawn failed.\n */\nasync function respawnBackground(router: JSRouter, uiRouter: UIRouter, packageName: string): Promise<boolean> {\n // Re-resolve the freshly built bundle (dev: HTTP off the dev server;\n // released: file:// snapshot) via the launcher's shared recipe. Carries\n // the declared permissions + manifest across the respawn — omitting them\n // would respawn the JSContext with no permissions, so SUBSCRIBE gates and\n // per-call dispatch checks would start rejecting after a background\n // hot-reload.\n const resolved = await miniappLauncher.resolveBundle(packageName)\n if (!resolved) {\n console.warn(`JSRuntime: respawn-bg could not resolve bundle for ${packageName}`)\n return false\n }\n // Force a respawn (kill + spawn) rather than launcher.ensureRunning,\n // which is idempotent and would no-op an already-registered context.\n await router.unregister(packageName)\n const ok = await router.spawnAndRegister(packageName, resolved.bgSource, {\n permissions: resolved.declaredPermissions,\n installedManifest: resolved.installedManifest,\n })\n if (!ok) {\n console.warn(`JSRuntime: respawn-bg failed for ${packageName}`)\n return false\n }\n // The respawned JSContext is a fresh MiniappSession with ui.bound false.\n // The mounted WebView won't re-fire glassly.ready() (it's latched), so\n // re-announce it so RPC replies (glassly.request) reach the UI again\n // instead of being dropped while unbound.\n uiRouter.notifyReopen(packageName)\n return true\n}\n\nfunction wireDevServerRespawnBackground(router: JSRouter, uiRouter: UIRouter): void {\n devServerBridge.onRespawnBackground(async (packageName) => {\n try {\n await respawnBackground(router, uiRouter, packageName)\n } catch (e) {\n console.warn(`JSRuntime: respawn-bg threw for ${packageName}:`, e)\n }\n })\n}\n\n/** Packages with a live background JSContext. */\nexport function runningMiniappPackages(): string[] {\n return engine?.router.registeredPackages() ?? []\n}\n\n/** Tear down a miniapp's background JSContext; a no-op when it isn't running. */\nexport async function stopMiniapp(packageName: string): Promise<void> {\n await engine?.router.unregister(packageName)\n}\n\n/**\n * Start (or restart) a miniapp's background so it reads its storage afresh (an\n * app state restore). Resolves false when the engine isn't up or the spawn failed.\n */\nexport async function respawnMiniapp(packageName: string): Promise<boolean> {\n const current = engine\n if (!current) return false\n try {\n return await respawnBackground(current.router, current.uiRouter, packageName)\n } catch (e) {\n console.warn(`JSRuntime: respawn threw for ${packageName}:`, e)\n return false\n }\n}\n\n/**\n * Construct (once) and return the JSRuntime engine singletons. Starts the router's\n * message pump on first call. Subsequent calls return the same instances.\n */\nexport function ensureMiniappEngine(): MiniappEngine {\n if (engine) {\n wireDevServerRespawnBackground(engine.router, engine.uiRouter)\n engine.router.start()\n return engine\n }\n\n // The Crust native module's published TS typing doesn't include the new\n // js* functions until expo prebuild regenerates it; cast to the binding\n // shapes the routers expect.\n const crust = CrustModule as unknown as JSCrustBinding\n\n const crashController = new JSCrashController({\n maxRetries: 3,\n })\n const uiRouter = new UIRouter({\n jsDispatchToJs: (packageName: string, envelope: Record<string, unknown>) =>\n (\n CrustModule as unknown as {\n jsDispatchToJs: (p: string, e: Record<string, unknown>) => Promise<void>\n }\n ).jsDispatchToJs(packageName, envelope),\n })\n const router = new JSRouter(localMiniappRuntime, crust)\n router.crashController = crashController\n router.uiRouter = uiRouter\n\n // Hand the router to the engine MiniappLauncher so headless launch/teardown\n // (apps.ts start/stop, the action broker, the WebView mount path) all spawn\n // through one place.\n miniappLauncher.configure({router})\n\n // Wire up the dev server's \"respawn-bg\" signal so a touch under\n // src/background/ kills + re-spawns the JSContext with the latest bundle.\n // The WebView reload path stays separate (devServerBridge.onReload).\n wireDevServerRespawnBackground(router, uiRouter)\n\n router.start()\n\n engine = {router, uiRouter, crashController}\n return engine\n}\n\n/** Stop the JSRuntime message pump and tear down router-managed JSContexts. */\nexport async function stopMiniappEngine(): Promise<void> {\n const current = engine\n if (!current) return\n // Null the singleton BEFORE the async teardown below: a concurrent\n // ensureMiniappEngine() must construct fresh instead of re-starting (and\n // re-subscribing) the router that is mid-teardown, which would leak a zombie\n // Crust subscription once this function finishes.\n engine = null\n\n devServerBridge.clearRespawnBackgroundHandler()\n current.router.stop()\n\n const packages = current.router.registeredPackages()\n await Promise.all(\n packages.map(async (packageName) => {\n try {\n await current.router.unregister(packageName)\n } catch (err) {\n console.warn(`JSRuntime: failed to unregister ${packageName} during stop:`, err)\n }\n }),\n )\n}\n\n/** Returns the engine singletons if already constructed, else null. */\nexport function getMiniappEngine(): MiniappEngine | null {\n return engine\n}\n"]}
|
|
@@ -67,6 +67,8 @@ declare class MiniappLauncher {
|
|
|
67
67
|
configure(deps: LauncherDeps): void;
|
|
68
68
|
private requireRouter;
|
|
69
69
|
/** True iff the package's background JS context is currently spawned. */
|
|
70
|
+
/** Tell an already-running miniapp the user opened it again and from where. */
|
|
71
|
+
notifyLaunch(packageName: string, launchSource: MiniappLaunchSource): void;
|
|
70
72
|
isRunning(packageName: string): boolean;
|
|
71
73
|
/**
|
|
72
74
|
* Resolve the background JS source + UI entry + declared permissions for a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MiniappLauncher.d.ts","sourceRoot":"","sources":["../../src/services/MiniappLauncher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AASH,OAA4B,EAAC,KAAK,wBAAwB,EAAC,MAAM,uBAAuB,CAAA;AACxF,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,kBAAkB,CAAA;AACzD,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,YAAY,CAAA;AAExC,UAAU,YAAY;IACpB,oEAAoE;IACpE,MAAM,EAAE,QAAQ,CAAA;CACjB;AAED,6EAA6E;AAC7E,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,mBAAmB,CAAA;CACnC;AAED,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,mBAAmB,EAAE,MAAM,EAAE,CAAA;IAC7B,iBAAiB,CAAC,EAAE,wBAAwB,CAAA;IAC5C,yEAAyE;IACzE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED,uDAAuD;AACvD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,cAAM,eAAe;IACnB,OAAO,CAAC,IAAI,CAA4B;IAExC;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2C;IAEpE,uEAAuE;IACvE,SAAS,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAInC,OAAO,CAAC,aAAa;IAOrB,yEAAyE;IACzE,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAIvC;;;;;;OAMG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAiH7F;;;;;;;;;;OAUG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"MiniappLauncher.d.ts","sourceRoot":"","sources":["../../src/services/MiniappLauncher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AASH,OAA4B,EAAC,KAAK,wBAAwB,EAAC,MAAM,uBAAuB,CAAA;AACxF,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,kBAAkB,CAAA;AACzD,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,YAAY,CAAA;AAExC,UAAU,YAAY;IACpB,oEAAoE;IACpE,MAAM,EAAE,QAAQ,CAAA;CACjB;AAED,6EAA6E;AAC7E,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,mBAAmB,CAAA;CACnC;AAED,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,mBAAmB,EAAE,MAAM,EAAE,CAAA;IAC7B,iBAAiB,CAAC,EAAE,wBAAwB,CAAA;IAC5C,yEAAyE;IACzE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED,uDAAuD;AACvD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,cAAM,eAAe;IACnB,OAAO,CAAC,IAAI,CAA4B;IAExC;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2C;IAEpE,uEAAuE;IACvE,SAAS,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAInC,OAAO,CAAC,aAAa;IAOrB,yEAAyE;IACzE,+EAA+E;IAC/E,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,mBAAmB,GAAG,IAAI;IAI1E,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAIvC;;;;;;OAMG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAiH7F;;;;;;;;;;OAUG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IA0BpF,iFAAiF;YACnE,KAAK;IAkCnB;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACG,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAsC7C;;;;OAIG;IACG,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,gBAAgB,SAAS,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAKzG,mDAAmD;IAC7C,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB9C,OAAO,CAAC,YAAY;IAKpB,4EAA4E;IAC5E,OAAO,CAAC,cAAc;CASvB;AAED,eAAO,MAAM,eAAe,iBAAwB,CAAA"}
|
|
@@ -50,6 +50,10 @@ class MiniappLauncher {
|
|
|
50
50
|
return this.deps.router;
|
|
51
51
|
}
|
|
52
52
|
/** True iff the package's background JS context is currently spawned. */
|
|
53
|
+
/** Tell an already-running miniapp the user opened it again and from where. */
|
|
54
|
+
notifyLaunch(packageName, launchSource) {
|
|
55
|
+
this.deps?.router.notifyLaunch(packageName, launchSource);
|
|
56
|
+
}
|
|
53
57
|
isRunning(packageName) {
|
|
54
58
|
return this.deps?.router.registeredPackages().includes(packageName) ?? false;
|
|
55
59
|
}
|
|
@@ -171,6 +175,8 @@ class MiniappLauncher {
|
|
|
171
175
|
*/
|
|
172
176
|
async ensureRunning(packageName, hints) {
|
|
173
177
|
const router = this.requireRouter();
|
|
178
|
+
// A context still tearing down from a stop is not one to reuse.
|
|
179
|
+
await router.whenUnregistered(packageName);
|
|
174
180
|
// Already spawned: best-effort resolve for the UI entry, never throw —
|
|
175
181
|
// a headless caller that doesn't need UI gets {null, null} fast even if
|
|
176
182
|
// the dev server has since dropped.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MiniappLauncher.js","sourceRoot":"","sources":["../../src/services/MiniappLauncher.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * MiniappLauncher — headless launch/teardown of a local miniapp's background\n * JS context.\n *\n * This is the one piece of \"run a miniapp\" that used to live inside the\n * `LocalMiniappView` React component (resolve the bundle → read the manifest →\n * `spawnAndRegister`), triggered only when the Compositor mounted that\n * component on foreground. Pulling it into the runtime means:\n *\n * - `start()` can actually spawn an app even when nothing foregrounds it\n * (e.g. a system app launching another miniapp via `session.miniapps`).\n * - the action broker can wake a stopped target headlessly — no UI at all.\n * - the dev hot-reload respawn path and the WebView mount path share one\n * resolve+spawn recipe instead of three copies.\n *\n * The launcher is **headless**: no React, no RN components. The engine-owned\n * MiniappEngine hands it the {@link JSRouter} instance once during\n * engine construction. OEMs embedding the runtime drive miniapp lifecycle\n * through the engine facade and stores, not a separate launcher configure API.\n *\n * Boundary: the launcher owns the **background** context. WebView *rendering*\n * (the UI layer) stays with the host — the launcher just hands back the\n * resolved UI entry so the host can mount its `<WebView>`.\n */\n\nimport {File} from \"expo-file-system\"\n\nimport {permissions} from \"../facades/permissions\"\nimport {decideDevLaunchRoute} from \"../utils/devMiniappLaunch\"\nimport {storage} from \"../utils/storage/storage\"\nimport appRegistry from \"./AppRegistry\"\nimport devServerBridge from \"./DevServerBridge\"\nimport localMiniappRuntime, {type InstalledMiniappManifest} from \"./LocalMiniappRuntime\"\nimport type {MiniappLaunchSource} from \"@glassly/miniapp\"\nimport type {JSRouter} from \"./JSRouter\"\n\ninterface LauncherDeps {\n /** The host-constructed router (needs the native Crust binding). */\n router: JSRouter\n}\n\n/** Hints the host may pass from a view's props to avoid re-deriving them. */\nexport interface LaunchHints {\n devUrl?: string\n version?: string\n devPort?: string\n /** Who started the app; reported to the miniapp on connect. */\n launchSource?: MiniappLaunchSource\n}\n\n/** Everything the launcher resolved for a package — bg source + UI entry. */\nexport interface ResolvedBundle {\n bgSource: string\n uiUri: string | null\n uiBaseDir: string | null\n declaredPermissions: string[]\n installedManifest?: InstalledMiniappManifest\n /** Set for dev miniapps (HTTP off the dev server); null for released. */\n devUrl: string | null\n devPort: number | null\n}\n\n/** What the host needs back to render the UI layer. */\nexport interface LaunchResult {\n uiUri: string | null\n uiBaseDir: string | null\n}\n\nclass MiniappLauncher {\n private deps: LauncherDeps | null = null\n\n /**\n * In-flight launches keyed by packageName. Both apps.ts start() and the\n * WebView mount call ensureRunning() before the first spawnAndRegister\n * resolves — without this, neither sees the package in registeredPackages()\n * yet and both spawn the same context (a clobber/double-spawn). Concurrent\n * callers share the same promise.\n */\n private readonly inFlight = new Map<string, Promise<LaunchResult>>()\n\n /** Wire the router in. Called once from MiniappEngine construction. */\n configure(deps: LauncherDeps): void {\n this.deps = deps\n }\n\n private requireRouter(): JSRouter {\n if (!this.deps) {\n throw new Error(\"MiniappLauncher not configured — ensureMiniappEngine() has not run\")\n }\n return this.deps.router\n }\n\n /** True iff the package's background JS context is currently spawned. */\n isRunning(packageName: string): boolean {\n return this.deps?.router.registeredPackages().includes(packageName) ?? false\n }\n\n /**\n * Resolve the background JS source + UI entry + declared permissions for a\n * package. Handles both dev (HTTP off the running dev server) and released\n * (file:// from the installed snapshot). Reads disk/network/storage; does\n * NOT spawn. Returns null when the bundle can't be resolved (dev server\n * unreachable, missing entry, no installed version).\n */\n async resolveBundle(packageName: string, hints?: LaunchHints): Promise<ResolvedBundle | null> {\n const devUrl = hints?.devUrl ?? this.storedDevUrl(packageName)\n\n // --- Dev: load directly off the local dev server over HTTP. ---\n if (devUrl) {\n const route = await decideDevLaunchRoute(packageName, devUrl)\n if (route.decision === \"offline\" || !route.manifest) return null\n const manifest = route.manifest\n const entry = manifest.entry as {background?: string; ui?: string} | undefined\n if (!entry?.background) return null\n\n // Use the host that actually answered — may differ from the stored IP\n // after a laptop Wi-Fi change (mDNS / Metro failover inside decideDevLaunchRoute).\n const base = route.resolvedUrl.replace(/\\/$/, \"\")\n // entry.* are bundle-root paths (dist/ stripped); the dev server serves\n // files relative to cwd, so prepend dist/.\n const bgUrl = `${base}/dist/${entry.background.replace(/^\\.?\\/+/, \"\")}`\n const uiUri = entry.ui ? `${base}/dist/${entry.ui.replace(/^\\.?\\/+/, \"\")}` : null\n\n const perms = manifest.permissions as Array<{type?: string} | string> | undefined\n const declaredPermissions = (perms ?? [])\n .map((p) => (typeof p === \"string\" ? p : p?.type))\n .filter((t): t is string => typeof t === \"string\")\n const installedManifest: InstalledMiniappManifest = {\n packageName: typeof manifest.packageName === \"string\" ? manifest.packageName : packageName,\n name: manifest.name,\n version: typeof manifest.version === \"string\" ? manifest.version : undefined,\n sdkVersion: typeof manifest.sdkVersion === \"string\" ? manifest.sdkVersion : undefined,\n minHostVersion: typeof manifest.minHostVersion === \"string\" ? manifest.minHostVersion : undefined,\n type: typeof manifest.type === \"string\" ? manifest.type : undefined,\n entry: manifest.entry as InstalledMiniappManifest[\"entry\"],\n permissions: manifest.permissions as InstalledMiniappManifest[\"permissions\"],\n hardwareRequirements: manifest.hardwareRequirements as InstalledMiniappManifest[\"hardwareRequirements\"],\n actions: manifest.actions as InstalledMiniappManifest[\"actions\"],\n widgets: manifest.widgets,\n }\n\n let bgSource: string\n try {\n const res = await fetch(bgUrl)\n if (!res.ok) return null\n bgSource = await res.text()\n } catch {\n return null\n }\n\n return {\n bgSource,\n uiUri,\n uiBaseDir: uiUri ? uiUri.replace(/\\/[^/]+$/, \"/\") : null,\n declaredPermissions,\n installedManifest,\n devUrl: route.resolvedUrl,\n devPort: this.resolveDevPort(hints?.devPort, packageName),\n }\n }\n\n // --- Released: resolve from the installed file:// snapshot. ---\n const version = hints?.version ?? (await appRegistry.getActiveVersion(packageName))\n if (!version) return null\n const entryPaths = appRegistry.getMiniappEntryPaths(packageName, version)\n if (!entryPaths?.background) return null\n\n const manifest = appRegistry.getMiniappManifest(packageName, version) as {\n packageName?: string\n name?: string\n version?: string\n sdkVersion?: string\n minHostVersion?: string\n type?: string\n entry?: {background?: string; ui?: string}\n permissions?: Array<{type: string; required?: boolean; description?: string}>\n hardwareRequirements?: Array<{type: string; level: string; description?: string}>\n actions?: Array<{id?: unknown; description?: unknown; parameters?: unknown; outputSchema?: unknown}>\n widgets?: unknown\n } | null\n const declaredPermissions = (manifest?.permissions ?? [])\n .map((p) => p.type)\n .filter((t): t is string => typeof t === \"string\")\n const installedManifest: InstalledMiniappManifest | undefined = manifest\n ? {\n packageName: manifest.packageName ?? packageName,\n name: manifest.name,\n version: manifest.version ?? version,\n sdkVersion: manifest.sdkVersion,\n minHostVersion: manifest.minHostVersion,\n type: manifest.type,\n entry: manifest.entry,\n permissions: manifest.permissions,\n hardwareRequirements: manifest.hardwareRequirements,\n actions: manifest.actions,\n widgets: manifest.widgets,\n }\n : undefined\n\n let bgSource: string\n try {\n bgSource = new File(entryPaths.background).textSync()\n } catch {\n return null\n }\n\n return {\n bgSource,\n uiUri: entryPaths.ui ?? null,\n uiBaseDir: entryPaths.ui ? entryPaths.ui.replace(/\\/[^/]+$/, \"/\") : null,\n declaredPermissions,\n installedManifest,\n devUrl: null,\n devPort: null,\n }\n }\n\n /**\n * Ensure a miniapp's background JS context is spawned. Idempotent — if the\n * context is already alive this is a no-op spawn-wise (it still resolves the\n * UI entry so a re-mounting host gets its `<WebView>` source back).\n *\n * Resolves once the context is SPAWNED. It does NOT block on the context's\n * CONNECT handshake — the host's WebView mount shouldn't wait on background\n * JS. Callers that need the context to be connected and ready to receive\n * messages (the action broker) follow up with\n * {@link LocalMiniappRuntime.waitForConnect}.\n */\n async ensureRunning(packageName: string, hints?: LaunchHints): Promise<LaunchResult> {\n const router = this.requireRouter()\n\n // Already spawned: best-effort resolve for the UI entry, never throw —\n // a headless caller that doesn't need UI gets {null, null} fast even if\n // the dev server has since dropped.\n if (router.registeredPackages().includes(packageName)) {\n const existing = await this.resolveBundle(packageName, hints).catch(() => null)\n return {uiUri: existing?.uiUri ?? null, uiBaseDir: existing?.uiBaseDir ?? null}\n }\n\n // Coalesce concurrent launches of the same package onto one promise.\n const pending = this.inFlight.get(packageName)\n if (pending) return pending\n\n const launch = this.spawn(packageName, hints)\n this.inFlight.set(packageName, launch)\n try {\n return await launch\n } finally {\n this.inFlight.delete(packageName)\n }\n }\n\n /** Resolve the bundle and spawn the context. Serialized via {@link inFlight}. */\n private async spawn(packageName: string, hints?: LaunchHints): Promise<LaunchResult> {\n const router = this.requireRouter()\n const resolved = await this.resolveBundle(packageName, hints)\n if (!resolved) {\n throw new Error(`MiniappLauncher: cannot resolve bundle for ${packageName}`)\n }\n\n // Re-check after the async resolve: a different path may have spawned it\n // while we were fetching/reading the bundle.\n if (!router.registeredPackages().includes(packageName)) {\n const ok = await router.spawnAndRegister(packageName, resolved.bgSource, {\n permissions: resolved.declaredPermissions,\n installedManifest: resolved.installedManifest,\n launchSource: hints?.launchSource,\n })\n if (!ok) {\n throw new Error(`MiniappLauncher: spawn failed for ${packageName}`)\n }\n\n // Dev: wire the hot-reload + log-forwarding sidecar. No-op in prod\n // (devServerBridge silently drops when no sidecar is up).\n if (resolved.devUrl && resolved.devPort != null) {\n devServerBridge.connect(packageName, resolved.devUrl, resolved.devPort)\n } else if (resolved.devUrl) {\n // Dev miniapp with no resolvable port — it still launches, but hot\n // reload + log forwarding won't connect. Surface it instead of failing\n // silently (the old inline path hard-failed with \"no dev port\").\n console.warn(`MiniappLauncher: ${packageName} has no dev port — hot reload + log forwarding disabled`)\n }\n }\n\n return {uiUri: resolved.uiUri, uiBaseDir: resolved.uiBaseDir}\n }\n\n /**\n * Start the Always On local miniapps at boot.\n *\n * Miniapps run in the phone's own JS engine, so a host-process kill tears\n * down their JSContext. Only Always On apps come back: whatever else was\n * running when the app was killed stays stopped until the wearer opens it\n * again. The spawn is headless (no foreground, no navigation) and registers\n * the package so the home tray/switcher project it as running, exactly as a\n * normal start would. The WebView, if any, re-attaches lazily when the user\n * opens the app.\n *\n * One dashboard runs at a time: a dashboard already running keeps its place,\n * else the first Always On dashboard by package name starts and the rest\n * stay stopped. The flag is exclusive after any user action (setAlwaysOn\n * clears it on the other dashboards), so this only arbitrates manifests\n * that both claim it.\n *\n * Idempotent and best-effort: skips already-spawned contexts, and skips apps\n * missing required OS permissions without prompting or clearing their Always\n * On choice. Not compatibility-gated: an Always On app shouldn't be dropped\n * just because glasses are momentarily disconnected at boot — it resumes\n * when they reconnect, same as mid-session.\n */\n async autostartLocalMiniapps(): Promise<void> {\n const apps = await appRegistry.getInstalledMiniapps()\n const dashboards = apps.filter((app) => app.type === \"dashboard\")\n const dashboard =\n dashboards.find((app) => this.isRunning(app.packageName))?.packageName ??\n dashboards\n .filter((app) => app.alwaysOn)\n .map((app) => app.packageName)\n .sort()[0] ??\n null\n for (const app of apps) {\n // Only phone-hosted miniapps have a JSContext to spawn. Offline\n // built-ins (`local:false, offline:true`) have no bundle to launch.\n if (!app.local || !app.alwaysOn) continue\n if (app.type === \"dashboard\" && app.packageName !== dashboard) continue\n if (this.isRunning(app.packageName)) continue\n try {\n const missing = await permissions.missingRequired(app.permissions ?? [])\n if (missing.length > 0) {\n console.info(\n `MiniappLauncher: skipping autostart for ${app.packageName} — missing required permissions: ${missing.join(\n \", \",\n )}`,\n )\n continue\n }\n } catch (e) {\n console.warn(`MiniappLauncher: skipping autostart for ${app.packageName} — permission check failed`, e)\n continue\n }\n try {\n await this.ensureRunning(app.packageName)\n } catch (e) {\n console.warn(`MiniappLauncher: autostart failed for ${app.packageName}`, e)\n }\n }\n }\n\n /**\n * Ensure the context is running AND has completed its CONNECT handshake.\n * Used by the action broker, which must not deliver to a context that hasn't\n * come up yet. Throws on spawn failure or connect timeout.\n */\n async ensureConnected(packageName: string, connectTimeoutMs = 10_000, hints?: LaunchHints): Promise<void> {\n await this.ensureRunning(packageName, hints)\n await localMiniappRuntime.waitForConnect(packageName, connectTimeoutMs)\n }\n\n /** Tear down a miniapp's background JS context. */\n async stop(packageName: string): Promise<void> {\n // If a launch is in flight, let it finish first. Otherwise unregister()\n // no-ops on the not-yet-registered package and the pending spawn then\n // re-registers the context — leaving it running despite the stop.\n const pending = this.inFlight.get(packageName)\n if (pending) {\n try {\n await pending\n } catch {\n // Launch failed — nothing got registered, so nothing to tear down.\n }\n }\n await this.deps?.router.unregister(packageName)\n }\n\n // --- internals -----------------------------------------------------------\n\n private storedDevUrl(packageName: string): string | undefined {\n const r = storage.load<string>(`${packageName}_dev_url`)\n return r.is_ok() ? r.value : undefined\n }\n\n /** resolveDevPort — moved verbatim from the old LocalMiniappView helper. */\n private resolveDevPort(searchParam: string | undefined, packageName: string): number | null {\n if (searchParam) {\n const n = parseInt(searchParam, 10)\n if (Number.isFinite(n)) return n\n }\n const stored = storage.load<number>(`${packageName}_dev_port`)\n if (stored.is_ok()) return stored.value\n return null\n }\n}\n\nexport const miniappLauncher = new MiniappLauncher()\n"]}
|
|
1
|
+
{"version":3,"file":"MiniappLauncher.js","sourceRoot":"","sources":["../../src/services/MiniappLauncher.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * MiniappLauncher — headless launch/teardown of a local miniapp's background\n * JS context.\n *\n * This is the one piece of \"run a miniapp\" that used to live inside the\n * `LocalMiniappView` React component (resolve the bundle → read the manifest →\n * `spawnAndRegister`), triggered only when the Compositor mounted that\n * component on foreground. Pulling it into the runtime means:\n *\n * - `start()` can actually spawn an app even when nothing foregrounds it\n * (e.g. a system app launching another miniapp via `session.miniapps`).\n * - the action broker can wake a stopped target headlessly — no UI at all.\n * - the dev hot-reload respawn path and the WebView mount path share one\n * resolve+spawn recipe instead of three copies.\n *\n * The launcher is **headless**: no React, no RN components. The engine-owned\n * MiniappEngine hands it the {@link JSRouter} instance once during\n * engine construction. OEMs embedding the runtime drive miniapp lifecycle\n * through the engine facade and stores, not a separate launcher configure API.\n *\n * Boundary: the launcher owns the **background** context. WebView *rendering*\n * (the UI layer) stays with the host — the launcher just hands back the\n * resolved UI entry so the host can mount its `<WebView>`.\n */\n\nimport {File} from \"expo-file-system\"\n\nimport {permissions} from \"../facades/permissions\"\nimport {decideDevLaunchRoute} from \"../utils/devMiniappLaunch\"\nimport {storage} from \"../utils/storage/storage\"\nimport appRegistry from \"./AppRegistry\"\nimport devServerBridge from \"./DevServerBridge\"\nimport localMiniappRuntime, {type InstalledMiniappManifest} from \"./LocalMiniappRuntime\"\nimport type {MiniappLaunchSource} from \"@glassly/miniapp\"\nimport type {JSRouter} from \"./JSRouter\"\n\ninterface LauncherDeps {\n /** The host-constructed router (needs the native Crust binding). */\n router: JSRouter\n}\n\n/** Hints the host may pass from a view's props to avoid re-deriving them. */\nexport interface LaunchHints {\n devUrl?: string\n version?: string\n devPort?: string\n /** Who started the app; reported to the miniapp on connect. */\n launchSource?: MiniappLaunchSource\n}\n\n/** Everything the launcher resolved for a package — bg source + UI entry. */\nexport interface ResolvedBundle {\n bgSource: string\n uiUri: string | null\n uiBaseDir: string | null\n declaredPermissions: string[]\n installedManifest?: InstalledMiniappManifest\n /** Set for dev miniapps (HTTP off the dev server); null for released. */\n devUrl: string | null\n devPort: number | null\n}\n\n/** What the host needs back to render the UI layer. */\nexport interface LaunchResult {\n uiUri: string | null\n uiBaseDir: string | null\n}\n\nclass MiniappLauncher {\n private deps: LauncherDeps | null = null\n\n /**\n * In-flight launches keyed by packageName. Both apps.ts start() and the\n * WebView mount call ensureRunning() before the first spawnAndRegister\n * resolves — without this, neither sees the package in registeredPackages()\n * yet and both spawn the same context (a clobber/double-spawn). Concurrent\n * callers share the same promise.\n */\n private readonly inFlight = new Map<string, Promise<LaunchResult>>()\n\n /** Wire the router in. Called once from MiniappEngine construction. */\n configure(deps: LauncherDeps): void {\n this.deps = deps\n }\n\n private requireRouter(): JSRouter {\n if (!this.deps) {\n throw new Error(\"MiniappLauncher not configured — ensureMiniappEngine() has not run\")\n }\n return this.deps.router\n }\n\n /** True iff the package's background JS context is currently spawned. */\n /** Tell an already-running miniapp the user opened it again and from where. */\n notifyLaunch(packageName: string, launchSource: MiniappLaunchSource): void {\n this.deps?.router.notifyLaunch(packageName, launchSource)\n }\n\n isRunning(packageName: string): boolean {\n return this.deps?.router.registeredPackages().includes(packageName) ?? false\n }\n\n /**\n * Resolve the background JS source + UI entry + declared permissions for a\n * package. Handles both dev (HTTP off the running dev server) and released\n * (file:// from the installed snapshot). Reads disk/network/storage; does\n * NOT spawn. Returns null when the bundle can't be resolved (dev server\n * unreachable, missing entry, no installed version).\n */\n async resolveBundle(packageName: string, hints?: LaunchHints): Promise<ResolvedBundle | null> {\n const devUrl = hints?.devUrl ?? this.storedDevUrl(packageName)\n\n // --- Dev: load directly off the local dev server over HTTP. ---\n if (devUrl) {\n const route = await decideDevLaunchRoute(packageName, devUrl)\n if (route.decision === \"offline\" || !route.manifest) return null\n const manifest = route.manifest\n const entry = manifest.entry as {background?: string; ui?: string} | undefined\n if (!entry?.background) return null\n\n // Use the host that actually answered — may differ from the stored IP\n // after a laptop Wi-Fi change (mDNS / Metro failover inside decideDevLaunchRoute).\n const base = route.resolvedUrl.replace(/\\/$/, \"\")\n // entry.* are bundle-root paths (dist/ stripped); the dev server serves\n // files relative to cwd, so prepend dist/.\n const bgUrl = `${base}/dist/${entry.background.replace(/^\\.?\\/+/, \"\")}`\n const uiUri = entry.ui ? `${base}/dist/${entry.ui.replace(/^\\.?\\/+/, \"\")}` : null\n\n const perms = manifest.permissions as Array<{type?: string} | string> | undefined\n const declaredPermissions = (perms ?? [])\n .map((p) => (typeof p === \"string\" ? p : p?.type))\n .filter((t): t is string => typeof t === \"string\")\n const installedManifest: InstalledMiniappManifest = {\n packageName: typeof manifest.packageName === \"string\" ? manifest.packageName : packageName,\n name: manifest.name,\n version: typeof manifest.version === \"string\" ? manifest.version : undefined,\n sdkVersion: typeof manifest.sdkVersion === \"string\" ? manifest.sdkVersion : undefined,\n minHostVersion: typeof manifest.minHostVersion === \"string\" ? manifest.minHostVersion : undefined,\n type: typeof manifest.type === \"string\" ? manifest.type : undefined,\n entry: manifest.entry as InstalledMiniappManifest[\"entry\"],\n permissions: manifest.permissions as InstalledMiniappManifest[\"permissions\"],\n hardwareRequirements: manifest.hardwareRequirements as InstalledMiniappManifest[\"hardwareRequirements\"],\n actions: manifest.actions as InstalledMiniappManifest[\"actions\"],\n widgets: manifest.widgets,\n }\n\n let bgSource: string\n try {\n const res = await fetch(bgUrl)\n if (!res.ok) return null\n bgSource = await res.text()\n } catch {\n return null\n }\n\n return {\n bgSource,\n uiUri,\n uiBaseDir: uiUri ? uiUri.replace(/\\/[^/]+$/, \"/\") : null,\n declaredPermissions,\n installedManifest,\n devUrl: route.resolvedUrl,\n devPort: this.resolveDevPort(hints?.devPort, packageName),\n }\n }\n\n // --- Released: resolve from the installed file:// snapshot. ---\n const version = hints?.version ?? (await appRegistry.getActiveVersion(packageName))\n if (!version) return null\n const entryPaths = appRegistry.getMiniappEntryPaths(packageName, version)\n if (!entryPaths?.background) return null\n\n const manifest = appRegistry.getMiniappManifest(packageName, version) as {\n packageName?: string\n name?: string\n version?: string\n sdkVersion?: string\n minHostVersion?: string\n type?: string\n entry?: {background?: string; ui?: string}\n permissions?: Array<{type: string; required?: boolean; description?: string}>\n hardwareRequirements?: Array<{type: string; level: string; description?: string}>\n actions?: Array<{id?: unknown; description?: unknown; parameters?: unknown; outputSchema?: unknown}>\n widgets?: unknown\n } | null\n const declaredPermissions = (manifest?.permissions ?? [])\n .map((p) => p.type)\n .filter((t): t is string => typeof t === \"string\")\n const installedManifest: InstalledMiniappManifest | undefined = manifest\n ? {\n packageName: manifest.packageName ?? packageName,\n name: manifest.name,\n version: manifest.version ?? version,\n sdkVersion: manifest.sdkVersion,\n minHostVersion: manifest.minHostVersion,\n type: manifest.type,\n entry: manifest.entry,\n permissions: manifest.permissions,\n hardwareRequirements: manifest.hardwareRequirements,\n actions: manifest.actions,\n widgets: manifest.widgets,\n }\n : undefined\n\n let bgSource: string\n try {\n bgSource = new File(entryPaths.background).textSync()\n } catch {\n return null\n }\n\n return {\n bgSource,\n uiUri: entryPaths.ui ?? null,\n uiBaseDir: entryPaths.ui ? entryPaths.ui.replace(/\\/[^/]+$/, \"/\") : null,\n declaredPermissions,\n installedManifest,\n devUrl: null,\n devPort: null,\n }\n }\n\n /**\n * Ensure a miniapp's background JS context is spawned. Idempotent — if the\n * context is already alive this is a no-op spawn-wise (it still resolves the\n * UI entry so a re-mounting host gets its `<WebView>` source back).\n *\n * Resolves once the context is SPAWNED. It does NOT block on the context's\n * CONNECT handshake — the host's WebView mount shouldn't wait on background\n * JS. Callers that need the context to be connected and ready to receive\n * messages (the action broker) follow up with\n * {@link LocalMiniappRuntime.waitForConnect}.\n */\n async ensureRunning(packageName: string, hints?: LaunchHints): Promise<LaunchResult> {\n const router = this.requireRouter()\n // A context still tearing down from a stop is not one to reuse.\n await router.whenUnregistered(packageName)\n\n // Already spawned: best-effort resolve for the UI entry, never throw —\n // a headless caller that doesn't need UI gets {null, null} fast even if\n // the dev server has since dropped.\n if (router.registeredPackages().includes(packageName)) {\n const existing = await this.resolveBundle(packageName, hints).catch(() => null)\n return {uiUri: existing?.uiUri ?? null, uiBaseDir: existing?.uiBaseDir ?? null}\n }\n\n // Coalesce concurrent launches of the same package onto one promise.\n const pending = this.inFlight.get(packageName)\n if (pending) return pending\n\n const launch = this.spawn(packageName, hints)\n this.inFlight.set(packageName, launch)\n try {\n return await launch\n } finally {\n this.inFlight.delete(packageName)\n }\n }\n\n /** Resolve the bundle and spawn the context. Serialized via {@link inFlight}. */\n private async spawn(packageName: string, hints?: LaunchHints): Promise<LaunchResult> {\n const router = this.requireRouter()\n const resolved = await this.resolveBundle(packageName, hints)\n if (!resolved) {\n throw new Error(`MiniappLauncher: cannot resolve bundle for ${packageName}`)\n }\n\n // Re-check after the async resolve: a different path may have spawned it\n // while we were fetching/reading the bundle.\n if (!router.registeredPackages().includes(packageName)) {\n const ok = await router.spawnAndRegister(packageName, resolved.bgSource, {\n permissions: resolved.declaredPermissions,\n installedManifest: resolved.installedManifest,\n launchSource: hints?.launchSource,\n })\n if (!ok) {\n throw new Error(`MiniappLauncher: spawn failed for ${packageName}`)\n }\n\n // Dev: wire the hot-reload + log-forwarding sidecar. No-op in prod\n // (devServerBridge silently drops when no sidecar is up).\n if (resolved.devUrl && resolved.devPort != null) {\n devServerBridge.connect(packageName, resolved.devUrl, resolved.devPort)\n } else if (resolved.devUrl) {\n // Dev miniapp with no resolvable port — it still launches, but hot\n // reload + log forwarding won't connect. Surface it instead of failing\n // silently (the old inline path hard-failed with \"no dev port\").\n console.warn(`MiniappLauncher: ${packageName} has no dev port — hot reload + log forwarding disabled`)\n }\n }\n\n return {uiUri: resolved.uiUri, uiBaseDir: resolved.uiBaseDir}\n }\n\n /**\n * Start the Always On local miniapps at boot.\n *\n * Miniapps run in the phone's own JS engine, so a host-process kill tears\n * down their JSContext. Only Always On apps come back: whatever else was\n * running when the app was killed stays stopped until the wearer opens it\n * again. The spawn is headless (no foreground, no navigation) and registers\n * the package so the home tray/switcher project it as running, exactly as a\n * normal start would. The WebView, if any, re-attaches lazily when the user\n * opens the app.\n *\n * One dashboard runs at a time: a dashboard already running keeps its place,\n * else the first Always On dashboard by package name starts and the rest\n * stay stopped. The flag is exclusive after any user action (setAlwaysOn\n * clears it on the other dashboards), so this only arbitrates manifests\n * that both claim it.\n *\n * Idempotent and best-effort: skips already-spawned contexts, and skips apps\n * missing required OS permissions without prompting or clearing their Always\n * On choice. Not compatibility-gated: an Always On app shouldn't be dropped\n * just because glasses are momentarily disconnected at boot — it resumes\n * when they reconnect, same as mid-session.\n */\n async autostartLocalMiniapps(): Promise<void> {\n const apps = await appRegistry.getInstalledMiniapps()\n const dashboards = apps.filter((app) => app.type === \"dashboard\")\n const dashboard =\n dashboards.find((app) => this.isRunning(app.packageName))?.packageName ??\n dashboards\n .filter((app) => app.alwaysOn)\n .map((app) => app.packageName)\n .sort()[0] ??\n null\n for (const app of apps) {\n // Only phone-hosted miniapps have a JSContext to spawn. Offline\n // built-ins (`local:false, offline:true`) have no bundle to launch.\n if (!app.local || !app.alwaysOn) continue\n if (app.type === \"dashboard\" && app.packageName !== dashboard) continue\n if (this.isRunning(app.packageName)) continue\n try {\n const missing = await permissions.missingRequired(app.permissions ?? [])\n if (missing.length > 0) {\n console.info(\n `MiniappLauncher: skipping autostart for ${app.packageName} — missing required permissions: ${missing.join(\n \", \",\n )}`,\n )\n continue\n }\n } catch (e) {\n console.warn(`MiniappLauncher: skipping autostart for ${app.packageName} — permission check failed`, e)\n continue\n }\n try {\n await this.ensureRunning(app.packageName)\n } catch (e) {\n console.warn(`MiniappLauncher: autostart failed for ${app.packageName}`, e)\n }\n }\n }\n\n /**\n * Ensure the context is running AND has completed its CONNECT handshake.\n * Used by the action broker, which must not deliver to a context that hasn't\n * come up yet. Throws on spawn failure or connect timeout.\n */\n async ensureConnected(packageName: string, connectTimeoutMs = 10_000, hints?: LaunchHints): Promise<void> {\n await this.ensureRunning(packageName, hints)\n await localMiniappRuntime.waitForConnect(packageName, connectTimeoutMs)\n }\n\n /** Tear down a miniapp's background JS context. */\n async stop(packageName: string): Promise<void> {\n // If a launch is in flight, let it finish first. Otherwise unregister()\n // no-ops on the not-yet-registered package and the pending spawn then\n // re-registers the context — leaving it running despite the stop.\n const pending = this.inFlight.get(packageName)\n if (pending) {\n try {\n await pending\n } catch {\n // Launch failed — nothing got registered, so nothing to tear down.\n }\n }\n await this.deps?.router.unregister(packageName)\n }\n\n // --- internals -----------------------------------------------------------\n\n private storedDevUrl(packageName: string): string | undefined {\n const r = storage.load<string>(`${packageName}_dev_url`)\n return r.is_ok() ? r.value : undefined\n }\n\n /** resolveDevPort — moved verbatim from the old LocalMiniappView helper. */\n private resolveDevPort(searchParam: string | undefined, packageName: string): number | null {\n if (searchParam) {\n const n = parseInt(searchParam, 10)\n if (Number.isFinite(n)) return n\n }\n const stored = storage.load<number>(`${packageName}_dev_port`)\n if (stored.is_ok()) return stored.value\n return null\n }\n}\n\nexport const miniappLauncher = new MiniappLauncher()\n"]}
|