@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,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview ActionsModule — `session.actions`.
|
|
3
|
+
*
|
|
4
|
+
* The MCP-shaped capability layer, both directions:
|
|
5
|
+
* - `invoke(pkg, actionId, params)` — call another miniapp's declared action.
|
|
6
|
+
* SYSTEM-only; headless-wakes the target if it's stopped.
|
|
7
|
+
* - `handle(actionId, fn)` — expose one of your own actions. Open to all
|
|
8
|
+
* miniapps. Mirrors `session.ui.handle` (one handler per id, throws on
|
|
9
|
+
* double-register, errors propagate to the caller).
|
|
10
|
+
*
|
|
11
|
+
* `invoke` ↔ `handle` are the two ends of one wire (keyed by `actionId`), which
|
|
12
|
+
* is why they share this namespace rather than being split across
|
|
13
|
+
* `session.miniapps`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {MiniappErrorCode, MiniappRequestType} from "../protocol"
|
|
17
|
+
import {MiniappSession} from "../session"
|
|
18
|
+
|
|
19
|
+
/** Context handed to an action handler. `callerPackageName` is host-stamped. */
|
|
20
|
+
export interface ActionContext {
|
|
21
|
+
/** The package that invoked this action (trustworthy — set by the host). */
|
|
22
|
+
callerPackageName: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ActionHandler = (params: Record<string, unknown>, ctx: ActionContext) => unknown | Promise<unknown>
|
|
26
|
+
|
|
27
|
+
export interface InvokeOptions {
|
|
28
|
+
/** Overall timeout in ms (default 30s, max 120s — host-enforced). */
|
|
29
|
+
timeoutMs?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface BufferedCall {
|
|
33
|
+
callId: string
|
|
34
|
+
params: Record<string, unknown>
|
|
35
|
+
ctx: ActionContext
|
|
36
|
+
timer: ReturnType<typeof setTimeout>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* How long the SDK holds an inbound ACTION_CALL waiting for its handler to
|
|
41
|
+
* register (a just-woken miniapp registers handlers a beat after CONNECT).
|
|
42
|
+
* If no handler appears in this window the call is failed with
|
|
43
|
+
* NO_ACTION_HANDLER. Mirrors the UI bus's pre-handler buffering.
|
|
44
|
+
*/
|
|
45
|
+
const HANDLER_WAIT_MS = 5_000
|
|
46
|
+
|
|
47
|
+
export class ActionsModule {
|
|
48
|
+
private readonly handlers = new Map<string, ActionHandler>()
|
|
49
|
+
/** Inbound calls buffered until their handler registers, keyed by actionId. */
|
|
50
|
+
private readonly buffered = new Map<string, BufferedCall[]>()
|
|
51
|
+
|
|
52
|
+
constructor(private readonly session: MiniappSession) {}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Invoke a declared action on another miniapp. Resolves with the handler's
|
|
56
|
+
* return value, rejects with a MiniappRequestError (NOT_PERMITTED,
|
|
57
|
+
* APP_NOT_FOUND, ACTION_NOT_FOUND, WAKE_FAILED, NO_ACTION_HANDLER,
|
|
58
|
+
* ACTION_TIMEOUT, PAYLOAD_TOO_LARGE, or the handler's own error). SYSTEM-only.
|
|
59
|
+
*/
|
|
60
|
+
invoke<TResult = unknown>(
|
|
61
|
+
packageName: string,
|
|
62
|
+
actionId: string,
|
|
63
|
+
params?: Record<string, unknown>,
|
|
64
|
+
opts?: InvokeOptions,
|
|
65
|
+
): Promise<TResult> {
|
|
66
|
+
return this.session.sendRequest<TResult>({
|
|
67
|
+
type: MiniappRequestType.ACTION_INVOKE,
|
|
68
|
+
targetPackageName: packageName,
|
|
69
|
+
actionId,
|
|
70
|
+
params: params ?? {},
|
|
71
|
+
...(opts?.timeoutMs != null ? {timeoutMs: opts.timeoutMs} : {}),
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Register a handler for one of this miniapp's declared actions. One handler
|
|
77
|
+
* per actionId — throws synchronously on double-register. The returned
|
|
78
|
+
* function unregisters. Declare the action in `miniapp.json` for it to be
|
|
79
|
+
* invokable; an undeclared id still registers but the host never routes to it.
|
|
80
|
+
*/
|
|
81
|
+
handle(actionId: string, handler: ActionHandler): () => void {
|
|
82
|
+
if (this.handlers.has(actionId)) {
|
|
83
|
+
throw new Error(`session.actions.handle: a handler is already registered for "${actionId}"`)
|
|
84
|
+
}
|
|
85
|
+
this.handlers.set(actionId, handler)
|
|
86
|
+
|
|
87
|
+
// Flush any calls that arrived before this handler registered.
|
|
88
|
+
const waiting = this.buffered.get(actionId)
|
|
89
|
+
if (waiting) {
|
|
90
|
+
this.buffered.delete(actionId)
|
|
91
|
+
for (const call of waiting) {
|
|
92
|
+
clearTimeout(call.timer)
|
|
93
|
+
void this.dispatch(call.callId, actionId, call.params, call.ctx)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return () => {
|
|
98
|
+
if (this.handlers.get(actionId) === handler) this.handlers.delete(actionId)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @internal — called by MiniappSession.handleIncoming on an ACTION_CALL.
|
|
104
|
+
* Routes to the registered handler, or buffers briefly for one to appear.
|
|
105
|
+
*/
|
|
106
|
+
_deliver(callId: string, actionId: string, params: Record<string, unknown>, ctx: ActionContext): void {
|
|
107
|
+
if (this.handlers.has(actionId)) {
|
|
108
|
+
void this.dispatch(callId, actionId, params, ctx)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
// No handler yet — buffer, and fail with NO_ACTION_HANDLER if none registers.
|
|
112
|
+
const timer = setTimeout(() => {
|
|
113
|
+
const arr = this.buffered.get(actionId)
|
|
114
|
+
if (arr) {
|
|
115
|
+
const idx = arr.findIndex((c) => c.callId === callId)
|
|
116
|
+
if (idx >= 0) arr.splice(idx, 1)
|
|
117
|
+
if (arr.length === 0) this.buffered.delete(actionId)
|
|
118
|
+
}
|
|
119
|
+
this.sendResult(callId, false, undefined, {
|
|
120
|
+
code: MiniappErrorCode.NO_ACTION_HANDLER,
|
|
121
|
+
message: `No handler registered for action "${actionId}"`,
|
|
122
|
+
})
|
|
123
|
+
}, HANDLER_WAIT_MS)
|
|
124
|
+
const arr = this.buffered.get(actionId) ?? []
|
|
125
|
+
arr.push({callId, params, ctx, timer})
|
|
126
|
+
this.buffered.set(actionId, arr)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async dispatch(
|
|
130
|
+
callId: string,
|
|
131
|
+
actionId: string,
|
|
132
|
+
params: Record<string, unknown>,
|
|
133
|
+
ctx: ActionContext,
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
const handler = this.handlers.get(actionId)
|
|
136
|
+
if (!handler) return
|
|
137
|
+
try {
|
|
138
|
+
const result = await handler(params, ctx)
|
|
139
|
+
this.sendResult(callId, true, result ?? null)
|
|
140
|
+
} catch (e) {
|
|
141
|
+
this.sendResult(callId, false, undefined, {
|
|
142
|
+
code: MiniappErrorCode.INTERNAL,
|
|
143
|
+
message: (e as Error)?.message ?? "action handler threw",
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private sendResult(callId: string, ok: boolean, result?: unknown, error?: {code: string; message: string}): void {
|
|
149
|
+
this.session.sendOneShot({
|
|
150
|
+
type: MiniappRequestType.ACTION_RESULT,
|
|
151
|
+
callId,
|
|
152
|
+
ok,
|
|
153
|
+
...(ok ? {result} : {error}),
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type {MiniappSession, MiniappAuthState} from "../session"
|
|
2
|
+
|
|
3
|
+
export interface AuthFetchOptions extends RequestInit {
|
|
4
|
+
/**
|
|
5
|
+
* Minimum remaining token lifetime before using the current token. Defaults to
|
|
6
|
+
* 30 seconds. If the token is absent or too close to expiry, the call waits
|
|
7
|
+
* for the host's next auth update.
|
|
8
|
+
*/
|
|
9
|
+
minTtlMs?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULT_MIN_TTL_MS = 30_000
|
|
13
|
+
|
|
14
|
+
export class AuthModule {
|
|
15
|
+
constructor(private readonly session: MiniappSession) {}
|
|
16
|
+
|
|
17
|
+
get current(): MiniappAuthState | null {
|
|
18
|
+
return this.session._getAuth()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get mentraUserId(): string | null {
|
|
22
|
+
return this.current?.mentraUserId ?? null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get oemId(): string | null {
|
|
26
|
+
return this.current?.oemId ?? null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getToken(options: {minTtlMs?: number} = {}): Promise<string> {
|
|
30
|
+
const auth = await this.session._waitForAuth(options.minTtlMs ?? DEFAULT_MIN_TTL_MS)
|
|
31
|
+
return auth.token
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async getAuthHeader(options: {minTtlMs?: number} = {}): Promise<string> {
|
|
35
|
+
return `Bearer ${await this.getToken(options)}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async fetch(input: RequestInfo | URL, init: AuthFetchOptions = {}): Promise<Response> {
|
|
39
|
+
const {minTtlMs, ...requestInit} = init
|
|
40
|
+
const token = await this.getToken({minTtlMs})
|
|
41
|
+
const headers = mergeHeaders(requestInit.headers, {Authorization: `Bearer ${token}`})
|
|
42
|
+
return fetch(input, {...requestInit, headers})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onUpdate(handler: (auth: MiniappAuthState) => void): () => void {
|
|
46
|
+
return this.session.on("auth", handler)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mergeHeaders(base: HeadersInit | undefined, next: Record<string, string>): Record<string, string> {
|
|
51
|
+
const headers: Record<string, string> = {}
|
|
52
|
+
|
|
53
|
+
if (Array.isArray(base)) {
|
|
54
|
+
for (const [key, value] of base) {
|
|
55
|
+
headers[key] = value
|
|
56
|
+
}
|
|
57
|
+
} else if (typeof Headers !== "undefined" && base instanceof Headers) {
|
|
58
|
+
base.forEach((value, key) => {
|
|
59
|
+
headers[key] = value
|
|
60
|
+
})
|
|
61
|
+
} else if (base && typeof base === "object") {
|
|
62
|
+
for (const [key, value] of Object.entries(base)) {
|
|
63
|
+
if (typeof value === "string") headers[key] = value
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {...headers, ...next}
|
|
68
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Dependency-free base64 codec for the miniapp background runtime.
|
|
3
|
+
*
|
|
4
|
+
* The background bundle runs in a per-miniapp JS engine (JavaScriptCore on iOS,
|
|
5
|
+
* QuickJS on Android) where `Buffer`, `btoa`/`atob`, and Node APIs are NOT
|
|
6
|
+
* guaranteed to exist. `session.blob` round-trips binary as base64 strings over
|
|
7
|
+
* the JSON bridge, so it needs an encoder/decoder that works with nothing but
|
|
8
|
+
* the language. This is that — standard RFC 4648 base64, no padding tricks.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
12
|
+
|
|
13
|
+
/** Reverse lookup table: char code → 6-bit value (or -1). Built once. */
|
|
14
|
+
const LOOKUP: Int16Array = (() => {
|
|
15
|
+
const t = new Int16Array(256).fill(-1)
|
|
16
|
+
for (let i = 0; i < ALPHABET.length; i++) t[ALPHABET.charCodeAt(i)] = i
|
|
17
|
+
t["=".charCodeAt(0)] = 0
|
|
18
|
+
return t
|
|
19
|
+
})()
|
|
20
|
+
|
|
21
|
+
/** Coerce a Uint8Array / ArrayBuffer / ArrayBufferView into a Uint8Array view (no copy when possible). */
|
|
22
|
+
export function toUint8Array(input: Uint8Array | ArrayBuffer | ArrayBufferView): Uint8Array {
|
|
23
|
+
if (input instanceof Uint8Array) return input
|
|
24
|
+
if (input instanceof ArrayBuffer) return new Uint8Array(input)
|
|
25
|
+
return new Uint8Array(input.buffer, input.byteOffset, input.byteLength)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Encode bytes to a base64 string. */
|
|
29
|
+
export function bytesToBase64(input: Uint8Array | ArrayBuffer): string {
|
|
30
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input)
|
|
31
|
+
const len = bytes.length
|
|
32
|
+
let out = ""
|
|
33
|
+
let i = 0
|
|
34
|
+
for (; i + 2 < len; i += 3) {
|
|
35
|
+
const n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]
|
|
36
|
+
out += ALPHABET[(n >> 18) & 63] + ALPHABET[(n >> 12) & 63] + ALPHABET[(n >> 6) & 63] + ALPHABET[n & 63]
|
|
37
|
+
}
|
|
38
|
+
const rem = len - i
|
|
39
|
+
if (rem === 1) {
|
|
40
|
+
const n = bytes[i] << 16
|
|
41
|
+
out += ALPHABET[(n >> 18) & 63] + ALPHABET[(n >> 12) & 63] + "=="
|
|
42
|
+
} else if (rem === 2) {
|
|
43
|
+
const n = (bytes[i] << 16) | (bytes[i + 1] << 8)
|
|
44
|
+
out += ALPHABET[(n >> 18) & 63] + ALPHABET[(n >> 12) & 63] + ALPHABET[(n >> 6) & 63] + "="
|
|
45
|
+
}
|
|
46
|
+
return out
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Decode a base64 string to bytes. Ignores whitespace; tolerant of missing padding. */
|
|
50
|
+
export function base64ToBytes(b64: string): Uint8Array {
|
|
51
|
+
// Count valid (non-whitespace) chars and trailing '=' to size the output exactly.
|
|
52
|
+
let validChars = 0
|
|
53
|
+
let pad = 0
|
|
54
|
+
for (let i = 0; i < b64.length; i++) {
|
|
55
|
+
const c = b64.charCodeAt(i)
|
|
56
|
+
if (c === 0x3d) {
|
|
57
|
+
pad++
|
|
58
|
+
validChars++
|
|
59
|
+
} else if (LOOKUP[c] !== -1) {
|
|
60
|
+
validChars++
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const fullGroups = Math.floor(validChars / 4)
|
|
64
|
+
const remChars = validChars - fullGroups * 4
|
|
65
|
+
let outLen = fullGroups * 3 - (pad > 0 && remChars === 0 ? pad : 0)
|
|
66
|
+
if (remChars === 2) outLen += 1
|
|
67
|
+
else if (remChars === 3) outLen += 2
|
|
68
|
+
if (outLen < 0) outLen = 0
|
|
69
|
+
|
|
70
|
+
const out = new Uint8Array(outLen)
|
|
71
|
+
let acc = 0
|
|
72
|
+
let bits = 0
|
|
73
|
+
let o = 0
|
|
74
|
+
for (let i = 0; i < b64.length && o < outLen; i++) {
|
|
75
|
+
const c = b64.charCodeAt(i)
|
|
76
|
+
if (c === 0x3d) break // '=' — end of data
|
|
77
|
+
const v = LOOKUP[c]
|
|
78
|
+
if (v === -1) continue // skip whitespace / stray chars
|
|
79
|
+
acc = (acc << 6) | v
|
|
80
|
+
bits += 6
|
|
81
|
+
if (bits >= 8) {
|
|
82
|
+
bits -= 8
|
|
83
|
+
out[o++] = (acc >> bits) & 0xff
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview BlobModule — phone-local persistent BINARY storage, keyed and
|
|
3
|
+
* scoped to (userId, packageName). The binary sibling of `session.storage`:
|
|
4
|
+
* where storage holds small strings, blob holds arbitrary bytes (PDFs, EPUBs,
|
|
5
|
+
* audio, video, model files, caches) as files on the phone.
|
|
6
|
+
*
|
|
7
|
+
* It deliberately mirrors `session.storage`'s shape — `set` / `get` / `delete` /
|
|
8
|
+
* `keys` / `has` / `clear` — so it's instantly familiar, plus the things files
|
|
9
|
+
* (and not strings) need:
|
|
10
|
+
* - `setFromUrl(key, url)` — the host downloads a URL straight to disk
|
|
11
|
+
* (like the web Cache API's `cache.add`, iOS `URLSession` download task, or
|
|
12
|
+
* Android `DownloadManager`). Bytes never cross the bridge.
|
|
13
|
+
* - `importFile()` — opens the OS file picker and stores the chosen file.
|
|
14
|
+
* Bytes never cross the bridge.
|
|
15
|
+
* - `share(key)` — hands the file to the OS share sheet.
|
|
16
|
+
* - `createWriteStream` / `createReadStream` — for streaming huge payloads
|
|
17
|
+
* (named like Node's `fs.createWriteStream`).
|
|
18
|
+
*
|
|
19
|
+
* BACKGROUND-ONLY. Binary doesn't belong in the WebView. A blob's `uri` is a
|
|
20
|
+
* `file://` path in the app's private storage — fine for `session.speaker.play`,
|
|
21
|
+
* `blob.share`, or another host capability, but a WebView generally can't load
|
|
22
|
+
* it directly (route rendering through a host viewer for now).
|
|
23
|
+
*
|
|
24
|
+
* Transfer model: the bridge moves JSON strings through a per-miniapp JS engine
|
|
25
|
+
* (JSC/QuickJS) with a watchdog, so in-JS writes/reads are CHUNKED (≤ ~1 MB raw
|
|
26
|
+
* per call). Prefer `setFromUrl` / `importFile` when you can — they keep the
|
|
27
|
+
* bytes entirely host-side.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import {MiniappRequestType} from "../protocol"
|
|
31
|
+
import {MiniappSession} from "../session"
|
|
32
|
+
import {base64ToBytes, bytesToBase64, toUint8Array} from "./base64"
|
|
33
|
+
|
|
34
|
+
/** Metadata for one stored blob. `uri` is a `file://` path. */
|
|
35
|
+
export interface BlobMeta {
|
|
36
|
+
/** The key it's stored under (caller-chosen). */
|
|
37
|
+
key: string
|
|
38
|
+
/** Optional display/file name (e.g. the original filename for an import). */
|
|
39
|
+
name?: string
|
|
40
|
+
/** MIME type, e.g. "application/pdf". */
|
|
41
|
+
mimeType: string
|
|
42
|
+
/** Size on disk in bytes. */
|
|
43
|
+
bytes: number
|
|
44
|
+
/** Epoch ms first written. */
|
|
45
|
+
createdAt: number
|
|
46
|
+
/** Epoch ms last written. */
|
|
47
|
+
updatedAt: number
|
|
48
|
+
/** Content md5 (lowercase hex), computed host-side (skipped for very large blobs). */
|
|
49
|
+
md5?: string
|
|
50
|
+
/** `file://` URI. Feed to `session.speaker.play`, `blob.share`, etc. */
|
|
51
|
+
uri: string
|
|
52
|
+
/** App-defined metadata persisted with the blob. */
|
|
53
|
+
meta?: Record<string, string | number | boolean>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface BlobSetOptions {
|
|
57
|
+
/** Default "application/octet-stream". */
|
|
58
|
+
mimeType?: string
|
|
59
|
+
name?: string
|
|
60
|
+
meta?: Record<string, string | number | boolean>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface BlobSetFromUrlOptions extends BlobSetOptions {
|
|
64
|
+
/** Request headers (e.g. an Authorization token) for the download. */
|
|
65
|
+
headers?: Record<string, string>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface BlobImportOptions {
|
|
69
|
+
/** Key to store under. Default: a generated key. */
|
|
70
|
+
key?: string
|
|
71
|
+
/** Restrict the picker to a MIME type, e.g. "application/pdf". */
|
|
72
|
+
mimeType?: string
|
|
73
|
+
meta?: Record<string, string | number | boolean>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Raw bytes per chunked write. ~1 MB raw → ~1.34 MB base64, under the watchdog. */
|
|
77
|
+
export const BLOB_WRITE_CHUNK_BYTES = 1024 * 1024
|
|
78
|
+
const BLOB_WRITE_CHUNK_B64 = Math.floor(BLOB_WRITE_CHUNK_BYTES / 3) * 4
|
|
79
|
+
/** Max bytes `bytes()` will buffer before throwing — read large blobs as a stream. */
|
|
80
|
+
export const BLOB_READ_ALL_MAX_BYTES = 32 * 1024 * 1024
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Streaming writer (`fs.createWriteStream`-like). Call `write`/`writeBase64` as
|
|
84
|
+
* many times as you like (each is auto-split into bridge-safe chunks), then
|
|
85
|
+
* `close()` to publish, or `abort()` to discard the partial blob.
|
|
86
|
+
*/
|
|
87
|
+
export class BlobWriter {
|
|
88
|
+
private settled = false
|
|
89
|
+
|
|
90
|
+
constructor(
|
|
91
|
+
private readonly session: MiniappSession,
|
|
92
|
+
readonly key: string,
|
|
93
|
+
) {}
|
|
94
|
+
|
|
95
|
+
/** Append raw bytes. Auto-chunked. */
|
|
96
|
+
async write(chunk: Uint8Array | ArrayBuffer): Promise<void> {
|
|
97
|
+
this.assertOpen()
|
|
98
|
+
const bytes = toUint8Array(chunk)
|
|
99
|
+
for (let off = 0; off < bytes.length; off += BLOB_WRITE_CHUNK_BYTES) {
|
|
100
|
+
await this.send(bytesToBase64(bytes.subarray(off, off + BLOB_WRITE_CHUNK_BYTES)))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Append already-base64-encoded bytes (e.g. straight from `mic.onAudioChunk`). */
|
|
105
|
+
async writeBase64(b64: string): Promise<void> {
|
|
106
|
+
this.assertOpen()
|
|
107
|
+
// Slice on 4-char boundaries so each chunk is whole base64 groups.
|
|
108
|
+
for (let off = 0; off < b64.length; off += BLOB_WRITE_CHUNK_B64) {
|
|
109
|
+
await this.send(b64.slice(off, off + BLOB_WRITE_CHUNK_B64))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Overwrite bytes at a fixed offset within the not-yet-closed blob (a seek
|
|
115
|
+
* write). Must stay within already-written bytes; does not grow the blob.
|
|
116
|
+
* Advanced — used e.g. to patch a container header on finalize.
|
|
117
|
+
*/
|
|
118
|
+
async writeAt(offset: number, chunk: Uint8Array | ArrayBuffer): Promise<void> {
|
|
119
|
+
this.assertOpen()
|
|
120
|
+
await this.session.sendRequest<{bytesWritten: number}>({
|
|
121
|
+
type: MiniappRequestType.BLOB_WRITE,
|
|
122
|
+
key: this.key,
|
|
123
|
+
offset,
|
|
124
|
+
base64: bytesToBase64(toUint8Array(chunk)),
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Finalize and return the blob's metadata. Optional `meta` merges into the record. */
|
|
129
|
+
async close(meta?: Record<string, string | number | boolean>): Promise<BlobMeta> {
|
|
130
|
+
this.assertOpen()
|
|
131
|
+
this.settled = true
|
|
132
|
+
return this.session.sendRequest<BlobMeta>({type: MiniappRequestType.BLOB_COMMIT, key: this.key, meta})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Discard the partial blob. Idempotent. */
|
|
136
|
+
async abort(): Promise<void> {
|
|
137
|
+
if (this.settled) return
|
|
138
|
+
this.settled = true
|
|
139
|
+
await this.session.sendRequest<void>({type: MiniappRequestType.BLOB_ABORT, key: this.key})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private assertOpen(): void {
|
|
143
|
+
if (this.settled) throw new Error("BlobWriter is already closed/aborted")
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private send(base64: string): Promise<{bytesWritten: number}> {
|
|
147
|
+
return this.session.sendRequest<{bytesWritten: number}>({
|
|
148
|
+
type: MiniappRequestType.BLOB_WRITE,
|
|
149
|
+
key: this.key,
|
|
150
|
+
base64,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Streaming reader (`fs.createReadStream`-like). */
|
|
156
|
+
export class BlobReader {
|
|
157
|
+
private closed = false
|
|
158
|
+
|
|
159
|
+
constructor(
|
|
160
|
+
private readonly session: MiniappSession,
|
|
161
|
+
readonly handle: string,
|
|
162
|
+
readonly meta: BlobMeta,
|
|
163
|
+
) {}
|
|
164
|
+
|
|
165
|
+
/** Read up to `maxBytes` (default one chunk). `done` is true at end of file. */
|
|
166
|
+
async read(maxBytes = BLOB_WRITE_CHUNK_BYTES): Promise<{bytes: Uint8Array; done: boolean}> {
|
|
167
|
+
if (this.closed) throw new Error("BlobReader is closed")
|
|
168
|
+
const res = await this.session.sendRequest<{base64: string; done: boolean}>({
|
|
169
|
+
type: MiniappRequestType.BLOB_READ,
|
|
170
|
+
handle: this.handle,
|
|
171
|
+
maxBytes,
|
|
172
|
+
})
|
|
173
|
+
return {bytes: base64ToBytes(res?.base64 ?? ""), done: !!res?.done}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async close(): Promise<void> {
|
|
177
|
+
if (this.closed) return
|
|
178
|
+
this.closed = true
|
|
179
|
+
await this.session.sendRequest<void>({type: MiniappRequestType.BLOB_CLOSE_READ, handle: this.handle})
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export class BlobModule {
|
|
184
|
+
constructor(private readonly session: MiniappSession) {}
|
|
185
|
+
|
|
186
|
+
// ── write ────────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
/** Store bytes under `key`. `data` may be a Uint8Array/ArrayBuffer or a base64 string. */
|
|
189
|
+
async set(key: string, data: Uint8Array | ArrayBuffer | string, opts: BlobSetOptions = {}): Promise<BlobMeta> {
|
|
190
|
+
const writer = await this.createWriteStream(key, opts)
|
|
191
|
+
try {
|
|
192
|
+
if (typeof data === "string") await writer.writeBase64(data)
|
|
193
|
+
else await writer.write(data)
|
|
194
|
+
return await writer.close()
|
|
195
|
+
} catch (err) {
|
|
196
|
+
await writer.abort().catch(() => {})
|
|
197
|
+
throw err
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Download `url` straight into a blob under `key`, host-side. The bytes never
|
|
203
|
+
* cross the bridge (like the web Cache API's `cache.add`). `opts.headers` lets
|
|
204
|
+
* you pass auth. Resolves to the stored blob's metadata.
|
|
205
|
+
*/
|
|
206
|
+
setFromUrl(key: string, url: string, opts: BlobSetFromUrlOptions = {}): Promise<BlobMeta> {
|
|
207
|
+
return this.session.sendRequest<BlobMeta>({
|
|
208
|
+
type: MiniappRequestType.BLOB_SET_FROM_URL,
|
|
209
|
+
key,
|
|
210
|
+
url,
|
|
211
|
+
mimeType: opts.mimeType,
|
|
212
|
+
name: opts.name,
|
|
213
|
+
headers: opts.headers,
|
|
214
|
+
meta: opts.meta,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Open the OS file picker and store the chosen file as a blob, host-side.
|
|
220
|
+
* Resolves to the blob's metadata, or `null` if the user cancelled.
|
|
221
|
+
*/
|
|
222
|
+
importFile(opts: BlobImportOptions = {}): Promise<BlobMeta | null> {
|
|
223
|
+
return this.session.sendRequest<BlobMeta | null>({
|
|
224
|
+
type: MiniappRequestType.BLOB_IMPORT,
|
|
225
|
+
key: opts.key,
|
|
226
|
+
mimeType: opts.mimeType,
|
|
227
|
+
meta: opts.meta,
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Open a streaming writer for `key` (for large/streamed payloads). */
|
|
232
|
+
async createWriteStream(key: string, opts: BlobSetOptions = {}): Promise<BlobWriter> {
|
|
233
|
+
const res = await this.session.sendRequest<{key: string}>({
|
|
234
|
+
type: MiniappRequestType.BLOB_CREATE,
|
|
235
|
+
key,
|
|
236
|
+
mimeType: opts.mimeType,
|
|
237
|
+
name: opts.name,
|
|
238
|
+
meta: opts.meta,
|
|
239
|
+
})
|
|
240
|
+
return new BlobWriter(this.session, res.key)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── read ─────────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
/** Metadata (incl. the `file://` uri) for `key`, or null if absent. */
|
|
246
|
+
get(key: string): Promise<BlobMeta | null> {
|
|
247
|
+
return this.session.sendRequest<BlobMeta | null>({type: MiniappRequestType.BLOB_GET, key})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Alias of `get` — the file-stat verb. */
|
|
251
|
+
stat(key: string): Promise<BlobMeta | null> {
|
|
252
|
+
return this.get(key)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** True iff a blob is stored under `key`. */
|
|
256
|
+
async has(key: string): Promise<boolean> {
|
|
257
|
+
return (await this.get(key)) !== null
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Every key this miniapp has stored, newest first. */
|
|
261
|
+
async keys(): Promise<string[]> {
|
|
262
|
+
return (await this.list()).map((m) => m.key)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Every blob this miniapp owns, newest first. */
|
|
266
|
+
async list(): Promise<BlobMeta[]> {
|
|
267
|
+
const res = await this.session.sendRequest<{blobs: BlobMeta[]}>({type: MiniappRequestType.BLOB_LIST})
|
|
268
|
+
return res?.blobs ?? []
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Open a streaming reader. Prefer `get(key).uri` when you just need to play/share it. */
|
|
272
|
+
async createReadStream(key: string): Promise<BlobReader> {
|
|
273
|
+
const res = await this.session.sendRequest<{handle: string; meta: BlobMeta}>({
|
|
274
|
+
type: MiniappRequestType.BLOB_OPEN_READ,
|
|
275
|
+
key,
|
|
276
|
+
})
|
|
277
|
+
return new BlobReader(this.session, res.handle, res.meta)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Read a whole blob into memory as bytes, or null if absent. Throws past the cap — stream big ones. */
|
|
281
|
+
async bytes(key: string): Promise<Uint8Array | null> {
|
|
282
|
+
let reader: BlobReader
|
|
283
|
+
try {
|
|
284
|
+
reader = await this.createReadStream(key)
|
|
285
|
+
} catch {
|
|
286
|
+
return null
|
|
287
|
+
}
|
|
288
|
+
const parts: Uint8Array[] = []
|
|
289
|
+
let total = 0
|
|
290
|
+
try {
|
|
291
|
+
for (;;) {
|
|
292
|
+
const {bytes, done} = await reader.read()
|
|
293
|
+
if (bytes.length) {
|
|
294
|
+
total += bytes.length
|
|
295
|
+
if (total > BLOB_READ_ALL_MAX_BYTES) {
|
|
296
|
+
throw new Error(`Blob "${key}" exceeds the in-memory read cap — stream it with createReadStream()`)
|
|
297
|
+
}
|
|
298
|
+
parts.push(bytes)
|
|
299
|
+
}
|
|
300
|
+
if (done) break
|
|
301
|
+
}
|
|
302
|
+
} finally {
|
|
303
|
+
await reader.close().catch(() => {})
|
|
304
|
+
}
|
|
305
|
+
const out = new Uint8Array(total)
|
|
306
|
+
let at = 0
|
|
307
|
+
for (const p of parts) {
|
|
308
|
+
out.set(p, at)
|
|
309
|
+
at += p.length
|
|
310
|
+
}
|
|
311
|
+
return out
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── manage ───────────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
/** Per-app usage + the quota ceiling, in bytes. */
|
|
317
|
+
usage(): Promise<{bytes: number; count: number; quotaBytes: number}> {
|
|
318
|
+
return this.session.sendRequest<{bytes: number; count: number; quotaBytes: number}>({
|
|
319
|
+
type: MiniappRequestType.BLOB_USAGE,
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Delete the blob under `key`. No-op if absent. */
|
|
324
|
+
async delete(key: string): Promise<void> {
|
|
325
|
+
await this.session.sendRequest<void>({type: MiniappRequestType.BLOB_DELETE, key})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Delete every blob this miniapp owns. */
|
|
329
|
+
async clear(): Promise<void> {
|
|
330
|
+
await this.session.sendRequest<void>({type: MiniappRequestType.BLOB_CLEAR})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Share the blob via the OS share sheet (host shares from disk — no bytes cross the bridge). */
|
|
334
|
+
async share(key: string): Promise<{success: boolean; cancelled?: boolean}> {
|
|
335
|
+
const res = await this.session.sendRequest<{success: boolean; cancelled?: boolean}>({
|
|
336
|
+
type: MiniappRequestType.BLOB_SHARE,
|
|
337
|
+
key,
|
|
338
|
+
})
|
|
339
|
+
return res ?? {success: false}
|
|
340
|
+
}
|
|
341
|
+
}
|