@glassly/engine 0.3.1-dev.496 → 0.3.1-dev.525

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.
Files changed (125) hide show
  1. package/build/engine.d.ts +9 -2
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/engine.js +5 -0
  4. package/build/engine.js.map +1 -1
  5. package/build/facades/dev.js +1 -1
  6. package/build/facades/dev.js.map +1 -1
  7. package/build/facades/miniapps.d.ts +5 -5
  8. package/build/facades/miniapps.d.ts.map +1 -1
  9. package/build/facades/miniapps.js +5 -6
  10. package/build/facades/miniapps.js.map +1 -1
  11. package/build/facades/speech.d.ts +10 -0
  12. package/build/facades/speech.d.ts.map +1 -1
  13. package/build/facades/speech.js +10 -0
  14. package/build/facades/speech.js.map +1 -1
  15. package/build/generated/releaseMetadata.js +3 -3
  16. package/build/generated/releaseMetadata.js.map +1 -1
  17. package/build/index.d.ts +1 -1
  18. package/build/index.d.ts.map +1 -1
  19. package/build/index.js +1 -1
  20. package/build/index.js.map +1 -1
  21. package/build/services/AppRegistry.d.ts +1 -7
  22. package/build/services/AppRegistry.d.ts.map +1 -1
  23. package/build/services/AppRegistry.js +15 -26
  24. package/build/services/AppRegistry.js.map +1 -1
  25. package/build/services/DevServerBridge.d.ts +1 -1
  26. package/build/services/DevServerBridge.js +2 -2
  27. package/build/services/DevServerBridge.js.map +1 -1
  28. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  29. package/build/services/DeviceEventRouter.js +2 -0
  30. package/build/services/DeviceEventRouter.js.map +1 -1
  31. package/build/services/JSRouter.js +1 -1
  32. package/build/services/JSRouter.js.map +1 -1
  33. package/build/services/LocalMiniappRuntime.d.ts +21 -6
  34. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  35. package/build/services/LocalMiniappRuntime.js +130 -48
  36. package/build/services/LocalMiniappRuntime.js.map +1 -1
  37. package/build/services/LocalTranslationService.d.ts +51 -0
  38. package/build/services/LocalTranslationService.d.ts.map +1 -0
  39. package/build/services/LocalTranslationService.js +141 -0
  40. package/build/services/LocalTranslationService.js.map +1 -0
  41. package/build/services/MiniappLauncher.d.ts +17 -23
  42. package/build/services/MiniappLauncher.d.ts.map +1 -1
  43. package/build/services/MiniappLauncher.js +30 -34
  44. package/build/services/MiniappLauncher.js.map +1 -1
  45. package/build/services/MiniappPauseCoordinator.d.ts +15 -0
  46. package/build/services/MiniappPauseCoordinator.d.ts.map +1 -0
  47. package/build/services/MiniappPauseCoordinator.js +93 -0
  48. package/build/services/MiniappPauseCoordinator.js.map +1 -0
  49. package/build/services/OfflineTranslationManager.d.ts +19 -0
  50. package/build/services/OfflineTranslationManager.d.ts.map +1 -0
  51. package/build/services/OfflineTranslationManager.js +35 -0
  52. package/build/services/OfflineTranslationManager.js.map +1 -0
  53. package/build/services/pages/OverlayPages.d.ts +5 -5
  54. package/build/services/pages/OverlayPages.d.ts.map +1 -1
  55. package/build/services/pages/OverlayPages.js +5 -5
  56. package/build/services/pages/OverlayPages.js.map +1 -1
  57. package/build/services/pages/overlayApps.d.ts +14 -11
  58. package/build/services/pages/overlayApps.d.ts.map +1 -1
  59. package/build/services/pages/overlayApps.js +21 -19
  60. package/build/services/pages/overlayApps.js.map +1 -1
  61. package/build/services/settingsBridge.d.ts +8 -8
  62. package/build/services/settingsBridge.d.ts.map +1 -1
  63. package/build/services/settingsBridge.js +12 -13
  64. package/build/services/settingsBridge.js.map +1 -1
  65. package/build/stores/apps.d.ts +0 -5
  66. package/build/stores/apps.d.ts.map +1 -1
  67. package/build/stores/apps.js +32 -52
  68. package/build/stores/apps.js.map +1 -1
  69. package/build/stores/settings.d.ts.map +1 -1
  70. package/build/stores/settings.js +0 -10
  71. package/build/stores/settings.js.map +1 -1
  72. package/build/types/applet.d.ts +3 -1
  73. package/build/types/applet.d.ts.map +1 -1
  74. package/build/types/applet.js.map +1 -1
  75. package/build/types/capabilities/even-realities-g1.d.ts.map +1 -1
  76. package/build/types/capabilities/even-realities-g1.js +1 -0
  77. package/build/types/capabilities/even-realities-g1.js.map +1 -1
  78. package/build/types/capabilities/even-realities-g2.d.ts.map +1 -1
  79. package/build/types/capabilities/even-realities-g2.js +1 -0
  80. package/build/types/capabilities/even-realities-g2.js.map +1 -1
  81. package/build/types/capabilities/mentra-display.d.ts.map +1 -1
  82. package/build/types/capabilities/mentra-display.js +1 -0
  83. package/build/types/capabilities/mentra-display.js.map +1 -1
  84. package/build/types/capabilities/nimo.d.ts.map +1 -1
  85. package/build/types/capabilities/nimo.js +1 -0
  86. package/build/types/capabilities/nimo.js.map +1 -1
  87. package/build/types/hardware.d.ts +9 -0
  88. package/build/types/hardware.d.ts.map +1 -1
  89. package/build/types/hardware.js +2 -0
  90. package/build/types/hardware.js.map +1 -1
  91. package/build/utils/miniappGlobals.js +1 -1
  92. package/build/utils/miniappGlobals.js.map +1 -1
  93. package/package.json +7 -7
  94. package/src/engine.ts +5 -0
  95. package/src/facades/dev.ts +1 -1
  96. package/src/facades/miniapps.ts +7 -7
  97. package/src/facades/speech.ts +12 -0
  98. package/src/generated/releaseMetadata.ts +3 -3
  99. package/src/index.ts +0 -2
  100. package/src/services/AppRegistry.ts +15 -27
  101. package/src/services/DevServerBridge.ts +2 -2
  102. package/src/services/DeviceEventRouter.ts +2 -0
  103. package/src/services/JSRouter.ts +1 -1
  104. package/src/services/LocalMiniappRuntime.ts +165 -66
  105. package/src/services/LocalTranslationService.ts +184 -0
  106. package/src/services/MiniappLauncher.ts +31 -34
  107. package/src/services/MiniappPauseCoordinator.ts +100 -0
  108. package/src/services/OfflineTranslationManager.ts +40 -0
  109. package/src/services/pages/OverlayPages.ts +5 -5
  110. package/src/services/pages/overlayApps.ts +19 -19
  111. package/src/services/settingsBridge.ts +12 -13
  112. package/src/stores/apps.ts +30 -58
  113. package/src/stores/settings.ts +0 -10
  114. package/src/types/applet.ts +3 -1
  115. package/src/types/capabilities/even-realities-g1.ts +1 -0
  116. package/src/types/capabilities/even-realities-g2.ts +1 -0
  117. package/src/types/capabilities/mentra-display.ts +1 -0
  118. package/src/types/capabilities/nimo.ts +1 -0
  119. package/src/types/hardware.ts +9 -0
  120. package/src/utils/miniappGlobals.ts +1 -1
  121. package/build/services/pages/activeDashboard.d.ts +0 -7
  122. package/build/services/pages/activeDashboard.d.ts.map +0 -1
  123. package/build/services/pages/activeDashboard.js +0 -21
  124. package/build/services/pages/activeDashboard.js.map +0 -1
  125. package/src/services/pages/activeDashboard.ts +0 -24
