@pickleball/expo-sdk 0.1.1 → 1.0.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/CHANGELOG.md +65 -0
- package/README.md +163 -40
- package/dist/advanced-DcbHBCGb.d.ts +300 -0
- package/dist/advanced-DeQwg7MK.d.cts +300 -0
- package/dist/advanced.cjs +47 -0
- package/dist/advanced.d.cts +3 -0
- package/dist/advanced.d.ts +3 -0
- package/dist/advanced.js +47 -0
- package/dist/camera-controls-By8tHigY.d.cts +97 -0
- package/dist/camera-controls-rCBUGGPC.d.ts +97 -0
- package/dist/chunk-2JHULJZ7.js +265 -0
- package/dist/chunk-62YV3PCD.cjs +139 -0
- package/dist/chunk-6F3HN6US.js +50 -0
- package/dist/chunk-6YE5BL3A.cjs +50 -0
- package/dist/chunk-7OX2DSKI.cjs +33 -0
- package/dist/chunk-7Z7TN5N3.js +318 -0
- package/dist/chunk-DHDX3ZOO.js +800 -0
- package/dist/chunk-GXQYMPHE.cjs +9 -0
- package/dist/chunk-IUIRVKJI.cjs +2290 -0
- package/dist/chunk-KFQGP6VL.js +33 -0
- package/dist/chunk-LCWGNGEN.cjs +265 -0
- package/dist/chunk-TBUEXRFQ.cjs +318 -0
- package/dist/chunk-XGP2MN5R.js +2290 -0
- package/dist/chunk-XL4CISVQ.js +9 -0
- package/dist/chunk-YGSQ25UA.cjs +800 -0
- package/dist/chunk-ZSKSXOCZ.js +139 -0
- package/dist/convex-command-subscription-Cabh9tND.d.ts +41 -0
- package/dist/convex-command-subscription-DUgpu75l.d.cts +41 -0
- package/dist/device-agent/index.cjs +1427 -0
- package/dist/device-agent/index.d.cts +493 -0
- package/dist/device-agent/index.d.ts +493 -0
- package/dist/device-agent/index.js +1427 -0
- package/dist/engine-BSvSdCSE.d.cts +741 -0
- package/dist/engine-BSvSdCSE.d.ts +741 -0
- package/dist/index.cjs +426 -6174
- package/dist/index.d.cts +299 -1400
- package/dist/index.d.ts +299 -1400
- package/dist/index.js +413 -6063
- package/dist/match/index.cjs +2456 -0
- package/dist/match/index.d.cts +253 -0
- package/dist/match/index.d.ts +253 -0
- package/dist/match/index.js +2456 -0
- package/dist/match/remote/index.cjs +21 -0
- package/dist/match/remote/index.d.cts +67 -0
- package/dist/match/remote/index.d.ts +67 -0
- package/dist/match/remote/index.js +21 -0
- package/dist/match/voice/index.cjs +23 -0
- package/dist/match/voice/index.d.cts +114 -0
- package/dist/match/voice/index.d.ts +114 -0
- package/dist/match/voice/index.js +23 -0
- package/dist/plugin.cjs +52 -71
- package/dist/plugin.d.cts +28 -1
- package/dist/plugin.d.ts +28 -1
- package/dist/plugin.js +26 -23
- package/dist/realtime/index.cjs +31 -0
- package/dist/realtime/index.d.cts +15 -0
- package/dist/realtime/index.d.ts +15 -0
- package/dist/realtime/index.js +31 -0
- package/dist/types-CrmcX408.d.cts +176 -0
- package/dist/types-CrmcX408.d.ts +176 -0
- package/dist/types-DiKWjwvN.d.cts +96 -0
- package/dist/types-DiKWjwvN.d.ts +96 -0
- package/dist/ui/index.cjs +1297 -0
- package/dist/ui/index.d.cts +493 -0
- package/dist/ui/index.d.ts +493 -0
- package/dist/ui/index.js +1297 -0
- package/dist/version.cjs +4 -32
- package/dist/version.js +2 -1
- package/package.json +95 -15
- package/src/advanced.ts +49 -0
- package/src/auto-resume.tsx +259 -0
- package/src/camera-controls.ts +99 -0
- package/src/camera-grant-provider.ts +392 -0
- package/src/camera-settings.ts +133 -0
- package/src/contracts.ts +73 -0
- package/src/device-agent/agent.ts +696 -0
- package/src/device-agent/beat-policy.ts +46 -0
- package/src/device-agent/component.tsx +60 -0
- package/src/device-agent/convex-beat-transport.ts +140 -0
- package/src/device-agent/convex-command-subscription.ts +101 -0
- package/src/device-agent/convex-control-channel.ts +88 -0
- package/src/device-agent/http-transport.ts +164 -0
- package/src/device-agent/index.ts +26 -0
- package/src/device-agent/mqtt-channel.ts +379 -0
- package/src/device-agent/mqtt-codec.ts +347 -0
- package/src/device-agent/tournament.ts +352 -0
- package/src/engine.ts +601 -20
- package/src/errors.ts +16 -0
- package/src/http-session-provider.ts +21 -183
- package/src/index.ts +117 -14
- package/src/local-video-quality.ts +43 -0
- package/src/match/index.ts +29 -0
- package/src/match/provider.tsx +4 -2
- package/src/match/remote/hid-gesture.ts +10 -2
- package/src/match/remote/types.ts +6 -4
- package/src/match/voice/scheduler.ts +6 -1
- package/src/match/voice/service.ts +16 -4
- package/src/native-runtime.ts +283 -77
- package/src/plugin.ts +54 -20
- package/src/provider.tsx +129 -22
- package/src/realtime/index.ts +56 -0
- package/src/resume-policy.ts +85 -0
- package/src/resume.ts +132 -0
- package/src/rtmp-room-adapter.ts +348 -0
- package/src/session-grant.ts +256 -0
- package/src/ui/camera-screen.tsx +484 -0
- package/src/ui/controls-sheet.tsx +445 -0
- package/src/ui/copy.ts +326 -0
- package/src/ui/helpers.ts +105 -0
- package/src/ui/hooks.ts +59 -0
- package/src/ui/index.ts +42 -0
- package/src/ui/preview-view.tsx +39 -0
- package/src/ui/primitives.tsx +304 -0
- package/src/uplink-check.ts +173 -0
- package/dist/chunk-VHLUJQG5.js +0 -17
- package/src/livekit-room-adapter.ts +0 -353
- package/src/livestream-screen.tsx +0 -980
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { a0 as LivestreamEngine, B as BootstrapInput, D as DeviceMetricsSample } from '../engine-BSvSdCSE.cjs';
|
|
2
|
+
import { C as ConvexDeviceRealtime } from '../convex-command-subscription-DUgpu75l.cjs';
|
|
3
|
+
export { c as createConvexCommandSubscription, a as createConvexDeviceRealtime } from '../convex-command-subscription-DUgpu75l.cjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Nhịp heartbeat thích ứng — bản CHÉP của luật trong
|
|
7
|
+
* `packages/shared/src/realtime.ts`.
|
|
8
|
+
*
|
|
9
|
+
* Vì sao chép chứ không import: gói expo-sdk cố ý KHÔNG phụ thuộc
|
|
10
|
+
* `@pickleball/shared` (guard `scripts/verify-pack.mjs` chặn), để app đối tác
|
|
11
|
+
* cài SDK không phải kéo theo cả gói dùng chung của backend. Đổi luật ở đây thì
|
|
12
|
+
* phải đổi cả hai nơi — bộ test hai bên dùng CÙNG bảng giá trị để phát hiện lệch.
|
|
13
|
+
*/
|
|
14
|
+
declare const BEAT_SLOW_MS = 60000;
|
|
15
|
+
declare const BEAT_FAST_MS = 12000;
|
|
16
|
+
interface MqttBeatHealth {
|
|
17
|
+
connected: boolean;
|
|
18
|
+
subscribed: boolean;
|
|
19
|
+
lastInboundAt: number;
|
|
20
|
+
healthySince: number;
|
|
21
|
+
}
|
|
22
|
+
/** Đủ CẢ BỐN vế mới được coi là kênh khoẻ. */
|
|
23
|
+
declare function mqttBeatHealthy(h: MqttBeatHealth, now: number): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Nhịp kế tiếp = min(luật nội bộ, hint của server), và THIẾU hint thì mặc định
|
|
26
|
+
* nhanh. Nhờ vậy máy chưa nhận được hint (server cũ, hoặc beat lỗi) không bao
|
|
27
|
+
* giờ tự ý giãn nhịp, còn tắt rollout chỉ là đổi một biến env phía server.
|
|
28
|
+
*/
|
|
29
|
+
declare function nextBeatDelayMs(input: {
|
|
30
|
+
health: MqttBeatHealth | null;
|
|
31
|
+
now: number;
|
|
32
|
+
serverHintMs?: number;
|
|
33
|
+
busy?: boolean;
|
|
34
|
+
}): number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* DeviceAgent — đăng ký thiết bị với backend khi app mở và giữ heartbeat
|
|
38
|
+
* presence (12s/lần khi foreground) qua proxy. Khi dashboard yêu cầu "Kết nối",
|
|
39
|
+
* heartbeat trả về connect-token → agent mở kênh điều khiển realtime (Convex
|
|
40
|
+
* websocket) để nhận lệnh start/stop livestream tức thì. Publish/unpublish sau
|
|
41
|
+
* khi vào standby vẫn đi đường LiveKit data channel sẵn có của engine.
|
|
42
|
+
*/
|
|
43
|
+
interface DeviceAgentDeviceInfo {
|
|
44
|
+
appVersion?: string;
|
|
45
|
+
deviceName?: string;
|
|
46
|
+
deviceModel?: string;
|
|
47
|
+
osVersion?: string;
|
|
48
|
+
}
|
|
49
|
+
interface DeviceCommand {
|
|
50
|
+
id: string;
|
|
51
|
+
type: "start_livestream" | "stop_livestream" | "publish_livestream" | "unpublish_livestream";
|
|
52
|
+
payload: {
|
|
53
|
+
title?: string;
|
|
54
|
+
visibility?: "public" | "private";
|
|
55
|
+
/** Arm-từ-xa chế độ giải đấu: phiên standby sẽ đậu vào sân này. */
|
|
56
|
+
courtRef?: string;
|
|
57
|
+
};
|
|
58
|
+
expiresAt: number;
|
|
59
|
+
}
|
|
60
|
+
interface DeviceControlSessionGrant {
|
|
61
|
+
id: string;
|
|
62
|
+
token: string;
|
|
63
|
+
convexUrl: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Một lần tiến trình app chết trước đó — Android `ApplicationExitInfo`, native
|
|
67
|
+
* đã quy đổi `reasonName` ("PACKAGE_UPDATED", "CRASH", "ANR", "OTHER"…). Xem
|
|
68
|
+
* NativeProcessExit của @pickleball/rtmp-native; iOS không có nguồn tương đương.
|
|
69
|
+
*/
|
|
70
|
+
interface DeviceProcessExit {
|
|
71
|
+
/** epoch ms lúc chết. */
|
|
72
|
+
at: number;
|
|
73
|
+
reason: number;
|
|
74
|
+
reasonName: string;
|
|
75
|
+
/** Chuỗi hệ thống, VD "stop com.google.android.webview due to installPackageLI". */
|
|
76
|
+
description?: string;
|
|
77
|
+
/** IMPORTANCE_* lúc chết — 100 = đang ở tiền cảnh. */
|
|
78
|
+
importance?: number;
|
|
79
|
+
pid?: number;
|
|
80
|
+
}
|
|
81
|
+
interface DeviceHeartbeatBody {
|
|
82
|
+
agentState?: string;
|
|
83
|
+
currentSessionId?: string | null;
|
|
84
|
+
appVersion?: string;
|
|
85
|
+
metrics?: Record<string, number | string>;
|
|
86
|
+
/**
|
|
87
|
+
* Hộp đen: lý do các lần tiến trình chết gần đây. App chỉ cần gửi vài nhịp
|
|
88
|
+
* đầu sau khi mở (server giữ bản mới nhất, ghi idempotent theo mốc).
|
|
89
|
+
*/
|
|
90
|
+
recentExits?: DeviceProcessExit[];
|
|
91
|
+
/**
|
|
92
|
+
* Trạng thái nguồn app tự khai: "rest" = chế độ nghỉ (camera/service đã tắt,
|
|
93
|
+
* máy đang nguội). Server chỉ ghi khi đổi — dashboard hiện badge "Đang nghỉ".
|
|
94
|
+
*/
|
|
95
|
+
powerState?: "rest" | "active";
|
|
96
|
+
/**
|
|
97
|
+
* Xin tỉ số kèm response beat — CHỈ bật khi kênh MQTT đứt (đường nhanh
|
|
98
|
+
* plv1/dev/{id}/score câm); MQTT khoẻ mà vẫn xin là server đọc doc tỉ số
|
|
99
|
+
* mỗi nhịp vô ích.
|
|
100
|
+
*/
|
|
101
|
+
wantScore?: boolean;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Field app tự gắn thêm vào MỖI heartbeat. Hàm được gọi ngay trước khi gửi,
|
|
105
|
+
* trả undefined/{} khi không có gì — ném lỗi thì bị nuốt, heartbeat vẫn đi.
|
|
106
|
+
*/
|
|
107
|
+
type DeviceHeartbeatExtras = Pick<DeviceHeartbeatBody, "recentExits" | "powerState" | "wantScore">;
|
|
108
|
+
interface DeviceAgentTransport {
|
|
109
|
+
register(input: BootstrapInput & DeviceAgentDeviceInfo): Promise<{
|
|
110
|
+
deviceId: string;
|
|
111
|
+
heartbeatIntervalMs: number;
|
|
112
|
+
}>;
|
|
113
|
+
heartbeat(input: BootstrapInput & DeviceHeartbeatBody): Promise<{
|
|
114
|
+
controlSession: DeviceControlSessionGrant | null;
|
|
115
|
+
/**
|
|
116
|
+
* Nhịp server gợi ý cho lượt beat kế (adaptive heartbeat). Optional để
|
|
117
|
+
* transport/server đời cũ vẫn hợp lệ — thiếu thì agent giữ nhịp nhanh.
|
|
118
|
+
*/
|
|
119
|
+
beatIntervalMs?: number;
|
|
120
|
+
}>;
|
|
121
|
+
}
|
|
122
|
+
/** Kênh điều khiển realtime — mặc định là Convex websocket (convex-control-channel). */
|
|
123
|
+
interface DeviceControlChannel {
|
|
124
|
+
attach(controlSessionId: string, token: string): Promise<void>;
|
|
125
|
+
/** Subscribe lệnh pending — callback gọi lại mỗi khi danh sách đổi. */
|
|
126
|
+
onCommands(controlSessionId: string, token: string, callback: (commands: DeviceCommand[]) => void): () => void;
|
|
127
|
+
/** Subscribe trạng thái kênh — "ended"/"expired" thì agent tự đóng. */
|
|
128
|
+
onState(controlSessionId: string, token: string, callback: (state: {
|
|
129
|
+
status: string;
|
|
130
|
+
}) => void): () => void;
|
|
131
|
+
ack(input: {
|
|
132
|
+
controlSessionId: string;
|
|
133
|
+
token: string;
|
|
134
|
+
commandId: string;
|
|
135
|
+
status: "acked" | "completed" | "failed";
|
|
136
|
+
errorMessage?: string;
|
|
137
|
+
resultSessionId?: string;
|
|
138
|
+
}): Promise<void>;
|
|
139
|
+
close(): void;
|
|
140
|
+
}
|
|
141
|
+
interface RemoteStartConfig {
|
|
142
|
+
consentVersion: string;
|
|
143
|
+
/** Tiêu đề mặc định khi dashboard không gửi title. */
|
|
144
|
+
defaultTitle?: string;
|
|
145
|
+
visibility?: "public" | "private";
|
|
146
|
+
metadata?: Record<string, string>;
|
|
147
|
+
}
|
|
148
|
+
type AppStateListener = (state: "active" | "background" | "inactive") => void;
|
|
149
|
+
interface DeviceAgentOptions {
|
|
150
|
+
engine: LivestreamEngine;
|
|
151
|
+
bootstrapInput: BootstrapInput;
|
|
152
|
+
transport: DeviceAgentTransport;
|
|
153
|
+
remoteStart: RemoteStartConfig;
|
|
154
|
+
deviceInfo?: DeviceAgentDeviceInfo;
|
|
155
|
+
onRemoteSessionStarted?: (sessionId: string) => void;
|
|
156
|
+
deviceMetrics?: () => Promise<DeviceMetricsSample | null>;
|
|
157
|
+
/** Field bổ sung cho heartbeat (VD recentExits) — xem DeviceHeartbeatExtras. */
|
|
158
|
+
heartbeatExtras?: () => DeviceHeartbeatExtras | undefined;
|
|
159
|
+
/**
|
|
160
|
+
* Sức khoẻ kênh MQTT của app (kênh nằm ở tầng app, không thuộc SDK). Có
|
|
161
|
+
* nguồn này VÀ server bật rollout thì nhịp beat giãn 12s → 60s; thiếu nó thì
|
|
162
|
+
* agent chạy y hệt trước đây. Ném lỗi trong callback được coi như "không có".
|
|
163
|
+
*/
|
|
164
|
+
mqttHealth?: () => MqttBeatHealth | null;
|
|
165
|
+
/** Factory kênh điều khiển — test truyền fake; mặc định Convex websocket. */
|
|
166
|
+
createControlChannel?: (convexUrl: string) => DeviceControlChannel;
|
|
167
|
+
/** Test hook: nguồn app-state; mặc định react-native AppState. */
|
|
168
|
+
subscribeAppState?: (listener: AppStateListener) => () => void;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Metrics lần này có đáng gửi so với lần đã gửi không. Hàm thuần để test dễ.
|
|
172
|
+
* Đáng gửi khi: chưa gửi lần nào; pin vừa THỦNG mốc thấp (đang ≥10% tụt xuống
|
|
173
|
+
* <10%); pin đổi ≥5% so với mốc đã gửi; đổi mức nhiệt (thermal) hoặc loại mạng.
|
|
174
|
+
* KHÔNG tính RAM/sóng/nhiệt-độ-pin: chúng nhích liên tục, để mốc 5 phút lo.
|
|
175
|
+
*/
|
|
176
|
+
declare function metricsCrossedThreshold(last: Record<string, number | string> | undefined, next: Record<string, number | string>): boolean;
|
|
177
|
+
declare class DeviceAgent {
|
|
178
|
+
private readonly options;
|
|
179
|
+
private running;
|
|
180
|
+
private registered;
|
|
181
|
+
private foreground;
|
|
182
|
+
private heartbeatIntervalMs;
|
|
183
|
+
/** Nhịp server gợi ý ở beat gần nhất — undefined = server chưa nói gì → 12s. */
|
|
184
|
+
private serverBeatHintMs;
|
|
185
|
+
private timer;
|
|
186
|
+
private beatInFlight;
|
|
187
|
+
private registerAttempt;
|
|
188
|
+
private unsubscribeAppState;
|
|
189
|
+
private heartbeatFailures;
|
|
190
|
+
/** Metrics đã gửi lần gần nhất + mốc thời gian, để lọc "chỉ gửi khi đổi". */
|
|
191
|
+
private lastSentMetrics?;
|
|
192
|
+
private lastMetricsSentAt;
|
|
193
|
+
private controlChannel;
|
|
194
|
+
private controlSessionId;
|
|
195
|
+
private controlUnsubscribers;
|
|
196
|
+
private executing;
|
|
197
|
+
private readonly handledCommandIds;
|
|
198
|
+
constructor(options: DeviceAgentOptions);
|
|
199
|
+
start(): void;
|
|
200
|
+
stop(): void;
|
|
201
|
+
private handleAppState;
|
|
202
|
+
/**
|
|
203
|
+
* Đang có phiên hoặc đã mở kênh điều khiển = lệnh tới qua kênh, nhịp thường
|
|
204
|
+
* là đủ. Rảnh và chưa có kênh = đang chờ lệnh, đập nhanh cho kịp dashboard.
|
|
205
|
+
*/
|
|
206
|
+
private nextBeatDelay;
|
|
207
|
+
private schedule;
|
|
208
|
+
private beat;
|
|
209
|
+
private register;
|
|
210
|
+
/**
|
|
211
|
+
* Lọc metrics cho heartbeat: chỉ trả (và ghi nhớ) khi vượt ngưỡng
|
|
212
|
+
* (metricsCrossedThreshold) hoặc đã quá METRICS_BASELINE_MS kể từ lần gửi.
|
|
213
|
+
* Trả undefined = nhịp này không kèm metrics.
|
|
214
|
+
*/
|
|
215
|
+
private selectMetricsForHeartbeat;
|
|
216
|
+
private heartbeat;
|
|
217
|
+
private openControlChannel;
|
|
218
|
+
private closeControlChannel;
|
|
219
|
+
private executeCommands;
|
|
220
|
+
private executeCommand;
|
|
221
|
+
private runStartLivestream;
|
|
222
|
+
private runStopLivestream;
|
|
223
|
+
/**
|
|
224
|
+
* standby → live theo lệnh dashboard. startPublishing() NUỐT lỗi (recordError
|
|
225
|
+
* vào snapshot thay vì throw) nên phải kiểm state sau khi gọi — không kiểm là
|
|
226
|
+
* lệnh được ack "completed" dù thiết bị chưa hề phát.
|
|
227
|
+
*/
|
|
228
|
+
private runPublish;
|
|
229
|
+
private runUnpublish;
|
|
230
|
+
private safeAck;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
type Fetch = typeof globalThis.fetch;
|
|
234
|
+
type HeadersInput = Record<string, string>;
|
|
235
|
+
interface HttpDeviceAgentTransportOptions {
|
|
236
|
+
/** Base URL của proxy đối tác (cùng proxy với session provider). */
|
|
237
|
+
baseUrl: string;
|
|
238
|
+
fetch?: Fetch;
|
|
239
|
+
headers?: HeadersInput | (() => HeadersInput | Promise<HeadersInput>);
|
|
240
|
+
timeoutMs?: number;
|
|
241
|
+
paths?: Partial<{
|
|
242
|
+
register: string;
|
|
243
|
+
heartbeat: string;
|
|
244
|
+
}>;
|
|
245
|
+
/**
|
|
246
|
+
* Nhận envelope `data` thô sau khi transport validate xong. Validator phía
|
|
247
|
+
* dưới drop field ngoài contract (deviceToken/pairing/claimed/ownerName của
|
|
248
|
+
* đường first-party) — app device-streamer đọc chúng qua hook này; contract
|
|
249
|
+
* partner cũ không đổi.
|
|
250
|
+
*/
|
|
251
|
+
onData?: (endpoint: "register" | "heartbeat", data: Record<string, unknown>) => void;
|
|
252
|
+
}
|
|
253
|
+
/** Transport HTTP cho DeviceAgent — mirror createHttpSessionProvider. */
|
|
254
|
+
declare function createHttpDeviceAgentTransport(options: HttpDeviceAgentTransportOptions): DeviceAgentTransport;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* TournamentController — "chế độ giải đấu" cho điện thoại camera đặt tại sân.
|
|
258
|
+
*
|
|
259
|
+
* Vận hành arm máy MỘT lần khi setup giải: controller giữ đúng một phiên
|
|
260
|
+
* standby gắn sân (metadata.courtRef) đậu trong phòng LiveKit cả ngày ($0
|
|
261
|
+
* egress). Backend đậu phiên vào bảng courts (bindStandbySession) để REST
|
|
262
|
+
* match-start của đối tác tiêu thụ: trọng tài bấm start → server claim phiên +
|
|
263
|
+
* flip live + lệnh "start" qua data channel — không ai chạm điện thoại.
|
|
264
|
+
*
|
|
265
|
+
* Khi phiên kết thúc VÌ BẤT KỲ LÝ DO GÌ (match-end, standby_timeout 15 phút,
|
|
266
|
+
* watchdog, rớt mạng, app khởi động lại) controller tự re-arm phiên standby
|
|
267
|
+
* mới cho cùng sân sau vài giây — một state machine duy nhất xử lý trận nối
|
|
268
|
+
* trận, reboot và hồi phục lỗi.
|
|
269
|
+
*/
|
|
270
|
+
|
|
271
|
+
/** Góc nhìn tối thiểu lên engine — test truyền fake object thường. */
|
|
272
|
+
interface TournamentEngineView {
|
|
273
|
+
getSnapshot(): {
|
|
274
|
+
state: string;
|
|
275
|
+
sessionId: string | null;
|
|
276
|
+
};
|
|
277
|
+
subscribe(listener: () => void): () => void;
|
|
278
|
+
prepare(): Promise<void>;
|
|
279
|
+
start(input: {
|
|
280
|
+
externalSessionId: string;
|
|
281
|
+
title: string;
|
|
282
|
+
visibility?: "public" | "private";
|
|
283
|
+
consentVersion: string;
|
|
284
|
+
metadata?: Record<string, string>;
|
|
285
|
+
}, options?: {
|
|
286
|
+
autoPublish?: boolean;
|
|
287
|
+
}): Promise<void>;
|
|
288
|
+
stop(reason: "user" | "error"): Promise<void>;
|
|
289
|
+
}
|
|
290
|
+
interface TournamentPersistedState {
|
|
291
|
+
courtRef: string;
|
|
292
|
+
}
|
|
293
|
+
/** Persist trạng thái armed (AsyncStorage/SQLite tuỳ app) — reboot tự re-arm. */
|
|
294
|
+
interface TournamentStorage {
|
|
295
|
+
load(): Promise<TournamentPersistedState | null>;
|
|
296
|
+
save(state: TournamentPersistedState | null): Promise<void>;
|
|
297
|
+
}
|
|
298
|
+
type TournamentPhase = "disarmed" | "arming" | "standby" | "live" | "retry_wait";
|
|
299
|
+
interface TournamentState {
|
|
300
|
+
armed: boolean;
|
|
301
|
+
courtRef: string | null;
|
|
302
|
+
phase: TournamentPhase;
|
|
303
|
+
lastError: string | null;
|
|
304
|
+
}
|
|
305
|
+
interface TournamentStandbyStart {
|
|
306
|
+
consentVersion: string;
|
|
307
|
+
/** Tiêu đề phiên chờ theo sân — mặc định "{courtRef} — chờ trận". */
|
|
308
|
+
titleFor?: (courtRef: string) => string;
|
|
309
|
+
visibility?: "public" | "private";
|
|
310
|
+
metadata?: Record<string, string>;
|
|
311
|
+
}
|
|
312
|
+
interface TournamentControllerOptions {
|
|
313
|
+
engine: TournamentEngineView | LivestreamEngine;
|
|
314
|
+
standbyStart: TournamentStandbyStart;
|
|
315
|
+
storage?: TournamentStorage;
|
|
316
|
+
onStateChange?: (state: TournamentState) => void;
|
|
317
|
+
/** Backoff re-arm khi lỗi liên tiếp (test truyền giá trị nhỏ). */
|
|
318
|
+
rearmBaseDelayMs?: number;
|
|
319
|
+
rearmMaxDelayMs?: number;
|
|
320
|
+
/**
|
|
321
|
+
* Thời gian NHƯỜNG cho engine tự cứu phiên khi nó ở "error" mà còn sessionId.
|
|
322
|
+
* Phải dài hơn reconnectTimeoutMs của engine (mặc định 30s) để engine kịp
|
|
323
|
+
* hoặc hồi, hoặc tự cắt bằng network_timeout, trước khi controller can thiệp.
|
|
324
|
+
*/
|
|
325
|
+
recoveryGraceMs?: number;
|
|
326
|
+
now?: () => number;
|
|
327
|
+
}
|
|
328
|
+
declare class TournamentController {
|
|
329
|
+
private readonly options;
|
|
330
|
+
private readonly engine;
|
|
331
|
+
private armed;
|
|
332
|
+
private courtRef;
|
|
333
|
+
private lastError;
|
|
334
|
+
private attemptInFlight;
|
|
335
|
+
private retryAttempt;
|
|
336
|
+
private retryTimer;
|
|
337
|
+
/** Mốc hết nhường cho engine tự cứu phiên; null = đang không trong pha đó. */
|
|
338
|
+
private recoveryDeadline;
|
|
339
|
+
private unsubscribe;
|
|
340
|
+
private disposed;
|
|
341
|
+
/** Tăng mỗi lần arm/disarm — huỷ attempt cũ đang chạy dở. */
|
|
342
|
+
private armGeneration;
|
|
343
|
+
constructor(options: TournamentControllerOptions);
|
|
344
|
+
getState(): TournamentState;
|
|
345
|
+
/** Khôi phục sau khi app mở lại (reboot giữa giải) — gọi sau engine.initialize(). */
|
|
346
|
+
restore(): Promise<void>;
|
|
347
|
+
arm(courtRef: string): Promise<void>;
|
|
348
|
+
/**
|
|
349
|
+
* Tắt chế độ giải đấu. Phiên standby đang chờ được nhả (stop); phiên đang
|
|
350
|
+
* LIVE là trận do đối tác điều khiển — không cắt, chỉ ngừng re-arm.
|
|
351
|
+
*/
|
|
352
|
+
disarm(): Promise<void>;
|
|
353
|
+
dispose(): void;
|
|
354
|
+
private emit;
|
|
355
|
+
private clearRetry;
|
|
356
|
+
/** Engine còn đang tự cứu CHÍNH phiên này → nhường, đừng cắt để arm phiên mới. */
|
|
357
|
+
private isEngineRecovering;
|
|
358
|
+
private scheduleRecoveryCheck;
|
|
359
|
+
private handleEngineChange;
|
|
360
|
+
private scheduleRearm;
|
|
361
|
+
private ensureStandby;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Kênh điều khiển realtime mặc định: Convex websocket subscribe các function
|
|
366
|
+
* deviceControl (auth bằng connect-token nhận qua heartbeat — thiết bị không
|
|
367
|
+
* có account, pattern pairingCode). Chỉ mở khi dashboard yêu cầu "Kết nối",
|
|
368
|
+
* đóng ngay khi kênh kết thúc → không giữ websocket lúc idle.
|
|
369
|
+
*/
|
|
370
|
+
declare function createConvexControlChannel(convexUrl: string): DeviceControlChannel;
|
|
371
|
+
|
|
372
|
+
interface ConvexBeatOptions {
|
|
373
|
+
/** deviceToken hiện tại (đọc mỗi nhịp — token có thể vừa được cấp/đổi). */
|
|
374
|
+
getDeviceToken: () => string | null | undefined;
|
|
375
|
+
/**
|
|
376
|
+
* Cùng hook với createHttpDeviceAgentTransport.onData: đường WS phải đổ
|
|
377
|
+
* envelope y hệt để handleTransportData (claim/pairing/assignedCourt/
|
|
378
|
+
* activeSession/preview) không phân biệt beat đến từ đường nào.
|
|
379
|
+
*/
|
|
380
|
+
onData?: (endpoint: "heartbeat", data: Record<string, unknown>) => void;
|
|
381
|
+
timeoutMs?: number;
|
|
382
|
+
/** Test hook. */
|
|
383
|
+
now?: () => number;
|
|
384
|
+
}
|
|
385
|
+
declare function withConvexBeat(base: DeviceAgentTransport, realtime: ConvexDeviceRealtime, options: ConvexBeatOptions): DeviceAgentTransport;
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Bật device-agent: đăng ký thiết bị + heartbeat presence khi app mở, nhận
|
|
389
|
+
* lệnh start/stop livestream từ dashboard qua kênh điều khiển on-demand.
|
|
390
|
+
* Render bên trong PickleballLiveProvider (thường là child đầu tiên).
|
|
391
|
+
* Tách khỏi provider để app không dùng tính năng này không bundle convex.
|
|
392
|
+
*/
|
|
393
|
+
interface PickleballDeviceAgentProps {
|
|
394
|
+
transport: DeviceAgentTransport;
|
|
395
|
+
remoteStart: RemoteStartConfig;
|
|
396
|
+
deviceInfo?: DeviceAgentDeviceInfo;
|
|
397
|
+
onRemoteSessionStarted?: (sessionId: string) => void;
|
|
398
|
+
createControlChannel?: (convexUrl: string) => DeviceControlChannel;
|
|
399
|
+
/** Field bổ sung cho heartbeat (VD recentExits) — đọc qua ref nên đổi hàm không dựng lại agent. */
|
|
400
|
+
heartbeatExtras?: () => DeviceHeartbeatExtras | undefined;
|
|
401
|
+
/**
|
|
402
|
+
* Sức khoẻ kênh MQTT của app — nguồn để agent giãn nhịp beat khi kênh khoẻ
|
|
403
|
+
* VÀ server bật rollout. Bỏ trống = agent giữ nhịp cũ.
|
|
404
|
+
*/
|
|
405
|
+
mqttHealth?: () => MqttBeatHealth | null;
|
|
406
|
+
}
|
|
407
|
+
declare function PickleballDeviceAgent(props: PickleballDeviceAgentProps): null;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Kênh MQTT của máy camera — vòng đời kết nối trên WebSocket.
|
|
411
|
+
*
|
|
412
|
+
* Vai trò trong kiến trúc: đây là "device plane". Presence do broker phát hiện
|
|
413
|
+
* ở tầng TCP (LWT) chứ không phải do server đoán từ nhịp heartbeat, nên máy
|
|
414
|
+
* chết được biết trong ~45s, và biết TỨC THÌ khi app đóng kết nối sạch. Lệnh
|
|
415
|
+
* start/stop cũng đi đường này; Convex websocket + poll 30s vẫn giữ nguyên làm
|
|
416
|
+
* lưới an toàn (broker sập không được giết match-start).
|
|
417
|
+
*
|
|
418
|
+
* Module này KHÔNG import `@pickleball/shared`: hợp đồng topic/ngưỡng do phía
|
|
419
|
+
* app truyền vào. Lý do là gói SDK này được publish ra npm và Metro đọc thẳng
|
|
420
|
+
* `src/`, nên một import workspace ở đây sẽ vỡ khi app ngoài repo cài gói.
|
|
421
|
+
* Nguồn duy nhất của hợp đồng vẫn là `packages/shared/src/realtime.ts` — chỉ
|
|
422
|
+
* khác là chỗ nối dây nằm ở app first-party.
|
|
423
|
+
*/
|
|
424
|
+
/** Bề mặt WebSocket tối thiểu mà kênh cần — đủ để test thay bằng đồ giả. */
|
|
425
|
+
interface MqttSocket {
|
|
426
|
+
binaryType: string;
|
|
427
|
+
send(data: ArrayBufferView | ArrayBuffer): void;
|
|
428
|
+
close(): void;
|
|
429
|
+
onopen: (() => void) | null;
|
|
430
|
+
onmessage: ((event: {
|
|
431
|
+
data: unknown;
|
|
432
|
+
}) => void) | null;
|
|
433
|
+
onerror: ((event: unknown) => void) | null;
|
|
434
|
+
onclose: ((event: {
|
|
435
|
+
code?: number;
|
|
436
|
+
}) => void) | null;
|
|
437
|
+
}
|
|
438
|
+
interface MqttChannelOptions {
|
|
439
|
+
/** wss://mqtt.cabinvn.com/mqtt */
|
|
440
|
+
url: string;
|
|
441
|
+
deviceId: string;
|
|
442
|
+
/** Token bền của máy — broker chuyển thẳng cho auth-hook Convex. */
|
|
443
|
+
deviceToken: string;
|
|
444
|
+
topics: {
|
|
445
|
+
status: string;
|
|
446
|
+
cmd: string;
|
|
447
|
+
cfg: string;
|
|
448
|
+
evt: string;
|
|
449
|
+
score: string;
|
|
450
|
+
};
|
|
451
|
+
/** Payload retained lúc kết nối + payload LWT lúc rớt. */
|
|
452
|
+
statusPayload: (online: boolean) => string;
|
|
453
|
+
keepaliveS: number;
|
|
454
|
+
/** Thang backoff (ms); phần tử cuối được lặp lại mãi. */
|
|
455
|
+
reconnectDelaysMs: readonly number[];
|
|
456
|
+
onMessage?: (topic: string, payload: string) => void;
|
|
457
|
+
onConnectionChange?: (connected: boolean) => void;
|
|
458
|
+
/** Auth bị từ chối (token thu hồi / máy chưa claim) — để app log/khai báo. */
|
|
459
|
+
onAuthRejected?: (reason: string) => void;
|
|
460
|
+
}
|
|
461
|
+
interface MqttChannelDeps {
|
|
462
|
+
createSocket?: (url: string, protocols: string[]) => MqttSocket;
|
|
463
|
+
setTimeout?: (fn: () => void, ms: number) => unknown;
|
|
464
|
+
clearTimeout?: (handle: unknown) => void;
|
|
465
|
+
now?: () => number;
|
|
466
|
+
/** Jitter ±20% chống bão reconnect đồng loạt sau khi broker restart. */
|
|
467
|
+
random?: () => number;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Sức khoẻ kênh cho quyết định giãn nhịp beat. `isConnected()` KHÔNG đủ: nối
|
|
471
|
+
* được mà chưa SUBACK thì máy không nghe được lệnh nào, còn TCP mở mà broker
|
|
472
|
+
* chết thì `connected` vẫn true tới tận keepalive kế.
|
|
473
|
+
*/
|
|
474
|
+
interface MqttChannelHealth {
|
|
475
|
+
connected: boolean;
|
|
476
|
+
subscribed: boolean;
|
|
477
|
+
lastInboundAt: number;
|
|
478
|
+
healthySince: number;
|
|
479
|
+
}
|
|
480
|
+
interface MqttChannel {
|
|
481
|
+
start(): void;
|
|
482
|
+
/** Đóng CHỦ ĐỘNG: gửi DISCONNECT nên broker KHÔNG bắn LWT. */
|
|
483
|
+
stop(): void;
|
|
484
|
+
isConnected(): boolean;
|
|
485
|
+
health(): MqttChannelHealth;
|
|
486
|
+
publish(topic: string, payload: string, opts?: {
|
|
487
|
+
qos?: 0 | 1;
|
|
488
|
+
retain?: boolean;
|
|
489
|
+
}): boolean;
|
|
490
|
+
}
|
|
491
|
+
declare function createMqttChannel(o: MqttChannelOptions, deps?: MqttChannelDeps): MqttChannel;
|
|
492
|
+
|
|
493
|
+
export { BEAT_FAST_MS, BEAT_SLOW_MS, type ConvexBeatOptions, ConvexDeviceRealtime, DeviceAgent, type DeviceAgentDeviceInfo, type DeviceAgentOptions, type DeviceAgentTransport, type DeviceCommand, type DeviceControlChannel, type DeviceControlSessionGrant, type DeviceHeartbeatBody, type DeviceHeartbeatExtras, type DeviceProcessExit, type HttpDeviceAgentTransportOptions, type MqttBeatHealth, type MqttChannel, type MqttChannelDeps, type MqttChannelHealth, type MqttChannelOptions, type MqttSocket, PickleballDeviceAgent, type PickleballDeviceAgentProps, type RemoteStartConfig, TournamentController, type TournamentControllerOptions, type TournamentEngineView, type TournamentPersistedState, type TournamentPhase, type TournamentStandbyStart, type TournamentState, type TournamentStorage, createConvexControlChannel, createHttpDeviceAgentTransport, createMqttChannel, metricsCrossedThreshold, mqttBeatHealthy, nextBeatDelayMs, withConvexBeat };
|