@mentra/miniapp 0.3.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +259 -0
- package/dist/background/index.d.ts +60 -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 +59 -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 +170 -0
- package/dist/modules/camera.d.ts.map +1 -0
- package/dist/modules/camera.js +116 -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 +160 -0
- package/dist/modules/events.d.ts.map +1 -0
- package/dist/modules/events.js +168 -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 +95 -0
- package/dist/modules/phone.d.ts.map +1 -0
- package/dist/modules/phone.js +110 -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 +169 -0
- package/dist/modules/speaker.d.ts.map +1 -0
- package/dist/modules/speaker.js +251 -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 +75 -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 +299 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +325 -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 +312 -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 +207 -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 +84 -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 +242 -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 +291 -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 +155 -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 +335 -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 +145 -0
- package/src/modules/translation.ts +149 -0
- package/src/modules/ui.ts +498 -0
- package/src/protocol.ts +378 -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 +886 -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 +251 -0
- package/src/transport/postmessage.ts +95 -0
- package/src/transport/types.ts +26 -0
- package/src/ui/index.ts +122 -0
package/src/session.ts
ADDED
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview MiniappSession — central session object for a local miniapp.
|
|
3
|
+
*
|
|
4
|
+
* Owns the transport, the request/response correlation map, the readiness queue,
|
|
5
|
+
* the PONG auto-reply, the visibility state, and all per-module instances.
|
|
6
|
+
*
|
|
7
|
+
* Lifecycle:
|
|
8
|
+
* const session = new MiniappSession()
|
|
9
|
+
* await session.connect() // sends CONNECT, resolves on CONNECT_ACK
|
|
10
|
+
* session.display.render([...])
|
|
11
|
+
* ...
|
|
12
|
+
* session.disconnect()
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {EventEmitter} from "eventemitter3"
|
|
16
|
+
|
|
17
|
+
import {makeRequestId, MiniappEnvelope, parseEnvelope, serializeEnvelope} from "./envelope"
|
|
18
|
+
import {getMentraOSGlobals, MiniappColorScheme} from "./globals"
|
|
19
|
+
import {MiniappErrorCode, MiniappRequestType, MiniappResponseType} from "./protocol"
|
|
20
|
+
import {createTransport, CreateTransportOptions} from "./transport/auto"
|
|
21
|
+
import {Transport} from "./transport/types"
|
|
22
|
+
import {CameraModule} from "./modules/camera"
|
|
23
|
+
import {AuthModule} from "./modules/auth"
|
|
24
|
+
import {CloudModule} from "./modules/cloud"
|
|
25
|
+
import {DashboardAPI} from "./modules/dashboard"
|
|
26
|
+
import {DisplayManager} from "./modules/display"
|
|
27
|
+
import {EventManager, type TranscriptionEventRoute, type UnsubscribeFn} from "./modules/events"
|
|
28
|
+
import {GlassesModule} from "./modules/glasses"
|
|
29
|
+
import {HeadingModule} from "./modules/heading"
|
|
30
|
+
import {ImuModule} from "./modules/imu"
|
|
31
|
+
import {InputModule} from "./modules/input"
|
|
32
|
+
import {LedModule} from "./modules/led"
|
|
33
|
+
import {LocationModule} from "./modules/location"
|
|
34
|
+
import {MicModule} from "./modules/mic"
|
|
35
|
+
import {NavigationModule} from "./modules/navigation"
|
|
36
|
+
import {PermissionsModule} from "./modules/permissions"
|
|
37
|
+
import {PhoneModule} from "./modules/phone"
|
|
38
|
+
import {TranscriptionModule} from "./modules/transcription"
|
|
39
|
+
import {TranslationModule} from "./modules/translation"
|
|
40
|
+
import {UIModuleImpl, type UIModule} from "./modules/ui"
|
|
41
|
+
import {SimpleStorage} from "./modules/storage"
|
|
42
|
+
import {SpeakerModule} from "./modules/speaker"
|
|
43
|
+
import {StreamModule} from "./modules/stream"
|
|
44
|
+
import {SystemModule} from "./modules/system"
|
|
45
|
+
import {MiniappsModule} from "./modules/miniapps"
|
|
46
|
+
import {ActionsModule} from "./modules/actions"
|
|
47
|
+
import {BlobModule} from "./modules/blob"
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Public types
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Typed display capabilities for the scene API. All limit fields are optional
|
|
55
|
+
* in the type because older hosts don't send them — treat absence as "unknown",
|
|
56
|
+
* not zero. Populated on the "ready" event (null in `start()`).
|
|
57
|
+
*/
|
|
58
|
+
export interface DisplayCapabilities {
|
|
59
|
+
/** Public drawable canvas in px — raw coordinate space for `display.render()` boxes. */
|
|
60
|
+
width?: number
|
|
61
|
+
height?: number
|
|
62
|
+
/** False ⇒ the device can't position elements; scenes degrade to text walls host-side. */
|
|
63
|
+
canPosition?: boolean
|
|
64
|
+
/** Element budgets. Rects share the text pool on container-based devices. */
|
|
65
|
+
maxTextElements?: number
|
|
66
|
+
maxImageElements?: number
|
|
67
|
+
/** Per-image dimension cap (box-level), when the device has one. */
|
|
68
|
+
maxImagePx?: {width: number; height: number}
|
|
69
|
+
shapes?: string[]
|
|
70
|
+
intensityLevels?: number
|
|
71
|
+
partialUpdate?: boolean
|
|
72
|
+
/** Legacy capability fields (resolution, isColor, maxTextLines, …) ride along. */
|
|
73
|
+
[key: string]: unknown
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Minimal snapshot of the currently-connected glasses. Phone-provided. */
|
|
77
|
+
export interface GlassesCapabilities {
|
|
78
|
+
/** Display block — null/absent on displayless devices (e.g. Mentra Live). */
|
|
79
|
+
display?: DisplayCapabilities | null
|
|
80
|
+
[key: string]: unknown
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type MiniappVisibility = "foreground" | "background"
|
|
84
|
+
|
|
85
|
+
export interface MiniappSessionOptions extends CreateTransportOptions {
|
|
86
|
+
/** Override auto-detected packageName. Normally provided via window.MentraOS. */
|
|
87
|
+
packageName?: string
|
|
88
|
+
/** Override the ready timeout. Default 10s. */
|
|
89
|
+
connectTimeoutMs?: number
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ConnectAckPayload {
|
|
93
|
+
type: MiniappResponseType.CONNECT_ACK
|
|
94
|
+
userId: string
|
|
95
|
+
packageName: string
|
|
96
|
+
capabilities: GlassesCapabilities | null
|
|
97
|
+
visibility?: MiniappVisibility
|
|
98
|
+
colorScheme?: MiniappColorScheme
|
|
99
|
+
/**
|
|
100
|
+
* Manifest-declared permission record. Mirrors cloud SDK v3's PermissionRecord:
|
|
101
|
+
* `{location, microphone, camera, notifications, calendar}` — booleans
|
|
102
|
+
* indicating whether the miniapp's manifest declared each. This is
|
|
103
|
+
* declaration-only; OS-grant state is not modeled.
|
|
104
|
+
*/
|
|
105
|
+
permissions?: PermissionRecord
|
|
106
|
+
/** Miniapp-scoped backend auth. Never a Core or runtime token. */
|
|
107
|
+
auth?: MiniappAuthState
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface MiniappAuthState {
|
|
111
|
+
mentraUserId: string
|
|
112
|
+
oemId?: string
|
|
113
|
+
token: string
|
|
114
|
+
expiresAt: number
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface AuthUpdatePayload {
|
|
118
|
+
type: MiniappResponseType.AUTH_UPDATE
|
|
119
|
+
auth?: MiniappAuthState
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface AuthRefreshResult {
|
|
123
|
+
auth?: MiniappAuthState
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Manifest-declared permission record. v3-aligned: lowercase canonical keys.
|
|
128
|
+
* Booleans indicate whether the miniapp declared each in its manifest.json.
|
|
129
|
+
*/
|
|
130
|
+
export type PermissionType = "location" | "microphone" | "camera" | "notifications" | "calendar"
|
|
131
|
+
export type PermissionRecord = Record<PermissionType, boolean>
|
|
132
|
+
|
|
133
|
+
const ALL_PERMISSION_TYPES: readonly PermissionType[] = [
|
|
134
|
+
"location",
|
|
135
|
+
"microphone",
|
|
136
|
+
"camera",
|
|
137
|
+
"notifications",
|
|
138
|
+
"calendar",
|
|
139
|
+
] as const
|
|
140
|
+
|
|
141
|
+
export class NotConnectedError extends Error {
|
|
142
|
+
readonly code = MiniappErrorCode.NOT_CONNECTED
|
|
143
|
+
constructor(message = "MiniappSession is not connected") {
|
|
144
|
+
super(message)
|
|
145
|
+
this.name = "NotConnectedError"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface MiniappRequestError {
|
|
150
|
+
code: string
|
|
151
|
+
message: string
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Implementation
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
interface PendingRequest {
|
|
159
|
+
requestId: string
|
|
160
|
+
resolve: (value: unknown) => void
|
|
161
|
+
reject: (error: MiniappRequestError) => void
|
|
162
|
+
/** Timeout handle; cleared when the response (or a transport failure) settles the request. */
|
|
163
|
+
timer?: ReturnType<typeof setTimeout>
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 10_000
|
|
167
|
+
|
|
168
|
+
// Hard ceiling on how long a single bridge request waits for the host's
|
|
169
|
+
// REQUEST_RESULT. Without it, a host that never responds (a hung cloud call, a
|
|
170
|
+
// GPS fix that never arrives, a native handler that stalls) leaves the request
|
|
171
|
+
// promise pending FOREVER — which is what stalled navigation at "Starting…"
|
|
172
|
+
// (the controller's `starting` guard never reset because start() never settled).
|
|
173
|
+
// 60s is generous: it covers a slow route computation while still guaranteeing
|
|
174
|
+
// the promise eventually rejects so callers can roll back / surface an error.
|
|
175
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000
|
|
176
|
+
|
|
177
|
+
type SessionEmitterEvents = {
|
|
178
|
+
ready: () => void
|
|
179
|
+
error: (error: Error) => void
|
|
180
|
+
/**
|
|
181
|
+
* Last-chance hook before the transport closes. Fires when the phone
|
|
182
|
+
* sends WILL_DISCONNECT, or when this session calls `disconnect()`
|
|
183
|
+
* locally. Handlers run synchronously and may issue one final
|
|
184
|
+
* `sendOneShot`/`sendRequest` (e.g. `display.render([])`); async work won't complete
|
|
185
|
+
* before the socket closes.
|
|
186
|
+
*/
|
|
187
|
+
beforeDisconnect: (reason: string) => void
|
|
188
|
+
disconnect: (reason: string) => void
|
|
189
|
+
visibility: (v: MiniappVisibility) => void
|
|
190
|
+
capabilities: (cap: GlassesCapabilities | null) => void
|
|
191
|
+
colorScheme: (scheme: MiniappColorScheme) => void
|
|
192
|
+
permissions: (perms: PermissionRecord) => void
|
|
193
|
+
speakerState: (event: import("./modules/speaker").SpeakerStateEvent) => void
|
|
194
|
+
auth: (auth: MiniappAuthState) => void
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export class MiniappSession {
|
|
198
|
+
public readonly auth: AuthModule
|
|
199
|
+
public readonly display: DisplayManager
|
|
200
|
+
/**
|
|
201
|
+
* Internal subscription registry + escape hatch.
|
|
202
|
+
*
|
|
203
|
+
* Domain modules (`session.mic`, `session.input`, etc.) are the canonical
|
|
204
|
+
* surface for typed event subscriptions. `events.subscribe(...)` remains as
|
|
205
|
+
* a forward-compat escape hatch for new event types not yet wrapped on a
|
|
206
|
+
* domain module.
|
|
207
|
+
*/
|
|
208
|
+
public readonly events: EventManager
|
|
209
|
+
public readonly speaker: SpeakerModule
|
|
210
|
+
public readonly camera: CameraModule
|
|
211
|
+
public readonly cloud: CloudModule
|
|
212
|
+
public readonly dashboard: DashboardAPI
|
|
213
|
+
public readonly glasses: GlassesModule
|
|
214
|
+
public readonly heading: HeadingModule
|
|
215
|
+
public readonly imu: ImuModule
|
|
216
|
+
public readonly input: InputModule
|
|
217
|
+
public readonly led: LedModule
|
|
218
|
+
public readonly location: LocationModule
|
|
219
|
+
public readonly mic: MicModule
|
|
220
|
+
public readonly navigation: NavigationModule
|
|
221
|
+
public readonly permissions: PermissionsModule
|
|
222
|
+
public readonly phone: PhoneModule
|
|
223
|
+
public readonly storage: SimpleStorage
|
|
224
|
+
/**
|
|
225
|
+
* Phone-local persistent BINARY storage (`session.blob`) — the binary
|
|
226
|
+
* counterpart to `session.storage`. Files on disk, scoped to this miniapp.
|
|
227
|
+
* Writes/reads are chunked so large payloads (e.g. captured audio fed in via
|
|
228
|
+
* `session.mic.onAudioChunk`) never cross the bridge in one message.
|
|
229
|
+
*/
|
|
230
|
+
public readonly blob: BlobModule
|
|
231
|
+
public readonly stream: StreamModule
|
|
232
|
+
public readonly system: SystemModule
|
|
233
|
+
public readonly transcription: TranscriptionModule
|
|
234
|
+
public readonly translation: TranslationModule
|
|
235
|
+
/**
|
|
236
|
+
* UI message bus to the bound WebView (when one is open).
|
|
237
|
+
* Background-only API surface; mirrors the WebView's `mentra` global
|
|
238
|
+
* with inverted buffering policy (background drops when no WebView is
|
|
239
|
+
* bound; the WebView buffers until ready).
|
|
240
|
+
*/
|
|
241
|
+
public readonly ui: UIModule
|
|
242
|
+
/**
|
|
243
|
+
* Inter-miniapp lifecycle + discovery (list / start / stop). SYSTEM-only —
|
|
244
|
+
* calls reject with NOT_PERMITTED unless this miniapp is a system app.
|
|
245
|
+
*/
|
|
246
|
+
public readonly miniapps: MiniappsModule
|
|
247
|
+
/**
|
|
248
|
+
* Inter-miniapp action layer. `invoke` (SYSTEM-only) calls another miniapp's
|
|
249
|
+
* declared action; `handle` (open to all) exposes one of your own.
|
|
250
|
+
*/
|
|
251
|
+
public readonly actions: ActionsModule
|
|
252
|
+
|
|
253
|
+
/** Phone-declared glasses capabilities. Null until CONNECT_ACK arrives. */
|
|
254
|
+
public capabilities: GlassesCapabilities | null = null
|
|
255
|
+
public userId = ""
|
|
256
|
+
public packageName = ""
|
|
257
|
+
public visibility: MiniappVisibility = "foreground"
|
|
258
|
+
/** Host color scheme. Seeded from window.MentraOS, updated via session events. */
|
|
259
|
+
public colorScheme: MiniappColorScheme = "light"
|
|
260
|
+
|
|
261
|
+
/** True after CONNECT_ACK. Observe with waitForReady() or the "ready" event. */
|
|
262
|
+
public ready = false
|
|
263
|
+
|
|
264
|
+
private readonly transport: Transport
|
|
265
|
+
private readonly connectTimeoutMs: number
|
|
266
|
+
private readonly emitter = new EventEmitter<SessionEmitterEvents>()
|
|
267
|
+
private authState: MiniappAuthState | null = null
|
|
268
|
+
private readonly authWaiters = new Set<{
|
|
269
|
+
minTtlMs: number
|
|
270
|
+
resolve: (auth: MiniappAuthState) => void
|
|
271
|
+
reject: (error: Error) => void
|
|
272
|
+
timer: ReturnType<typeof setTimeout>
|
|
273
|
+
}>()
|
|
274
|
+
private authRefreshPromise: Promise<MiniappAuthState | null> | null = null
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Outbound queue for anything sent before CONNECT_ACK. Flushed in FIFO order
|
|
278
|
+
* once the phone responds with CONNECT_ACK.
|
|
279
|
+
*/
|
|
280
|
+
private readonly outboundQueue: string[] = []
|
|
281
|
+
private readonly pendingRequests = new Map<string, PendingRequest>()
|
|
282
|
+
private connectPromise: Promise<void> | null = null
|
|
283
|
+
private disposed = false
|
|
284
|
+
|
|
285
|
+
/** Manifest-declared permission cache. Updated on CONNECT_ACK / PERMISSIONS_UPDATE. */
|
|
286
|
+
private _permissions: PermissionRecord = {
|
|
287
|
+
location: false,
|
|
288
|
+
microphone: false,
|
|
289
|
+
camera: false,
|
|
290
|
+
notifications: false,
|
|
291
|
+
calendar: false,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
constructor(options: MiniappSessionOptions = {}) {
|
|
295
|
+
this.transport = createTransport(options)
|
|
296
|
+
this.connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS
|
|
297
|
+
|
|
298
|
+
const injected = getMentraOSGlobals()
|
|
299
|
+
this.packageName = options.packageName ?? injected.packageName ?? ""
|
|
300
|
+
if (injected.colorScheme === "light" || injected.colorScheme === "dark") {
|
|
301
|
+
this.colorScheme = injected.colorScheme
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.auth = new AuthModule(this)
|
|
305
|
+
this.events = new EventManager(this)
|
|
306
|
+
this.speaker = new SpeakerModule(this)
|
|
307
|
+
this.camera = new CameraModule(this)
|
|
308
|
+
this.cloud = new CloudModule(this)
|
|
309
|
+
this.dashboard = new DashboardAPI(this)
|
|
310
|
+
this.display = new DisplayManager(this)
|
|
311
|
+
this.glasses = new GlassesModule(this)
|
|
312
|
+
this.heading = new HeadingModule(this)
|
|
313
|
+
this.imu = new ImuModule(this)
|
|
314
|
+
this.input = new InputModule(this)
|
|
315
|
+
this.led = new LedModule(this)
|
|
316
|
+
this.location = new LocationModule(this)
|
|
317
|
+
this.mic = new MicModule(this)
|
|
318
|
+
this.navigation = new NavigationModule(this)
|
|
319
|
+
this.permissions = new PermissionsModule(this)
|
|
320
|
+
this.phone = new PhoneModule(this)
|
|
321
|
+
this.storage = new SimpleStorage(this)
|
|
322
|
+
this.blob = new BlobModule(this)
|
|
323
|
+
this.stream = new StreamModule(this)
|
|
324
|
+
this.system = new SystemModule(this)
|
|
325
|
+
this.transcription = new TranscriptionModule(this)
|
|
326
|
+
this.translation = new TranslationModule(this)
|
|
327
|
+
this.ui = new UIModuleImpl(this)
|
|
328
|
+
this.miniapps = new MiniappsModule(this)
|
|
329
|
+
this.actions = new ActionsModule(this)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* @internal — synchronous lookup against the cached manifest-declared
|
|
334
|
+
* permission record from CONNECT_ACK / PERMISSIONS_UPDATE. Domain modules
|
|
335
|
+
* use this to expose their `hasPermission` getters without going to the
|
|
336
|
+
* wire. Returns false until CONNECT_ACK arrives.
|
|
337
|
+
*
|
|
338
|
+
* `manifestKey` is the manifest's UPPER_CASE permission name
|
|
339
|
+
* (MICROPHONE, CAMERA, LOCATION, READ_NOTIFICATIONS, etc.). Maps to v3's
|
|
340
|
+
* lowercase canonical keys internally.
|
|
341
|
+
*/
|
|
342
|
+
_hasManifestPermission(manifestKey: string): boolean {
|
|
343
|
+
const canonical = manifestKeyToCanonical(manifestKey)
|
|
344
|
+
if (!canonical) return false
|
|
345
|
+
return this._permissions[canonical] === true
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @internal — read the current manifest-declared permission record.
|
|
350
|
+
* Powers session.permissions.getAll(). Returns a fresh shallow copy so
|
|
351
|
+
* callers can't mutate internal state.
|
|
352
|
+
*/
|
|
353
|
+
_getPermissions(): PermissionRecord {
|
|
354
|
+
return {...this._permissions}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** @internal — current miniapp-scoped backend auth, if the host provided one. */
|
|
358
|
+
_getAuth(): MiniappAuthState | null {
|
|
359
|
+
return this.authState ? {...this.authState} : null
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* @internal — wait for a scoped miniapp token. Used by session.auth; not part
|
|
364
|
+
* of the public SDK surface because authors should never manage wire events.
|
|
365
|
+
*/
|
|
366
|
+
_waitForAuth(minTtlMs: number, timeoutMs = 10_000): Promise<MiniappAuthState> {
|
|
367
|
+
const current = this.authState
|
|
368
|
+
if (current && this.authHasTtl(current, minTtlMs)) {
|
|
369
|
+
return Promise.resolve({...current})
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
void this.requestAuthRefresh(minTtlMs)
|
|
373
|
+
|
|
374
|
+
return new Promise((resolve, reject) => {
|
|
375
|
+
const waiter = {
|
|
376
|
+
minTtlMs,
|
|
377
|
+
resolve,
|
|
378
|
+
reject,
|
|
379
|
+
timer: setTimeout(() => {
|
|
380
|
+
this.authWaiters.delete(waiter)
|
|
381
|
+
reject(new NotConnectedError("Miniapp auth token is not available"))
|
|
382
|
+
}, timeoutMs),
|
|
383
|
+
}
|
|
384
|
+
this.authWaiters.add(waiter)
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* @internal — subscribe to a raw stream type. Domain modules call this; it
|
|
390
|
+
* delegates to the EventManager registry. Underscore prefix signals "not
|
|
391
|
+
* part of the public SDK surface — use session.mic.onAudioChunk /
|
|
392
|
+
* session.transcription.on(...)
|
|
393
|
+
* etc. instead."
|
|
394
|
+
*/
|
|
395
|
+
_subscribe(
|
|
396
|
+
streamType: string,
|
|
397
|
+
handler: (data: unknown) => void,
|
|
398
|
+
options: {forceLocal?: boolean} = {},
|
|
399
|
+
): UnsubscribeFn {
|
|
400
|
+
return this.events.subscribe(streamType, handler, options)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// -------------------------------------------------------------------------
|
|
404
|
+
// Connection lifecycle
|
|
405
|
+
// -------------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Connect to LocalMiniappRuntime. Idempotent — calling multiple times
|
|
409
|
+
* returns the same Promise.
|
|
410
|
+
*/
|
|
411
|
+
connect(): Promise<void> {
|
|
412
|
+
if (this.disposed) {
|
|
413
|
+
return Promise.reject(new NotConnectedError("MiniappSession was disposed"))
|
|
414
|
+
}
|
|
415
|
+
if (this.connectPromise) return this.connectPromise
|
|
416
|
+
|
|
417
|
+
// Register the readiness listener BEFORE any awaits so that a synchronous
|
|
418
|
+
// CONNECT_ACK delivery in tests (or the phone) can't race the subscription.
|
|
419
|
+
const readyPromise = new Promise<void>((resolve, reject) => {
|
|
420
|
+
const timer = setTimeout(() => {
|
|
421
|
+
const err = new Error("MiniappSession: CONNECT_ACK timeout")
|
|
422
|
+
this.failAllPending({code: MiniappErrorCode.NOT_CONNECTED, message: err.message})
|
|
423
|
+
this.emitter.emit("error", err)
|
|
424
|
+
reject(err)
|
|
425
|
+
}, this.connectTimeoutMs)
|
|
426
|
+
|
|
427
|
+
this.emitter.once("ready", () => {
|
|
428
|
+
clearTimeout(timer)
|
|
429
|
+
resolve()
|
|
430
|
+
})
|
|
431
|
+
this.emitter.once("error", (err) => {
|
|
432
|
+
clearTimeout(timer)
|
|
433
|
+
reject(err)
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
this.connectPromise = (async () => {
|
|
438
|
+
this.transport.onMessage((raw) => this.handleIncoming(raw))
|
|
439
|
+
this.transport.onDisconnect((reason) => this.handleTransportDisconnect(reason))
|
|
440
|
+
await this.transport.open()
|
|
441
|
+
|
|
442
|
+
const requestId = makeRequestId()
|
|
443
|
+
const connectPayload = {
|
|
444
|
+
type: MiniappRequestType.CONNECT,
|
|
445
|
+
packageName: this.packageName,
|
|
446
|
+
}
|
|
447
|
+
this.transport.send(serializeEnvelope({payload: connectPayload, requestId}))
|
|
448
|
+
|
|
449
|
+
await readyPromise
|
|
450
|
+
})()
|
|
451
|
+
|
|
452
|
+
return this.connectPromise
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Resolves when `ready` becomes true, or rejects if connect failed. */
|
|
456
|
+
waitForReady(): Promise<void> {
|
|
457
|
+
if (this.ready) return Promise.resolve()
|
|
458
|
+
return this.connect()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
isConnected(): boolean {
|
|
462
|
+
return this.ready && this.transport.isOpen()
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
disconnect(): void {
|
|
466
|
+
if (this.disposed) return
|
|
467
|
+
this.disposed = true
|
|
468
|
+
// Give listeners one synchronous chance to flush final messages
|
|
469
|
+
// (e.g. display.render([])) before we tear down the transport.
|
|
470
|
+
try {
|
|
471
|
+
this.emitter.emit("beforeDisconnect", "disconnect called")
|
|
472
|
+
} catch (err) {
|
|
473
|
+
// A throwing handler must not block teardown.
|
|
474
|
+
console.warn("[MiniappSession] beforeDisconnect handler threw:", err)
|
|
475
|
+
}
|
|
476
|
+
this.failAllPending({code: MiniappErrorCode.REQUEST_ABORTED, message: "Session disconnected"})
|
|
477
|
+
try {
|
|
478
|
+
this.transport.close()
|
|
479
|
+
} catch {
|
|
480
|
+
// ignore
|
|
481
|
+
}
|
|
482
|
+
this.ready = false
|
|
483
|
+
this.emitter.emit("disconnect", "disconnect called")
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// -------------------------------------------------------------------------
|
|
487
|
+
// Outbound traffic — modules call these
|
|
488
|
+
// -------------------------------------------------------------------------
|
|
489
|
+
|
|
490
|
+
/** Send a fire-and-forget request that does not need a response. */
|
|
491
|
+
sendOneShot(payload: object): void {
|
|
492
|
+
const envelope: MiniappEnvelope = {payload}
|
|
493
|
+
this.enqueueOrSend(serializeEnvelope(envelope))
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Send a request and get a Promise that resolves with the REQUEST_RESULT payload.
|
|
498
|
+
* Rejects with a MiniappRequestError if the phone returns an error result.
|
|
499
|
+
*
|
|
500
|
+
* `opts.timeoutMs` overrides the default request timeout. Pass `0` to disable
|
|
501
|
+
* it entirely for inherently long-running requests whose duration is unbounded
|
|
502
|
+
* (e.g. audio playback that resolves only when the clip finishes) — those still
|
|
503
|
+
* settle via REQUEST_RESULT or `failAllPending` on disconnect, so they can't
|
|
504
|
+
* leak. Most requests should keep the default ceiling.
|
|
505
|
+
*/
|
|
506
|
+
sendRequest<TResult = unknown>(payload: object, opts?: {timeoutMs?: number}): Promise<TResult> {
|
|
507
|
+
if (this.disposed) {
|
|
508
|
+
return Promise.reject(new NotConnectedError())
|
|
509
|
+
}
|
|
510
|
+
const requestId = makeRequestId()
|
|
511
|
+
const envelope: MiniappEnvelope = {payload, requestId}
|
|
512
|
+
const timeoutMs = opts?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
|
|
513
|
+
return new Promise<TResult>((resolve, reject) => {
|
|
514
|
+
// Reject (and drop) the request if the host never sends a REQUEST_RESULT,
|
|
515
|
+
// so the promise can't hang forever. The REQUEST_RESULT / failAllPending
|
|
516
|
+
// paths clear this timer before settling. A non-positive timeout opts out
|
|
517
|
+
// (long-running requests rely on the host result / disconnect to settle).
|
|
518
|
+
const timer =
|
|
519
|
+
timeoutMs > 0
|
|
520
|
+
? setTimeout(() => {
|
|
521
|
+
const pending = this.pendingRequests.get(requestId)
|
|
522
|
+
if (!pending) return
|
|
523
|
+
this.pendingRequests.delete(requestId)
|
|
524
|
+
pending.reject({
|
|
525
|
+
code: MiniappErrorCode.ACTION_TIMEOUT,
|
|
526
|
+
message: "Request timed out waiting for a response from the host",
|
|
527
|
+
})
|
|
528
|
+
}, timeoutMs)
|
|
529
|
+
: undefined
|
|
530
|
+
this.pendingRequests.set(requestId, {
|
|
531
|
+
requestId,
|
|
532
|
+
resolve: resolve as (v: unknown) => void,
|
|
533
|
+
reject,
|
|
534
|
+
timer,
|
|
535
|
+
})
|
|
536
|
+
this.enqueueOrSend(serializeEnvelope(envelope))
|
|
537
|
+
})
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// -------------------------------------------------------------------------
|
|
541
|
+
// Event emitter — external API
|
|
542
|
+
// -------------------------------------------------------------------------
|
|
543
|
+
|
|
544
|
+
on<K extends keyof SessionEmitterEvents>(event: K, handler: SessionEmitterEvents[K]): () => void {
|
|
545
|
+
this.emitter.on(event, handler as (...args: unknown[]) => void)
|
|
546
|
+
return () => this.emitter.off(event, handler as (...args: unknown[]) => void)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
off<K extends keyof SessionEmitterEvents>(event: K, handler: SessionEmitterEvents[K]): void {
|
|
550
|
+
this.emitter.off(event, handler as (...args: unknown[]) => void)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Last-chance hook before the transport closes. Fires either when the
|
|
555
|
+
* phone notifies the session of an imminent disconnect (~50ms grace
|
|
556
|
+
* window before the socket is torn down) or when this session's
|
|
557
|
+
* `disconnect()` is called locally. Use it to flush final cleanup
|
|
558
|
+
* messages — e.g. `display.render([])` — synchronously. Async work
|
|
559
|
+
* started here will not complete before the socket closes.
|
|
560
|
+
*/
|
|
561
|
+
onBeforeDisconnect(handler: (reason: string) => void): () => void {
|
|
562
|
+
return this.on("beforeDisconnect", handler)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
onVisibilityChange(handler: (v: MiniappVisibility) => void): () => void {
|
|
566
|
+
return this.on("visibility", handler)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
onCapabilitiesChange(handler: (cap: GlassesCapabilities | null) => void): () => void {
|
|
570
|
+
return this.on("capabilities", handler)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
onColorSchemeChange(handler: (scheme: MiniappColorScheme) => void): () => void {
|
|
574
|
+
return this.on("colorScheme", handler)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// -------------------------------------------------------------------------
|
|
578
|
+
// Internal — transport glue
|
|
579
|
+
// -------------------------------------------------------------------------
|
|
580
|
+
|
|
581
|
+
private enqueueOrSend(raw: string): void {
|
|
582
|
+
if (this.ready) {
|
|
583
|
+
try {
|
|
584
|
+
this.transport.send(raw)
|
|
585
|
+
} catch (err) {
|
|
586
|
+
// If send fails post-ready, treat as transport error.
|
|
587
|
+
this.emitter.emit("error", err as Error)
|
|
588
|
+
}
|
|
589
|
+
return
|
|
590
|
+
}
|
|
591
|
+
this.outboundQueue.push(raw)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private flushQueue(): void {
|
|
595
|
+
const queue = this.outboundQueue.splice(0)
|
|
596
|
+
for (const raw of queue) {
|
|
597
|
+
try {
|
|
598
|
+
this.transport.send(raw)
|
|
599
|
+
} catch (err) {
|
|
600
|
+
this.emitter.emit("error", err as Error)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
private handleIncoming(raw: string): void {
|
|
606
|
+
const envelope = parseEnvelope(raw)
|
|
607
|
+
if (!envelope) return
|
|
608
|
+
|
|
609
|
+
const payload = envelope.payload as {type?: string} & Record<string, unknown>
|
|
610
|
+
const type = payload?.type
|
|
611
|
+
|
|
612
|
+
switch (type) {
|
|
613
|
+
case MiniappResponseType.CONNECT_ACK: {
|
|
614
|
+
const ack = payload as unknown as ConnectAckPayload
|
|
615
|
+
this.userId = ack.userId ?? ""
|
|
616
|
+
if (ack.packageName) this.packageName = ack.packageName
|
|
617
|
+
this.capabilities = ack.capabilities ?? null
|
|
618
|
+
if (ack.visibility) this.visibility = ack.visibility
|
|
619
|
+
if (ack.colorScheme === "light" || ack.colorScheme === "dark") {
|
|
620
|
+
this.colorScheme = ack.colorScheme
|
|
621
|
+
}
|
|
622
|
+
if (ack.auth) {
|
|
623
|
+
this.applyAuth(ack.auth)
|
|
624
|
+
if (!this.userId) this.userId = ack.auth.mentraUserId
|
|
625
|
+
}
|
|
626
|
+
// Populate the manifest-declared permission cache. Older runtimes
|
|
627
|
+
// that don't send `permissions` leave the all-false default in place
|
|
628
|
+
// — `hasPermission` getters will simply return false.
|
|
629
|
+
if (ack.permissions) this.applyPermissions(ack.permissions)
|
|
630
|
+
this.ready = true
|
|
631
|
+
this.flushQueue()
|
|
632
|
+
this.emitter.emit("ready")
|
|
633
|
+
// Don't resolve request correlation here — CONNECT_ACK has no requestId.
|
|
634
|
+
return
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
case MiniappResponseType.AUTH_UPDATE: {
|
|
638
|
+
const next = (payload as unknown as AuthUpdatePayload).auth
|
|
639
|
+
if (next) {
|
|
640
|
+
this.applyAuth(next)
|
|
641
|
+
if (!this.userId) this.userId = next.mentraUserId
|
|
642
|
+
}
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
case MiniappResponseType.PERMISSIONS_UPDATE: {
|
|
647
|
+
const next = payload.permissions as PermissionRecord | undefined
|
|
648
|
+
if (next) this.applyPermissions(next)
|
|
649
|
+
return
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
case MiniappResponseType.SPEAKER_STATE: {
|
|
653
|
+
const state = payload.state as "idle" | "loading" | "playing" | "stopped" | "error" | undefined
|
|
654
|
+
if (!state) return
|
|
655
|
+
const event = {
|
|
656
|
+
state,
|
|
657
|
+
errorCode: payload.errorCode as string | undefined,
|
|
658
|
+
errorMessage: payload.errorMessage as string | undefined,
|
|
659
|
+
durationMs: payload.durationMs as number | undefined,
|
|
660
|
+
}
|
|
661
|
+
this.speaker._applyState(event)
|
|
662
|
+
this.emitter.emit("speakerState", event)
|
|
663
|
+
return
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
case MiniappRequestType.PING: {
|
|
667
|
+
// Phone → miniapp keepalive ping. Auto-reply with PONG.
|
|
668
|
+
const pong: object = {type: MiniappResponseType.PONG}
|
|
669
|
+
const env: MiniappEnvelope = {
|
|
670
|
+
payload: pong,
|
|
671
|
+
...(envelope.requestId ? {requestId: envelope.requestId} : {}),
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
this.transport.send(serializeEnvelope(env))
|
|
675
|
+
} catch {
|
|
676
|
+
// Ignore; next ping will fail too and runtime will unregister.
|
|
677
|
+
}
|
|
678
|
+
return
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
case MiniappResponseType.EVENT: {
|
|
682
|
+
const streamType = payload.streamType as string | undefined
|
|
683
|
+
if (!streamType) return
|
|
684
|
+
this.events._forwardEvent(
|
|
685
|
+
streamType,
|
|
686
|
+
payload.data,
|
|
687
|
+
payload.transcriptionRoute as TranscriptionEventRoute | undefined,
|
|
688
|
+
)
|
|
689
|
+
return
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
case MiniappResponseType.ACTION_CALL: {
|
|
693
|
+
// Another miniapp invoked one of our declared actions. Route to the
|
|
694
|
+
// registered handler (or buffer briefly for one to register). The SDK
|
|
695
|
+
// replies with an ACTION_RESULT request keyed by callId.
|
|
696
|
+
const callId = payload.callId as string | undefined
|
|
697
|
+
const actionId = payload.actionId as string | undefined
|
|
698
|
+
if (!callId || !actionId) return
|
|
699
|
+
const params = (payload.params as Record<string, unknown> | undefined) ?? {}
|
|
700
|
+
const callerPackageName = (payload.callerPackageName as string | undefined) ?? ""
|
|
701
|
+
this.actions._deliver(callId, actionId, params, {callerPackageName})
|
|
702
|
+
return
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
case MiniappResponseType.CAPABILITIES_UPDATE: {
|
|
706
|
+
const cap = (payload.capabilities as GlassesCapabilities | null) ?? null
|
|
707
|
+
this.capabilities = cap
|
|
708
|
+
this.emitter.emit("capabilities", cap)
|
|
709
|
+
return
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
case MiniappResponseType.VISIBILITY_CHANGE: {
|
|
713
|
+
const next = payload.visibility as MiniappVisibility | undefined
|
|
714
|
+
if (next === "foreground" || next === "background") {
|
|
715
|
+
this.visibility = next
|
|
716
|
+
this.emitter.emit("visibility", next)
|
|
717
|
+
}
|
|
718
|
+
return
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
case MiniappResponseType.COLOR_SCHEME_CHANGE: {
|
|
722
|
+
const next = payload.colorScheme as MiniappColorScheme | undefined
|
|
723
|
+
if (next === "light" || next === "dark") {
|
|
724
|
+
this.colorScheme = next
|
|
725
|
+
this.emitter.emit("colorScheme", next)
|
|
726
|
+
}
|
|
727
|
+
return
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
case MiniappResponseType.REQUEST_RESULT: {
|
|
731
|
+
const requestId = envelope.requestId
|
|
732
|
+
if (!requestId) return
|
|
733
|
+
const pending = this.pendingRequests.get(requestId)
|
|
734
|
+
if (!pending) return
|
|
735
|
+
this.pendingRequests.delete(requestId)
|
|
736
|
+
if (pending.timer) clearTimeout(pending.timer)
|
|
737
|
+
if (payload.ok === false) {
|
|
738
|
+
const err = (payload.error as MiniappRequestError | undefined) ?? {
|
|
739
|
+
code: MiniappErrorCode.INTERNAL,
|
|
740
|
+
message: "Unknown error",
|
|
741
|
+
}
|
|
742
|
+
pending.reject(err)
|
|
743
|
+
} else {
|
|
744
|
+
pending.resolve(payload.data ?? null)
|
|
745
|
+
}
|
|
746
|
+
return
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
case MiniappResponseType.WILL_DISCONNECT: {
|
|
750
|
+
const reason = (payload.reason as string | undefined) ?? "phone unregistering"
|
|
751
|
+
try {
|
|
752
|
+
this.emitter.emit("beforeDisconnect", reason)
|
|
753
|
+
} catch (err) {
|
|
754
|
+
console.warn("[MiniappSession] beforeDisconnect handler threw:", err)
|
|
755
|
+
}
|
|
756
|
+
return
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
case MiniappResponseType.ERROR: {
|
|
760
|
+
const err = new Error((payload.message as string | undefined) ?? "MiniappSession error")
|
|
761
|
+
this.emitter.emit("error", err)
|
|
762
|
+
return
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
default:
|
|
766
|
+
// Unknown type — drop silently. Forward-compat.
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private handleTransportDisconnect(reason: string): void {
|
|
772
|
+
this.ready = false
|
|
773
|
+
this.failAllPending({code: MiniappErrorCode.NOT_CONNECTED, message: `Transport disconnected: ${reason}`})
|
|
774
|
+
this.emitter.emit("disconnect", reason)
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private failAllPending(error: MiniappRequestError): void {
|
|
778
|
+
for (const pending of this.pendingRequests.values()) {
|
|
779
|
+
if (pending.timer) clearTimeout(pending.timer)
|
|
780
|
+
pending.reject(error)
|
|
781
|
+
}
|
|
782
|
+
this.pendingRequests.clear()
|
|
783
|
+
// Auth waiters live outside pendingRequests — they're resolved by an
|
|
784
|
+
// AUTH_UPDATE push, not a correlated REQUEST_RESULT — so a transport drop,
|
|
785
|
+
// dispose, or CONNECT_ACK timeout must reject them here too. Otherwise an
|
|
786
|
+
// in-flight session.auth.getToken()/fetch() hangs until its 10s waiter
|
|
787
|
+
// timeout instead of failing fast with the disconnect error.
|
|
788
|
+
for (const waiter of Array.from(this.authWaiters)) {
|
|
789
|
+
clearTimeout(waiter.timer)
|
|
790
|
+
this.authWaiters.delete(waiter)
|
|
791
|
+
waiter.reject(new NotConnectedError(error.message))
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
private authHasTtl(auth: MiniappAuthState, minTtlMs: number): boolean {
|
|
796
|
+
return auth.expiresAt - Date.now() > minTtlMs
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private requestAuthRefresh(minTtlMs: number): Promise<MiniappAuthState | null> {
|
|
800
|
+
if (this.authRefreshPromise) return this.authRefreshPromise
|
|
801
|
+
if (!this.ready || !this.transport.isOpen()) return Promise.resolve(null)
|
|
802
|
+
|
|
803
|
+
this.authRefreshPromise = this.sendRequest<AuthRefreshResult>({
|
|
804
|
+
type: MiniappRequestType.AUTH_REFRESH,
|
|
805
|
+
minTtlMs,
|
|
806
|
+
})
|
|
807
|
+
.then((result) => {
|
|
808
|
+
const auth = result?.auth
|
|
809
|
+
if (auth) this.applyAuth(auth)
|
|
810
|
+
return auth ?? null
|
|
811
|
+
})
|
|
812
|
+
.catch((err) => {
|
|
813
|
+
console.warn("[MiniappSession] auth refresh failed:", err)
|
|
814
|
+
return null
|
|
815
|
+
})
|
|
816
|
+
.finally(() => {
|
|
817
|
+
this.authRefreshPromise = null
|
|
818
|
+
})
|
|
819
|
+
|
|
820
|
+
return this.authRefreshPromise
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
private applyAuth(next: MiniappAuthState): void {
|
|
824
|
+
this.authState = {...next}
|
|
825
|
+
for (const waiter of Array.from(this.authWaiters)) {
|
|
826
|
+
if (!this.authHasTtl(next, waiter.minTtlMs)) continue
|
|
827
|
+
clearTimeout(waiter.timer)
|
|
828
|
+
this.authWaiters.delete(waiter)
|
|
829
|
+
waiter.resolve({...next})
|
|
830
|
+
}
|
|
831
|
+
this.emitter.emit("auth", {...next})
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Update the cached permission record. Idempotent: emits "permissions"
|
|
836
|
+
* only when the record actually changed. Sanitizes incoming objects to
|
|
837
|
+
* the v3 PermissionType union.
|
|
838
|
+
*/
|
|
839
|
+
private applyPermissions(next: Partial<PermissionRecord>): void {
|
|
840
|
+
let changed = false
|
|
841
|
+
const updated: PermissionRecord = {...this._permissions}
|
|
842
|
+
for (const k of ALL_PERMISSION_TYPES) {
|
|
843
|
+
const v = next[k] === true
|
|
844
|
+
if (updated[k] !== v) {
|
|
845
|
+
updated[k] = v
|
|
846
|
+
changed = true
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (changed) {
|
|
850
|
+
this._permissions = updated
|
|
851
|
+
this.emitter.emit("permissions", {...updated})
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Map a manifest UPPER_CASE permission name to the lowercase canonical key
|
|
858
|
+
* used by `session.permissions`. Returns null for unknown manifest keys.
|
|
859
|
+
*
|
|
860
|
+
* BACKGROUND_LOCATION + POST_NOTIFICATIONS map onto the same canonical keys
|
|
861
|
+
* as their non-suffixed counterparts (location / notifications) since
|
|
862
|
+
* `has()` is "do I have *any* form of this permission declared".
|
|
863
|
+
*/
|
|
864
|
+
function manifestKeyToCanonical(manifestKey: string): PermissionType | null {
|
|
865
|
+
switch (manifestKey.toUpperCase()) {
|
|
866
|
+
case "MICROPHONE":
|
|
867
|
+
return "microphone"
|
|
868
|
+
case "CAMERA":
|
|
869
|
+
return "camera"
|
|
870
|
+
case "LOCATION":
|
|
871
|
+
case "BACKGROUND_LOCATION":
|
|
872
|
+
return "location"
|
|
873
|
+
case "READ_NOTIFICATIONS":
|
|
874
|
+
case "POST_NOTIFICATIONS":
|
|
875
|
+
return "notifications"
|
|
876
|
+
case "CALENDAR":
|
|
877
|
+
return "calendar"
|
|
878
|
+
default:
|
|
879
|
+
return null
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** @internal — for the permissions module's onUpdate plumbing. */
|
|
884
|
+
export function _allPermissionTypes(): readonly PermissionType[] {
|
|
885
|
+
return ALL_PERMISSION_TYPES
|
|
886
|
+
}
|