@glassly/miniapp 0.3.0-dev.1

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 (312) hide show
  1. package/README.md +275 -0
  2. package/dist/background/index.d.ts +64 -0
  3. package/dist/background/index.d.ts.map +1 -0
  4. package/dist/background/index.js +26 -0
  5. package/dist/background/index.js.map +1 -0
  6. package/dist/background/register.d.ts +39 -0
  7. package/dist/background/register.d.ts.map +1 -0
  8. package/dist/background/register.js +72 -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 +60 -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/hardware.d.ts +38 -0
  23. package/dist/hardware.d.ts.map +1 -0
  24. package/dist/hardware.js +31 -0
  25. package/dist/hardware.js.map +1 -0
  26. package/dist/index.d.ts +65 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +34 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/modules/actions.d.ts +81 -0
  31. package/dist/modules/actions.d.ts.map +1 -0
  32. package/dist/modules/actions.js +137 -0
  33. package/dist/modules/actions.js.map +1 -0
  34. package/dist/modules/ai.d.ts +211 -0
  35. package/dist/modules/ai.d.ts.map +1 -0
  36. package/dist/modules/ai.js +117 -0
  37. package/dist/modules/ai.js.map +1 -0
  38. package/dist/modules/auth.d.ts +25 -0
  39. package/dist/modules/auth.d.ts.map +1 -0
  40. package/dist/modules/auth.js +52 -0
  41. package/dist/modules/auth.js.map +1 -0
  42. package/dist/modules/base64.d.ts +16 -0
  43. package/dist/modules/base64.d.ts.map +1 -0
  44. package/dist/modules/base64.js +92 -0
  45. package/dist/modules/base64.js.map +1 -0
  46. package/dist/modules/blob.d.ts +161 -0
  47. package/dist/modules/blob.d.ts.map +1 -0
  48. package/dist/modules/blob.js +277 -0
  49. package/dist/modules/blob.js.map +1 -0
  50. package/dist/modules/camera.d.ts +183 -0
  51. package/dist/modules/camera.d.ts.map +1 -0
  52. package/dist/modules/camera.js +119 -0
  53. package/dist/modules/camera.js.map +1 -0
  54. package/dist/modules/cloud.d.ts +34 -0
  55. package/dist/modules/cloud.d.ts.map +1 -0
  56. package/dist/modules/cloud.js +70 -0
  57. package/dist/modules/cloud.js.map +1 -0
  58. package/dist/modules/dashboard.d.ts +24 -0
  59. package/dist/modules/dashboard.d.ts.map +1 -0
  60. package/dist/modules/dashboard.js +39 -0
  61. package/dist/modules/dashboard.js.map +1 -0
  62. package/dist/modules/display.d.ts +167 -0
  63. package/dist/modules/display.d.ts.map +1 -0
  64. package/dist/modules/display.js +83 -0
  65. package/dist/modules/display.js.map +1 -0
  66. package/dist/modules/events.d.ts +178 -0
  67. package/dist/modules/events.d.ts.map +1 -0
  68. package/dist/modules/events.js +168 -0
  69. package/dist/modules/events.js.map +1 -0
  70. package/dist/modules/glasses.d.ts +36 -0
  71. package/dist/modules/glasses.d.ts.map +1 -0
  72. package/dist/modules/glasses.js +52 -0
  73. package/dist/modules/glasses.js.map +1 -0
  74. package/dist/modules/heading.d.ts +16 -0
  75. package/dist/modules/heading.d.ts.map +1 -0
  76. package/dist/modules/heading.js +20 -0
  77. package/dist/modules/heading.js.map +1 -0
  78. package/dist/modules/imu.d.ts +31 -0
  79. package/dist/modules/imu.d.ts.map +1 -0
  80. package/dist/modules/imu.js +40 -0
  81. package/dist/modules/imu.js.map +1 -0
  82. package/dist/modules/input.d.ts +34 -0
  83. package/dist/modules/input.d.ts.map +1 -0
  84. package/dist/modules/input.js +50 -0
  85. package/dist/modules/input.js.map +1 -0
  86. package/dist/modules/keys.d.ts +30 -0
  87. package/dist/modules/keys.d.ts.map +1 -0
  88. package/dist/modules/keys.js +37 -0
  89. package/dist/modules/keys.js.map +1 -0
  90. package/dist/modules/languages.d.ts +42 -0
  91. package/dist/modules/languages.d.ts.map +1 -0
  92. package/dist/modules/languages.js +57 -0
  93. package/dist/modules/languages.js.map +1 -0
  94. package/dist/modules/led.d.ts +35 -0
  95. package/dist/modules/led.d.ts.map +1 -0
  96. package/dist/modules/led.js +39 -0
  97. package/dist/modules/led.js.map +1 -0
  98. package/dist/modules/location.d.ts +27 -0
  99. package/dist/modules/location.d.ts.map +1 -0
  100. package/dist/modules/location.js +35 -0
  101. package/dist/modules/location.js.map +1 -0
  102. package/dist/modules/memory.d.ts +126 -0
  103. package/dist/modules/memory.d.ts.map +1 -0
  104. package/dist/modules/memory.js +141 -0
  105. package/dist/modules/memory.js.map +1 -0
  106. package/dist/modules/mic.d.ts +61 -0
  107. package/dist/modules/mic.d.ts.map +1 -0
  108. package/dist/modules/mic.js +94 -0
  109. package/dist/modules/mic.js.map +1 -0
  110. package/dist/modules/miniapps.d.ts +82 -0
  111. package/dist/modules/miniapps.d.ts.map +1 -0
  112. package/dist/modules/miniapps.js +58 -0
  113. package/dist/modules/miniapps.js.map +1 -0
  114. package/dist/modules/navigation.d.ts +332 -0
  115. package/dist/modules/navigation.d.ts.map +1 -0
  116. package/dist/modules/navigation.js +387 -0
  117. package/dist/modules/navigation.js.map +1 -0
  118. package/dist/modules/permissions.d.ts +58 -0
  119. package/dist/modules/permissions.d.ts.map +1 -0
  120. package/dist/modules/permissions.js +66 -0
  121. package/dist/modules/permissions.js.map +1 -0
  122. package/dist/modules/phone.d.ts +95 -0
  123. package/dist/modules/phone.d.ts.map +1 -0
  124. package/dist/modules/phone.js +110 -0
  125. package/dist/modules/phone.js.map +1 -0
  126. package/dist/modules/pivots/engine.d.ts +169 -0
  127. package/dist/modules/pivots/engine.d.ts.map +1 -0
  128. package/dist/modules/pivots/engine.js +778 -0
  129. package/dist/modules/pivots/engine.js.map +1 -0
  130. package/dist/modules/pivots/geometry.d.ts +84 -0
  131. package/dist/modules/pivots/geometry.d.ts.map +1 -0
  132. package/dist/modules/pivots/geometry.js +371 -0
  133. package/dist/modules/pivots/geometry.js.map +1 -0
  134. package/dist/modules/pivots/instructions.d.ts +139 -0
  135. package/dist/modules/pivots/instructions.d.ts.map +1 -0
  136. package/dist/modules/pivots/instructions.js +281 -0
  137. package/dist/modules/pivots/instructions.js.map +1 -0
  138. package/dist/modules/speaker.d.ts +175 -0
  139. package/dist/modules/speaker.d.ts.map +1 -0
  140. package/dist/modules/speaker.js +249 -0
  141. package/dist/modules/speaker.js.map +1 -0
  142. package/dist/modules/speakers.d.ts +29 -0
  143. package/dist/modules/speakers.d.ts.map +1 -0
  144. package/dist/modules/speakers.js +38 -0
  145. package/dist/modules/speakers.js.map +1 -0
  146. package/dist/modules/storage.d.ts +50 -0
  147. package/dist/modules/storage.d.ts.map +1 -0
  148. package/dist/modules/storage.js +99 -0
  149. package/dist/modules/storage.js.map +1 -0
  150. package/dist/modules/stream.d.ts +133 -0
  151. package/dist/modules/stream.d.ts.map +1 -0
  152. package/dist/modules/stream.js +53 -0
  153. package/dist/modules/stream.js.map +1 -0
  154. package/dist/modules/system.d.ts +71 -0
  155. package/dist/modules/system.d.ts.map +1 -0
  156. package/dist/modules/system.js +65 -0
  157. package/dist/modules/system.js.map +1 -0
  158. package/dist/modules/transcription.d.ts +99 -0
  159. package/dist/modules/transcription.d.ts.map +1 -0
  160. package/dist/modules/transcription.js +126 -0
  161. package/dist/modules/transcription.js.map +1 -0
  162. package/dist/modules/translation.d.ts +65 -0
  163. package/dist/modules/translation.d.ts.map +1 -0
  164. package/dist/modules/translation.js +121 -0
  165. package/dist/modules/translation.js.map +1 -0
  166. package/dist/modules/ui.d.ts +186 -0
  167. package/dist/modules/ui.d.ts.map +1 -0
  168. package/dist/modules/ui.js +289 -0
  169. package/dist/modules/ui.js.map +1 -0
  170. package/dist/protocol.d.ts +384 -0
  171. package/dist/protocol.d.ts.map +1 -0
  172. package/dist/protocol.js +413 -0
  173. package/dist/protocol.js.map +1 -0
  174. package/dist/react/MiniappHeader.d.ts +44 -0
  175. package/dist/react/MiniappHeader.d.ts.map +1 -0
  176. package/dist/react/MiniappHeader.js +32 -0
  177. package/dist/react/MiniappHeader.js.map +1 -0
  178. package/dist/react/MiniappProvider.d.ts +33 -0
  179. package/dist/react/MiniappProvider.d.ts.map +1 -0
  180. package/dist/react/MiniappProvider.js +15 -0
  181. package/dist/react/MiniappProvider.js.map +1 -0
  182. package/dist/react/capsuleHeaderLayout.d.ts +3 -0
  183. package/dist/react/capsuleHeaderLayout.d.ts.map +1 -0
  184. package/dist/react/capsuleHeaderLayout.js +5 -0
  185. package/dist/react/capsuleHeaderLayout.js.map +1 -0
  186. package/dist/react/index.d.ts +13 -0
  187. package/dist/react/index.d.ts.map +1 -0
  188. package/dist/react/index.js +9 -0
  189. package/dist/react/index.js.map +1 -0
  190. package/dist/react/useCapabilities.d.ts +26 -0
  191. package/dist/react/useCapabilities.d.ts.map +1 -0
  192. package/dist/react/useCapabilities.js +27 -0
  193. package/dist/react/useCapabilities.js.map +1 -0
  194. package/dist/react/useCapsuleHeaderStyle.d.ts +27 -0
  195. package/dist/react/useCapsuleHeaderStyle.d.ts.map +1 -0
  196. package/dist/react/useCapsuleHeaderStyle.js +39 -0
  197. package/dist/react/useCapsuleHeaderStyle.js.map +1 -0
  198. package/dist/react/useColorScheme.d.ts +18 -0
  199. package/dist/react/useColorScheme.d.ts.map +1 -0
  200. package/dist/react/useColorScheme.js +24 -0
  201. package/dist/react/useColorScheme.js.map +1 -0
  202. package/dist/react/useConnected.d.ts +17 -0
  203. package/dist/react/useConnected.d.ts.map +1 -0
  204. package/dist/react/useConnected.js +39 -0
  205. package/dist/react/useConnected.js.map +1 -0
  206. package/dist/react/useRpc.d.ts +29 -0
  207. package/dist/react/useRpc.d.ts.map +1 -0
  208. package/dist/react/useRpc.js +82 -0
  209. package/dist/react/useRpc.js.map +1 -0
  210. package/dist/react/useSafeArea.d.ts +21 -0
  211. package/dist/react/useSafeArea.d.ts.map +1 -0
  212. package/dist/react/useSafeArea.js +22 -0
  213. package/dist/react/useSafeArea.js.map +1 -0
  214. package/dist/session.d.ts +355 -0
  215. package/dist/session.d.ts.map +1 -0
  216. package/dist/session.js +688 -0
  217. package/dist/session.js.map +1 -0
  218. package/dist/sub-path-types.test-d.d.ts +40 -0
  219. package/dist/sub-path-types.test-d.d.ts.map +1 -0
  220. package/dist/sub-path-types.test-d.js +37 -0
  221. package/dist/sub-path-types.test-d.js.map +1 -0
  222. package/dist/transport/auto.d.ts +35 -0
  223. package/dist/transport/auto.d.ts.map +1 -0
  224. package/dist/transport/auto.js +124 -0
  225. package/dist/transport/auto.js.map +1 -0
  226. package/dist/transport/dispatch.d.ts +43 -0
  227. package/dist/transport/dispatch.d.ts.map +1 -0
  228. package/dist/transport/dispatch.js +91 -0
  229. package/dist/transport/dispatch.js.map +1 -0
  230. package/dist/transport/local-socket.d.ts +28 -0
  231. package/dist/transport/local-socket.d.ts.map +1 -0
  232. package/dist/transport/local-socket.js +80 -0
  233. package/dist/transport/local-socket.js.map +1 -0
  234. package/dist/transport/mock.d.ts +54 -0
  235. package/dist/transport/mock.d.ts.map +1 -0
  236. package/dist/transport/mock.js +210 -0
  237. package/dist/transport/mock.js.map +1 -0
  238. package/dist/transport/postmessage.d.ts +36 -0
  239. package/dist/transport/postmessage.d.ts.map +1 -0
  240. package/dist/transport/postmessage.js +78 -0
  241. package/dist/transport/postmessage.js.map +1 -0
  242. package/dist/transport/types.d.ts +25 -0
  243. package/dist/transport/types.d.ts.map +1 -0
  244. package/dist/transport/types.js +11 -0
  245. package/dist/transport/types.js.map +1 -0
  246. package/dist/ui/index.d.ts +107 -0
  247. package/dist/ui/index.d.ts.map +1 -0
  248. package/dist/ui/index.js +32 -0
  249. package/dist/ui/index.js.map +1 -0
  250. package/package.json +84 -0
  251. package/src/background/index.ts +177 -0
  252. package/src/background/register.ts +89 -0
  253. package/src/dev-reload.ts +59 -0
  254. package/src/envelope.ts +54 -0
  255. package/src/globals.ts +62 -0
  256. package/src/hardware.ts +40 -0
  257. package/src/index.ts +216 -0
  258. package/src/modules/actions.ts +189 -0
  259. package/src/modules/ai.ts +281 -0
  260. package/src/modules/auth.ts +68 -0
  261. package/src/modules/base64.ts +87 -0
  262. package/src/modules/blob.ts +341 -0
  263. package/src/modules/camera.ts +258 -0
  264. package/src/modules/cloud.ts +94 -0
  265. package/src/modules/dashboard.ts +51 -0
  266. package/src/modules/display.ts +207 -0
  267. package/src/modules/events.ts +318 -0
  268. package/src/modules/glasses.ts +58 -0
  269. package/src/modules/heading.ts +23 -0
  270. package/src/modules/imu.ts +44 -0
  271. package/src/modules/input.ts +66 -0
  272. package/src/modules/keys.ts +46 -0
  273. package/src/modules/languages.ts +72 -0
  274. package/src/modules/led.ts +61 -0
  275. package/src/modules/location.ts +39 -0
  276. package/src/modules/memory.ts +224 -0
  277. package/src/modules/mic.ts +103 -0
  278. package/src/modules/miniapps.ts +103 -0
  279. package/src/modules/navigation.ts +674 -0
  280. package/src/modules/permissions.ts +83 -0
  281. package/src/modules/phone.ts +155 -0
  282. package/src/modules/pivots/engine.ts +863 -0
  283. package/src/modules/pivots/geometry.ts +409 -0
  284. package/src/modules/pivots/instructions.ts +341 -0
  285. package/src/modules/speaker.ts +347 -0
  286. package/src/modules/speakers.ts +40 -0
  287. package/src/modules/storage.ts +109 -0
  288. package/src/modules/stream.ts +168 -0
  289. package/src/modules/system.ts +113 -0
  290. package/src/modules/transcription.ts +180 -0
  291. package/src/modules/translation.ts +156 -0
  292. package/src/modules/ui.ts +481 -0
  293. package/src/protocol.ts +475 -0
  294. package/src/react/MiniappHeader.tsx +114 -0
  295. package/src/react/MiniappProvider.tsx +50 -0
  296. package/src/react/capsuleHeaderLayout.ts +11 -0
  297. package/src/react/index.ts +12 -0
  298. package/src/react/useCapabilities.ts +29 -0
  299. package/src/react/useCapsuleHeaderStyle.ts +55 -0
  300. package/src/react/useColorScheme.ts +26 -0
  301. package/src/react/useConnected.ts +47 -0
  302. package/src/react/useRpc.ts +103 -0
  303. package/src/react/useSafeArea.ts +36 -0
  304. package/src/session.ts +969 -0
  305. package/src/sub-path-types.test-d.ts +60 -0
  306. package/src/transport/auto.ts +143 -0
  307. package/src/transport/dispatch.ts +106 -0
  308. package/src/transport/local-socket.ts +91 -0
  309. package/src/transport/mock.ts +255 -0
  310. package/src/transport/postmessage.ts +95 -0
  311. package/src/transport/types.ts +26 -0
  312. package/src/ui/index.ts +129 -0
