@pickleball/expo-sdk 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +69 -0
  3. package/dist/advanced-BXDrDl1h.d.ts +316 -0
  4. package/dist/advanced-BdAa2Erv.d.cts +316 -0
  5. package/dist/advanced.cjs +35 -0
  6. package/dist/advanced.d.cts +3 -0
  7. package/dist/advanced.d.ts +3 -0
  8. package/dist/advanced.js +35 -0
  9. package/dist/chunk-2JHULJZ7.js +265 -0
  10. package/dist/chunk-6JOMESNT.js +9 -0
  11. package/dist/chunk-7OX2DSKI.cjs +33 -0
  12. package/dist/chunk-DHDX3ZOO.js +800 -0
  13. package/dist/chunk-FODE2W2H.js +1953 -0
  14. package/dist/chunk-KFQGP6VL.js +33 -0
  15. package/dist/chunk-LCWGNGEN.cjs +265 -0
  16. package/dist/chunk-TBPR2QND.cjs +1953 -0
  17. package/dist/chunk-TRLASNPQ.cjs +9 -0
  18. package/dist/chunk-YGSQ25UA.cjs +800 -0
  19. package/dist/device-agent/index.cjs +498 -0
  20. package/dist/device-agent/index.d.cts +157 -0
  21. package/dist/device-agent/index.d.ts +157 -0
  22. package/dist/device-agent/index.js +498 -0
  23. package/dist/engine-CCSSqPbZ.d.cts +516 -0
  24. package/dist/engine-CCSSqPbZ.d.ts +516 -0
  25. package/dist/index.cjs +339 -5569
  26. package/dist/index.d.cts +22 -1406
  27. package/dist/index.d.ts +22 -1406
  28. package/dist/index.js +269 -5401
  29. package/dist/match/index.cjs +2456 -0
  30. package/dist/match/index.d.cts +253 -0
  31. package/dist/match/index.d.ts +253 -0
  32. package/dist/match/index.js +2456 -0
  33. package/dist/match/remote/index.cjs +21 -0
  34. package/dist/match/remote/index.d.cts +67 -0
  35. package/dist/match/remote/index.d.ts +67 -0
  36. package/dist/match/remote/index.js +21 -0
  37. package/dist/match/voice/index.cjs +23 -0
  38. package/dist/match/voice/index.d.cts +114 -0
  39. package/dist/match/voice/index.d.ts +114 -0
  40. package/dist/match/voice/index.js +23 -0
  41. package/dist/plugin.cjs +53 -70
  42. package/dist/plugin.d.cts +13 -0
  43. package/dist/plugin.d.ts +13 -0
  44. package/dist/plugin.js +20 -15
  45. package/dist/types-CrmcX408.d.cts +176 -0
  46. package/dist/types-CrmcX408.d.ts +176 -0
  47. package/dist/types-DiKWjwvN.d.cts +96 -0
  48. package/dist/types-DiKWjwvN.d.ts +96 -0
  49. package/dist/version.cjs +4 -32
  50. package/dist/version.js +2 -1
  51. package/package.json +68 -4
  52. package/src/advanced.ts +47 -0
  53. package/src/device-agent/agent.ts +443 -0
  54. package/src/device-agent/component.tsx +49 -0
  55. package/src/device-agent/convex-control-channel.ts +82 -0
  56. package/src/device-agent/http-transport.ts +143 -0
  57. package/src/device-agent/index.ts +4 -0
  58. package/src/engine.ts +21 -5
  59. package/src/errors.ts +16 -0
  60. package/src/http-session-provider.ts +19 -13
  61. package/src/index.ts +38 -14
  62. package/src/livekit-room-adapter.ts +39 -2
  63. package/src/livestream-screen.tsx +26 -4
  64. package/src/match/index.ts +29 -0
  65. package/src/match/provider.tsx +4 -2
  66. package/src/match/remote/hid-gesture.ts +10 -2
  67. package/src/match/remote/types.ts +6 -4
  68. package/src/match/voice/scheduler.ts +6 -1
  69. package/src/match/voice/service.ts +16 -4
  70. package/src/native-runtime.ts +54 -4
  71. package/src/plugin.ts +35 -14
  72. package/src/provider.tsx +35 -4
  73. package/src/rtmp-room-adapter.ts +196 -0
  74. package/dist/chunk-VHLUJQG5.js +0 -17
