@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.
- package/README.md +259 -0
- package/dist/background/index.d.ts +59 -0
- package/dist/background/index.d.ts.map +1 -0
- package/dist/background/index.js +25 -0
- package/dist/background/index.js.map +1 -0
- package/dist/background/register.d.ts +38 -0
- package/dist/background/register.d.ts.map +1 -0
- package/dist/background/register.js +75 -0
- package/dist/background/register.js.map +1 -0
- package/dist/dev-reload.d.ts +21 -0
- package/dist/dev-reload.d.ts.map +1 -0
- package/dist/dev-reload.js +62 -0
- package/dist/dev-reload.js.map +1 -0
- package/dist/envelope.d.ts +23 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +47 -0
- package/dist/envelope.js.map +1 -0
- package/dist/globals.d.ts +54 -0
- package/dist/globals.d.ts.map +1 -0
- package/dist/globals.js +12 -0
- package/dist/globals.js.map +1 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/actions.d.ts +54 -0
- package/dist/modules/actions.d.ts.map +1 -0
- package/dist/modules/actions.js +122 -0
- package/dist/modules/actions.js.map +1 -0
- package/dist/modules/auth.d.ts +25 -0
- package/dist/modules/auth.d.ts.map +1 -0
- package/dist/modules/auth.js +52 -0
- package/dist/modules/auth.js.map +1 -0
- package/dist/modules/base64.d.ts +16 -0
- package/dist/modules/base64.d.ts.map +1 -0
- package/dist/modules/base64.js +92 -0
- package/dist/modules/base64.js.map +1 -0
- package/dist/modules/blob.d.ts +161 -0
- package/dist/modules/blob.d.ts.map +1 -0
- package/dist/modules/blob.js +277 -0
- package/dist/modules/blob.js.map +1 -0
- package/dist/modules/camera.d.ts +137 -0
- package/dist/modules/camera.d.ts.map +1 -0
- package/dist/modules/camera.js +104 -0
- package/dist/modules/camera.js.map +1 -0
- package/dist/modules/cloud.d.ts +34 -0
- package/dist/modules/cloud.d.ts.map +1 -0
- package/dist/modules/cloud.js +70 -0
- package/dist/modules/cloud.js.map +1 -0
- package/dist/modules/dashboard.d.ts +16 -0
- package/dist/modules/dashboard.d.ts.map +1 -0
- package/dist/modules/dashboard.js +27 -0
- package/dist/modules/dashboard.js.map +1 -0
- package/dist/modules/display.d.ts +106 -0
- package/dist/modules/display.d.ts.map +1 -0
- package/dist/modules/display.js +57 -0
- package/dist/modules/display.js.map +1 -0
- package/dist/modules/events.d.ts +167 -0
- package/dist/modules/events.d.ts.map +1 -0
- package/dist/modules/events.js +123 -0
- package/dist/modules/events.js.map +1 -0
- package/dist/modules/glasses.d.ts +28 -0
- package/dist/modules/glasses.d.ts.map +1 -0
- package/dist/modules/glasses.js +39 -0
- package/dist/modules/glasses.js.map +1 -0
- package/dist/modules/heading.d.ts +16 -0
- package/dist/modules/heading.d.ts.map +1 -0
- package/dist/modules/heading.js +20 -0
- package/dist/modules/heading.js.map +1 -0
- package/dist/modules/imu.d.ts +31 -0
- package/dist/modules/imu.d.ts.map +1 -0
- package/dist/modules/imu.js +40 -0
- package/dist/modules/imu.js.map +1 -0
- package/dist/modules/input.d.ts +34 -0
- package/dist/modules/input.d.ts.map +1 -0
- package/dist/modules/input.js +50 -0
- package/dist/modules/input.js.map +1 -0
- package/dist/modules/led.d.ts +35 -0
- package/dist/modules/led.d.ts.map +1 -0
- package/dist/modules/led.js +39 -0
- package/dist/modules/led.js.map +1 -0
- package/dist/modules/location.d.ts +27 -0
- package/dist/modules/location.d.ts.map +1 -0
- package/dist/modules/location.js +35 -0
- package/dist/modules/location.js.map +1 -0
- package/dist/modules/mic.d.ts +44 -0
- package/dist/modules/mic.d.ts.map +1 -0
- package/dist/modules/mic.js +67 -0
- package/dist/modules/mic.js.map +1 -0
- package/dist/modules/miniapps.d.ts +72 -0
- package/dist/modules/miniapps.d.ts.map +1 -0
- package/dist/modules/miniapps.js +48 -0
- package/dist/modules/miniapps.js.map +1 -0
- package/dist/modules/navigation.d.ts +326 -0
- package/dist/modules/navigation.d.ts.map +1 -0
- package/dist/modules/navigation.js +386 -0
- package/dist/modules/navigation.js.map +1 -0
- package/dist/modules/permissions.d.ts +58 -0
- package/dist/modules/permissions.d.ts.map +1 -0
- package/dist/modules/permissions.js +66 -0
- package/dist/modules/permissions.js.map +1 -0
- package/dist/modules/phone.d.ts +61 -0
- package/dist/modules/phone.d.ts.map +1 -0
- package/dist/modules/phone.js +94 -0
- package/dist/modules/phone.js.map +1 -0
- package/dist/modules/pivots/engine.d.ts +169 -0
- package/dist/modules/pivots/engine.d.ts.map +1 -0
- package/dist/modules/pivots/engine.js +779 -0
- package/dist/modules/pivots/engine.js.map +1 -0
- package/dist/modules/pivots/geometry.d.ts +84 -0
- package/dist/modules/pivots/geometry.d.ts.map +1 -0
- package/dist/modules/pivots/geometry.js +371 -0
- package/dist/modules/pivots/geometry.js.map +1 -0
- package/dist/modules/pivots/instructions.d.ts +139 -0
- package/dist/modules/pivots/instructions.d.ts.map +1 -0
- package/dist/modules/pivots/instructions.js +280 -0
- package/dist/modules/pivots/instructions.js.map +1 -0
- package/dist/modules/speaker.d.ts +163 -0
- package/dist/modules/speaker.d.ts.map +1 -0
- package/dist/modules/speaker.js +250 -0
- package/dist/modules/speaker.js.map +1 -0
- package/dist/modules/storage.d.ts +50 -0
- package/dist/modules/storage.d.ts.map +1 -0
- package/dist/modules/storage.js +99 -0
- package/dist/modules/storage.js.map +1 -0
- package/dist/modules/stream.d.ts +129 -0
- package/dist/modules/stream.d.ts.map +1 -0
- package/dist/modules/stream.js +52 -0
- package/dist/modules/stream.js.map +1 -0
- package/dist/modules/system.d.ts +45 -0
- package/dist/modules/system.d.ts.map +1 -0
- package/dist/modules/system.js +42 -0
- package/dist/modules/system.js.map +1 -0
- package/dist/modules/transcription.d.ts +67 -0
- package/dist/modules/transcription.d.ts.map +1 -0
- package/dist/modules/transcription.js +106 -0
- package/dist/modules/transcription.js.map +1 -0
- package/dist/modules/translation.d.ts +59 -0
- package/dist/modules/translation.d.ts.map +1 -0
- package/dist/modules/translation.js +119 -0
- package/dist/modules/translation.js.map +1 -0
- package/dist/modules/ui.d.ts +186 -0
- package/dist/modules/ui.d.ts.map +1 -0
- package/dist/modules/ui.js +293 -0
- package/dist/modules/ui.js.map +1 -0
- package/dist/protocol.d.ts +294 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +320 -0
- package/dist/protocol.js.map +1 -0
- package/dist/react/MentraProvider.d.ts +33 -0
- package/dist/react/MentraProvider.d.ts.map +1 -0
- package/dist/react/MentraProvider.js +15 -0
- package/dist/react/MentraProvider.js.map +1 -0
- package/dist/react/MiniappHeader.d.ts +44 -0
- package/dist/react/MiniappHeader.d.ts.map +1 -0
- package/dist/react/MiniappHeader.js +32 -0
- package/dist/react/MiniappHeader.js.map +1 -0
- package/dist/react/index.d.ts +13 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +9 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/useCapabilities.d.ts +26 -0
- package/dist/react/useCapabilities.d.ts.map +1 -0
- package/dist/react/useCapabilities.js +27 -0
- package/dist/react/useCapabilities.js.map +1 -0
- package/dist/react/useCapsuleHeaderStyle.d.ts +27 -0
- package/dist/react/useCapsuleHeaderStyle.d.ts.map +1 -0
- package/dist/react/useCapsuleHeaderStyle.js +36 -0
- package/dist/react/useCapsuleHeaderStyle.js.map +1 -0
- package/dist/react/useColorScheme.d.ts +18 -0
- package/dist/react/useColorScheme.d.ts.map +1 -0
- package/dist/react/useColorScheme.js +24 -0
- package/dist/react/useColorScheme.js.map +1 -0
- package/dist/react/useConnected.d.ts +17 -0
- package/dist/react/useConnected.d.ts.map +1 -0
- package/dist/react/useConnected.js +39 -0
- package/dist/react/useConnected.js.map +1 -0
- package/dist/react/useRpc.d.ts +29 -0
- package/dist/react/useRpc.d.ts.map +1 -0
- package/dist/react/useRpc.js +82 -0
- package/dist/react/useRpc.js.map +1 -0
- package/dist/react/useSafeArea.d.ts +21 -0
- package/dist/react/useSafeArea.d.ts.map +1 -0
- package/dist/react/useSafeArea.js +22 -0
- package/dist/react/useSafeArea.js.map +1 -0
- package/dist/session.d.ts +310 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +653 -0
- package/dist/session.js.map +1 -0
- package/dist/sub-path-types.test-d.d.ts +28 -0
- package/dist/sub-path-types.test-d.d.ts.map +1 -0
- package/dist/sub-path-types.test-d.js +37 -0
- package/dist/sub-path-types.test-d.js.map +1 -0
- package/dist/transport/auto.d.ts +35 -0
- package/dist/transport/auto.d.ts.map +1 -0
- package/dist/transport/auto.js +125 -0
- package/dist/transport/auto.js.map +1 -0
- package/dist/transport/dispatch.d.ts +43 -0
- package/dist/transport/dispatch.d.ts.map +1 -0
- package/dist/transport/dispatch.js +91 -0
- package/dist/transport/dispatch.js.map +1 -0
- package/dist/transport/local-socket.d.ts +28 -0
- package/dist/transport/local-socket.d.ts.map +1 -0
- package/dist/transport/local-socket.js +80 -0
- package/dist/transport/local-socket.js.map +1 -0
- package/dist/transport/mock.d.ts +54 -0
- package/dist/transport/mock.d.ts.map +1 -0
- package/dist/transport/mock.js +205 -0
- package/dist/transport/mock.js.map +1 -0
- package/dist/transport/postmessage.d.ts +36 -0
- package/dist/transport/postmessage.d.ts.map +1 -0
- package/dist/transport/postmessage.js +78 -0
- package/dist/transport/postmessage.js.map +1 -0
- package/dist/transport/types.d.ts +25 -0
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/transport/types.js +11 -0
- package/dist/transport/types.js.map +1 -0
- package/dist/ui/index.d.ts +98 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +32 -0
- package/dist/ui/index.js.map +1 -0
- package/package.json +75 -0
- package/src/background/index.ts +152 -0
- package/src/background/register.ts +85 -0
- package/src/dev-reload.ts +60 -0
- package/src/envelope.ts +54 -0
- package/src/globals.ts +62 -0
- package/src/index.ts +171 -0
- package/src/modules/actions.ts +156 -0
- package/src/modules/auth.ts +68 -0
- package/src/modules/base64.ts +87 -0
- package/src/modules/blob.ts +341 -0
- package/src/modules/camera.ts +196 -0
- package/src/modules/cloud.ts +94 -0
- package/src/modules/dashboard.ts +31 -0
- package/src/modules/display.ts +122 -0
- package/src/modules/events.ts +264 -0
- package/src/modules/glasses.ts +44 -0
- package/src/modules/heading.ts +23 -0
- package/src/modules/imu.ts +44 -0
- package/src/modules/input.ts +71 -0
- package/src/modules/led.ts +61 -0
- package/src/modules/location.ts +39 -0
- package/src/modules/mic.ts +74 -0
- package/src/modules/miniapps.ts +90 -0
- package/src/modules/navigation.ts +670 -0
- package/src/modules/permissions.ts +84 -0
- package/src/modules/phone.ts +120 -0
- package/src/modules/pivots/engine.ts +863 -0
- package/src/modules/pivots/geometry.ts +409 -0
- package/src/modules/pivots/instructions.ts +343 -0
- package/src/modules/speaker.ts +328 -0
- package/src/modules/storage.ts +109 -0
- package/src/modules/stream.ts +163 -0
- package/src/modules/system.ts +76 -0
- package/src/modules/transcription.ts +128 -0
- package/src/modules/translation.ts +149 -0
- package/src/modules/ui.ts +498 -0
- package/src/protocol.ts +371 -0
- package/src/react/MentraProvider.tsx +50 -0
- package/src/react/MiniappHeader.tsx +122 -0
- package/src/react/index.ts +12 -0
- package/src/react/useCapabilities.ts +29 -0
- package/src/react/useCapsuleHeaderStyle.ts +54 -0
- package/src/react/useColorScheme.ts +26 -0
- package/src/react/useConnected.ts +47 -0
- package/src/react/useRpc.ts +106 -0
- package/src/react/useSafeArea.ts +40 -0
- package/src/session.ts +878 -0
- package/src/sub-path-types.test-d.ts +49 -0
- package/src/transport/auto.ts +143 -0
- package/src/transport/dispatch.ts +106 -0
- package/src/transport/local-socket.ts +93 -0
- package/src/transport/mock.ts +248 -0
- package/src/transport/postmessage.ts +95 -0
- package/src/transport/types.ts +26 -0
- package/src/ui/index.ts +122 -0
|
@@ -0,0 +1,498 @@
|
|
|
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: `mentra.send(channel, payload)`
|
|
8
|
+
* - subscribe: `session.ui.on(channel, cb)` / `mentra.on(channel, cb)`
|
|
9
|
+
*
|
|
10
|
+
* 2. **RPC** (request/response, UI → background only)
|
|
11
|
+
* - UI side: `await mentra.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
|
+
* - `mentra.send` BUFFERS until `mentra.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 `mentra.request` / `session.ui.handle`
|
|
49
|
+
* accept only `Rpc<...>` channels; `mentra.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 `mentra.request`. */
|
|
69
|
+
export interface RpcRequestOptions {
|
|
70
|
+
/** Abort the in-flight call. Sends UI_CANCEL to the handler. */
|
|
71
|
+
signal?: AbortSignal
|
|
72
|
+
/** Reject with `MentraRpcTimeoutError` 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 `mentra.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 MentraRpcError extends Error {
|
|
88
|
+
constructor(message: string, options?: {cause?: {code?: string}}) {
|
|
89
|
+
super(message)
|
|
90
|
+
this.name = "MentraRpcError"
|
|
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 `mentra.request` when its `{timeout}` elapses. */
|
|
101
|
+
export class MentraRpcTimeoutError extends Error {
|
|
102
|
+
constructor(message = "RPC timed out") {
|
|
103
|
+
super(message)
|
|
104
|
+
this.name = "MentraRpcTimeoutError"
|
|
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 `mentra.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
|
+
* `mentra.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>>
|
|
240
|
+
implements UIModule<TChannels>
|
|
241
|
+
{
|
|
242
|
+
/** True between UI_OPEN and the matching UI_CLOSE. */
|
|
243
|
+
private bound = false
|
|
244
|
+
/** Monotonic outbound seq number. Reset on bind. */
|
|
245
|
+
private nextSeq = 1
|
|
246
|
+
/** Open-lifecycle handlers — fire on every UI_OPEN. */
|
|
247
|
+
private readonly openHandlers: Set<() => void> = new Set()
|
|
248
|
+
private readonly closeHandlers: Set<() => void> = new Set()
|
|
249
|
+
/** channel → set of subscribers. */
|
|
250
|
+
private readonly channelHandlers: Map<string, Set<UIChannelHandler>> = new Map()
|
|
251
|
+
|
|
252
|
+
/** channel → single registered RPC handler. */
|
|
253
|
+
private readonly rpcHandlers: Map<
|
|
254
|
+
string,
|
|
255
|
+
(payload: unknown, ctx: RpcHandlerContext) => Promise<unknown> | unknown
|
|
256
|
+
> = new Map()
|
|
257
|
+
|
|
258
|
+
/** requestId → AbortController for in-flight RPC handler invocations. */
|
|
259
|
+
private readonly inflightRpc: Map<string, AbortController> = new Map()
|
|
260
|
+
|
|
261
|
+
constructor(private readonly session: MiniappSession) {
|
|
262
|
+
// The session forwards UI_OPEN / UI_CLOSE / UI_MESSAGE envelopes via
|
|
263
|
+
// its internal stream subscriber surface. The UIModule registers
|
|
264
|
+
// once via session._subscribe to receive them. The "_ui" stream
|
|
265
|
+
// name is internal — not exposed in the public stream list — so
|
|
266
|
+
// the host router knows to route lifecycle frames here without
|
|
267
|
+
// bumping any existing stream type.
|
|
268
|
+
this.session._subscribe("_ui", (env: unknown) => this.handleInbound(env as UIInboundEnvelope))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Arrow-property bindings make every public method safe to destructure
|
|
272
|
+
// (`const {send} = session.ui`) or pass as a bare callback — a plain
|
|
273
|
+
// method loses `this` and crashes on `this.bound`.
|
|
274
|
+
isOpen = (): boolean => {
|
|
275
|
+
return this.bound
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
onOpen = (cb: () => void): UIUnsubscribe => {
|
|
279
|
+
this.openHandlers.add(cb)
|
|
280
|
+
if (this.bound) {
|
|
281
|
+
// Late subscriber — fire once for the current binding so callers
|
|
282
|
+
// that wire onOpen *after* the WebView mounted don't miss it.
|
|
283
|
+
// Gate on session readiness for the same reason handleInbound does:
|
|
284
|
+
// a late onOpen must still observe populated capabilities, not the
|
|
285
|
+
// pre-CONNECT_ACK null snapshot. If not yet ready, the "ready"
|
|
286
|
+
// listener registered in handleInbound's UI_OPEN branch will fan
|
|
287
|
+
// out to every openHandler (this one included), so we don't wire a
|
|
288
|
+
// second listener here.
|
|
289
|
+
if (this.session.ready) {
|
|
290
|
+
try {
|
|
291
|
+
cb()
|
|
292
|
+
} catch (e) {
|
|
293
|
+
// eslint-disable-next-line no-console
|
|
294
|
+
console.warn("session.ui.onOpen late-fire threw:", e)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return () => {
|
|
299
|
+
this.openHandlers.delete(cb)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
onClose = (cb: () => void): UIUnsubscribe => {
|
|
304
|
+
this.closeHandlers.add(cb)
|
|
305
|
+
return () => {
|
|
306
|
+
this.closeHandlers.delete(cb)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
send = <C extends keyof TChannels & string>(channel: C, payload: TChannels[C]): void => {
|
|
311
|
+
if (!this.bound) {
|
|
312
|
+
// Per spec: drop silently when no WebView is bound. Background
|
|
313
|
+
// is the source of truth — the WebView re-fetches state on next
|
|
314
|
+
// open via session.ui.onOpen.
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
const seq = this.nextSeq++
|
|
318
|
+
const envelope: UISendEnvelope = {type: "UI_SEND", channel, payload, seq}
|
|
319
|
+
this.session.sendOneShot(envelope)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
on = <C extends keyof TChannels & string>(
|
|
323
|
+
channel: C,
|
|
324
|
+
cb: UIChannelHandler<TChannels[C]>,
|
|
325
|
+
): UIUnsubscribe => {
|
|
326
|
+
let set = this.channelHandlers.get(channel as string)
|
|
327
|
+
if (!set) {
|
|
328
|
+
set = new Set()
|
|
329
|
+
this.channelHandlers.set(channel as string, set)
|
|
330
|
+
}
|
|
331
|
+
set.add(cb as UIChannelHandler)
|
|
332
|
+
return () => {
|
|
333
|
+
set!.delete(cb as UIChannelHandler)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
handle = <C extends keyof TChannels & string>(
|
|
338
|
+
channel: IsRpc<TChannels[C]> extends true ? C : never,
|
|
339
|
+
handler: (
|
|
340
|
+
payload: RpcReq<TChannels[C]>,
|
|
341
|
+
ctx?: RpcHandlerContext,
|
|
342
|
+
) => Promise<RpcRes<TChannels[C]>> | RpcRes<TChannels[C]>,
|
|
343
|
+
): UIUnsubscribe => {
|
|
344
|
+
const key = channel as unknown as string
|
|
345
|
+
if (this.rpcHandlers.has(key)) {
|
|
346
|
+
throw new Error(`session.ui.handle: a handler is already registered for "${key}"`)
|
|
347
|
+
}
|
|
348
|
+
this.rpcHandlers.set(
|
|
349
|
+
key,
|
|
350
|
+
handler as unknown as (payload: unknown, ctx: RpcHandlerContext) => Promise<unknown> | unknown,
|
|
351
|
+
)
|
|
352
|
+
return () => {
|
|
353
|
+
this.rpcHandlers.delete(key)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Fire every registered open handler once. Guarded per-handler. */
|
|
358
|
+
private fireOpenHandlers(): void {
|
|
359
|
+
for (const h of this.openHandlers) {
|
|
360
|
+
try {
|
|
361
|
+
h()
|
|
362
|
+
} catch (e) {
|
|
363
|
+
// eslint-disable-next-line no-console
|
|
364
|
+
console.warn("session.ui.onOpen handler threw", e)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** @internal — handle UI_OPEN / UI_CLOSE / UI_MESSAGE envelopes from the host. */
|
|
370
|
+
private handleInbound(env: UIInboundEnvelope): void {
|
|
371
|
+
if (env.type === "UI_OPEN") {
|
|
372
|
+
this.bound = true
|
|
373
|
+
this.nextSeq = 1
|
|
374
|
+
// onOpen handlers almost always read session.capabilities /
|
|
375
|
+
// session.ready to hydrate the fresh WebView. The WebView's
|
|
376
|
+
// `mentra.ready()` (which produces this UI_OPEN) races the
|
|
377
|
+
// background session's CONNECT_ACK — on a fast bridge UI_OPEN can
|
|
378
|
+
// arrive first, leaving capabilities null and the UI rendering a
|
|
379
|
+
// "no glasses" snapshot that never self-corrects. Gate the open
|
|
380
|
+
// fan-out on session readiness so handlers always observe a
|
|
381
|
+
// populated session. The WebView is marked bound immediately
|
|
382
|
+
// (send/isOpen work, inbound stays buffered) — only the open
|
|
383
|
+
// callbacks wait.
|
|
384
|
+
if (this.session.ready) {
|
|
385
|
+
this.fireOpenHandlers()
|
|
386
|
+
} else {
|
|
387
|
+
const off = this.session.on("ready", () => {
|
|
388
|
+
off()
|
|
389
|
+
// If the WebView closed during the connect window, don't fire
|
|
390
|
+
// stale open handlers — a later UI_OPEN will re-trigger them.
|
|
391
|
+
if (this.bound) this.fireOpenHandlers()
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
if (env.type === "UI_CLOSE") {
|
|
397
|
+
this.bound = false
|
|
398
|
+
for (const h of this.closeHandlers) {
|
|
399
|
+
try {
|
|
400
|
+
h()
|
|
401
|
+
} catch (e) {
|
|
402
|
+
// eslint-disable-next-line no-console
|
|
403
|
+
console.warn("session.ui.onClose handler threw", e)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
if (env.type === "UI_MESSAGE") {
|
|
409
|
+
// RPC call: requestId set → dispatch to handle() handler.
|
|
410
|
+
if (typeof env.requestId === "string") {
|
|
411
|
+
this.dispatchRpcCall(env.channel, env.payload, env.requestId)
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
// Broadcast: fan out to on() subscribers.
|
|
415
|
+
const set = this.channelHandlers.get(env.channel)
|
|
416
|
+
if (!set || set.size === 0) return
|
|
417
|
+
for (const h of set) {
|
|
418
|
+
try {
|
|
419
|
+
h(env.payload)
|
|
420
|
+
} catch (e) {
|
|
421
|
+
// eslint-disable-next-line no-console
|
|
422
|
+
console.warn(`session.ui.on(${env.channel}) threw`, e)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
if (env.type === "UI_CANCEL") {
|
|
428
|
+
const ctrl = this.inflightRpc.get(env.requestId)
|
|
429
|
+
if (ctrl) {
|
|
430
|
+
try {
|
|
431
|
+
ctrl.abort()
|
|
432
|
+
} catch {
|
|
433
|
+
/* ignore */
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** @internal — invoke a registered RPC handler and send back the reply. */
|
|
441
|
+
private dispatchRpcCall(channel: string, payload: unknown, requestId: string): void {
|
|
442
|
+
const handler = this.rpcHandlers.get(channel)
|
|
443
|
+
if (!handler) {
|
|
444
|
+
// No handler registered. Reply with a structured error so the
|
|
445
|
+
// UI's request promise rejects with a useful message.
|
|
446
|
+
this.sendRpcReply(channel, requestId, {
|
|
447
|
+
ok: false,
|
|
448
|
+
error: {message: `no handler registered for "${channel}"`},
|
|
449
|
+
})
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
const ctrl = new AbortController()
|
|
453
|
+
this.inflightRpc.set(requestId, ctrl)
|
|
454
|
+
const ctx: RpcHandlerContext = {signal: ctrl.signal}
|
|
455
|
+
|
|
456
|
+
const finish = (
|
|
457
|
+
envelope:
|
|
458
|
+
| {ok: true; result: unknown}
|
|
459
|
+
| {ok: false; error: RpcErrorEnvelope},
|
|
460
|
+
): void => {
|
|
461
|
+
this.inflightRpc.delete(requestId)
|
|
462
|
+
// If the controller already aborted (UI cancelled), drop the
|
|
463
|
+
// reply — the UI side has already removed its listener.
|
|
464
|
+
if (ctrl.signal.aborted) return
|
|
465
|
+
this.sendRpcReply(channel, requestId, envelope)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let result: unknown
|
|
469
|
+
try {
|
|
470
|
+
result = handler(payload, ctx)
|
|
471
|
+
} catch (e) {
|
|
472
|
+
finish({ok: false, error: rpcErrorFromUnknown(e)})
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
if (result && typeof (result as {then?: unknown}).then === "function") {
|
|
476
|
+
;(result as Promise<unknown>).then(
|
|
477
|
+
(v) => finish({ok: true, result: v}),
|
|
478
|
+
(e) => finish({ok: false, error: rpcErrorFromUnknown(e)}),
|
|
479
|
+
)
|
|
480
|
+
} else {
|
|
481
|
+
finish({ok: true, result})
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** @internal — send a UI_SEND envelope tagged with a requestId. */
|
|
486
|
+
private sendRpcReply(
|
|
487
|
+
channel: string,
|
|
488
|
+
requestId: string,
|
|
489
|
+
payload:
|
|
490
|
+
| {ok: true; result: unknown}
|
|
491
|
+
| {ok: false; error: RpcErrorEnvelope},
|
|
492
|
+
): void {
|
|
493
|
+
if (!this.bound) return
|
|
494
|
+
const seq = this.nextSeq++
|
|
495
|
+
const envelope: UISendEnvelope = {type: "UI_SEND", channel, payload, seq, requestId}
|
|
496
|
+
this.session.sendOneShot(envelope)
|
|
497
|
+
}
|
|
498
|
+
}
|