@@ -0,0 +1,51 @@
1
+ /** One concrete-target `translation:<source>:<target>` stream and who listens to it. */
2
+ export interface LocalTranslationSubscription {
3
+ /** Subscribed source: a language code, or `*` / `auto` for any. */
4
+ source: string;
5
+ target: string;
6
+ /** At least one listener on the default (cloud) route. */
7
+ cloud: boolean;
8
+ /** At least one listener that requires on-device translation. */
9
+ forceLocal: boolean;
10
+ }
11
+ export interface LocalTranscript {
12
+ text: string;
13
+ isFinal?: boolean;
14
+ speakerId?: number | string;
15
+ startTime?: number;
16
+ endTime?: number;
17
+ }
18
+ interface Host {
19
+ /** Fan a `translation:<source>:<target>` event out to miniapps. */
20
+ deliver: (stream: string, data: Record<string, unknown>) => void;
21
+ /** Whether the primary (cloud / Soniox-direct) path is up. */
22
+ primaryUp: () => boolean;
23
+ /** A wanted pair has no language pack on the device. */
24
+ onPairUnavailable?: (source: string, target: string) => void;
25
+ }
26
+ /**
27
+ * Turns on-device transcripts into on-device translations. Apple's Translation
28
+ * framework has no streaming API, so each update is a whole `translate` call:
29
+ * one runs at a time per language pair, a newer interim replaces a waiting
30
+ * one, and finals are never dropped.
31
+ *
32
+ * A pair is translated here when a subscriber forces local, or when the
33
+ * primary path is down and would otherwise leave default subscribers silent.
34
+ */
35
+ export declare class LocalTranslationService {
36
+ private subscriptions;
37
+ private lanes;
38
+ private host;
39
+ private utteranceCounter;
40
+ attach(host: Host): void;
41
+ setSubscriptions(subscriptions: LocalTranslationSubscription[]): void;
42
+ onLocalTranscript(transcript: LocalTranscript, sourceLanguage: string): void;
43
+ private wantedTargets;
44
+ private enqueue;
45
+ private drain;
46
+ private handleFailure;
47
+ private emit;
48
+ }
49
+ declare const localTranslationService: LocalTranslationService;
50
+ export default localTranslationService;
51
+ //# sourceMappingURL=LocalTranslationService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalTranslationService.d.ts","sourceRoot":"","sources":["../../src/services/LocalTranslationService.ts"],"names":[],"mappings":"AAEA,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;IAIxB,gBAAgB,CAAC,aAAa,EAAE,4BAA4B,EAAE,GAAG,IAAI;IAQrE,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"}
@@ -0,0 +1,141 @@
1
+ import { offlineTranslationManager } from "./OfflineTranslationManager";
2
+ /** Interims are translated at most this often; finals always are. */
3
+ const INTERIM_MIN_INTERVAL_MS = 300;
4
+ const FAILURE_BACKOFF_MS = 5000;
5
+ const baseLanguage = (code) => code.split("-")[0].toLowerCase();
6
+ /**
7
+ * Turns on-device transcripts into on-device translations. Apple's Translation
8
+ * framework has no streaming API, so each update is a whole `translate` call:
9
+ * one runs at a time per language pair, a newer interim replaces a waiting
10
+ * one, and finals are never dropped.
11
+ *
12
+ * A pair is translated here when a subscriber forces local, or when the
13
+ * primary path is down and would otherwise leave default subscribers silent.
14
+ */
15
+ export class LocalTranslationService {
16
+ subscriptions = [];
17
+ lanes = new Map();
18
+ host = null;
19
+ utteranceCounter = 0;
20
+ attach(host) {
21
+ this.host = host;
22
+ }
23
+ setSubscriptions(subscriptions) {
24
+ this.subscriptions = subscriptions;
25
+ const live = new Set(subscriptions.map((s) => s.target));
26
+ for (const key of this.lanes.keys()) {
27
+ if (!live.has(key.substring(key.indexOf(">") + 1)))
28
+ this.lanes.delete(key);
29
+ }
30
+ }
31
+ onLocalTranscript(transcript, sourceLanguage) {
32
+ const text = transcript.text?.trim();
33
+ if (!text || !this.host || !offlineTranslationManager.isSupported())
34
+ return;
35
+ const isFinal = transcript.isFinal === true;
36
+ const utteranceId = `local-${this.utteranceCounter}`;
37
+ if (isFinal)
38
+ this.utteranceCounter++;
39
+ for (const target of this.wantedTargets(sourceLanguage)) {
40
+ const job = { text, isFinal, utteranceId, transcript };
41
+ if (baseLanguage(target) === baseLanguage(sourceLanguage)) {
42
+ // Same-language passthrough, as the cloud emits it: no originalText.
43
+ this.emit(sourceLanguage, target, job, text, undefined);
44
+ continue;
45
+ }
46
+ this.enqueue(sourceLanguage, target, job);
47
+ }
48
+ }
49
+ wantedTargets(sourceLanguage) {
50
+ const targets = new Set();
51
+ let primaryUp = null;
52
+ for (const sub of this.subscriptions) {
53
+ const anySource = sub.source === "*" || sub.source === "auto";
54
+ if (!anySource && baseLanguage(sub.source) !== baseLanguage(sourceLanguage))
55
+ continue;
56
+ if (!sub.forceLocal) {
57
+ primaryUp ??= this.host.primaryUp();
58
+ if (primaryUp || !sub.cloud)
59
+ continue;
60
+ }
61
+ targets.add(sub.target);
62
+ }
63
+ return targets;
64
+ }
65
+ enqueue(source, target, job) {
66
+ const key = `${source}>${target}`;
67
+ let lane = this.lanes.get(key);
68
+ if (!lane) {
69
+ lane = { busy: false, finals: [], pendingInterim: null, lastInterimAt: 0, retryAt: 0, reportedUnavailable: false };
70
+ this.lanes.set(key, lane);
71
+ }
72
+ const now = Date.now();
73
+ if (now < lane.retryAt)
74
+ return;
75
+ if (job.isFinal) {
76
+ lane.pendingInterim = null;
77
+ lane.finals.push(job);
78
+ }
79
+ else {
80
+ if (now - lane.lastInterimAt < INTERIM_MIN_INTERVAL_MS)
81
+ return;
82
+ lane.lastInterimAt = now;
83
+ lane.pendingInterim = job;
84
+ }
85
+ if (!lane.busy)
86
+ void this.drain(source, target, lane);
87
+ }
88
+ async drain(source, target, lane) {
89
+ lane.busy = true;
90
+ try {
91
+ for (;;) {
92
+ const job = lane.finals.shift() ?? lane.pendingInterim;
93
+ if (!job)
94
+ break;
95
+ if (!job.isFinal)
96
+ lane.pendingInterim = null;
97
+ try {
98
+ const translated = await offlineTranslationManager.translate(job.text, source, target);
99
+ this.emit(source, target, job, translated, job.text);
100
+ }
101
+ catch (err) {
102
+ this.handleFailure(source, target, lane, err);
103
+ break;
104
+ }
105
+ }
106
+ }
107
+ finally {
108
+ lane.busy = false;
109
+ }
110
+ }
111
+ handleFailure(source, target, lane, err) {
112
+ const message = err?.message ?? String(err);
113
+ console.warn(`[LocalTranslation] ${source}>${target} failed: ${message}`);
114
+ lane.finals = [];
115
+ lane.pendingInterim = null;
116
+ lane.retryAt = Date.now() + FAILURE_BACKOFF_MS;
117
+ if (message.includes("not_installed") && !lane.reportedUnavailable) {
118
+ lane.reportedUnavailable = true;
119
+ this.host?.onPairUnavailable?.(source, target);
120
+ }
121
+ }
122
+ emit(source, target, job, text, originalText) {
123
+ this.host?.deliver(`translation:${source}:${target}`, {
124
+ type: "translation",
125
+ text,
126
+ originalText,
127
+ isFinal: job.isFinal,
128
+ utteranceId: job.utteranceId,
129
+ startTime: job.transcript.startTime,
130
+ endTime: job.transcript.endTime,
131
+ speakerId: job.transcript.speakerId,
132
+ transcribeLanguage: source,
133
+ translateLanguage: target,
134
+ didTranslate: originalText !== undefined,
135
+ provider: "apple-translation",
136
+ });
137
+ }
138
+ }
139
+ const localTranslationService = new LocalTranslationService();
140
+ export default localTranslationService;
141
+ //# sourceMappingURL=LocalTranslationService.js.map
@@ -0,0 +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"]}
@@ -91,33 +91,27 @@ declare class MiniappLauncher {
91
91
  /** Resolve the bundle and spawn the context. Serialized via {@link inFlight}. */
92
92
  private spawn;
93
93
  /**
94
- * Re-spawn local miniapps that were running when the app was last killed.
94
+ * Start the Always On local miniapps at boot.
95
95
  *
96
96
  * Miniapps run in the phone's own JS engine, so a host-process kill tears
97
- * down their JSContext with nothing to resurrect them. Without this they
98
- * would silently stay stopped on the next launch even though the user left
99
- * them running.
97
+ * down their JSContext. Only Always On apps come back: whatever else was
98
+ * running when the app was killed stays stopped until the wearer opens it
99
+ * again. The spawn is headless (no foreground, no navigation) and registers
100
+ * the package so the home tray/switcher project it as running, exactly as a
101
+ * normal start would. The WebView, if any, re-attaches lazily when the user
102
+ * opens the app.
100
103
  *
101
- * Each local miniapp persists its running flag to disk on start/stop
102
- * (`saveLocalAppRunningState`, via the applet's `onStart`/`onStop`). We read
103
- * those flags back and re-spawn the background context headlessly (no
104
- * foreground, no navigation) — the spawn registers the package so the home
105
- * tray/switcher project it as running, exactly as a normal start would. The
106
- * WebView, if any, re-attaches lazily when the user opens the app.
104
+ * One dashboard runs at a time: a dashboard already running keeps its place,
105
+ * else the first Always On dashboard by package name starts and the rest
106
+ * stay stopped. The flag is exclusive after any user action (setAlwaysOn
107
+ * clears it on the other dashboards), so this only arbitrates manifests
108
+ * that both claim it.
107
109
  *
108
- * Idempotent and best-effort: skips already-spawned contexts, and clears the
109
- * persisted flag for any app whose bundle can no longer be resolved
110
- * (uninstalled, or dev server gone) so a dead entry doesn't retry every boot.
111
- * Not compatibility-gated: a previously-running background app shouldn't be
112
- * dropped just because glasses are momentarily disconnected at boot — it
113
- * resumes when they reconnect, same as mid-session.
114
- *
115
- * Always On apps (the dashboard) start whether or not they were running
116
- * when the app was last killed, and get a switcher card like any other app.
117
- * A dashboard that does not hold the role (ADR-0009) is not started by its
118
- * Always On flag.
119
- * Both restore paths skip apps missing required OS permissions without
120
- * prompting or clearing their saved running/Always On choice.
110
+ * Idempotent and best-effort: skips already-spawned contexts, and skips apps
111
+ * missing required OS permissions without prompting or clearing their Always
112
+ * On choice. Not compatibility-gated: an Always On app shouldn't be dropped
113
+ * just because glasses are momentarily disconnected at boot — it resumes
114
+ * when they reconnect, same as mid-session.
121
115
  */
122
116
  autostartLocalMiniapps(): Promise<void>;
123
117
  /**
@@ -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;AAExF,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;IA8G7F;;;;;;;;;;OAUG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAwBpF,iFAAiF;YACnE,KAAK;IAkCnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACG,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAkC7C;;;;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"}
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;IA8G7F;;;;;;;;;;OAUG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAwBpF,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"}
@@ -26,10 +26,9 @@ import { File } from "expo-file-system";
26
26
  import { permissions } from "../facades/permissions";
27
27
  import { decideDevLaunchRoute } from "../utils/devMiniappLaunch";
28
28
  import { storage } from "../utils/storage/storage";
29
- import appRegistry, { getLocalAppRunningState, saveLocalAppRunningState } from "./AppRegistry";
29
+ import appRegistry from "./AppRegistry";
30
30
  import devServerBridge from "./DevServerBridge";
31
31
  import localMiniappRuntime from "./LocalMiniappRuntime";
32
- import { activeDashboardAmong } from "./pages/activeDashboard";
33
32
  class MiniappLauncher {
34
33
  deps = null;
35
34
  /**
@@ -223,45 +222,43 @@ class MiniappLauncher {
223
222
  return { uiUri: resolved.uiUri, uiBaseDir: resolved.uiBaseDir };
224
223
  }
225
224
  /**
226
- * Re-spawn local miniapps that were running when the app was last killed.
225
+ * Start the Always On local miniapps at boot.
227
226
  *
228
227
  * Miniapps run in the phone's own JS engine, so a host-process kill tears
229
- * down their JSContext with nothing to resurrect them. Without this they
230
- * would silently stay stopped on the next launch even though the user left
231
- * them running.
228
+ * down their JSContext. Only Always On apps come back: whatever else was
229
+ * running when the app was killed stays stopped until the wearer opens it
230
+ * again. The spawn is headless (no foreground, no navigation) and registers
231
+ * the package so the home tray/switcher project it as running, exactly as a
232
+ * normal start would. The WebView, if any, re-attaches lazily when the user
233
+ * opens the app.
232
234
  *
233
- * Each local miniapp persists its running flag to disk on start/stop
234
- * (`saveLocalAppRunningState`, via the applet's `onStart`/`onStop`). We read
235
- * those flags back and re-spawn the background context headlessly (no
236
- * foreground, no navigation) — the spawn registers the package so the home
237
- * tray/switcher project it as running, exactly as a normal start would. The
238
- * WebView, if any, re-attaches lazily when the user opens the app.
235
+ * One dashboard runs at a time: a dashboard already running keeps its place,
236
+ * else the first Always On dashboard by package name starts and the rest
237
+ * stay stopped. The flag is exclusive after any user action (setAlwaysOn
238
+ * clears it on the other dashboards), so this only arbitrates manifests
239
+ * that both claim it.
239
240
  *
240
- * Idempotent and best-effort: skips already-spawned contexts, and clears the
241
- * persisted flag for any app whose bundle can no longer be resolved
242
- * (uninstalled, or dev server gone) so a dead entry doesn't retry every boot.
243
- * Not compatibility-gated: a previously-running background app shouldn't be
244
- * dropped just because glasses are momentarily disconnected at boot — it
245
- * resumes when they reconnect, same as mid-session.
246
- *
247
- * Always On apps (the dashboard) start whether or not they were running
248
- * when the app was last killed, and get a switcher card like any other app.
249
- * A dashboard that does not hold the role (ADR-0009) is not started by its
250
- * Always On flag.
251
- * Both restore paths skip apps missing required OS permissions without
252
- * prompting or clearing their saved running/Always On choice.
241
+ * Idempotent and best-effort: skips already-spawned contexts, and skips apps
242
+ * missing required OS permissions without prompting or clearing their Always
243
+ * On choice. Not compatibility-gated: an Always On app shouldn't be dropped
244
+ * just because glasses are momentarily disconnected at boot — it resumes
245
+ * when they reconnect, same as mid-session.
253
246
  */
254
247
  async autostartLocalMiniapps() {
255
248
  const apps = await appRegistry.getInstalledMiniapps();
256
- const activeDashboard = activeDashboardAmong(apps);
249
+ const dashboards = apps.filter((app) => app.type === "dashboard");
250
+ const dashboard = dashboards.find((app) => this.isRunning(app.packageName))?.packageName ??
251
+ dashboards
252
+ .filter((app) => app.alwaysOn)
253
+ .map((app) => app.packageName)
254
+ .sort()[0] ??
255
+ null;
257
256
  for (const app of apps) {
258
- // Only phone-hosted miniapps have a JSContext to re-spawn. Offline
259
- // built-ins (`local:false, offline:true`) restore their native running
260
- // state elsewhere and have no bundle to launch.
261
- if (!app.local)
257
+ // Only phone-hosted miniapps have a JSContext to spawn. Offline
258
+ // built-ins (`local:false, offline:true`) have no bundle to launch.
259
+ if (!app.local || !app.alwaysOn)
262
260
  continue;
263
- const alwaysOn = app.alwaysOn && (app.type !== "dashboard" || app.packageName === activeDashboard);
264
- if (!alwaysOn && !getLocalAppRunningState(app.packageName))
261
+ if (app.type === "dashboard" && app.packageName !== dashboard)
265
262
  continue;
266
263
  if (this.isRunning(app.packageName))
267
264
  continue;
@@ -280,8 +277,7 @@ class MiniappLauncher {
280
277
  await this.ensureRunning(app.packageName);
281
278
  }
282
279
  catch (e) {
283
- console.warn(`MiniappLauncher: autostart failed for ${app.packageName} — clearing stale running flag`, e);
284
- saveLocalAppRunningState(app.packageName, false);
280
+ console.warn(`MiniappLauncher: autostart failed for ${app.packageName}`, e);
285
281
  }
286
282
  }
287
283
  }
@@ -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, {getLocalAppRunningState, saveLocalAppRunningState} from \"./AppRegistry\"\nimport devServerBridge from \"./DevServerBridge\"\nimport localMiniappRuntime, {type InstalledMiniappManifest} from \"./LocalMiniappRuntime\"\nimport {activeDashboardAmong} from \"./pages/activeDashboard\"\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 }\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 } | 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 }\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 * Re-spawn local miniapps that were running when the app was last killed.\n *\n * Miniapps run in the phone's own JS engine, so a host-process kill tears\n * down their JSContext with nothing to resurrect them. Without this they\n * would silently stay stopped on the next launch even though the user left\n * them running.\n *\n * Each local miniapp persists its running flag to disk on start/stop\n * (`saveLocalAppRunningState`, via the applet's `onStart`/`onStop`). We read\n * those flags back and re-spawn the background context headlessly (no\n * foreground, no navigation) — the spawn registers the package so the home\n * tray/switcher project it as running, exactly as a normal start would. The\n * WebView, if any, re-attaches lazily when the user opens the app.\n *\n * Idempotent and best-effort: skips already-spawned contexts, and clears the\n * persisted flag for any app whose bundle can no longer be resolved\n * (uninstalled, or dev server gone) so a dead entry doesn't retry every boot.\n * Not compatibility-gated: a previously-running background app shouldn't be\n * dropped just because glasses are momentarily disconnected at boot — it\n * resumes when they reconnect, same as mid-session.\n *\n * Always On apps (the dashboard) start whether or not they were running\n * when the app was last killed, and get a switcher card like any other app.\n * A dashboard that does not hold the role (ADR-0009) is not started by its\n * Always On flag.\n * Both restore paths skip apps missing required OS permissions without\n * prompting or clearing their saved running/Always On choice.\n */\n async autostartLocalMiniapps(): Promise<void> {\n const apps = await appRegistry.getInstalledMiniapps()\n const activeDashboard = activeDashboardAmong(apps)\n for (const app of apps) {\n // Only phone-hosted miniapps have a JSContext to re-spawn. Offline\n // built-ins (`local:false, offline:true`) restore their native running\n // state elsewhere and have no bundle to launch.\n if (!app.local) continue\n const alwaysOn = app.alwaysOn && (app.type !== \"dashboard\" || app.packageName === activeDashboard)\n if (!alwaysOn && !getLocalAppRunningState(app.packageName)) 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} — clearing stale running flag`, e)\n saveLocalAppRunningState(app.packageName, false)\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 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 }\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 } | 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 }\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"]}
@@ -0,0 +1,15 @@
1
+ import { type GlassesConnectionStatus } from "./GlassesReadiness";
2
+ import { type IdentitySnapshot } from "./PairingIdentity";
3
+ export declare const MINIAPP_PAUSE_GRACE_MS = 10000;
4
+ export type MiniappPauseReason = "glasses_disconnected";
5
+ type Listener = (paused: boolean) => void;
6
+ /** Paired to real glasses that are not connected. */
7
+ export declare function shouldPauseMiniapps(identity: IdentitySnapshot, connection: GlassesConnectionStatus): boolean;
8
+ export declare function isMiniappsPaused(): boolean;
9
+ /** Fires on pause/resume transitions only. */
10
+ export declare function onMiniappsPauseChange(listener: Listener): () => void;
11
+ /** Start after the persisted settings are loaded so the pairing identity reads true. */
12
+ export declare function startMiniappPauseCoordinator(): void;
13
+ export declare function stopMiniappPauseCoordinator(): void;
14
+ export {};
15
+ //# sourceMappingURL=MiniappPauseCoordinator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MiniappPauseCoordinator.d.ts","sourceRoot":"","sources":["../../src/services/MiniappPauseCoordinator.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAqB,KAAK,uBAAuB,EAAC,MAAM,oBAAoB,CAAA;AAEnF,OAAO,EAAmD,KAAK,gBAAgB,EAAC,MAAM,mBAAmB,CAAA;AAEzG,eAAO,MAAM,sBAAsB,QAAS,CAAA;AAE5C,MAAM,MAAM,kBAAkB,GAAG,sBAAsB,CAAA;AAEvD,KAAK,QAAQ,GAAG,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAA;AAQzC,qDAAqD;AACrD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,UAAU,EAAE,uBAAuB,GAAG,OAAO,CAE5G;AAED,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAED,8CAA8C;AAC9C,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,IAAI,CAKpE;AAsCD,wFAAwF;AACxF,wBAAgB,4BAA4B,IAAI,IAAI,CAKnD;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAMlD"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * MiniappPauseCoordinator — pauses every local miniapp while the paired
3
+ * glasses are disconnected.
4
+ *
5
+ * A paused miniapp keeps its JSContext and state. The runtime tells it
6
+ * (`session.on("pause")`), stops fanning out streams to it, and drops the mic
7
+ * and transcription demand its subscriptions were holding, until the glasses
8
+ * are back. The pause waits out a grace window so a BLE blip does not pause
9
+ * and resume an app within a second; resume is immediate on reconnect.
10
+ *
11
+ * Only a phone paired to real glasses pauses. With nothing paired, or with the
12
+ * simulated device, there is nothing to reconnect to and miniapps keep running
13
+ * (the mirror and dev flows depend on that).
14
+ */
15
+ import { useGlassesStore } from "../stores/glasses";
16
+ import { BgTimer } from "../utils/timers";
17
+ import { isGlassesConnected } from "./GlassesReadiness";
18
+ import { isPairedToRealGlasses } from "./PairedGlassesLatch";
19
+ import { projectPairingIdentity, subscribePairingIdentity } from "./PairingIdentity";
20
+ export const MINIAPP_PAUSE_GRACE_MS = 10_000;
21
+ let started = false;
22
+ let paused = false;
23
+ let graceTimer = null;
24
+ const listeners = new Set();
25
+ let unsubscribers = [];
26
+ /** Paired to real glasses that are not connected. */
27
+ export function shouldPauseMiniapps(identity, connection) {
28
+ return isPairedToRealGlasses(identity) && !isGlassesConnected(connection);
29
+ }
30
+ export function isMiniappsPaused() {
31
+ return paused;
32
+ }
33
+ /** Fires on pause/resume transitions only. */
34
+ export function onMiniappsPauseChange(listener) {
35
+ listeners.add(listener);
36
+ return () => {
37
+ listeners.delete(listener);
38
+ };
39
+ }
40
+ function setPaused(next) {
41
+ if (paused === next)
42
+ return;
43
+ paused = next;
44
+ console.log(`MiniappPauseCoordinator: ${next ? "paused" : "resumed"} local miniapps`);
45
+ for (const listener of listeners) {
46
+ try {
47
+ listener(next);
48
+ }
49
+ catch (e) {
50
+ console.warn("MiniappPauseCoordinator: listener threw", e);
51
+ }
52
+ }
53
+ }
54
+ function clearGrace() {
55
+ if (graceTimer === null)
56
+ return;
57
+ BgTimer.clearTimeout(graceTimer);
58
+ graceTimer = null;
59
+ }
60
+ function wanted() {
61
+ return shouldPauseMiniapps(projectPairingIdentity(), useGlassesStore.getState().connection);
62
+ }
63
+ function evaluate() {
64
+ if (!wanted()) {
65
+ clearGrace();
66
+ setPaused(false);
67
+ return;
68
+ }
69
+ if (paused || graceTimer !== null)
70
+ return;
71
+ graceTimer = BgTimer.setTimeout(() => {
72
+ graceTimer = null;
73
+ if (wanted())
74
+ setPaused(true);
75
+ }, MINIAPP_PAUSE_GRACE_MS);
76
+ }
77
+ /** Start after the persisted settings are loaded so the pairing identity reads true. */
78
+ export function startMiniappPauseCoordinator() {
79
+ if (started)
80
+ return;
81
+ started = true;
82
+ unsubscribers = [useGlassesStore.subscribe((s) => s.connection.state, evaluate), subscribePairingIdentity(evaluate)];
83
+ evaluate();
84
+ }
85
+ export function stopMiniappPauseCoordinator() {
86
+ for (const unsubscribe of unsubscribers)
87
+ unsubscribe();
88
+ unsubscribers = [];
89
+ clearGrace();
90
+ paused = false;
91
+ started = false;
92
+ }
93
+ //# sourceMappingURL=MiniappPauseCoordinator.js.map