@@ -0,0 +1,157 @@
1
+ import { Q as LivestreamEngine, B as BootstrapInput, D as DeviceMetricsSample } from '../engine-CCSSqPbZ.js';
2
+
3
+ /**
4
+ * DeviceAgent — đăng ký thiết bị với backend khi app mở và giữ heartbeat
5
+ * presence (12s/lần khi foreground) qua proxy. Khi dashboard yêu cầu "Kết nối",
6
+ * heartbeat trả về connect-token → agent mở kênh điều khiển realtime (Convex
7
+ * websocket) để nhận lệnh start/stop livestream tức thì. Publish/unpublish sau
8
+ * khi vào standby vẫn đi đường LiveKit data channel sẵn có của engine.
9
+ */
10
+ interface DeviceAgentDeviceInfo {
11
+ appVersion?: string;
12
+ deviceName?: string;
13
+ deviceModel?: string;
14
+ osVersion?: string;
15
+ }
16
+ interface DeviceCommand {
17
+ id: string;
18
+ type: "start_livestream" | "stop_livestream";
19
+ payload: {
20
+ title?: string;
21
+ visibility?: "public" | "private";
22
+ };
23
+ expiresAt: number;
24
+ }
25
+ interface DeviceControlSessionGrant {
26
+ id: string;
27
+ token: string;
28
+ convexUrl: string;
29
+ }
30
+ interface DeviceHeartbeatBody {
31
+ agentState?: string;
32
+ currentSessionId?: string | null;
33
+ appVersion?: string;
34
+ metrics?: Record<string, number | string>;
35
+ }
36
+ interface DeviceAgentTransport {
37
+ register(input: BootstrapInput & DeviceAgentDeviceInfo): Promise<{
38
+ deviceId: string;
39
+ heartbeatIntervalMs: number;
40
+ }>;
41
+ heartbeat(input: BootstrapInput & DeviceHeartbeatBody): Promise<{
42
+ controlSession: DeviceControlSessionGrant | null;
43
+ }>;
44
+ }
45
+ /** Kênh điều khiển realtime — mặc định là Convex websocket (convex-control-channel). */
46
+ interface DeviceControlChannel {
47
+ attach(controlSessionId: string, token: string): Promise<void>;
48
+ /** Subscribe lệnh pending — callback gọi lại mỗi khi danh sách đổi. */
49
+ onCommands(controlSessionId: string, token: string, callback: (commands: DeviceCommand[]) => void): () => void;
50
+ /** Subscribe trạng thái kênh — "ended"/"expired" thì agent tự đóng. */
51
+ onState(controlSessionId: string, token: string, callback: (state: {
52
+ status: string;
53
+ }) => void): () => void;
54
+ ack(input: {
55
+ controlSessionId: string;
56
+ token: string;
57
+ commandId: string;
58
+ status: "acked" | "completed" | "failed";
59
+ errorMessage?: string;
60
+ resultSessionId?: string;
61
+ }): Promise<void>;
62
+ close(): void;
63
+ }
64
+ interface RemoteStartConfig {
65
+ consentVersion: string;
66
+ /** Tiêu đề mặc định khi dashboard không gửi title. */
67
+ defaultTitle?: string;
68
+ visibility?: "public" | "private";
69
+ metadata?: Record<string, string>;
70
+ }
71
+ type AppStateListener = (state: "active" | "background" | "inactive") => void;
72
+ interface DeviceAgentOptions {
73
+ engine: LivestreamEngine;
74
+ bootstrapInput: BootstrapInput;
75
+ transport: DeviceAgentTransport;
76
+ remoteStart: RemoteStartConfig;
77
+ deviceInfo?: DeviceAgentDeviceInfo;
78
+ onRemoteSessionStarted?: (sessionId: string) => void;
79
+ deviceMetrics?: () => Promise<DeviceMetricsSample | null>;
80
+ /** Factory kênh điều khiển — test truyền fake; mặc định Convex websocket. */
81
+ createControlChannel?: (convexUrl: string) => DeviceControlChannel;
82
+ /** Test hook: nguồn app-state; mặc định react-native AppState. */
83
+ subscribeAppState?: (listener: AppStateListener) => () => void;
84
+ }
85
+ declare class DeviceAgent {
86
+ private readonly options;
87
+ private running;
88
+ private registered;
89
+ private foreground;
90
+ private heartbeatIntervalMs;
91
+ private timer;
92
+ private beatInFlight;
93
+ private registerAttempt;
94
+ private unsubscribeAppState;
95
+ private heartbeatFailures;
96
+ private controlChannel;
97
+ private controlSessionId;
98
+ private controlUnsubscribers;
99
+ private executing;
100
+ private readonly handledCommandIds;
101
+ constructor(options: DeviceAgentOptions);
102
+ start(): void;
103
+ stop(): void;
104
+ private handleAppState;
105
+ private schedule;
106
+ private beat;
107
+ private register;
108
+ private heartbeat;
109
+ private openControlChannel;
110
+ private closeControlChannel;
111
+ private executeCommands;
112
+ private executeCommand;
113
+ private runStartLivestream;
114
+ private runStopLivestream;
115
+ private safeAck;
116
+ }
117
+
118
+ type Fetch = typeof globalThis.fetch;
119
+ type HeadersInput = Record<string, string>;
120
+ interface HttpDeviceAgentTransportOptions {
121
+ /** Base URL của proxy đối tác (cùng proxy với session provider). */
122
+ baseUrl: string;
123
+ fetch?: Fetch;
124
+ headers?: HeadersInput | (() => HeadersInput | Promise<HeadersInput>);
125
+ timeoutMs?: number;
126
+ paths?: Partial<{
127
+ register: string;
128
+ heartbeat: string;
129
+ }>;
130
+ }
131
+ /** Transport HTTP cho DeviceAgent — mirror createHttpSessionProvider. */
132
+ declare function createHttpDeviceAgentTransport(options: HttpDeviceAgentTransportOptions): DeviceAgentTransport;
133
+
134
+ /**
135
+ * Kênh điều khiển realtime mặc định: Convex websocket subscribe các function
136
+ * deviceControl (auth bằng connect-token nhận qua heartbeat — thiết bị không
137
+ * có account, pattern pairingCode). Chỉ mở khi dashboard yêu cầu "Kết nối",
138
+ * đóng ngay khi kênh kết thúc → không giữ websocket lúc idle.
139
+ */
140
+ declare function createConvexControlChannel(convexUrl: string): DeviceControlChannel;
141
+
142
+ /**
143
+ * Bật device-agent: đăng ký thiết bị + heartbeat presence khi app mở, nhận
144
+ * lệnh start/stop livestream từ dashboard qua kênh điều khiển on-demand.
145
+ * Render bên trong PickleballLiveProvider (thường là child đầu tiên).
146
+ * Tách khỏi provider để app không dùng tính năng này không bundle convex.
147
+ */
148
+ interface PickleballDeviceAgentProps {
149
+ transport: DeviceAgentTransport;
150
+ remoteStart: RemoteStartConfig;
151
+ deviceInfo?: DeviceAgentDeviceInfo;
152
+ onRemoteSessionStarted?: (sessionId: string) => void;
153
+ createControlChannel?: (convexUrl: string) => DeviceControlChannel;
154
+ }
155
+ declare function PickleballDeviceAgent(props: PickleballDeviceAgentProps): null;
156
+
157
+ export { DeviceAgent, type DeviceAgentDeviceInfo, type DeviceAgentOptions, type DeviceAgentTransport, type DeviceCommand, type DeviceControlChannel, type DeviceControlSessionGrant, type DeviceHeartbeatBody, type HttpDeviceAgentTransportOptions, PickleballDeviceAgent, type PickleballDeviceAgentProps, type RemoteStartConfig, createConvexControlChannel, createHttpDeviceAgentTransport };
@@ -0,0 +1,498 @@
1
+ import {
2
+ ExpoSdkConfigurationError,
3
+ ExpoSdkTimeoutError,
4
+ InvalidResponseError,
5
+ NetworkUnavailableError,
6
+ PickleballExpoError,
7
+ errorFromCode,
8
+ normalizeError,
9
+ useLivestreamHost
10
+ } from "../chunk-FODE2W2H.js";
11
+ import {
12
+ __esm,
13
+ __export,
14
+ __require,
15
+ __toCommonJS
16
+ } from "../chunk-KFQGP6VL.js";
17
+
18
+ // src/device-agent/convex-control-channel.ts
19
+ var convex_control_channel_exports = {};
20
+ __export(convex_control_channel_exports, {
21
+ createConvexControlChannel: () => createConvexControlChannel
22
+ });
23
+ import { ConvexClient } from "convex/browser";
24
+ import { anyApi } from "convex/server";
25
+ function createConvexControlChannel(convexUrl) {
26
+ const client = new ConvexClient(convexUrl);
27
+ const deviceControl = anyApi.deviceControl;
28
+ function toCommands(value) {
29
+ if (!Array.isArray(value)) return [];
30
+ const commands = [];
31
+ for (const item of value) {
32
+ if (item === null || typeof item !== "object") continue;
33
+ const record = item;
34
+ if (typeof record.id !== "string" || record.type !== "start_livestream" && record.type !== "stop_livestream" || typeof record.expiresAt !== "number") {
35
+ continue;
36
+ }
37
+ const payload = record.payload !== null && typeof record.payload === "object" && !Array.isArray(record.payload) ? record.payload : {};
38
+ commands.push({
39
+ id: record.id,
40
+ type: record.type,
41
+ payload: {
42
+ ...typeof payload.title === "string" ? { title: payload.title } : {},
43
+ ...payload.visibility === "public" || payload.visibility === "private" ? { visibility: payload.visibility } : {}
44
+ },
45
+ expiresAt: record.expiresAt
46
+ });
47
+ }
48
+ return commands;
49
+ }
50
+ return {
51
+ async attach(controlSessionId, token) {
52
+ await client.mutation(deviceControl.deviceAttach, { controlSessionId, token });
53
+ },
54
+ onCommands(controlSessionId, token, callback) {
55
+ return client.onUpdate(
56
+ deviceControl.pendingCommands,
57
+ { controlSessionId, token },
58
+ (value) => callback(toCommands(value))
59
+ );
60
+ },
61
+ onState(controlSessionId, token, callback) {
62
+ return client.onUpdate(
63
+ deviceControl.controlSessionState,
64
+ { controlSessionId, token },
65
+ (value) => {
66
+ const status = value !== null && typeof value === "object" && typeof value.status === "string" ? value.status : "ended";
67
+ callback({ status });
68
+ }
69
+ );
70
+ },
71
+ async ack(input) {
72
+ await client.mutation(deviceControl.ackCommand, { ...input });
73
+ },
74
+ close() {
75
+ void client.close();
76
+ }
77
+ };
78
+ }
79
+ var init_convex_control_channel = __esm({
80
+ "src/device-agent/convex-control-channel.ts"() {
81
+ "use strict";
82
+ }
83
+ });
84
+
85
+ // src/device-agent/agent.ts
86
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 12e3;
87
+ var REGISTER_RETRY_BASE_MS = 2e3;
88
+ var REGISTER_RETRY_MAX_MS = 6e4;
89
+ var METRIC_FIELDS = [
90
+ "ramFreeMb",
91
+ "ramTotalMb",
92
+ "storageFreeGb",
93
+ "batteryPct",
94
+ "batteryTempC",
95
+ "thermal",
96
+ "netType",
97
+ "linkMbps",
98
+ "rssi",
99
+ "connQuality"
100
+ ];
101
+ function defaultSubscribeAppState(listener) {
102
+ const { AppState } = __require("react-native");
103
+ const subscription = AppState.addEventListener("change", (state) => {
104
+ listener(
105
+ state === "active" ? "active" : state === "background" ? "background" : "inactive"
106
+ );
107
+ });
108
+ return () => subscription.remove();
109
+ }
110
+ function defaultCreateControlChannel(convexUrl) {
111
+ const { createConvexControlChannel: createConvexControlChannel2 } = (init_convex_control_channel(), __toCommonJS(convex_control_channel_exports));
112
+ return createConvexControlChannel2(convexUrl);
113
+ }
114
+ function pickMetrics(sample) {
115
+ if (!sample) return void 0;
116
+ const metrics = {};
117
+ for (const field of METRIC_FIELDS) {
118
+ const value = sample[field];
119
+ if (typeof value === "number" && Number.isFinite(value)) metrics[field] = value;
120
+ else if (typeof value === "string" && value.trim() !== "") metrics[field] = value;
121
+ }
122
+ return Object.keys(metrics).length > 0 ? metrics : void 0;
123
+ }
124
+ var DeviceAgent = class {
125
+ options;
126
+ running = false;
127
+ registered = false;
128
+ foreground = true;
129
+ heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS;
130
+ timer = null;
131
+ beatInFlight = false;
132
+ registerAttempt = 0;
133
+ unsubscribeAppState = null;
134
+ heartbeatFailures = 0;
135
+ controlChannel = null;
136
+ controlSessionId = null;
137
+ controlUnsubscribers = [];
138
+ executing = false;
139
+ handledCommandIds = /* @__PURE__ */ new Set();
140
+ constructor(options) {
141
+ this.options = options;
142
+ }
143
+ start() {
144
+ if (this.running) return;
145
+ this.running = true;
146
+ this.unsubscribeAppState = (this.options.subscribeAppState ?? defaultSubscribeAppState)((state) => this.handleAppState(state));
147
+ this.schedule(0);
148
+ }
149
+ stop() {
150
+ this.running = false;
151
+ if (this.timer) clearTimeout(this.timer);
152
+ this.timer = null;
153
+ this.unsubscribeAppState?.();
154
+ this.unsubscribeAppState = null;
155
+ this.closeControlChannel();
156
+ }
157
+ handleAppState(state) {
158
+ const wasForeground = this.foreground;
159
+ this.foreground = state === "active";
160
+ if (!this.running) return;
161
+ if (this.foreground && !wasForeground) {
162
+ this.schedule(0);
163
+ }
164
+ if (!this.foreground) {
165
+ if (this.timer) clearTimeout(this.timer);
166
+ this.timer = null;
167
+ this.closeControlChannel();
168
+ }
169
+ }
170
+ schedule(delayMs) {
171
+ if (!this.running || !this.foreground) return;
172
+ if (this.timer) clearTimeout(this.timer);
173
+ this.timer = setTimeout(() => {
174
+ void this.beat();
175
+ }, delayMs);
176
+ }
177
+ async beat() {
178
+ if (!this.running || !this.foreground || this.beatInFlight) return;
179
+ this.beatInFlight = true;
180
+ try {
181
+ if (!this.registered) {
182
+ await this.register();
183
+ if (!this.registered) return;
184
+ }
185
+ await this.heartbeat();
186
+ this.schedule(this.heartbeatIntervalMs);
187
+ } finally {
188
+ this.beatInFlight = false;
189
+ }
190
+ }
191
+ async register() {
192
+ try {
193
+ const result = await this.options.transport.register({
194
+ ...this.options.bootstrapInput,
195
+ ...this.options.deviceInfo
196
+ });
197
+ if (result.heartbeatIntervalMs > 0) {
198
+ this.heartbeatIntervalMs = result.heartbeatIntervalMs;
199
+ }
200
+ this.registered = true;
201
+ this.registerAttempt = 0;
202
+ } catch {
203
+ const delay = Math.min(
204
+ REGISTER_RETRY_BASE_MS * 2 ** this.registerAttempt,
205
+ REGISTER_RETRY_MAX_MS
206
+ );
207
+ this.registerAttempt += 1;
208
+ this.schedule(delay);
209
+ }
210
+ }
211
+ async heartbeat() {
212
+ const snapshot = this.options.engine.getSnapshot();
213
+ let metrics;
214
+ try {
215
+ metrics = pickMetrics(await this.options.deviceMetrics?.() ?? null);
216
+ } catch {
217
+ metrics = void 0;
218
+ }
219
+ let result;
220
+ try {
221
+ result = await this.options.transport.heartbeat({
222
+ ...this.options.bootstrapInput,
223
+ agentState: snapshot.state,
224
+ currentSessionId: snapshot.sessionId,
225
+ ...this.options.deviceInfo?.appVersion ? { appVersion: this.options.deviceInfo.appVersion } : {},
226
+ ...metrics ? { metrics } : {}
227
+ });
228
+ } catch {
229
+ this.heartbeatFailures += 1;
230
+ if (this.heartbeatFailures >= 3) {
231
+ this.registered = false;
232
+ this.heartbeatFailures = 0;
233
+ }
234
+ return;
235
+ }
236
+ this.heartbeatFailures = 0;
237
+ if (result.controlSession && !this.controlChannel) {
238
+ this.openControlChannel(result.controlSession);
239
+ }
240
+ }
241
+ openControlChannel(grant) {
242
+ let channel;
243
+ try {
244
+ channel = (this.options.createControlChannel ?? defaultCreateControlChannel)(
245
+ grant.convexUrl
246
+ );
247
+ } catch {
248
+ return;
249
+ }
250
+ this.controlChannel = channel;
251
+ this.controlSessionId = grant.id;
252
+ void channel.attach(grant.id, grant.token).then(() => {
253
+ if (this.controlChannel !== channel) return;
254
+ this.controlUnsubscribers.push(
255
+ channel.onCommands(grant.id, grant.token, (commands) => {
256
+ void this.executeCommands(channel, grant, commands);
257
+ }),
258
+ channel.onState(grant.id, grant.token, (state) => {
259
+ if (state.status === "ended" || state.status === "expired") {
260
+ this.closeControlChannel();
261
+ }
262
+ })
263
+ );
264
+ }).catch(() => {
265
+ if (this.controlChannel === channel) this.closeControlChannel();
266
+ });
267
+ }
268
+ closeControlChannel() {
269
+ for (const unsubscribe of this.controlUnsubscribers) {
270
+ try {
271
+ unsubscribe();
272
+ } catch {
273
+ }
274
+ }
275
+ this.controlUnsubscribers = [];
276
+ try {
277
+ this.controlChannel?.close();
278
+ } catch {
279
+ }
280
+ this.controlChannel = null;
281
+ this.controlSessionId = null;
282
+ this.handledCommandIds.clear();
283
+ }
284
+ async executeCommands(channel, grant, commands) {
285
+ for (const command of commands) {
286
+ if (this.controlChannel !== channel) return;
287
+ if (this.handledCommandIds.has(command.id)) continue;
288
+ this.handledCommandIds.add(command.id);
289
+ if (this.executing) {
290
+ await this.safeAck(channel, grant, command.id, "failed", "Thi\u1EBFt b\u1ECB \u0111ang b\u1EADn");
291
+ continue;
292
+ }
293
+ this.executing = true;
294
+ try {
295
+ await this.executeCommand(channel, grant, command);
296
+ } finally {
297
+ this.executing = false;
298
+ }
299
+ }
300
+ }
301
+ async executeCommand(channel, grant, command) {
302
+ if (command.expiresAt <= Date.now()) {
303
+ await this.safeAck(channel, grant, command.id, "failed", "L\u1EC7nh \u0111\xE3 h\u1EBFt h\u1EA1n");
304
+ return;
305
+ }
306
+ await this.safeAck(channel, grant, command.id, "acked");
307
+ try {
308
+ if (command.type === "start_livestream") {
309
+ await this.runStartLivestream(command);
310
+ const sessionId = this.options.engine.getSnapshot().sessionId;
311
+ await this.safeAck(
312
+ channel,
313
+ grant,
314
+ command.id,
315
+ "completed",
316
+ void 0,
317
+ sessionId ?? void 0
318
+ );
319
+ if (sessionId) this.options.onRemoteSessionStarted?.(sessionId);
320
+ } else {
321
+ await this.runStopLivestream();
322
+ await this.safeAck(channel, grant, command.id, "completed");
323
+ }
324
+ } catch (error) {
325
+ await this.safeAck(
326
+ channel,
327
+ grant,
328
+ command.id,
329
+ "failed",
330
+ normalizeError(error).message
331
+ );
332
+ }
333
+ }
334
+ async runStartLivestream(command) {
335
+ const engine = this.options.engine;
336
+ const state = engine.getSnapshot().state;
337
+ if (state !== "idle" && state !== "ended" && state !== "preview") {
338
+ throw new Error(`Thi\u1EBFt b\u1ECB kh\xF4ng s\u1EB5n s\xE0ng (tr\u1EA1ng th\xE1i ${state})`);
339
+ }
340
+ if (state !== "preview") {
341
+ await engine.prepare();
342
+ }
343
+ const remote = this.options.remoteStart;
344
+ await engine.start(
345
+ {
346
+ externalSessionId: `remote-${command.id}`,
347
+ title: command.payload.title?.trim() || remote.defaultTitle || "Livestream",
348
+ visibility: command.payload.visibility ?? remote.visibility ?? "public",
349
+ consentVersion: remote.consentVersion,
350
+ metadata: { ...remote.metadata, deviceCommandId: command.id },
351
+ standby: true
352
+ },
353
+ { autoPublish: false }
354
+ );
355
+ }
356
+ async runStopLivestream() {
357
+ const engine = this.options.engine;
358
+ if (engine.getSnapshot().sessionId) {
359
+ await engine.stop("user");
360
+ }
361
+ }
362
+ async safeAck(channel, grant, commandId, status, errorMessage, resultSessionId) {
363
+ try {
364
+ await channel.ack({
365
+ controlSessionId: grant.id,
366
+ token: grant.token,
367
+ commandId,
368
+ status,
369
+ ...errorMessage !== void 0 ? { errorMessage } : {},
370
+ ...resultSessionId !== void 0 ? { resultSessionId } : {}
371
+ });
372
+ } catch {
373
+ }
374
+ }
375
+ };
376
+
377
+ // src/device-agent/http-transport.ts
378
+ var defaultPaths = {
379
+ register: "/devices/register",
380
+ heartbeat: "/devices/heartbeat"
381
+ };
382
+ function createHttpDeviceAgentTransport(options) {
383
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
384
+ if (baseUrl === "") {
385
+ throw new ExpoSdkConfigurationError("baseUrl must be a non-empty string");
386
+ }
387
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
388
+ if (typeof fetchImplementation !== "function") {
389
+ throw new ExpoSdkConfigurationError("A Fetch API implementation is required");
390
+ }
391
+ const paths = { ...defaultPaths, ...options.paths };
392
+ async function post(path, body) {
393
+ const controller = new AbortController();
394
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 1e4);
395
+ try {
396
+ const extraHeaders = typeof options.headers === "function" ? await options.headers() : options.headers ?? {};
397
+ const response = await fetchImplementation(
398
+ `${baseUrl}/${path.replace(/^\/+/, "")}`,
399
+ {
400
+ method: "POST",
401
+ headers: { "Content-Type": "application/json", ...extraHeaders },
402
+ body: JSON.stringify(body),
403
+ signal: controller.signal,
404
+ redirect: "error"
405
+ }
406
+ );
407
+ const envelope = await response.json().catch(() => {
408
+ throw new InvalidResponseError();
409
+ });
410
+ if (!response.ok) throw parseProxyError(envelope);
411
+ if (!isRecord(envelope) || !("data" in envelope)) {
412
+ throw new InvalidResponseError();
413
+ }
414
+ return envelope.data;
415
+ } catch (error) {
416
+ if (controller.signal.aborted) throw new ExpoSdkTimeoutError();
417
+ if (error instanceof PickleballExpoError) throw error;
418
+ if (error instanceof TypeError) {
419
+ throw new NetworkUnavailableError(void 0, { cause: error });
420
+ }
421
+ throw error;
422
+ } finally {
423
+ clearTimeout(timeout);
424
+ }
425
+ }
426
+ return {
427
+ async register(input) {
428
+ const data = await post(paths.register, input);
429
+ if (!isRecord(data) || typeof data.deviceId !== "string" || typeof data.heartbeatIntervalMs !== "number" || !Number.isFinite(data.heartbeatIntervalMs)) {
430
+ throw new InvalidResponseError();
431
+ }
432
+ return {
433
+ deviceId: data.deviceId,
434
+ heartbeatIntervalMs: data.heartbeatIntervalMs
435
+ };
436
+ },
437
+ async heartbeat(input) {
438
+ const data = await post(paths.heartbeat, input);
439
+ if (!isRecord(data)) throw new InvalidResponseError();
440
+ const grant = data.controlSession;
441
+ if (grant === null || grant === void 0) return { controlSession: null };
442
+ if (!isRecord(grant) || typeof grant.id !== "string" || typeof grant.token !== "string" || typeof grant.convexUrl !== "string" || grant.convexUrl === "") {
443
+ throw new InvalidResponseError();
444
+ }
445
+ return {
446
+ controlSession: {
447
+ id: grant.id,
448
+ token: grant.token,
449
+ convexUrl: grant.convexUrl
450
+ }
451
+ };
452
+ }
453
+ };
454
+ }
455
+ function parseProxyError(envelope) {
456
+ if (isRecord(envelope) && isRecord(envelope.error) && typeof envelope.error.code === "string") {
457
+ return errorFromCode(
458
+ envelope.error.code,
459
+ typeof envelope.error.message === "string" ? envelope.error.message : void 0
460
+ );
461
+ }
462
+ return new InvalidResponseError();
463
+ }
464
+ function isRecord(value) {
465
+ return typeof value === "object" && value !== null && !Array.isArray(value);
466
+ }
467
+
468
+ // src/device-agent/index.ts
469
+ init_convex_control_channel();
470
+
471
+ // src/device-agent/component.tsx
472
+ import { useEffect, useRef } from "react";
473
+ function PickleballDeviceAgent(props) {
474
+ const host = useLivestreamHost();
475
+ const propsRef = useRef(props);
476
+ propsRef.current = props;
477
+ useEffect(() => {
478
+ const agent = new DeviceAgent({
479
+ engine: host.engine,
480
+ bootstrapInput: host.bootstrapInput,
481
+ deviceMetrics: host.deviceMetrics,
482
+ transport: propsRef.current.transport,
483
+ remoteStart: propsRef.current.remoteStart,
484
+ deviceInfo: propsRef.current.deviceInfo,
485
+ createControlChannel: propsRef.current.createControlChannel,
486
+ onRemoteSessionStarted: (sessionId) => propsRef.current.onRemoteSessionStarted?.(sessionId)
487
+ });
488
+ agent.start();
489
+ return () => agent.stop();
490
+ }, [host]);
491
+ return null;
492
+ }
493
+ export {
494
+ DeviceAgent,
495
+ PickleballDeviceAgent,
496
+ createConvexControlChannel,
497
+ createHttpDeviceAgentTransport
498
+ };