@mentra/miniapp 0.3.0-beta.0

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 (277) hide show
  1. package/README.md +259 -0
  2. package/dist/background/index.d.ts +60 -0
  3. package/dist/background/index.d.ts.map +1 -0
  4. package/dist/background/index.js +25 -0
  5. package/dist/background/index.js.map +1 -0
  6. package/dist/background/register.d.ts +38 -0
  7. package/dist/background/register.d.ts.map +1 -0
  8. package/dist/background/register.js +75 -0
  9. package/dist/background/register.js.map +1 -0
  10. package/dist/dev-reload.d.ts +21 -0
  11. package/dist/dev-reload.d.ts.map +1 -0
  12. package/dist/dev-reload.js +62 -0
  13. package/dist/dev-reload.js.map +1 -0
  14. package/dist/envelope.d.ts +23 -0
  15. package/dist/envelope.d.ts.map +1 -0
  16. package/dist/envelope.js +47 -0
  17. package/dist/envelope.js.map +1 -0
  18. package/dist/globals.d.ts +54 -0
  19. package/dist/globals.d.ts.map +1 -0
  20. package/dist/globals.js +12 -0
  21. package/dist/globals.js.map +1 -0
  22. package/dist/index.d.ts +59 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +31 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/modules/actions.d.ts +54 -0
  27. package/dist/modules/actions.d.ts.map +1 -0
  28. package/dist/modules/actions.js +122 -0
  29. package/dist/modules/actions.js.map +1 -0
  30. package/dist/modules/auth.d.ts +25 -0
  31. package/dist/modules/auth.d.ts.map +1 -0
  32. package/dist/modules/auth.js +52 -0
  33. package/dist/modules/auth.js.map +1 -0
  34. package/dist/modules/base64.d.ts +16 -0
  35. package/dist/modules/base64.d.ts.map +1 -0
  36. package/dist/modules/base64.js +92 -0
  37. package/dist/modules/base64.js.map +1 -0
  38. package/dist/modules/blob.d.ts +161 -0
  39. package/dist/modules/blob.d.ts.map +1 -0
  40. package/dist/modules/blob.js +277 -0
  41. package/dist/modules/blob.js.map +1 -0
  42. package/dist/modules/camera.d.ts +170 -0
  43. package/dist/modules/camera.d.ts.map +1 -0
  44. package/dist/modules/camera.js +116 -0
  45. package/dist/modules/camera.js.map +1 -0
  46. package/dist/modules/cloud.d.ts +34 -0
  47. package/dist/modules/cloud.d.ts.map +1 -0
  48. package/dist/modules/cloud.js +70 -0
  49. package/dist/modules/cloud.js.map +1 -0
  50. package/dist/modules/dashboard.d.ts +16 -0
  51. package/dist/modules/dashboard.d.ts.map +1 -0
  52. package/dist/modules/dashboard.js +27 -0
  53. package/dist/modules/dashboard.js.map +1 -0
  54. package/dist/modules/display.d.ts +106 -0
  55. package/dist/modules/display.d.ts.map +1 -0
  56. package/dist/modules/display.js +57 -0
  57. package/dist/modules/display.js.map +1 -0
  58. package/dist/modules/events.d.ts +160 -0
  59. package/dist/modules/events.d.ts.map +1 -0
  60. package/dist/modules/events.js +168 -0
  61. package/dist/modules/events.js.map +1 -0
  62. package/dist/modules/glasses.d.ts +28 -0
  63. package/dist/modules/glasses.d.ts.map +1 -0
  64. package/dist/modules/glasses.js +39 -0
  65. package/dist/modules/glasses.js.map +1 -0
  66. package/dist/modules/heading.d.ts +16 -0
  67. package/dist/modules/heading.d.ts.map +1 -0
  68. package/dist/modules/heading.js +20 -0
  69. package/dist/modules/heading.js.map +1 -0
  70. package/dist/modules/imu.d.ts +31 -0
  71. package/dist/modules/imu.d.ts.map +1 -0
  72. package/dist/modules/imu.js +40 -0
  73. package/dist/modules/imu.js.map +1 -0
  74. package/dist/modules/input.d.ts +34 -0
  75. package/dist/modules/input.d.ts.map +1 -0
  76. package/dist/modules/input.js +50 -0
  77. package/dist/modules/input.js.map +1 -0
  78. package/dist/modules/led.d.ts +35 -0
  79. package/dist/modules/led.d.ts.map +1 -0
  80. package/dist/modules/led.js +39 -0
  81. package/dist/modules/led.js.map +1 -0
  82. package/dist/modules/location.d.ts +27 -0
  83. package/dist/modules/location.d.ts.map +1 -0
  84. package/dist/modules/location.js +35 -0
  85. package/dist/modules/location.js.map +1 -0
  86. package/dist/modules/mic.d.ts +44 -0
  87. package/dist/modules/mic.d.ts.map +1 -0
  88. package/dist/modules/mic.js +67 -0
  89. package/dist/modules/mic.js.map +1 -0
  90. package/dist/modules/miniapps.d.ts +72 -0
  91. package/dist/modules/miniapps.d.ts.map +1 -0
  92. package/dist/modules/miniapps.js +48 -0
  93. package/dist/modules/miniapps.js.map +1 -0
  94. package/dist/modules/navigation.d.ts +326 -0
  95. package/dist/modules/navigation.d.ts.map +1 -0
  96. package/dist/modules/navigation.js +386 -0
  97. package/dist/modules/navigation.js.map +1 -0
  98. package/dist/modules/permissions.d.ts +58 -0
  99. package/dist/modules/permissions.d.ts.map +1 -0
  100. package/dist/modules/permissions.js +66 -0
  101. package/dist/modules/permissions.js.map +1 -0
  102. package/dist/modules/phone.d.ts +95 -0
  103. package/dist/modules/phone.d.ts.map +1 -0
  104. package/dist/modules/phone.js +110 -0
  105. package/dist/modules/phone.js.map +1 -0
  106. package/dist/modules/pivots/engine.d.ts +169 -0
  107. package/dist/modules/pivots/engine.d.ts.map +1 -0
  108. package/dist/modules/pivots/engine.js +779 -0
  109. package/dist/modules/pivots/engine.js.map +1 -0
  110. package/dist/modules/pivots/geometry.d.ts +84 -0
  111. package/dist/modules/pivots/geometry.d.ts.map +1 -0
  112. package/dist/modules/pivots/geometry.js +371 -0
  113. package/dist/modules/pivots/geometry.js.map +1 -0
  114. package/dist/modules/pivots/instructions.d.ts +139 -0
  115. package/dist/modules/pivots/instructions.d.ts.map +1 -0
  116. package/dist/modules/pivots/instructions.js +280 -0
  117. package/dist/modules/pivots/instructions.js.map +1 -0
  118. package/dist/modules/speaker.d.ts +169 -0
  119. package/dist/modules/speaker.d.ts.map +1 -0
  120. package/dist/modules/speaker.js +251 -0
  121. package/dist/modules/speaker.js.map +1 -0
  122. package/dist/modules/storage.d.ts +50 -0
  123. package/dist/modules/storage.d.ts.map +1 -0
  124. package/dist/modules/storage.js +99 -0
  125. package/dist/modules/storage.js.map +1 -0
  126. package/dist/modules/stream.d.ts +129 -0
  127. package/dist/modules/stream.d.ts.map +1 -0
  128. package/dist/modules/stream.js +52 -0
  129. package/dist/modules/stream.js.map +1 -0
  130. package/dist/modules/system.d.ts +45 -0
  131. package/dist/modules/system.d.ts.map +1 -0
  132. package/dist/modules/system.js +42 -0
  133. package/dist/modules/system.js.map +1 -0
  134. package/dist/modules/transcription.d.ts +75 -0
  135. package/dist/modules/transcription.d.ts.map +1 -0
  136. package/dist/modules/transcription.js +106 -0
  137. package/dist/modules/transcription.js.map +1 -0
  138. package/dist/modules/translation.d.ts +59 -0
  139. package/dist/modules/translation.d.ts.map +1 -0
  140. package/dist/modules/translation.js +119 -0
  141. package/dist/modules/translation.js.map +1 -0
  142. package/dist/modules/ui.d.ts +186 -0
  143. package/dist/modules/ui.d.ts.map +1 -0
  144. package/dist/modules/ui.js +293 -0
  145. package/dist/modules/ui.js.map +1 -0
  146. package/dist/protocol.d.ts +299 -0
  147. package/dist/protocol.d.ts.map +1 -0
  148. package/dist/protocol.js +325 -0
  149. package/dist/protocol.js.map +1 -0
  150. package/dist/react/MentraProvider.d.ts +33 -0
  151. package/dist/react/MentraProvider.d.ts.map +1 -0
  152. package/dist/react/MentraProvider.js +15 -0
  153. package/dist/react/MentraProvider.js.map +1 -0
  154. package/dist/react/MiniappHeader.d.ts +44 -0
  155. package/dist/react/MiniappHeader.d.ts.map +1 -0
  156. package/dist/react/MiniappHeader.js +32 -0
  157. package/dist/react/MiniappHeader.js.map +1 -0
  158. package/dist/react/index.d.ts +13 -0
  159. package/dist/react/index.d.ts.map +1 -0
  160. package/dist/react/index.js +9 -0
  161. package/dist/react/index.js.map +1 -0
  162. package/dist/react/useCapabilities.d.ts +26 -0
  163. package/dist/react/useCapabilities.d.ts.map +1 -0
  164. package/dist/react/useCapabilities.js +27 -0
  165. package/dist/react/useCapabilities.js.map +1 -0
  166. package/dist/react/useCapsuleHeaderStyle.d.ts +27 -0
  167. package/dist/react/useCapsuleHeaderStyle.d.ts.map +1 -0
  168. package/dist/react/useCapsuleHeaderStyle.js +36 -0
  169. package/dist/react/useCapsuleHeaderStyle.js.map +1 -0
  170. package/dist/react/useColorScheme.d.ts +18 -0
  171. package/dist/react/useColorScheme.d.ts.map +1 -0
  172. package/dist/react/useColorScheme.js +24 -0
  173. package/dist/react/useColorScheme.js.map +1 -0
  174. package/dist/react/useConnected.d.ts +17 -0
  175. package/dist/react/useConnected.d.ts.map +1 -0
  176. package/dist/react/useConnected.js +39 -0
  177. package/dist/react/useConnected.js.map +1 -0
  178. package/dist/react/useRpc.d.ts +29 -0
  179. package/dist/react/useRpc.d.ts.map +1 -0
  180. package/dist/react/useRpc.js +82 -0
  181. package/dist/react/useRpc.js.map +1 -0
  182. package/dist/react/useSafeArea.d.ts +21 -0
  183. package/dist/react/useSafeArea.d.ts.map +1 -0
  184. package/dist/react/useSafeArea.js +22 -0
  185. package/dist/react/useSafeArea.js.map +1 -0
  186. package/dist/session.d.ts +312 -0
  187. package/dist/session.d.ts.map +1 -0
  188. package/dist/session.js +653 -0
  189. package/dist/session.js.map +1 -0
  190. package/dist/sub-path-types.test-d.d.ts +28 -0
  191. package/dist/sub-path-types.test-d.d.ts.map +1 -0
  192. package/dist/sub-path-types.test-d.js +37 -0
  193. package/dist/sub-path-types.test-d.js.map +1 -0
  194. package/dist/transport/auto.d.ts +35 -0
  195. package/dist/transport/auto.d.ts.map +1 -0
  196. package/dist/transport/auto.js +125 -0
  197. package/dist/transport/auto.js.map +1 -0
  198. package/dist/transport/dispatch.d.ts +43 -0
  199. package/dist/transport/dispatch.d.ts.map +1 -0
  200. package/dist/transport/dispatch.js +91 -0
  201. package/dist/transport/dispatch.js.map +1 -0
  202. package/dist/transport/local-socket.d.ts +28 -0
  203. package/dist/transport/local-socket.d.ts.map +1 -0
  204. package/dist/transport/local-socket.js +80 -0
  205. package/dist/transport/local-socket.js.map +1 -0
  206. package/dist/transport/mock.d.ts +54 -0
  207. package/dist/transport/mock.d.ts.map +1 -0
  208. package/dist/transport/mock.js +207 -0
  209. package/dist/transport/mock.js.map +1 -0
  210. package/dist/transport/postmessage.d.ts +36 -0
  211. package/dist/transport/postmessage.d.ts.map +1 -0
  212. package/dist/transport/postmessage.js +78 -0
  213. package/dist/transport/postmessage.js.map +1 -0
  214. package/dist/transport/types.d.ts +25 -0
  215. package/dist/transport/types.d.ts.map +1 -0
  216. package/dist/transport/types.js +11 -0
  217. package/dist/transport/types.js.map +1 -0
  218. package/dist/ui/index.d.ts +98 -0
  219. package/dist/ui/index.d.ts.map +1 -0
  220. package/dist/ui/index.js +32 -0
  221. package/dist/ui/index.js.map +1 -0
  222. package/package.json +84 -0
  223. package/src/background/index.ts +152 -0
  224. package/src/background/register.ts +85 -0
  225. package/src/dev-reload.ts +60 -0
  226. package/src/envelope.ts +54 -0
  227. package/src/globals.ts +62 -0
  228. package/src/index.ts +171 -0
  229. package/src/modules/actions.ts +156 -0
  230. package/src/modules/auth.ts +68 -0
  231. package/src/modules/base64.ts +87 -0
  232. package/src/modules/blob.ts +341 -0
  233. package/src/modules/camera.ts +242 -0
  234. package/src/modules/cloud.ts +94 -0
  235. package/src/modules/dashboard.ts +31 -0
  236. package/src/modules/display.ts +122 -0
  237. package/src/modules/events.ts +291 -0
  238. package/src/modules/glasses.ts +44 -0
  239. package/src/modules/heading.ts +23 -0
  240. package/src/modules/imu.ts +44 -0
  241. package/src/modules/input.ts +71 -0
  242. package/src/modules/led.ts +61 -0
  243. package/src/modules/location.ts +39 -0
  244. package/src/modules/mic.ts +74 -0
  245. package/src/modules/miniapps.ts +90 -0
  246. package/src/modules/navigation.ts +670 -0
  247. package/src/modules/permissions.ts +84 -0
  248. package/src/modules/phone.ts +155 -0
  249. package/src/modules/pivots/engine.ts +863 -0
  250. package/src/modules/pivots/geometry.ts +409 -0
  251. package/src/modules/pivots/instructions.ts +343 -0
  252. package/src/modules/speaker.ts +335 -0
  253. package/src/modules/storage.ts +109 -0
  254. package/src/modules/stream.ts +163 -0
  255. package/src/modules/system.ts +76 -0
  256. package/src/modules/transcription.ts +145 -0
  257. package/src/modules/translation.ts +149 -0
  258. package/src/modules/ui.ts +498 -0
  259. package/src/protocol.ts +378 -0
  260. package/src/react/MentraProvider.tsx +50 -0
  261. package/src/react/MiniappHeader.tsx +122 -0
  262. package/src/react/index.ts +12 -0
  263. package/src/react/useCapabilities.ts +29 -0
  264. package/src/react/useCapsuleHeaderStyle.ts +54 -0
  265. package/src/react/useColorScheme.ts +26 -0
  266. package/src/react/useConnected.ts +47 -0
  267. package/src/react/useRpc.ts +106 -0
  268. package/src/react/useSafeArea.ts +40 -0
  269. package/src/session.ts +886 -0
  270. package/src/sub-path-types.test-d.ts +49 -0
  271. package/src/transport/auto.ts +143 -0
  272. package/src/transport/dispatch.ts +106 -0
  273. package/src/transport/local-socket.ts +93 -0
  274. package/src/transport/mock.ts +251 -0
  275. package/src/transport/postmessage.ts +95 -0
  276. package/src/transport/types.ts +26 -0
  277. package/src/ui/index.ts +122 -0
