@mentra/miniapp 0.3.0-dev.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 +59 -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 +58 -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 +137 -0
  43. package/dist/modules/camera.d.ts.map +1 -0
  44. package/dist/modules/camera.js +104 -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 +167 -0
  59. package/dist/modules/events.d.ts.map +1 -0
  60. package/dist/modules/events.js +123 -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 +61 -0
  103. package/dist/modules/phone.d.ts.map +1 -0
  104. package/dist/modules/phone.js +94 -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 +163 -0
  119. package/dist/modules/speaker.d.ts.map +1 -0
  120. package/dist/modules/speaker.js +250 -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 +67 -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 +294 -0
  147. package/dist/protocol.d.ts.map +1 -0
  148. package/dist/protocol.js +320 -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 +310 -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 +205 -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 +75 -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 +196 -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 +264 -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 +120 -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 +328 -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 +128 -0
  257. package/src/modules/translation.ts +149 -0
  258. package/src/modules/ui.ts +498 -0
  259. package/src/protocol.ts +371 -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 +878 -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 +248 -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,49 @@
1
+ /**
2
+ * Compile-time test for the two-layer SDK split.
3
+ *
4
+ * The package exports two sub-paths with distinct type-roots:
5
+ * - `@mentra/miniapp/background` exposes `MiniappSession` + module
6
+ * types; it does NOT declare the `mentra` global.
7
+ * - `@mentra/miniapp/ui` exposes the `MentraUiGlobal` / `MentraTyped`
8
+ * types + React adapters. It deliberately does NOT declare a
9
+ * `var mentra` on `globalThis` — that would lock every consumer
10
+ * into the SDK's default `Record<string, unknown>` channel shape
11
+ * and break per-miniapp `Channels` registries via TS's `var`
12
+ * declaration-merging rules. Each miniapp declares its own typed
13
+ * `var mentra: MentraTyped<MyChannels>` in `shared/channels.ts`.
14
+ *
15
+ * Wrong-layer imports should be caught at compile time. This file is
16
+ * type-only (`.test-d.ts`) — no runtime assertions. `bun typecheck` /
17
+ * `bun x tsc -p tsconfig.json` is the executor.
18
+ *
19
+ * NOTE: this file lives inside `src/` so it ships with the package and
20
+ * downstream consumers' typechecks would also catch a regression. The
21
+ * `@ts-expect-error` lines fail the build if the named symbol *does*
22
+ * become reachable on the wrong path, which is the contract we want.
23
+ */
24
+
25
+ // ----- @mentra/miniapp/background ---------------------------------------
26
+ import type {MiniappSession} from "./background/index"
27
+
28
+ // ✅ MiniappSession is reachable from the background entry.
29
+ const _bg: MiniappSession | undefined = undefined
30
+ void _bg
31
+
32
+ // ----- @mentra/miniapp/ui -----------------------------------------------
33
+ import type {MentraUiGlobal, MentraTyped} from "./ui/index"
34
+
35
+ // ✅ MentraUiGlobal is reachable from the ui entry.
36
+ const _ui: MentraUiGlobal | undefined = undefined
37
+ void _ui
38
+
39
+ // ✅ MentraTyped<TChannels> is the canonical way to type the global.
40
+ // Consumers do `declare global { var mentra: MentraTyped<Channels> }`
41
+ // inside their own code; the SDK no longer ships the declaration.
42
+ export type _Typed = MentraTyped<{foo: number}>
43
+
44
+ // ----- compile-time guardrails ------------------------------------------
45
+ // (TypeScript doesn't ship a `// @ts-expect-no-error` mirror, so we just
46
+ // rely on the named imports above failing if the corresponding files
47
+ // disappear. The asymmetric exports are enforced by package.json's
48
+ // `exports` map at consumer time; this file proves the source files
49
+ // expose what they should.)
@@ -0,0 +1,143 @@
1
+ /**
2
+ * @fileoverview Auto-detect the right Transport based on environment.
3
+ */
4
+
5
+ import {DispatchTransport} from "./dispatch"
6
+ import {LocalSocketTransport, LocalSocketTransportOptions} from "./local-socket"
7
+ import {MockTransport, isMockExplicitlyRequested} from "./mock"
8
+ import {PostMessageTransport} from "./postmessage"
9
+ import {Transport, TransportDisconnectHandler, TransportMessageHandler} from "./types"
10
+
11
+ const LOCAL_SOCKET_OPEN_TIMEOUT_MS = 500
12
+
13
+ export interface CreateTransportOptions {
14
+ /** Force a specific transport. Skip auto-detection. */
15
+ transport?: Transport
16
+ /** For LocalSocketTransport fallback — override the ws URL. */
17
+ localSocketUrl?: string
18
+ }
19
+
20
+ /**
21
+ * Return a Transport appropriate for the current environment.
22
+ *
23
+ * Detection order (first match wins):
24
+ *
25
+ * 1. **DispatchTransport** — when running in a per-miniapp JSContext
26
+ * (no DOM, but `__dispatch` is installed on `globalThis` by the
27
+ * MentraJS runtime). This is the production path.
28
+ * 2. **PostMessageTransport** — inside a MentraOS WebView
29
+ * (`window.ReactNativeWebView` defined). This is the existing UI
30
+ * layer path and the only transport miniapp UI bundles need.
31
+ * 3. **MockTransport** — explicit opt-in via `?mentra=mock` or
32
+ * `localStorage.MENTRA_MOCK=1`. Useful for Storybook / unit tests.
33
+ * 4. **LocalSocketWithMockFallback** — external browser, races a
34
+ * `ws://127.0.0.1` LocalSocketTransport open against a 500ms timeout
35
+ * and falls back to MockTransport so the page doesn't hang.
36
+ * 5. **MockTransport** — no `WebSocket` at all (some embedded contexts,
37
+ * jsdom without a polyfill). Keeps the SDK from throwing.
38
+ *
39
+ * DispatchTransport is checked FIRST because the JSContext doesn't have
40
+ * a `window` global, so it'd otherwise fall through to LocalSocket and
41
+ * race a non-existent dev WebSocket — annoying and confusing in logs.
42
+ */
43
+ export function createTransport(options: CreateTransportOptions = {}): Transport {
44
+ if (options.transport) return options.transport
45
+
46
+ // 1) JSContext background world — single check for the native bridge.
47
+ if (DispatchTransport.isAvailable()) {
48
+ return new DispatchTransport()
49
+ }
50
+
51
+ if (typeof window !== "undefined" && window.ReactNativeWebView) {
52
+ return new PostMessageTransport()
53
+ }
54
+
55
+ // Explicit opt-in to mock — useful for stories / framework-level testing.
56
+ if (isMockExplicitlyRequested()) {
57
+ return new MockTransport()
58
+ }
59
+
60
+ if (typeof WebSocket !== "undefined") {
61
+ const localSocketOptions: LocalSocketTransportOptions = {}
62
+ if (options.localSocketUrl) localSocketOptions.url = options.localSocketUrl
63
+ return new LocalSocketWithMockFallback(localSocketOptions)
64
+ }
65
+
66
+ // No WebSocket at all (some embedded contexts, jsdom without a polyfill).
67
+ // MockTransport keeps the SDK from throwing during construction.
68
+ return new MockTransport()
69
+ }
70
+
71
+ /**
72
+ * Internal wrapper transport: tries LocalSocketTransport first; if open() hasn't
73
+ * resolved within LOCAL_SOCKET_OPEN_TIMEOUT_MS, swaps in MockTransport so a
74
+ * miniapp opened in a regular browser tab gets a synthetic CONNECT_ACK instead
75
+ * of a 10-second hang while the SDK's CONNECT_ACK timeout expires.
76
+ *
77
+ * Once a delegate is selected, all subsequent calls forward to it.
78
+ */
79
+ class LocalSocketWithMockFallback implements Transport {
80
+ private active: Transport | null = null
81
+ private messageHandler: TransportMessageHandler | null = null
82
+ private disconnectHandler: TransportDisconnectHandler | null = null
83
+
84
+ constructor(private readonly options: LocalSocketTransportOptions) {}
85
+
86
+ async open(): Promise<void> {
87
+ const local = new LocalSocketTransport(this.options)
88
+ let timedOut = false
89
+ const timeout = new Promise<void>((_, reject) => {
90
+ setTimeout(() => {
91
+ timedOut = true
92
+ reject(new Error("LocalSocketTransport open timed out"))
93
+ }, LOCAL_SOCKET_OPEN_TIMEOUT_MS)
94
+ })
95
+
96
+ try {
97
+ await Promise.race([local.open(), timeout])
98
+ // LocalSocket succeeded.
99
+ this.active = local
100
+ } catch {
101
+ try {
102
+ local.close()
103
+ } catch {
104
+ /* ignore */
105
+ }
106
+ const mock = new MockTransport()
107
+ await mock.open()
108
+ this.active = mock
109
+ // eslint-disable-next-line no-console
110
+ console.log(
111
+ timedOut
112
+ ? "[mentra-miniapp] No phone WebSocket reachable; using MockTransport so the page can render."
113
+ : "[mentra-miniapp] LocalSocketTransport failed; using MockTransport.",
114
+ )
115
+ }
116
+
117
+ if (this.messageHandler) this.active.onMessage(this.messageHandler)
118
+ if (this.disconnectHandler) this.active.onDisconnect(this.disconnectHandler)
119
+ }
120
+
121
+ send(raw: string): void {
122
+ if (!this.active) throw new Error("LocalSocketWithMockFallback: send() before open()")
123
+ this.active.send(raw)
124
+ }
125
+
126
+ onMessage(handler: TransportMessageHandler): void {
127
+ this.messageHandler = handler
128
+ this.active?.onMessage(handler)
129
+ }
130
+
131
+ onDisconnect(handler: TransportDisconnectHandler): void {
132
+ this.disconnectHandler = handler
133
+ this.active?.onDisconnect(handler)
134
+ }
135
+
136
+ close(): void {
137
+ this.active?.close()
138
+ }
139
+
140
+ isOpen(): boolean {
141
+ return this.active?.isOpen() === true
142
+ }
143
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * @fileoverview DispatchTransport — Transport implementation that rides
3
+ * the native `__dispatch(iface, method, argsJson)` bridge exposed by the
4
+ * MentraJS runtime (per-miniapp JSContext on iOS-JSC, Zipline/QuickJS
5
+ * context on Android).
6
+ *
7
+ * The existing `MiniappSession` (session.ts) is transport-agnostic — it
8
+ * serialises envelopes into JSON strings and hands them to Transport.send().
9
+ * The native side has historically been on the other end of a
10
+ * `window.ReactNativeWebView.postMessage(...)` shim (PostMessageTransport)
11
+ * inside a WebView. In the new architecture, background JS runs in a
12
+ * JSContext that has no WebView — so the wire is `__dispatch` instead.
13
+ *
14
+ * What this transport does:
15
+ * - send(raw) — calls __dispatch("__bridge", "send", [raw]). The host
16
+ * side `MentraJSRouter` consumes the raw string verbatim, same shape
17
+ * as PostMessageTransport's wire payload.
18
+ * - The polyfill bundle (`startup.ts`) wires __deliver({kind:'bridge',raw})
19
+ * back into this transport's message handler so the host can push
20
+ * unsolicited events (mic_pcm, transcription, button events, etc.)
21
+ * into the SDK.
22
+ * - open()/close() are no-ops — the JSContext is alive as long as the
23
+ * host has the runtime spawned; nothing to handshake here.
24
+ *
25
+ * On Android Zipline `__dispatch` is suspending under the hood; on
26
+ * iOS-JSC it's a synchronous Swift block. Both shapes are JSON-string in,
27
+ * JSON-string out, so the JS-side surface is identical.
28
+ */
29
+
30
+ import {Transport, TransportDisconnectHandler, TransportMessageHandler} from "./types"
31
+
32
+ /**
33
+ * Shape of the deliver envelope used by background code to surface
34
+ * RN-originated transport messages back to the SDK. The polyfill bundle
35
+ * fans these into __mentraDeliverBridgeRaw.
36
+ */
37
+ interface BridgeDeliverGlobal {
38
+ __mentraDeliverBridgeRaw?: (raw: string) => void
39
+ __dispatch?: (iface: string, method: string, argsJson: string) => string | null
40
+ }
41
+
42
+ export class DispatchTransport implements Transport {
43
+ private messageHandler: TransportMessageHandler | null = null
44
+ private disconnectHandler: TransportDisconnectHandler | null = null
45
+ private open_ = false
46
+
47
+ /** True iff `__dispatch` is bound on the current globalThis. */
48
+ static isAvailable(): boolean {
49
+ return typeof (globalThis as unknown as BridgeDeliverGlobal).__dispatch === "function"
50
+ }
51
+
52
+ async open(): Promise<void> {
53
+ if (!DispatchTransport.isAvailable()) {
54
+ throw new Error("DispatchTransport: __dispatch is not installed on globalThis")
55
+ }
56
+ // Wire up the inbound delivery hook so the polyfill's __deliver can
57
+ // route bridge frames here. Installed lazily so multiple transports
58
+ // can share the same global if a test spins up two.
59
+ const g = globalThis as unknown as BridgeDeliverGlobal
60
+ g.__mentraDeliverBridgeRaw = (raw: string) => {
61
+ if (this.messageHandler) this.messageHandler(raw)
62
+ }
63
+ this.open_ = true
64
+ }
65
+
66
+ send(raw: string): void {
67
+ if (!this.open_) {
68
+ throw new Error("DispatchTransport: send() before open()")
69
+ }
70
+ const g = globalThis as unknown as BridgeDeliverGlobal
71
+ if (typeof g.__dispatch !== "function") {
72
+ // Native bridge gone — surface as a transport-level disconnect so
73
+ // upstream callers don't hang on pending requests.
74
+ this.open_ = false
75
+ this.disconnectHandler?.("DispatchTransport: __dispatch disappeared")
76
+ return
77
+ }
78
+ // Wire format: stringify the args array as a single-element array
79
+ // containing the raw envelope. The router unwraps it.
80
+ try {
81
+ g.__dispatch("__bridge", "send", JSON.stringify([raw]))
82
+ } catch (e) {
83
+ this.disconnectHandler?.(`DispatchTransport: __dispatch threw: ${String(e)}`)
84
+ }
85
+ }
86
+
87
+ onMessage(handler: TransportMessageHandler): void {
88
+ this.messageHandler = handler
89
+ }
90
+
91
+ onDisconnect(handler: TransportDisconnectHandler): void {
92
+ this.disconnectHandler = handler
93
+ }
94
+
95
+ close(): void {
96
+ this.open_ = false
97
+ const g = globalThis as unknown as BridgeDeliverGlobal
98
+ if (g.__mentraDeliverBridgeRaw) {
99
+ delete g.__mentraDeliverBridgeRaw
100
+ }
101
+ }
102
+
103
+ isOpen(): boolean {
104
+ return this.open_
105
+ }
106
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @fileoverview LocalSocket transport — browser fallback.
3
+ *
4
+ * When the miniapp runs in an external browser (Safari/Chrome, not inside the
5
+ * MentraOS React Native WebView), it connects to a localhost WebSocket that
6
+ * MentraOS exposes via MiniSockets.
7
+ *
8
+ * Default endpoint: ws://127.0.0.1:8765. Override via the `url` option for
9
+ * dev / testing.
10
+ */
11
+
12
+ import {Transport, TransportDisconnectHandler, TransportMessageHandler} from "./types"
13
+
14
+ export interface LocalSocketTransportOptions {
15
+ url?: string
16
+ }
17
+
18
+ const DEFAULT_URL = "ws://127.0.0.1:8765"
19
+
20
+ export class LocalSocketTransport implements Transport {
21
+ private url: string
22
+ private ws: WebSocket | null = null
23
+ private messageHandler: TransportMessageHandler | null = null
24
+ private disconnectHandler: TransportDisconnectHandler | null = null
25
+
26
+ constructor(options: LocalSocketTransportOptions = {}) {
27
+ this.url = options.url ?? DEFAULT_URL
28
+ }
29
+
30
+ async open(): Promise<void> {
31
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) return
32
+ if (typeof WebSocket === "undefined") {
33
+ throw new Error("LocalSocketTransport: browser WebSocket global not available")
34
+ }
35
+
36
+ await new Promise<void>((resolve, reject) => {
37
+ const ws = new WebSocket(this.url)
38
+ let settled = false
39
+ const settle = (fn: () => void) => {
40
+ if (settled) return
41
+ settled = true
42
+ fn()
43
+ }
44
+
45
+ ws.onopen = () => {
46
+ this.ws = ws
47
+ settle(() => resolve())
48
+ }
49
+ ws.onerror = (ev) => {
50
+ settle(() =>
51
+ reject(new Error(`LocalSocketTransport: failed to connect to ${this.url}: ${String(ev)}`)),
52
+ )
53
+ }
54
+ ws.onmessage = (ev: MessageEvent) => {
55
+ const data = ev.data
56
+ if (typeof data === "string") this.messageHandler?.(data)
57
+ }
58
+ ws.onclose = (ev) => {
59
+ if (this.ws === ws) this.ws = null
60
+ this.disconnectHandler?.(`closed: ${ev.code} ${ev.reason}`)
61
+ }
62
+ })
63
+ }
64
+
65
+ send(raw: string): void {
66
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
67
+ throw new Error("LocalSocketTransport: not connected")
68
+ }
69
+ this.ws.send(raw)
70
+ }
71
+
72
+ onMessage(handler: TransportMessageHandler): void {
73
+ this.messageHandler = handler
74
+ }
75
+
76
+ onDisconnect(handler: TransportDisconnectHandler): void {
77
+ this.disconnectHandler = handler
78
+ }
79
+
80
+ close(): void {
81
+ if (!this.ws) return
82
+ try {
83
+ this.ws.close()
84
+ } catch {
85
+ // ignore
86
+ }
87
+ this.ws = null
88
+ }
89
+
90
+ isOpen(): boolean {
91
+ return !!this.ws && this.ws.readyState === WebSocket.OPEN
92
+ }
93
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * @fileoverview MockTransport — browser-tab fallback so the SDK doesn't hang.
3
+ *
4
+ * Activates when:
5
+ * - `window.ReactNativeWebView` is undefined (not in MentraOS WebView), AND
6
+ * - The first LocalSocketTransport connection attempt fails fast, OR
7
+ * - The author opts in via `?mentra=mock` query param / `localStorage.MENTRA_MOCK = "1"`.
8
+ *
9
+ * Behaviors:
10
+ * - On `open()`: synthesize a CONNECT_ACK envelope so `session.connect()` resolves.
11
+ * - On `send(envelope)`: parse, log to console with `[mock-transport]` prefix,
12
+ * auto-reply with synthetic results for any request that needs one.
13
+ * - Does NOT emit any glasses events. Subscribing succeeds silently.
14
+ *
15
+ * This is the Stage-1 stopgap from `agents/miniapp-quick-fixes-spec.md` #6.
16
+ * The full simulator (event injection, glasses-display preview, hardware bridge)
17
+ * is Stage 2 — see `agents/miniapp-browser-testing-simulator-spec.md`.
18
+ */
19
+
20
+ import {parseEnvelope, serializeEnvelope, type MiniappEnvelope} from "../envelope"
21
+ import {MiniappRequestType, MiniappResponseType} from "../protocol"
22
+ import type {Transport, TransportDisconnectHandler, TransportMessageHandler} from "./types"
23
+
24
+ const LOG_PREFIX = "[mock-transport]"
25
+
26
+ /**
27
+ * Returns true if the current environment requested the mock transport
28
+ * explicitly. Checks `?mentra=mock` query param and `localStorage.MENTRA_MOCK`.
29
+ */
30
+ export function isMockExplicitlyRequested(): boolean {
31
+ if (typeof window === "undefined") return false
32
+ try {
33
+ if (typeof window.location !== "undefined" && window.location.search) {
34
+ const params = new URLSearchParams(window.location.search)
35
+ if (params.get("mentra") === "mock") return true
36
+ }
37
+ } catch {
38
+ // ignore
39
+ }
40
+ try {
41
+ if (typeof localStorage !== "undefined" && localStorage.getItem("MENTRA_MOCK") === "1") {
42
+ return true
43
+ }
44
+ } catch {
45
+ // localStorage may throw in some embedded contexts; ignore
46
+ }
47
+ return false
48
+ }
49
+
50
+ export interface MockTransportOptions {
51
+ /** Override the synthetic userId. Default "mock-user". */
52
+ userId?: string
53
+ /** Override the synthetic miniapp auth token. Default "mock-miniapp-token". */
54
+ authToken?: string
55
+ /** Override the synthetic packageName when window.MentraOS isn't set. */
56
+ packageName?: string
57
+ /** Suppress the [mock-transport] console logs. Default false. */
58
+ silent?: boolean
59
+ }
60
+
61
+ export class MockTransport implements Transport {
62
+ private messageHandler: TransportMessageHandler | null = null
63
+ private disconnectHandler: TransportDisconnectHandler | null = null
64
+ private open_ = false
65
+ private readonly userId: string
66
+ private readonly authToken: string
67
+ private readonly packageName: string | null
68
+ private readonly silent: boolean
69
+
70
+ constructor(options: MockTransportOptions = {}) {
71
+ this.userId = options.userId ?? "mock-user"
72
+ this.authToken = options.authToken ?? "mock-miniapp-token"
73
+ this.packageName = options.packageName ?? null
74
+ this.silent = options.silent === true
75
+ }
76
+
77
+ async open(): Promise<void> {
78
+ if (this.open_) return
79
+ this.open_ = true
80
+ this.log("transport opened (no real host; synthetic responses only)")
81
+ }
82
+
83
+ send(raw: string): void {
84
+ if (!this.open_) {
85
+ throw new Error("MockTransport: send() before open()")
86
+ }
87
+ const envelope = parseEnvelope(raw)
88
+ if (!envelope) {
89
+ this.log("dropped unparseable envelope:", raw.slice(0, 200))
90
+ return
91
+ }
92
+ const payload = envelope.payload as {type?: string} & Record<string, unknown>
93
+ const type = payload?.type
94
+ this.log(`recv ${type ?? "<unknown>"}${envelope.requestId ? ` (rid=${envelope.requestId})` : ""}`)
95
+
96
+ // Handle the protocol's synchronous handshake/responses.
97
+ switch (type) {
98
+ case MiniappRequestType.CONNECT:
99
+ this.deliverConnectAck(payload)
100
+ return
101
+
102
+ case MiniappRequestType.PING:
103
+ // Real phone sends PING. We never do — but if app code somehow does,
104
+ // ignore it. The SDK auto-replies to inbound PINGs, not vice versa.
105
+ return
106
+
107
+ case MiniappRequestType.SUBSCRIBE:
108
+ // Subscribe is fire-and-forget; no response expected. Stage 1 emits no
109
+ // events. Future Stage 2 simulator can synthesize events here.
110
+ return
111
+
112
+ default:
113
+ // Anything that has a requestId expects a REQUEST_RESULT. Reply with a
114
+ // synthetic empty success so app code that awaits the promise resolves.
115
+ if (envelope.requestId) {
116
+ this.deliverSyntheticResult(envelope.requestId, type ?? "<unknown>", payload)
117
+ }
118
+ return
119
+ }
120
+ }
121
+
122
+ onMessage(handler: TransportMessageHandler): void {
123
+ this.messageHandler = handler
124
+ }
125
+
126
+ onDisconnect(handler: TransportDisconnectHandler): void {
127
+ this.disconnectHandler = handler
128
+ }
129
+
130
+ close(): void {
131
+ if (!this.open_) return
132
+ this.open_ = false
133
+ this.disconnectHandler?.("MockTransport.close()")
134
+ }
135
+
136
+ isOpen(): boolean {
137
+ return this.open_
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+
142
+ private deliverConnectAck(connectPayload: Record<string, unknown>): void {
143
+ const incomingPackage = (connectPayload.packageName as string | undefined) ?? this.packageName ?? "com.mock.app"
144
+ const ackPayload = {
145
+ type: MiniappResponseType.CONNECT_ACK,
146
+ userId: this.userId,
147
+ packageName: incomingPackage,
148
+ capabilities: null,
149
+ visibility: "foreground",
150
+ colorScheme: "light",
151
+ auth: {
152
+ mentraUserId: this.userId,
153
+ oemId: "mock",
154
+ token: this.authToken,
155
+ expiresAt: Date.now() + 60 * 60 * 1000,
156
+ },
157
+ }
158
+ const envelope: MiniappEnvelope = {payload: ackPayload}
159
+ this.log(`-> CONNECT_ACK userId=${this.userId} pkg=${incomingPackage}`)
160
+ // Deliver asynchronously so it doesn't fire during the same tick as send().
161
+ queueMicrotask(() => this.messageHandler?.(serializeEnvelope(envelope)))
162
+ }
163
+
164
+ private deliverSyntheticResult(
165
+ requestId: string,
166
+ requestType: string,
167
+ requestPayload: Record<string, unknown>,
168
+ ): void {
169
+ const data = syntheticDataFor(requestId, requestType, requestPayload)
170
+ const responsePayload = {
171
+ type: MiniappResponseType.REQUEST_RESULT,
172
+ ok: true,
173
+ data,
174
+ }
175
+ const envelope: MiniappEnvelope = {payload: responsePayload, requestId}
176
+ this.log(`-> REQUEST_RESULT (rid=${requestId}) synthetic ${requestType}`)
177
+ queueMicrotask(() => this.messageHandler?.(serializeEnvelope(envelope)))
178
+ }
179
+
180
+ private log(...args: unknown[]): void {
181
+ if (this.silent) return
182
+
183
+ console.log(LOG_PREFIX, ...args)
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Synthetic payload for an unrecognized request that nonetheless awaits a
189
+ * REQUEST_RESULT. Returns enough shape to satisfy callers without crashing.
190
+ */
191
+ function syntheticDataFor(requestId: string, requestType: string, requestPayload: Record<string, unknown>): unknown {
192
+ switch (requestType) {
193
+ case MiniappRequestType.CAMERA_FOV: {
194
+ const presetFov =
195
+ requestPayload.preset === "narrow"
196
+ ? 82
197
+ : requestPayload.preset === "wide"
198
+ ? 118
199
+ : requestPayload.preset === "standard"
200
+ ? 102
201
+ : undefined
202
+ const fov = typeof requestPayload.fov === "number" ? requestPayload.fov : (presetFov ?? 102)
203
+ const roi = requestPayload.roiPosition
204
+ const roiPosition = roi === "bottom" ? "bottom" : roi === "top" ? "top" : "center"
205
+ return {
206
+ requestId,
207
+ fov,
208
+ roiPosition,
209
+ timestamp: Date.now(),
210
+ }
211
+ }
212
+
213
+ case MiniappRequestType.PHOTO:
214
+ // 1×1 transparent PNG so consumers that try to render don't 404.
215
+ return {
216
+ photoUrl:
217
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
218
+ requestId: "mock-photo",
219
+ }
220
+
221
+ case MiniappRequestType.RGB_LED:
222
+ return {type: "rgb_led_control_response", state: "success", requestId}
223
+
224
+ case MiniappRequestType.LOCATION_POLL:
225
+ // San Francisco fallback — using 0,0 puts dev sessions in the
226
+ // Atlantic, which makes maps/navigation unusable in the WebView.
227
+ return {lat: 37.7956, lng: -122.3933, accuracy: 0, timestamp: Date.now()}
228
+
229
+ case MiniappRequestType.STORAGE_GET:
230
+ return {value: null}
231
+
232
+ case MiniappRequestType.STORAGE_LIST:
233
+ return {keys: []}
234
+
235
+ case MiniappRequestType.SPEAK:
236
+ return {audioUrl: null, durationMs: 0}
237
+
238
+ case MiniappRequestType.SHARE:
239
+ case MiniappRequestType.OPEN_URL:
240
+ case MiniappRequestType.COPY_CLIPBOARD:
241
+ case MiniappRequestType.DOWNLOAD:
242
+ case MiniappRequestType.REQUEST_WIFI_SETUP:
243
+ return {ok: true}
244
+
245
+ default:
246
+ return null
247
+ }
248
+ }