@pickleball/expo-sdk 0.2.0 → 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 +41 -0
- package/README.md +155 -101
- package/dist/advanced-DcbHBCGb.d.ts +300 -0
- package/dist/advanced-DeQwg7MK.d.cts +300 -0
- package/dist/advanced.cjs +14 -2
- package/dist/advanced.d.cts +2 -2
- package/dist/advanced.d.ts +2 -2
- package/dist/advanced.js +17 -5
- package/dist/camera-controls-By8tHigY.d.cts +97 -0
- package/dist/camera-controls-rCBUGGPC.d.ts +97 -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-7Z7TN5N3.js +318 -0
- package/dist/{chunk-TRLASNPQ.cjs → chunk-GXQYMPHE.cjs} +1 -1
- package/dist/{chunk-TBPR2QND.cjs → chunk-IUIRVKJI.cjs} +736 -399
- package/dist/chunk-TBUEXRFQ.cjs +318 -0
- package/dist/{chunk-FODE2W2H.js → chunk-XGP2MN5R.js} +688 -351
- package/dist/{chunk-6JOMESNT.js → chunk-XL4CISVQ.js} +1 -1
- 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 +973 -44
- package/dist/device-agent/index.d.cts +339 -3
- package/dist/device-agent/index.d.ts +339 -3
- package/dist/device-agent/index.js +940 -11
- package/dist/{engine-CCSSqPbZ.d.cts → engine-BSvSdCSE.d.cts} +227 -2
- package/dist/{engine-CCSSqPbZ.d.ts → engine-BSvSdCSE.d.ts} +227 -2
- package/dist/index.cjs +419 -937
- package/dist/index.d.cts +315 -32
- package/dist/index.d.ts +315 -32
- package/dist/index.js +423 -941
- package/dist/plugin.cjs +11 -13
- package/dist/plugin.d.cts +15 -1
- package/dist/plugin.d.ts +15 -1
- package/dist/plugin.js +7 -9
- 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/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 +2 -2
- package/dist/version.js +1 -1
- package/package.json +31 -15
- package/src/advanced.ts +11 -9
- 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 +262 -9
- package/src/device-agent/beat-policy.ts +46 -0
- package/src/device-agent/component.tsx +11 -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 +7 -1
- package/src/device-agent/http-transport.ts +22 -1
- package/src/device-agent/index.ts +22 -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 +580 -15
- package/src/http-session-provider.ts +21 -189
- package/src/index.ts +84 -5
- package/src/local-video-quality.ts +43 -0
- package/src/native-runtime.ts +234 -78
- package/src/plugin.ts +19 -6
- package/src/provider.tsx +94 -18
- 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 +164 -12
- 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/advanced-BXDrDl1h.d.ts +0 -316
- package/dist/advanced-BdAa2Erv.d.cts +0 -316
- package/src/livekit-room-adapter.ts +0 -390
- package/src/livestream-screen.tsx +0 -1002
|
@@ -1,4 +1,37 @@
|
|
|
1
|
-
import {
|
|
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;
|
|
2
35
|
|
|
3
36
|
/**
|
|
4
37
|
* DeviceAgent — đăng ký thiết bị với backend khi app mở và giữ heartbeat
|
|
@@ -15,10 +48,12 @@ interface DeviceAgentDeviceInfo {
|
|
|
15
48
|
}
|
|
16
49
|
interface DeviceCommand {
|
|
17
50
|
id: string;
|
|
18
|
-
type: "start_livestream" | "stop_livestream";
|
|
51
|
+
type: "start_livestream" | "stop_livestream" | "publish_livestream" | "unpublish_livestream";
|
|
19
52
|
payload: {
|
|
20
53
|
title?: string;
|
|
21
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;
|
|
22
57
|
};
|
|
23
58
|
expiresAt: number;
|
|
24
59
|
}
|
|
@@ -27,12 +62,49 @@ interface DeviceControlSessionGrant {
|
|
|
27
62
|
token: string;
|
|
28
63
|
convexUrl: string;
|
|
29
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
|
+
}
|
|
30
81
|
interface DeviceHeartbeatBody {
|
|
31
82
|
agentState?: string;
|
|
32
83
|
currentSessionId?: string | null;
|
|
33
84
|
appVersion?: string;
|
|
34
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;
|
|
35
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">;
|
|
36
108
|
interface DeviceAgentTransport {
|
|
37
109
|
register(input: BootstrapInput & DeviceAgentDeviceInfo): Promise<{
|
|
38
110
|
deviceId: string;
|
|
@@ -40,6 +112,11 @@ interface DeviceAgentTransport {
|
|
|
40
112
|
}>;
|
|
41
113
|
heartbeat(input: BootstrapInput & DeviceHeartbeatBody): Promise<{
|
|
42
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;
|
|
43
120
|
}>;
|
|
44
121
|
}
|
|
45
122
|
/** Kênh điều khiển realtime — mặc định là Convex websocket (convex-control-channel). */
|
|
@@ -77,22 +154,42 @@ interface DeviceAgentOptions {
|
|
|
77
154
|
deviceInfo?: DeviceAgentDeviceInfo;
|
|
78
155
|
onRemoteSessionStarted?: (sessionId: string) => void;
|
|
79
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;
|
|
80
165
|
/** Factory kênh điều khiển — test truyền fake; mặc định Convex websocket. */
|
|
81
166
|
createControlChannel?: (convexUrl: string) => DeviceControlChannel;
|
|
82
167
|
/** Test hook: nguồn app-state; mặc định react-native AppState. */
|
|
83
168
|
subscribeAppState?: (listener: AppStateListener) => () => void;
|
|
84
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;
|
|
85
177
|
declare class DeviceAgent {
|
|
86
178
|
private readonly options;
|
|
87
179
|
private running;
|
|
88
180
|
private registered;
|
|
89
181
|
private foreground;
|
|
90
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;
|
|
91
185
|
private timer;
|
|
92
186
|
private beatInFlight;
|
|
93
187
|
private registerAttempt;
|
|
94
188
|
private unsubscribeAppState;
|
|
95
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;
|
|
96
193
|
private controlChannel;
|
|
97
194
|
private controlSessionId;
|
|
98
195
|
private controlUnsubscribers;
|
|
@@ -102,9 +199,20 @@ declare class DeviceAgent {
|
|
|
102
199
|
start(): void;
|
|
103
200
|
stop(): void;
|
|
104
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;
|
|
105
207
|
private schedule;
|
|
106
208
|
private beat;
|
|
107
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;
|
|
108
216
|
private heartbeat;
|
|
109
217
|
private openControlChannel;
|
|
110
218
|
private closeControlChannel;
|
|
@@ -112,6 +220,13 @@ declare class DeviceAgent {
|
|
|
112
220
|
private executeCommand;
|
|
113
221
|
private runStartLivestream;
|
|
114
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;
|
|
115
230
|
private safeAck;
|
|
116
231
|
}
|
|
117
232
|
|
|
@@ -127,10 +242,125 @@ interface HttpDeviceAgentTransportOptions {
|
|
|
127
242
|
register: string;
|
|
128
243
|
heartbeat: string;
|
|
129
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;
|
|
130
252
|
}
|
|
131
253
|
/** Transport HTTP cho DeviceAgent — mirror createHttpSessionProvider. */
|
|
132
254
|
declare function createHttpDeviceAgentTransport(options: HttpDeviceAgentTransportOptions): DeviceAgentTransport;
|
|
133
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
|
+
|
|
134
364
|
/**
|
|
135
365
|
* Kênh điều khiển realtime mặc định: Convex websocket subscribe các function
|
|
136
366
|
* deviceControl (auth bằng connect-token nhận qua heartbeat — thiết bị không
|
|
@@ -139,6 +369,21 @@ declare function createHttpDeviceAgentTransport(options: HttpDeviceAgentTranspor
|
|
|
139
369
|
*/
|
|
140
370
|
declare function createConvexControlChannel(convexUrl: string): DeviceControlChannel;
|
|
141
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
|
+
|
|
142
387
|
/**
|
|
143
388
|
* Bật device-agent: đăng ký thiết bị + heartbeat presence khi app mở, nhận
|
|
144
389
|
* lệnh start/stop livestream từ dashboard qua kênh điều khiển on-demand.
|
|
@@ -151,7 +396,98 @@ interface PickleballDeviceAgentProps {
|
|
|
151
396
|
deviceInfo?: DeviceAgentDeviceInfo;
|
|
152
397
|
onRemoteSessionStarted?: (sessionId: string) => void;
|
|
153
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;
|
|
154
406
|
}
|
|
155
407
|
declare function PickleballDeviceAgent(props: PickleballDeviceAgentProps): null;
|
|
156
408
|
|
|
157
|
-
|
|
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 };
|