@@ -0,0 +1,156 @@
1
+ /**
2
+ * @fileoverview TranslationModule — top-level translation API.
3
+ *
4
+ * Mirrors cloud SDK v3's TranslationManager surface:
5
+ *
6
+ * session.translation.on(handler) // any pair
7
+ * session.translation.to(target, handler) // any-source → target
8
+ * session.translation.to([targets], handler) // any-source → many
9
+ * session.translation.fromTo(source, target, handler) // specific pair
10
+ * session.translation.fromTo(source, [targets], handler) // source → many
11
+ * session.translation.stop() // tear down all
12
+ *
13
+ * Internally each subscribe registers against a `translation:<source>:<target>`
14
+ * stream pattern; LocalMiniappRuntime's wildcard matcher routes incoming
15
+ * `translation:<src>:<dst>` events to any matching `*` patterns.
16
+ *
17
+ * MICROPHONE permission required.
18
+ */
19
+
20
+ import {MiniappStreamType} from "../protocol"
21
+ import {MiniappSession} from "../session"
22
+ import type {TranslationData, UnsubscribeFn} from "./events"
23
+ import {requireTranscriptionLanguage} from "./languages"
24
+ import type {TranscriptionLanguage} from "./languages"
25
+
26
+ export type TranslationHandler = (data: TranslationData) => void
27
+
28
+ /**
29
+ * Translation source: a registry language, or "auto" to let the cloud detect
30
+ * the spoken language.
31
+ */
32
+ export type TranslationSource = TranscriptionLanguage | "auto"
33
+
34
+ export class TranslationModule {
35
+ private readonly unsubs = new Set<UnsubscribeFn>()
36
+
37
+ constructor(private readonly session: MiniappSession) {}
38
+
39
+ /**
40
+ * Subscribe to every translation event from the cloud, regardless of
41
+ * source or target language.
42
+ *
43
+ * Uses the wildcard stream `translation:*:*` — cheap to register on the
44
+ * client, expensive on the cloud side (cloud fans every active pair out
45
+ * to this subscriber). Prefer `to` or `fromTo` when you know the
46
+ * language(s) you care about.
47
+ */
48
+ on(handler: TranslationHandler): UnsubscribeFn {
49
+ return this.track(
50
+ this.session._subscribe(`${MiniappStreamType.TRANSLATION}:*:*`, handler as (data: unknown) => void),
51
+ )
52
+ }
53
+
54
+ /**
55
+ * Subscribe to any translation that lands at the given target language(s),
56
+ * regardless of source. Useful for "I'm a Spanish speaker, translate
57
+ * whatever I hear to Spanish."
58
+ */
59
+ to(target: TranscriptionLanguage | TranscriptionLanguage[], handler: TranslationHandler): UnsubscribeFn {
60
+ const targets = (Array.isArray(target) ? target : [target]).map((t) =>
61
+ requireTranscriptionLanguage(t, "translation.to(target)"),
62
+ )
63
+ if (targets.length === 0) return () => {}
64
+ const unsubs: UnsubscribeFn[] = []
65
+ for (const t of targets) {
66
+ unsubs.push(
67
+ this.session._subscribe(`${MiniappStreamType.TRANSLATION}:*:${t}`, handler as (data: unknown) => void),
68
+ )
69
+ }
70
+ const composite = () => {
71
+ for (const u of unsubs) {
72
+ try {
73
+ u()
74
+ } catch {
75
+ /* ignore */
76
+ }
77
+ }
78
+ }
79
+ return this.track(composite)
80
+ }
81
+
82
+ /**
83
+ * Subscribe to a specific source → target translation pair. Pass an
84
+ * array for `target` to fan a single handler across multiple targets
85
+ * from the same source.
86
+ */
87
+ fromTo(
88
+ source: TranslationSource,
89
+ target: TranscriptionLanguage | TranscriptionLanguage[],
90
+ handler: TranslationHandler,
91
+ ): UnsubscribeFn {
92
+ const validSource = source === "auto" ? "auto" : requireTranscriptionLanguage(source, "translation.fromTo(source)")
93
+ const targets = (Array.isArray(target) ? target : [target]).map((t) =>
94
+ requireTranscriptionLanguage(t, "translation.fromTo(target)"),
95
+ )
96
+ if (targets.length === 0) return () => {}
97
+ const unsubs: UnsubscribeFn[] = []
98
+ for (const t of targets) {
99
+ unsubs.push(
100
+ this.session._subscribe(
101
+ `${MiniappStreamType.TRANSLATION}:${validSource}:${t}`,
102
+ handler as (data: unknown) => void,
103
+ ),
104
+ )
105
+ }
106
+ const composite = () => {
107
+ for (const u of unsubs) {
108
+ try {
109
+ u()
110
+ } catch {
111
+ /* ignore */
112
+ }
113
+ }
114
+ }
115
+ return this.track(composite)
116
+ }
117
+
118
+ /**
119
+ * @deprecated Renamed to `fromTo(source, target, handler)` for cloud
120
+ * SDK v3 parity. This alias will be removed in a future release.
121
+ */
122
+ forLanguagePair(
123
+ fromLang: TranslationSource,
124
+ toLang: TranscriptionLanguage,
125
+ handler: TranslationHandler,
126
+ ): UnsubscribeFn {
127
+ return this.fromTo(fromLang, toLang, handler)
128
+ }
129
+
130
+ /** Tear down every translation subscription this module owns. */
131
+ stop(): void {
132
+ for (const u of this.unsubs) {
133
+ try {
134
+ u()
135
+ } catch {
136
+ /* ignore */
137
+ }
138
+ }
139
+ this.unsubs.clear()
140
+ }
141
+
142
+ /** True iff `MICROPHONE` is declared in the miniapp's manifest. */
143
+ get hasPermission(): boolean {
144
+ return this.session._hasManifestPermission("MICROPHONE")
145
+ }
146
+
147
+ // ------------------------------------------------------------------------
148
+
149
+ private track(unsub: UnsubscribeFn): UnsubscribeFn {
150
+ this.unsubs.add(unsub)
151
+ return () => {
152
+ this.unsubs.delete(unsub)
153
+ unsub()
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,481 @@
1
+ /**
2
+ * session.ui — bus between a background JSContext miniapp and its
3
+ * on-demand UI WebView. Supports two interaction patterns:
4
+ *
5
+ * 1. **Broadcast** (fire-and-forget, either direction)
6
+ * - background → UI: `session.ui.send(channel, payload)`
7
+ * - UI → background: `glassly.send(channel, payload)`
8
+ * - subscribe: `session.ui.on(channel, cb)` / `glassly.on(channel, cb)`
9
+ *
10
+ * 2. **RPC** (request/response, UI → background only)
11
+ * - UI side: `await glassly.request(channel, payload, options?)`
12
+ * - background side: `session.ui.handle(channel, (payload, ctx?) => result)`
13
+ * - single handler per channel; double-register throws synchronously.
14
+ * - errors thrown in the handler reject the caller's promise.
15
+ * - cancellation via `options.signal` aborts the handler's `ctx.signal`
16
+ * and drops the eventual reply.
17
+ *
18
+ * Broadcast vs. RPC is declared at the channel level: wrap a channel's
19
+ * payload type in `Rpc<Req, Res>` in the shared registry to make it RPC.
20
+ * Wrong-API-for-channel is a compile-time error.
21
+ *
22
+ * Buffering:
23
+ * - `glassly.send` BUFFERS until `glassly.ready()` acks. The WebView is
24
+ * the short-lived side and shouldn't drop user input.
25
+ * - `session.ui.send` silently DROPS when no WebView is bound.
26
+ * Background is the source of truth; UI state shouldn't accumulate.
27
+ * - Per-channel inbound buffering (up to 32 payloads) covers the
28
+ * `controller pushed before React attached the listener` race — see
29
+ * the WebView shim for details.
30
+ *
31
+ * Wire envelopes (internal — not part of the SDK surface):
32
+ * - `UI_OPEN` — WebView posted `{type:"ready"}`.
33
+ * - `UI_CLOSE` — host tore down the WebView.
34
+ * - `UI_MESSAGE` — WebView → background. `requestId` set on RPC calls.
35
+ * - `UI_SEND` — background → WebView. `requestId` set on RPC replies.
36
+ * - `UI_CANCEL` — either direction. Carries only `requestId`; aborts
37
+ * the in-flight handler's signal.
38
+ */
39
+ import type {MiniappSession} from "../session"
40
+
41
+ export type UIChannelHandler<T = unknown> = (payload: T) => void
42
+ export type UIUnsubscribe = () => void
43
+
44
+ /**
45
+ * Brand for declaring an RPC channel in the shared Channels registry.
46
+ *
47
+ * Wrap a channel's payload type in `Rpc<Req, Res>` to mark it as
48
+ * request/response. The SDK's `glassly.request` / `session.ui.handle`
49
+ * accept only `Rpc<...>` channels; `glassly.send` / `session.ui.on`
50
+ * accept only non-RPC channels. Using the wrong API for the wrong
51
+ * channel is a compile-time error.
52
+ *
53
+ * export interface Channels {
54
+ * "live-transcript": {text: string} // broadcast
55
+ * "compute-route": Rpc<RouteOpts, RouteResult> // RPC
56
+ * }
57
+ */
58
+ declare const __rpc_brand: unique symbol
59
+ export type Rpc<Req, Res> = {readonly [__rpc_brand]: true; readonly req: Req; readonly res: Res}
60
+
61
+ /** True if `T` is an `Rpc<...>` channel entry. */
62
+ export type IsRpc<T> = T extends Rpc<unknown, unknown> ? true : false
63
+ /** Request payload type of an `Rpc<Req, Res>` entry. */
64
+ export type RpcReq<T> = T extends Rpc<infer Req, unknown> ? Req : never
65
+ /** Response payload type of an `Rpc<Req, Res>` entry. */
66
+ export type RpcRes<T> = T extends Rpc<unknown, infer Res> ? Res : never
67
+
68
+ /** Options accepted by `glassly.request`. */
69
+ export interface RpcRequestOptions {
70
+ /** Abort the in-flight call. Sends UI_CANCEL to the handler. */
71
+ signal?: AbortSignal
72
+ /** Reject with `RpcTimeoutError` after this many ms. No default. */
73
+ timeout?: number
74
+ }
75
+
76
+ /** Context passed as the optional 2nd arg to an `ui.handle` handler. */
77
+ export interface RpcHandlerContext {
78
+ /** Aborts when the UI side cancels the call (or its timeout fires). */
79
+ signal: AbortSignal
80
+ }
81
+
82
+ /**
83
+ * Error thrown by `glassly.request` when the handler threw or returned an
84
+ * error envelope. Plain `Error` subclass — distinguished by `err.name`.
85
+ * `err.cause` is `{code?: string}` if the handler attached one.
86
+ */
87
+ export class RpcError extends Error {
88
+ constructor(message: string, options?: {cause?: {code?: string}}) {
89
+ super(message)
90
+ this.name = "RpcError"
91
+ // Assign `cause` directly: the package's tsconfig targets ES2020 lib
92
+ // where `Error`'s ctor is typed as 1-arity (no `ErrorOptions`).
93
+ // Modern JS engines still allow setting `cause` as a plain property.
94
+ if (options?.cause !== undefined) {
95
+ ;(this as Error & {cause?: unknown}).cause = options.cause
96
+ }
97
+ }
98
+ }
99
+
100
+ /** Thrown by `glassly.request` when its `{timeout}` elapses. */
101
+ export class RpcTimeoutError extends Error {
102
+ constructor(message = "RPC timed out") {
103
+ super(message)
104
+ this.name = "RpcTimeoutError"
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Public surface mirrored on `session.ui`. Generic over a `Channels`
110
+ * type-map so miniapps importing the typed `shared/channels.ts` get
111
+ * compile-time enforcement on channel names + payload shapes.
112
+ *
113
+ * Broadcast vs. RPC channels are distinguished at the type level:
114
+ * - Channel value `Rpc<Req, Res>` → only `handle()` accepts it on
115
+ * background; only `glassly.request(...)` accepts it on UI.
116
+ * - Channel value anything else → only `send()`/`on()` accept it
117
+ * on both sides.
118
+ *
119
+ * The default `Record<string, unknown>` mapping lets unannotated usage
120
+ * compile — the SDK doesn't impose a registry of its own.
121
+ */
122
+ export interface UIModule<TChannels extends object = Record<string, unknown>> {
123
+ /** True iff a WebView is currently bound to this miniapp. */
124
+ isOpen(): boolean
125
+
126
+ /**
127
+ * Subscribe to the "WebView mounted + ready()" lifecycle event. If
128
+ * a WebView is already mounted when subscribe() is called, the
129
+ * handler fires immediately for the current binding.
130
+ */
131
+ onOpen(cb: () => void): UIUnsubscribe
132
+
133
+ /**
134
+ * Subscribe to the "WebView unmounted" lifecycle event. Fires once
135
+ * per close; if no WebView is bound at subscribe time the handler
136
+ * stays armed for the next mount → close cycle.
137
+ */
138
+ onClose(cb: () => void): UIUnsubscribe
139
+
140
+ /**
141
+ * Broadcast a typed message to the bound WebView. Silently drops if
142
+ * no WebView is bound. Compile-error if `C` is an RPC channel.
143
+ */
144
+ send<C extends keyof TChannels & string>(
145
+ channel: IsRpc<TChannels[C]> extends true ? never : C,
146
+ payload: TChannels[C],
147
+ ): void
148
+
149
+ /**
150
+ * Subscribe to broadcast messages from the bound WebView. Returns an
151
+ * unsubscribe fn. Compile-error if `C` is an RPC channel — use
152
+ * `handle()` for RPC channels.
153
+ */
154
+ on<C extends keyof TChannels & string>(
155
+ channel: IsRpc<TChannels[C]> extends true ? never : C,
156
+ cb: UIChannelHandler<TChannels[C]>,
157
+ ): UIUnsubscribe
158
+
159
+ /**
160
+ * Register the single handler for an RPC channel. The UI side calls
161
+ * `glassly.request(channel, payload, options?)`; this handler resolves
162
+ * the call.
163
+ *
164
+ * Throws synchronously if a handler is already registered for the
165
+ * channel. Returns a deregister fn that removes the handler.
166
+ *
167
+ * Compile-error if `C` is a broadcast (non-Rpc) channel.
168
+ */
169
+ handle<C extends keyof TChannels & string>(
170
+ channel: IsRpc<TChannels[C]> extends true ? C : never,
171
+ handler: (
172
+ payload: RpcReq<TChannels[C]>,
173
+ ctx?: RpcHandlerContext,
174
+ ) => Promise<RpcRes<TChannels[C]>> | RpcRes<TChannels[C]>,
175
+ ): UIUnsubscribe
176
+ }
177
+
178
+ /**
179
+ * Wire-level envelope types. Internal — not exported.
180
+ *
181
+ * `requestId` is set on RPC frames (call, result, cancel). Broadcast
182
+ * `UI_MESSAGE` / `UI_SEND` frames carry no `requestId`. `UI_CANCEL`
183
+ * frames carry only `requestId` (no channel, no payload).
184
+ */
185
+ type UISendEnvelope =
186
+ | {type: "UI_SEND"; channel: string; payload: unknown; seq: number; requestId?: string}
187
+ | {type: "UI_CANCEL"; requestId: string}
188
+
189
+ type UIInboundEnvelope =
190
+ | {type: "UI_MESSAGE"; channel: string; payload: unknown; seq: number; requestId?: string}
191
+ | {type: "UI_OPEN"}
192
+ | {type: "UI_CLOSE"}
193
+ | {type: "UI_CANCEL"; requestId: string}
194
+
195
+ function rpcErrorFromUnknown(e: unknown): RpcErrorEnvelope {
196
+ if (e instanceof Error) {
197
+ // `cause` is ES2022; this package's lib targets ES2020 where Error
198
+ // has no `cause` field. Read it via a structural cast. Diagnostic fields
199
+ // (code/stage/transport) may live as own props (PhotoError-style) or on
200
+ // the cause — own props win.
201
+ const own = e as Error & {code?: string; stage?: string; transport?: string; cause?: unknown}
202
+ const cause = own.cause as {code?: string} | undefined
203
+ return compactEnvelope({
204
+ message: e.message,
205
+ code: own.code ?? cause?.code,
206
+ stage: own.stage,
207
+ transport: own.transport,
208
+ })
209
+ }
210
+ if (e && typeof e === "object") {
211
+ // Hosts reject RPCs with plain structured objects ({code, message, stage,
212
+ // transport}); stringifying them produced the infamous "[object Object]".
213
+ const o = e as {message?: unknown; code?: unknown; stage?: unknown; transport?: unknown}
214
+ return compactEnvelope({
215
+ message: typeof o.message === "string" ? o.message : JSON.stringify(e),
216
+ code: typeof o.code === "string" ? o.code : undefined,
217
+ stage: typeof o.stage === "string" ? o.stage : undefined,
218
+ transport: typeof o.transport === "string" ? o.transport : undefined,
219
+ })
220
+ }
221
+ return {message: String(e)}
222
+ }
223
+
224
+ export interface RpcErrorEnvelope {
225
+ message: string
226
+ code?: string
227
+ stage?: string
228
+ transport?: string
229
+ }
230
+
231
+ function compactEnvelope(env: RpcErrorEnvelope): RpcErrorEnvelope {
232
+ const out: RpcErrorEnvelope = {message: env.message}
233
+ if (env.code) out.code = env.code
234
+ if (env.stage) out.stage = env.stage
235
+ if (env.transport) out.transport = env.transport
236
+ return out
237
+ }
238
+
239
+ export class UIModuleImpl<TChannels extends object = Record<string, unknown>> implements UIModule<TChannels> {
240
+ /** True between UI_OPEN and the matching UI_CLOSE. */
241
+ private bound = false
242
+ /** Monotonic outbound seq number. Reset on bind. */
243
+ private nextSeq = 1
244
+ /** Open-lifecycle handlers — fire on every UI_OPEN. */
245
+ private readonly openHandlers: Set<() => void> = new Set()
246
+ private readonly closeHandlers: Set<() => void> = new Set()
247
+ /** channel → set of subscribers. */
248
+ private readonly channelHandlers: Map<string, Set<UIChannelHandler>> = new Map()
249
+
250
+ /** channel → single registered RPC handler. */
251
+ private readonly rpcHandlers: Map<string, (payload: unknown, ctx: RpcHandlerContext) => Promise<unknown> | unknown> =
252
+ new Map()
253
+
254
+ /** requestId → AbortController for in-flight RPC handler invocations. */
255
+ private readonly inflightRpc: Map<string, AbortController> = new Map()
256
+
257
+ constructor(private readonly session: MiniappSession) {
258
+ // The session forwards UI_OPEN / UI_CLOSE / UI_MESSAGE envelopes via
259
+ // its internal stream subscriber surface. The UIModule registers
260
+ // once via session._subscribe to receive them. The "_ui" stream
261
+ // name is internal — not exposed in the public stream list — so
262
+ // the host router knows to route lifecycle frames here without
263
+ // bumping any existing stream type.
264
+ this.session._subscribe("_ui", (env: unknown) => this.handleInbound(env as UIInboundEnvelope))
265
+ }
266
+
267
+ // Arrow-property bindings make every public method safe to destructure
268
+ // (`const {send} = session.ui`) or pass as a bare callback — a plain
269
+ // method loses `this` and crashes on `this.bound`.
270
+ isOpen = (): boolean => {
271
+ return this.bound
272
+ }
273
+
274
+ onOpen = (cb: () => void): UIUnsubscribe => {
275
+ this.openHandlers.add(cb)
276
+ if (this.bound) {
277
+ // Late subscriber — fire once for the current binding so callers
278
+ // that wire onOpen *after* the WebView mounted don't miss it.
279
+ // Gate on session readiness for the same reason handleInbound does:
280
+ // a late onOpen must still observe populated capabilities, not the
281
+ // pre-CONNECT_ACK null snapshot. If not yet ready, the "ready"
282
+ // listener registered in handleInbound's UI_OPEN branch will fan
283
+ // out to every openHandler (this one included), so we don't wire a
284
+ // second listener here.
285
+ if (this.session.ready) {
286
+ try {
287
+ cb()
288
+ } catch (e) {
289
+ console.warn("session.ui.onOpen late-fire threw:", e)
290
+ }
291
+ }
292
+ }
293
+ return () => {
294
+ this.openHandlers.delete(cb)
295
+ }
296
+ }
297
+
298
+ onClose = (cb: () => void): UIUnsubscribe => {
299
+ this.closeHandlers.add(cb)
300
+ return () => {
301
+ this.closeHandlers.delete(cb)
302
+ }
303
+ }
304
+
305
+ send = <C extends keyof TChannels & string>(channel: C, payload: TChannels[C]): void => {
306
+ if (!this.bound) {
307
+ // Per spec: drop silently when no WebView is bound. Background
308
+ // is the source of truth — the WebView re-fetches state on next
309
+ // open via session.ui.onOpen.
310
+ return
311
+ }
312
+ const seq = this.nextSeq++
313
+ const envelope: UISendEnvelope = {type: "UI_SEND", channel, payload, seq}
314
+ this.session.sendOneShot(envelope)
315
+ }
316
+
317
+ on = <C extends keyof TChannels & string>(channel: C, cb: UIChannelHandler<TChannels[C]>): UIUnsubscribe => {
318
+ let set = this.channelHandlers.get(channel as string)
319
+ if (!set) {
320
+ set = new Set()
321
+ this.channelHandlers.set(channel as string, set)
322
+ }
323
+ set.add(cb as UIChannelHandler)
324
+ return () => {
325
+ set!.delete(cb as UIChannelHandler)
326
+ }
327
+ }
328
+
329
+ handle = <C extends keyof TChannels & string>(
330
+ channel: IsRpc<TChannels[C]> extends true ? C : never,
331
+ handler: (
332
+ payload: RpcReq<TChannels[C]>,
333
+ ctx?: RpcHandlerContext,
334
+ ) => Promise<RpcRes<TChannels[C]>> | RpcRes<TChannels[C]>,
335
+ ): UIUnsubscribe => {
336
+ const key = channel as unknown as string
337
+ if (this.rpcHandlers.has(key)) {
338
+ throw new Error(`session.ui.handle: a handler is already registered for "${key}"`)
339
+ }
340
+ this.rpcHandlers.set(
341
+ key,
342
+ handler as unknown as (payload: unknown, ctx: RpcHandlerContext) => Promise<unknown> | unknown,
343
+ )
344
+ return () => {
345
+ this.rpcHandlers.delete(key)
346
+ }
347
+ }
348
+
349
+ /** Fire every registered open handler once. Guarded per-handler. */
350
+ private fireOpenHandlers(): void {
351
+ for (const h of this.openHandlers) {
352
+ try {
353
+ h()
354
+ } catch (e) {
355
+ console.warn("session.ui.onOpen handler threw", e)
356
+ }
357
+ }
358
+ }
359
+
360
+ /** @internal — handle UI_OPEN / UI_CLOSE / UI_MESSAGE envelopes from the host. */
361
+ private handleInbound(env: UIInboundEnvelope): void {
362
+ if (env.type === "UI_OPEN") {
363
+ this.bound = true
364
+ this.nextSeq = 1
365
+ // onOpen handlers almost always read session.capabilities /
366
+ // session.ready to hydrate the fresh WebView. The WebView's
367
+ // `glassly.ready()` (which produces this UI_OPEN) races the
368
+ // background session's CONNECT_ACK — on a fast bridge UI_OPEN can
369
+ // arrive first, leaving capabilities null and the UI rendering a
370
+ // "no glasses" snapshot that never self-corrects. Gate the open
371
+ // fan-out on session readiness so handlers always observe a
372
+ // populated session. The WebView is marked bound immediately
373
+ // (send/isOpen work, inbound stays buffered) — only the open
374
+ // callbacks wait.
375
+ if (this.session.ready) {
376
+ this.fireOpenHandlers()
377
+ } else {
378
+ const off = this.session.on("ready", () => {
379
+ off()
380
+ // If the WebView closed during the connect window, don't fire
381
+ // stale open handlers — a later UI_OPEN will re-trigger them.
382
+ if (this.bound) this.fireOpenHandlers()
383
+ })
384
+ }
385
+ return
386
+ }
387
+ if (env.type === "UI_CLOSE") {
388
+ this.bound = false
389
+ for (const h of this.closeHandlers) {
390
+ try {
391
+ h()
392
+ } catch (e) {
393
+ console.warn("session.ui.onClose handler threw", e)
394
+ }
395
+ }
396
+ return
397
+ }
398
+ if (env.type === "UI_MESSAGE") {
399
+ // RPC call: requestId set → dispatch to handle() handler.
400
+ if (typeof env.requestId === "string") {
401
+ this.dispatchRpcCall(env.channel, env.payload, env.requestId)
402
+ return
403
+ }
404
+ // Broadcast: fan out to on() subscribers.
405
+ const set = this.channelHandlers.get(env.channel)
406
+ if (!set || set.size === 0) return
407
+ for (const h of set) {
408
+ try {
409
+ h(env.payload)
410
+ } catch (e) {
411
+ console.warn(`session.ui.on(${env.channel}) threw`, e)
412
+ }
413
+ }
414
+ return
415
+ }
416
+ if (env.type === "UI_CANCEL") {
417
+ const ctrl = this.inflightRpc.get(env.requestId)
418
+ if (ctrl) {
419
+ try {
420
+ ctrl.abort()
421
+ } catch {
422
+ /* ignore */
423
+ }
424
+ }
425
+ return
426
+ }
427
+ }
428
+
429
+ /** @internal — invoke a registered RPC handler and send back the reply. */
430
+ private dispatchRpcCall(channel: string, payload: unknown, requestId: string): void {
431
+ const handler = this.rpcHandlers.get(channel)
432
+ if (!handler) {
433
+ // No handler registered. Reply with a structured error so the
434
+ // UI's request promise rejects with a useful message.
435
+ this.sendRpcReply(channel, requestId, {
436
+ ok: false,
437
+ error: {message: `no handler registered for "${channel}"`},
438
+ })
439
+ return
440
+ }
441
+ const ctrl = new AbortController()
442
+ this.inflightRpc.set(requestId, ctrl)
443
+ const ctx: RpcHandlerContext = {signal: ctrl.signal}
444
+
445
+ const finish = (envelope: {ok: true; result: unknown} | {ok: false; error: RpcErrorEnvelope}): void => {
446
+ this.inflightRpc.delete(requestId)
447
+ // If the controller already aborted (UI cancelled), drop the
448
+ // reply — the UI side has already removed its listener.
449
+ if (ctrl.signal.aborted) return
450
+ this.sendRpcReply(channel, requestId, envelope)
451
+ }
452
+
453
+ let result: unknown
454
+ try {
455
+ result = handler(payload, ctx)
456
+ } catch (e) {
457
+ finish({ok: false, error: rpcErrorFromUnknown(e)})
458
+ return
459
+ }
460
+ if (result && typeof (result as {then?: unknown}).then === "function") {
461
+ ;(result as Promise<unknown>).then(
462
+ (v) => finish({ok: true, result: v}),
463
+ (e) => finish({ok: false, error: rpcErrorFromUnknown(e)}),
464
+ )
465
+ } else {
466
+ finish({ok: true, result})
467
+ }
468
+ }
469
+
470
+ /** @internal — send a UI_SEND envelope tagged with a requestId. */
471
+ private sendRpcReply(
472
+ channel: string,
473
+ requestId: string,
474
+ payload: {ok: true; result: unknown} | {ok: false; error: RpcErrorEnvelope},
475
+ ): void {
476
+ if (!this.bound) return
477
+ const seq = this.nextSeq++
478
+ const envelope: UISendEnvelope = {type: "UI_SEND", channel, payload, seq, requestId}
479
+ this.session.sendOneShot(envelope)
480
+ }
481
+ }