@@ -0,0 +1,291 @@
1
+ /**
2
+ * @fileoverview EventManager — internal subscription registry + escape hatch.
3
+ *
4
+ * Most miniapp authors should NOT touch this directly. Use the typed methods
5
+ * on domain modules instead:
6
+ * - session.transcription.on(...)
7
+ * - session.mic.onAudioChunk(...) / onVoiceActivity(...)
8
+ * - session.input.onButtonPress(...)
9
+ * - session.imu.onHeadPosition(...)
10
+ * - session.location.onUpdate(...)
11
+ * - session.glasses.onBattery(...) / onConnection(...)
12
+ * - session.phone.notifications.on(...) / session.phone.onBattery(...)
13
+ *
14
+ * This module's only public method is `subscribe(rawStreamType, handler)` —
15
+ * a forward-compat escape hatch for new event types not yet wrapped on a
16
+ * domain module. Officially undocumented; the typed methods on domain
17
+ * modules are the canonical surface.
18
+ *
19
+ * Internally, EventManager owns:
20
+ * 1. The ref-count map. Outbound SUBSCRIBE is only sent when a stream's
21
+ * ref count transitions 0↔1, so multiple components listening for the
22
+ * same stream issue one wire-level subscribe.
23
+ * 2. Inbound event fan-out via `_forwardEvent(streamType, data)`, called
24
+ * by MiniappSession.handleIncoming when an EVENT envelope arrives.
25
+ *
26
+ * Domain modules call back into the session via session._subscribe(...) which
27
+ * in turn delegates to this class — the session is the integration point;
28
+ * domain modules don't see this class directly.
29
+ */
30
+
31
+ import {EventEmitter} from "eventemitter3"
32
+
33
+ import {MiniappRequestType, MiniappStreamType} from "../protocol"
34
+ import {MiniappSession} from "../session"
35
+
36
+ export type UnsubscribeFn = () => void
37
+
38
+ /** @internal Host-selected delivery target for a transcription event. */
39
+ export type TranscriptionEventRoute = "default" | "forceLocal" | "all"
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Shared event data shapes — re-exported by index.ts so consumers can type
43
+ // their handlers without importing this internal module.
44
+ // ---------------------------------------------------------------------------
45
+
46
+ export interface TranscriptionData {
47
+ text: string
48
+ isFinal: boolean
49
+ language?: string
50
+ }
51
+
52
+ export interface TranslationData {
53
+ text: string
54
+ isFinal: boolean
55
+ sourceLanguage: string
56
+ targetLanguage: string
57
+ /** Source-language text of the same utterance, when the provider supplies it. */
58
+ originalText?: string
59
+ /** Stable id correlating interim + final results of one utterance. */
60
+ utteranceId?: string
61
+ /** Speaker id when the provider reports diarization. */
62
+ speakerId?: string
63
+ }
64
+
65
+ export interface ButtonPressData {
66
+ buttonId: string
67
+ pressType: "short" | "long"
68
+ }
69
+
70
+ export interface HeadPositionData {
71
+ position: "up" | "down"
72
+ }
73
+
74
+ export interface AccelData {
75
+ /** Accelerometer axes in g (gravity-normalized). */
76
+ x: number
77
+ y: number
78
+ z: number
79
+ /** Unix ms timestamp of the reading. */
80
+ timestamp: number
81
+ }
82
+
83
+ export interface LocationData {
84
+ lat: number
85
+ lng: number
86
+ /** Accuracy in meters, if the platform reported it. */
87
+ accuracy?: number
88
+ /** Unix ms timestamp of the fix. */
89
+ timestamp?: number
90
+ /** Set when this event is a response to a single-location request. */
91
+ correlationId?: string
92
+ }
93
+
94
+ export interface HeadingData {
95
+ /** Compass heading in degrees, 0 = north, 90 = east. */
96
+ degrees: number
97
+ }
98
+
99
+ export interface BatteryData {
100
+ level: number
101
+ charging: boolean
102
+ }
103
+
104
+ export interface ConnectionData {
105
+ connected: boolean
106
+ modelName?: string
107
+ }
108
+
109
+ /** Glasses Wi-Fi state. `connected` is false on glasses without Wi-Fi or when offline. */
110
+ export interface WifiData {
111
+ connected: boolean
112
+ ssid?: string
113
+ localIp?: string
114
+ }
115
+
116
+ export interface PhoneNotificationData {
117
+ /** Stable id from the phone's notification listener. */
118
+ notificationId: string
119
+ /** Human app name (e.g. "Messages"). */
120
+ app: string
121
+ title: string
122
+ content: string
123
+ /** Android priority string; empty on iOS. */
124
+ priority: string
125
+ timestamp: number
126
+ /** Reverse-DNS package/bundle id of the originating app. */
127
+ packageName: string
128
+ }
129
+
130
+ export interface NotificationDismissedData {
131
+ /** Same id as the matching post event from `notifications.on(...)`. */
132
+ notificationId: string
133
+ /** Android NotificationKey. More stable than notificationId across reposts. */
134
+ notificationKey?: string
135
+ /** Reverse-DNS package/bundle id of the originating app. */
136
+ packageName?: string
137
+ /** Unix ms timestamp of the dismissal. */
138
+ timestamp: number
139
+ }
140
+
141
+ export interface VadData {
142
+ /** True while the user is speaking (voice detected), false when silent. */
143
+ status: boolean
144
+ }
145
+
146
+ export interface TouchData {
147
+ /** The gesture: single_tap, double_tap, triple_tap, long_press, swipe_up, or swipe_down. */
148
+ kind: "single_tap" | "double_tap" | "triple_tap" | "long_press" | "swipe_up" | "swipe_down" | string
149
+ }
150
+
151
+ export interface AudioChunkData {
152
+ /** PCM or LC3, base64-encoded. Format depends on phone's mic mode. */
153
+ data: string
154
+ sampleRate?: number
155
+ format?: string
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Registry
160
+ // ---------------------------------------------------------------------------
161
+
162
+ export class EventManager {
163
+ private readonly emitter = new EventEmitter()
164
+ /** Stream -> ref count. Outbound SUBSCRIBE tracks active streams and routing changes. */
165
+ private readonly refCounts = new Map<string, number>()
166
+ /** Transcription stream -> refs that require on-device-only processing. */
167
+ private readonly forceLocalRefCounts = new Map<string, number>()
168
+
169
+ constructor(private readonly session: MiniappSession) {}
170
+
171
+ /**
172
+ * Generic subscribe. `stream` is the raw wire value, including any language
173
+ * suffix like `"transcription:en-US"`. Forward-compat escape hatch for new
174
+ * event types not yet wrapped on a domain module. Most authors should use
175
+ * typed methods on domain modules instead.
176
+ *
177
+ * Stream names beginning with `_` (e.g. `_ui` for the session.ui bus) are
178
+ * INTERNAL — they ride the local _forwardEvent fan-out and never appear in
179
+ * the outbound SUBSCRIBE list. Native already knows to deliver UI envelopes
180
+ * via a separate envelope `type`; the EventManager only routes them
181
+ * locally.
182
+ */
183
+ subscribe(stream: string, handler: (data: unknown) => void, options: {forceLocal?: boolean} = {}): UnsubscribeFn {
184
+ const isInternal = stream.startsWith("_")
185
+ const forceLocal = options.forceLocal === true && stream.startsWith(`${MiniappStreamType.TRANSCRIPTION}:`)
186
+ const listener = (data: unknown, route?: TranscriptionEventRoute) => {
187
+ if (stream.startsWith(`${MiniappStreamType.TRANSCRIPTION}:`)) {
188
+ // Hosts that predate routed transcription events omit `route`. Treat
189
+ // those events as the default/cloud path so a forceLocal listener can
190
+ // never accidentally receive cloud text.
191
+ if (forceLocal ? route !== "forceLocal" && route !== "all" : route === "forceLocal") return
192
+ }
193
+ handler(data)
194
+ }
195
+ this.emitter.on(stream, listener)
196
+ if (!isInternal) {
197
+ const before = this.refCounts.get(stream) ?? 0
198
+ this.refCounts.set(stream, before + 1)
199
+ const forceLocalBefore = this.forceLocalRefCounts.get(stream) ?? 0
200
+ const defaultBefore = before - forceLocalBefore
201
+ if (forceLocal) this.forceLocalRefCounts.set(stream, forceLocalBefore + 1)
202
+ if (before === 0 || (forceLocal && forceLocalBefore === 0) || (!forceLocal && defaultBefore === 0)) {
203
+ this.sendSubscriptionUpdate()
204
+ }
205
+ }
206
+ return () => {
207
+ this.emitter.off(stream, listener)
208
+ if (isInternal) return
209
+ const current = this.refCounts.get(stream) ?? 0
210
+ const forceLocalCurrent = this.forceLocalRefCounts.get(stream) ?? 0
211
+ let subscriptionChanged = current <= 1
212
+ if (current <= 1) {
213
+ this.refCounts.delete(stream)
214
+ } else {
215
+ this.refCounts.set(stream, current - 1)
216
+ }
217
+ if (forceLocal) {
218
+ if (forceLocalCurrent <= 1) {
219
+ this.forceLocalRefCounts.delete(stream)
220
+ subscriptionChanged = true
221
+ } else {
222
+ this.forceLocalRefCounts.set(stream, forceLocalCurrent - 1)
223
+ }
224
+ } else if (current - forceLocalCurrent <= 1) {
225
+ subscriptionChanged = true
226
+ }
227
+ if (subscriptionChanged) this.sendSubscriptionUpdate()
228
+ }
229
+ }
230
+
231
+ /** Unsubscribe every handler on every stream this EventManager owns. */
232
+ unsubscribeAll(): void {
233
+ this.emitter.removeAllListeners()
234
+ this.refCounts.clear()
235
+ this.forceLocalRefCounts.clear()
236
+ this.sendSubscriptionUpdate()
237
+ }
238
+
239
+ // -------------------------------------------------------------------------
240
+ // Internal — called by MiniappSession when EVENT arrives from phone
241
+ // -------------------------------------------------------------------------
242
+
243
+ /** @internal */
244
+ _forwardEvent(stream: string, data: unknown, transcriptionRoute?: TranscriptionEventRoute): void {
245
+ this.emitter.emit(stream, data, transcriptionRoute)
246
+
247
+ // Wildcard fan-out: handlers register under wildcard patterns
248
+ // ("transcription:auto", "translation:*:<target>", …) but the host
249
+ // delivers events under the CONCRETE stream key
250
+ // ("transcription:en-US", "translation:en:es"). Re-emit on every
251
+ // pattern the concrete key satisfies — the same pattern set
252
+ // LocalMiniappRuntime.forwardEvent matches on the host side —
253
+ // otherwise a wildcard subscriber never fires.
254
+ if (stream.startsWith("transcription:") && stream !== "transcription:auto") {
255
+ this.emitter.emit("transcription:auto", data, transcriptionRoute)
256
+ } else if (stream.startsWith("translation:")) {
257
+ const parts = stream.split(":")
258
+ const patterns = new Set<string>(["translation:auto"])
259
+ if (parts.length === 3) {
260
+ const [, source, target] = parts
261
+ patterns.add("translation:*:*")
262
+ patterns.add(`translation:*:${target}`)
263
+ patterns.add(`translation:${source}:*`)
264
+ }
265
+ patterns.delete(stream) // exact key already emitted above
266
+ for (const pattern of patterns) {
267
+ this.emitter.emit(pattern, data)
268
+ }
269
+ }
270
+ }
271
+
272
+ private sendSubscriptionUpdate(): void {
273
+ const subscriptions = Array.from(this.refCounts.keys()).map((stream) => {
274
+ if (stream === MiniappStreamType.LOCATION_UPDATE) return {stream: "location_stream", rate: "realtime"}
275
+
276
+ const forceLocalRefs = this.forceLocalRefCounts.get(stream) ?? 0
277
+ if (forceLocalRefs === 0) return stream
278
+
279
+ const totalRefs = this.refCounts.get(stream) ?? 0
280
+ return {
281
+ stream,
282
+ forceLocal: true,
283
+ ...(totalRefs > forceLocalRefs ? {includeCloud: true} : {}),
284
+ }
285
+ })
286
+ this.session.sendOneShot({
287
+ type: MiniappRequestType.SUBSCRIBE,
288
+ subscriptions,
289
+ })
290
+ }
291
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @fileoverview GlassesModule — device-state events for the glasses themselves.
3
+ *
4
+ * Reports on the glasses hardware: battery level + charging state, connection
5
+ * status (connected/disconnected, model name). The phone has its own battery
6
+ * + connection events on `session.phone`.
7
+ */
8
+
9
+ import {MiniappRequestType, MiniappStreamType} from "../protocol"
10
+ import {MiniappSession} from "../session"
11
+ import type {BatteryData, ConnectionData, UnsubscribeFn, WifiData} from "./events"
12
+
13
+ export class GlassesModule {
14
+ constructor(private readonly session: MiniappSession) {}
15
+
16
+ onBattery(handler: (data: BatteryData) => void): UnsubscribeFn {
17
+ return this.session._subscribe(MiniappStreamType.GLASSES_BATTERY, handler as (data: unknown) => void)
18
+ }
19
+
20
+ onConnection(handler: (data: ConnectionData) => void): UnsubscribeFn {
21
+ return this.session._subscribe(MiniappStreamType.GLASSES_CONNECTION, handler as (data: unknown) => void)
22
+ }
23
+
24
+ /**
25
+ * Subscribe to glasses Wi-Fi state. Fires the current state on subscribe and
26
+ * on every change. `connected` is false on glasses without Wi-Fi. Streaming
27
+ * requires Wi-Fi — pair this with {@link requestWifiSetup} to prompt the user.
28
+ */
29
+ onWifi(handler: (data: WifiData) => void): UnsubscribeFn {
30
+ return this.session._subscribe(MiniappStreamType.GLASSES_WIFI, handler as (data: unknown) => void)
31
+ }
32
+
33
+ /**
34
+ * Ask the host to open the glasses Wi-Fi setup flow on the phone. Mirrors the
35
+ * cloud SDK's `session.requestWifiSetup(reason)`. `reason` is a user-facing
36
+ * line shown in the setup prompt (e.g. "Streaming needs your glasses on Wi-Fi").
37
+ */
38
+ async requestWifiSetup(reason?: string): Promise<void> {
39
+ await this.session.sendRequest<void>({
40
+ type: MiniappRequestType.REQUEST_WIFI_SETUP,
41
+ reason,
42
+ })
43
+ }
44
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @fileoverview HeadingModule — phone compass heading events.
3
+ *
4
+ * Works on both Android and iOS. LOCATION permission must be declared in miniapp.json.
5
+ */
6
+
7
+ import {MiniappStreamType} from "../protocol"
8
+ import {MiniappSession} from "../session"
9
+ import type {HeadingData, UnsubscribeFn} from "./events"
10
+
11
+ export class HeadingModule {
12
+ constructor(private readonly session: MiniappSession) {}
13
+
14
+ /** True iff `LOCATION` is declared in the miniapp's manifest. */
15
+ get hasPermission(): boolean {
16
+ return this.session._hasManifestPermission("LOCATION")
17
+ }
18
+
19
+ /** Subscribe to continuous compass heading updates. */
20
+ onUpdate(handler: (data: HeadingData) => void): UnsubscribeFn {
21
+ return this.session._subscribe(MiniappStreamType.HEADING_UPDATE, handler as (data: unknown) => void)
22
+ }
23
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @fileoverview ImuModule — head position + motion events.
3
+ *
4
+ * Glasses' inertial measurement unit. Exposes head-up/down position and a
5
+ * raw accelerometer stream (`onAccel`). Gyroscope / magnetometer / fused
6
+ * orientation are future work — when added they'll surface as a combined IMU
7
+ * event rather than extending this single-sensor accel stream.
8
+ */
9
+
10
+ import {MiniappRequestType, MiniappStreamType} from "../protocol"
11
+ import {MiniappSession} from "../session"
12
+ import type {AccelData, HeadPositionData, UnsubscribeFn} from "./events"
13
+
14
+ export class ImuModule {
15
+ constructor(private readonly session: MiniappSession) {}
16
+
17
+ onHeadPosition(handler: (data: HeadPositionData) => void): UnsubscribeFn {
18
+ return this.session._subscribe(MiniappStreamType.HEAD_POSITION, handler as (data: unknown) => void)
19
+ }
20
+
21
+ /**
22
+ * Subscribe to raw accelerometer readings (`{x, y, z}` in g) from the
23
+ * glasses IMU. G2 only today; on glasses without an exposed IMU stream the
24
+ * subscription succeeds but no events fire.
25
+ */
26
+ onAccel(handler: (data: AccelData) => void): UnsubscribeFn {
27
+ return this.session._subscribe(MiniappStreamType.ACCEL_DATA, handler as (data: unknown) => void)
28
+ }
29
+
30
+ /**
31
+ * Explicitly enable or disable raw accelerometer streaming on the glasses.
32
+ *
33
+ * Subscribing via `onAccel` already auto-enables the sensor and unsubscribing
34
+ * disables it, so most callers don't need this. Use it when you want to drive
35
+ * the IMU directly — e.g. a diagnostic toggle. G2 only today; a no-op on
36
+ * glasses without an exposed IMU stream.
37
+ */
38
+ setEnabled(enabled: boolean): Promise<void> {
39
+ return this.session.sendRequest<void>({
40
+ type: MiniappRequestType.IMU_SET_ENABLED,
41
+ enabled,
42
+ })
43
+ }
44
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @fileoverview InputModule — physical control events on the glasses.
3
+ *
4
+ * Combines button + touch surfaces. Future input modes (gesture, voice
5
+ * command, eye tracking) will extend this module rather than spawning new
6
+ * top-level modules.
7
+ *
8
+ * Touch overloads:
9
+ *
10
+ * session.input.onTouch(handler)
11
+ * session.input.onTouch("single_tap", handler)
12
+ * session.input.onTouch(["swipe_up", "swipe_down"], handler)
13
+ *
14
+ * Per-gesture filtering rides on `touch_event:<gesture>` stream variants
15
+ * the phone runtime fans out alongside the bare `touch_event` stream.
16
+ */
17
+
18
+ import {MiniappStreamType} from "../protocol"
19
+ import {MiniappSession} from "../session"
20
+ import type {ButtonPressData, TouchData, UnsubscribeFn} from "./events"
21
+
22
+ export class InputModule {
23
+ constructor(private readonly session: MiniappSession) {}
24
+
25
+ onButtonPress(handler: (data: ButtonPressData) => void): UnsubscribeFn {
26
+ return this.session._subscribe(MiniappStreamType.BUTTON_PRESS, handler as (data: unknown) => void)
27
+ }
28
+
29
+ /**
30
+ * Subscribe to touch events.
31
+ *
32
+ * onTouch(handler) — all touch events
33
+ * onTouch("single_tap", handler) — only single taps
34
+ * onTouch(["a","b"], handler) — multiple gestures, single subscription
35
+ */
36
+ onTouch(handler: (data: TouchData) => void): UnsubscribeFn
37
+ onTouch(gesture: string, handler: (data: TouchData) => void): UnsubscribeFn
38
+ onTouch(gestures: string[], handler: (data: TouchData) => void): UnsubscribeFn
39
+ onTouch(
40
+ gestureOrHandler: string | string[] | ((data: TouchData) => void),
41
+ maybeHandler?: (data: TouchData) => void,
42
+ ): UnsubscribeFn {
43
+ // Plain handler — subscribe to all touches.
44
+ if (typeof gestureOrHandler === "function") {
45
+ return this.session._subscribe(
46
+ MiniappStreamType.TOUCH_EVENT,
47
+ gestureOrHandler as (data: unknown) => void,
48
+ )
49
+ }
50
+
51
+ const handler = maybeHandler!
52
+ const gestures = Array.isArray(gestureOrHandler) ? gestureOrHandler : [gestureOrHandler]
53
+ if (gestures.length === 0) return () => {}
54
+
55
+ const unsubs: UnsubscribeFn[] = []
56
+ for (const g of gestures) {
57
+ unsubs.push(
58
+ this.session._subscribe(`${MiniappStreamType.TOUCH_EVENT}:${g}`, handler as (data: unknown) => void),
59
+ )
60
+ }
61
+ return () => {
62
+ for (const u of unsubs) {
63
+ try {
64
+ u()
65
+ } catch {
66
+ /* ignore */
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @fileoverview LedModule — glasses RGB LED control.
3
+ *
4
+ * API mirrors the cloud SDK's LED module. Colors are named strings
5
+ * (the phone maps them to per-device LED indices). Actions are "on" / "off".
6
+ */
7
+
8
+ import {MiniappRequestType} from "../protocol"
9
+ import {MiniappSession} from "../session"
10
+
11
+ export type LedColor = "red" | "green" | "blue" | "orange" | "white"
12
+
13
+ export type LedControlResult = {
14
+ type?: "rgb_led_control_response"
15
+ state: "success"
16
+ requestId: string
17
+ }
18
+
19
+ export interface LedControlOptions {
20
+ color?: LedColor
21
+ /** LED on duration in ms. */
22
+ ontime?: number
23
+ /** LED off duration in ms. */
24
+ offtime?: number
25
+ /** Number of on/off cycles. */
26
+ count?: number
27
+ }
28
+
29
+ export class LedModule {
30
+ constructor(private readonly session: MiniappSession) {}
31
+
32
+ /** Turn an LED on with the given pattern. Resolves after the glasses acknowledge the command. */
33
+ async turnOn(options: LedControlOptions = {}): Promise<LedControlResult> {
34
+ return this.session.sendRequest<LedControlResult>({
35
+ type: MiniappRequestType.RGB_LED,
36
+ action: "on",
37
+ color: options.color ?? "red",
38
+ ontime: options.ontime ?? 1000,
39
+ offtime: options.offtime ?? 0,
40
+ count: options.count ?? 1,
41
+ })
42
+ }
43
+
44
+ /** Turn all LEDs off. Resolves after the glasses acknowledge the command. */
45
+ async turnOff(): Promise<LedControlResult> {
46
+ return this.session.sendRequest<LedControlResult>({
47
+ type: MiniappRequestType.RGB_LED,
48
+ action: "off",
49
+ })
50
+ }
51
+
52
+ /** Blink pattern — repeats `count` times with ontime/offtime. */
53
+ async blink(color: LedColor, ontime: number, offtime: number, count: number): Promise<LedControlResult> {
54
+ return this.turnOn({color, ontime, offtime, count})
55
+ }
56
+
57
+ /** Solid LED for a fixed duration. */
58
+ async solid(color: LedColor, duration: number): Promise<LedControlResult> {
59
+ return this.turnOn({color, ontime: duration, offtime: 0, count: 1})
60
+ }
61
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @fileoverview LocationModule — phone location events.
3
+ *
4
+ * Exposes both a continuous `onUpdate` subscription and a one-shot
5
+ * `getOnce()` poll over `MiniappRequestType.LOCATION_POLL`.
6
+ *
7
+ * LOCATION permission must be declared in miniapp.json for either path to
8
+ * succeed; the phone runtime rejects with PERMISSION_NOT_DECLARED
9
+ * otherwise.
10
+ */
11
+
12
+ import {MiniappRequestType, MiniappStreamType} from "../protocol"
13
+ import {MiniappSession} from "../session"
14
+ import type {LocationData, UnsubscribeFn} from "./events"
15
+
16
+ export class LocationModule {
17
+ constructor(private readonly session: MiniappSession) {}
18
+
19
+ /** True iff `LOCATION` is declared in the miniapp's manifest. */
20
+ get hasPermission(): boolean {
21
+ return this.session._hasManifestPermission("LOCATION")
22
+ }
23
+
24
+ /** Subscribe to continuous location updates. */
25
+ onUpdate(handler: (data: LocationData) => void): UnsubscribeFn {
26
+ return this.session._subscribe(MiniappStreamType.LOCATION_UPDATE, handler as (data: unknown) => void)
27
+ }
28
+
29
+ /**
30
+ * Request a single location fix. Resolves with the next available
31
+ * reading from the phone. Useful at app load to seed UI before a
32
+ * continuous stream of updates begins.
33
+ */
34
+ getOnce(): Promise<LocationData> {
35
+ return this.session.sendRequest<LocationData>({
36
+ type: MiniappRequestType.LOCATION_POLL,
37
+ })
38
+ }
39
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @fileoverview MicModule — low-level audio input subscriptions.
3
+ *
4
+ * Mirrors cloud SDK v3's MicManager naming. Houses raw audio chunks + VAD;
5
+ * transcription and translation hoisted to top-level (`session.transcription`
6
+ * / `session.translation`) in the v3-alignment round so authors don't have to
7
+ * mentally model "transcription is a microphone thing" — it's just a
8
+ * top-level domain. This module was called `MicrophoneModule` /
9
+ * `session.microphone` before that round.
10
+ *
11
+ * Audio *output* (TTS, file playback) lives on `session.speaker`.
12
+ *
13
+ * MICROPHONE permission must be declared in miniapp.json for any of these
14
+ * subscriptions to succeed; the phone runtime rejects with
15
+ * PERMISSION_NOT_DECLARED otherwise.
16
+ */
17
+
18
+ import {MiniappStreamType} from "../protocol"
19
+ import {MiniappSession} from "../session"
20
+ import type {AudioChunkData, UnsubscribeFn, VadData} from "./events"
21
+
22
+ export class MicModule {
23
+ /** All active unsubscribe functions for stop() to tear down at once. */
24
+ private readonly unsubs = new Set<UnsubscribeFn>()
25
+
26
+ constructor(private readonly session: MiniappSession) {}
27
+
28
+ /**
29
+ * Subscribe to voice activity detection (VAD) events. `data.status` is
30
+ * `true` while the user is speaking, `false` when silent.
31
+ */
32
+ onVoiceActivity(handler: (data: VadData) => void): UnsubscribeFn {
33
+ return this.track(this.session._subscribe(MiniappStreamType.VAD, handler as (data: unknown) => void))
34
+ }
35
+
36
+ /**
37
+ * Subscribe to raw audio chunks. Format depends on the phone's mic mode
38
+ * (PCM or LC3, base64-encoded).
39
+ */
40
+ onAudioChunk(handler: (data: AudioChunkData) => void): UnsubscribeFn {
41
+ return this.track(this.session._subscribe(MiniappStreamType.AUDIO_CHUNK, handler as (data: unknown) => void))
42
+ }
43
+
44
+ /**
45
+ * Tear down every subscription this module owns. Useful when a component
46
+ * is unmounting and wants to free everything at once without tracking
47
+ * individual unsubscribe functions.
48
+ */
49
+ stop(): void {
50
+ for (const u of this.unsubs) {
51
+ try {
52
+ u()
53
+ } catch {
54
+ /* ignore */
55
+ }
56
+ }
57
+ this.unsubs.clear()
58
+ }
59
+
60
+ /** True iff `MICROPHONE` is declared in the miniapp's manifest. */
61
+ get hasPermission(): boolean {
62
+ return this.session._hasManifestPermission("MICROPHONE")
63
+ }
64
+
65
+ // ------------------------------------------------------------------------
66
+
67
+ private track(unsub: UnsubscribeFn): UnsubscribeFn {
68
+ this.unsubs.add(unsub)
69
+ return () => {
70
+ this.unsubs.delete(unsub)
71
+ unsub()
72
+ }
73
+ }
74
+ }