@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,13 +1,17 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class;
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2;
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
var _chunk6YE5BL3Acjs = require('../chunk-6YE5BL3A.cjs');
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
var _chunkIUIRVKJIcjs = require('../chunk-IUIRVKJI.cjs');
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
|
|
@@ -31,7 +35,7 @@ function createConvexControlChannel(convexUrl) {
|
|
|
31
35
|
for (const item of value) {
|
|
32
36
|
if (item === null || typeof item !== "object") continue;
|
|
33
37
|
const record = item;
|
|
34
|
-
if (typeof record.id !== "string" || record.type !== "start_livestream" && record.type !== "stop_livestream" || typeof record.expiresAt !== "number") {
|
|
38
|
+
if (typeof record.id !== "string" || record.type !== "start_livestream" && record.type !== "stop_livestream" && record.type !== "publish_livestream" && record.type !== "unpublish_livestream" || typeof record.expiresAt !== "number") {
|
|
35
39
|
continue;
|
|
36
40
|
}
|
|
37
41
|
const payload = record.payload !== null && typeof record.payload === "object" && !Array.isArray(record.payload) ? record.payload : {};
|
|
@@ -40,7 +44,8 @@ function createConvexControlChannel(convexUrl) {
|
|
|
40
44
|
type: record.type,
|
|
41
45
|
payload: {
|
|
42
46
|
...typeof payload.title === "string" ? { title: payload.title } : {},
|
|
43
|
-
...payload.visibility === "public" || payload.visibility === "private" ? { visibility: payload.visibility } : {}
|
|
47
|
+
...payload.visibility === "public" || payload.visibility === "private" ? { visibility: payload.visibility } : {},
|
|
48
|
+
...typeof payload.courtRef === "string" && payload.courtRef.trim() !== "" ? { courtRef: payload.courtRef } : {}
|
|
44
49
|
},
|
|
45
50
|
expiresAt: record.expiresAt
|
|
46
51
|
});
|
|
@@ -82,10 +87,32 @@ var init_convex_control_channel = _chunk7OX2DSKIcjs.__esm.call(void 0, {
|
|
|
82
87
|
}
|
|
83
88
|
});
|
|
84
89
|
|
|
90
|
+
// src/device-agent/beat-policy.ts
|
|
91
|
+
var BEAT_SLOW_MS = 6e4;
|
|
92
|
+
var BEAT_FAST_MS = 12e3;
|
|
93
|
+
var MQTT_HEALTHY_SILENCE_MS = 45e3;
|
|
94
|
+
var MQTT_HEALTHY_HOLD_MS = 12e4;
|
|
95
|
+
function mqttBeatHealthy(h, now) {
|
|
96
|
+
if (!h.connected || !h.subscribed) return false;
|
|
97
|
+
if (h.lastInboundAt <= 0 || now - h.lastInboundAt >= MQTT_HEALTHY_SILENCE_MS) return false;
|
|
98
|
+
if (h.healthySince <= 0 || now - h.healthySince < MQTT_HEALTHY_HOLD_MS) return false;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
function nextBeatDelayMs(input) {
|
|
102
|
+
const hint = _nullishCoalesce(input.serverHintMs, () => ( BEAT_FAST_MS));
|
|
103
|
+
if (input.busy || !input.health) return BEAT_FAST_MS;
|
|
104
|
+
const local = mqttBeatHealthy(input.health, input.now) ? BEAT_SLOW_MS : BEAT_FAST_MS;
|
|
105
|
+
return Math.min(local, hint);
|
|
106
|
+
}
|
|
107
|
+
|
|
85
108
|
// src/device-agent/agent.ts
|
|
86
109
|
var DEFAULT_HEARTBEAT_INTERVAL_MS = 12e3;
|
|
110
|
+
var IDLE_HEARTBEAT_INTERVAL_MS = 12e3;
|
|
87
111
|
var REGISTER_RETRY_BASE_MS = 2e3;
|
|
88
112
|
var REGISTER_RETRY_MAX_MS = 6e4;
|
|
113
|
+
var METRICS_BASELINE_MS = 5 * 6e4;
|
|
114
|
+
var METRICS_BATTERY_LOW_PCT = 10;
|
|
115
|
+
var METRICS_BATTERY_DELTA_PCT = 5;
|
|
89
116
|
var METRIC_FIELDS = [
|
|
90
117
|
"ramFreeMb",
|
|
91
118
|
"ramTotalMb",
|
|
@@ -96,7 +123,9 @@ var METRIC_FIELDS = [
|
|
|
96
123
|
"netType",
|
|
97
124
|
"linkMbps",
|
|
98
125
|
"rssi",
|
|
99
|
-
"connQuality"
|
|
126
|
+
"connQuality",
|
|
127
|
+
"signalPct",
|
|
128
|
+
"localIp"
|
|
100
129
|
];
|
|
101
130
|
function defaultSubscribeAppState(listener) {
|
|
102
131
|
const { AppState } = _chunk7OX2DSKIcjs.__require.call(void 0, "react-native");
|
|
@@ -121,23 +150,44 @@ function pickMetrics(sample) {
|
|
|
121
150
|
}
|
|
122
151
|
return Object.keys(metrics).length > 0 ? metrics : void 0;
|
|
123
152
|
}
|
|
153
|
+
function metricsCrossedThreshold(last, next) {
|
|
154
|
+
if (!last) return true;
|
|
155
|
+
const b0 = typeof last.batteryPct === "number" ? last.batteryPct : void 0;
|
|
156
|
+
const b1 = typeof next.batteryPct === "number" ? next.batteryPct : void 0;
|
|
157
|
+
if (b1 !== void 0) {
|
|
158
|
+
if (b1 < METRICS_BATTERY_LOW_PCT && (b0 === void 0 || b0 >= METRICS_BATTERY_LOW_PCT)) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
if (b0 !== void 0 && Math.abs(b1 - b0) >= METRICS_BATTERY_DELTA_PCT) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (next.thermal !== last.thermal) return true;
|
|
166
|
+
if (next.netType !== last.netType) return true;
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
124
169
|
var DeviceAgent = (_class = class {
|
|
125
170
|
|
|
126
171
|
__init() {this.running = false}
|
|
127
172
|
__init2() {this.registered = false}
|
|
128
173
|
__init3() {this.foreground = true}
|
|
129
174
|
__init4() {this.heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS}
|
|
175
|
+
/** Nhịp server gợi ý ở beat gần nhất — undefined = server chưa nói gì → 12s. */
|
|
176
|
+
|
|
130
177
|
__init5() {this.timer = null}
|
|
131
178
|
__init6() {this.beatInFlight = false}
|
|
132
179
|
__init7() {this.registerAttempt = 0}
|
|
133
180
|
__init8() {this.unsubscribeAppState = null}
|
|
134
181
|
__init9() {this.heartbeatFailures = 0}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
182
|
+
/** Metrics đã gửi lần gần nhất + mốc thời gian, để lọc "chỉ gửi khi đổi". */
|
|
183
|
+
|
|
184
|
+
__init10() {this.lastMetricsSentAt = 0}
|
|
185
|
+
__init11() {this.controlChannel = null}
|
|
186
|
+
__init12() {this.controlSessionId = null}
|
|
187
|
+
__init13() {this.controlUnsubscribers = []}
|
|
188
|
+
__init14() {this.executing = false}
|
|
189
|
+
__init15() {this.handledCommandIds = /* @__PURE__ */ new Set()}
|
|
190
|
+
constructor(options) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);_class.prototype.__init7.call(this);_class.prototype.__init8.call(this);_class.prototype.__init9.call(this);_class.prototype.__init10.call(this);_class.prototype.__init11.call(this);_class.prototype.__init12.call(this);_class.prototype.__init13.call(this);_class.prototype.__init14.call(this);_class.prototype.__init15.call(this);
|
|
141
191
|
this.options = options;
|
|
142
192
|
}
|
|
143
193
|
start() {
|
|
@@ -167,6 +217,28 @@ var DeviceAgent = (_class = class {
|
|
|
167
217
|
this.closeControlChannel();
|
|
168
218
|
}
|
|
169
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Đang có phiên hoặc đã mở kênh điều khiển = lệnh tới qua kênh, nhịp thường
|
|
222
|
+
* là đủ. Rảnh và chưa có kênh = đang chờ lệnh, đập nhanh cho kịp dashboard.
|
|
223
|
+
*/
|
|
224
|
+
nextBeatDelay() {
|
|
225
|
+
const base = this.controlChannel ? this.heartbeatIntervalMs : this.options.engine.getSnapshot().sessionId ? this.heartbeatIntervalMs : Math.min(IDLE_HEARTBEAT_INTERVAL_MS, this.heartbeatIntervalMs);
|
|
226
|
+
const health = (() => {
|
|
227
|
+
try {
|
|
228
|
+
return _nullishCoalesce(_optionalChain([this, 'access', _3 => _3.options, 'access', _4 => _4.mqttHealth, 'optionalCall', _5 => _5()]), () => ( null));
|
|
229
|
+
} catch (e) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
})();
|
|
233
|
+
const adaptive = nextBeatDelayMs({
|
|
234
|
+
health,
|
|
235
|
+
now: Date.now(),
|
|
236
|
+
serverHintMs: this.serverBeatHintMs,
|
|
237
|
+
// Đang chạy lệnh / đang mở kênh điều khiển = có người chờ kết quả.
|
|
238
|
+
busy: this.executing || this.controlChannel !== null
|
|
239
|
+
});
|
|
240
|
+
return Math.max(base, adaptive);
|
|
241
|
+
}
|
|
170
242
|
schedule(delayMs) {
|
|
171
243
|
if (!this.running || !this.foreground) return;
|
|
172
244
|
if (this.timer) clearTimeout(this.timer);
|
|
@@ -183,7 +255,7 @@ var DeviceAgent = (_class = class {
|
|
|
183
255
|
if (!this.registered) return;
|
|
184
256
|
}
|
|
185
257
|
await this.heartbeat();
|
|
186
|
-
this.schedule(this.
|
|
258
|
+
this.schedule(this.nextBeatDelay());
|
|
187
259
|
} finally {
|
|
188
260
|
this.beatInFlight = false;
|
|
189
261
|
}
|
|
@@ -199,7 +271,7 @@ var DeviceAgent = (_class = class {
|
|
|
199
271
|
}
|
|
200
272
|
this.registered = true;
|
|
201
273
|
this.registerAttempt = 0;
|
|
202
|
-
} catch (
|
|
274
|
+
} catch (e2) {
|
|
203
275
|
const delay = Math.min(
|
|
204
276
|
REGISTER_RETRY_BASE_MS * 2 ** this.registerAttempt,
|
|
205
277
|
REGISTER_RETRY_MAX_MS
|
|
@@ -208,24 +280,51 @@ var DeviceAgent = (_class = class {
|
|
|
208
280
|
this.schedule(delay);
|
|
209
281
|
}
|
|
210
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Lọc metrics cho heartbeat: chỉ trả (và ghi nhớ) khi vượt ngưỡng
|
|
285
|
+
* (metricsCrossedThreshold) hoặc đã quá METRICS_BASELINE_MS kể từ lần gửi.
|
|
286
|
+
* Trả undefined = nhịp này không kèm metrics.
|
|
287
|
+
*/
|
|
288
|
+
selectMetricsForHeartbeat(metrics) {
|
|
289
|
+
if (!metrics) return void 0;
|
|
290
|
+
const now = Date.now();
|
|
291
|
+
const crossed = metricsCrossedThreshold(this.lastSentMetrics, metrics);
|
|
292
|
+
const baselineDue = now - this.lastMetricsSentAt >= METRICS_BASELINE_MS;
|
|
293
|
+
if (!crossed && !baselineDue) return void 0;
|
|
294
|
+
this.lastSentMetrics = metrics;
|
|
295
|
+
this.lastMetricsSentAt = now;
|
|
296
|
+
return metrics;
|
|
297
|
+
}
|
|
211
298
|
async heartbeat() {
|
|
212
299
|
const snapshot = this.options.engine.getSnapshot();
|
|
213
300
|
let metrics;
|
|
214
301
|
try {
|
|
215
|
-
metrics = pickMetrics(await _asyncNullishCoalesce(await _optionalChain([this, 'access',
|
|
216
|
-
} catch (
|
|
302
|
+
metrics = pickMetrics(await _asyncNullishCoalesce(await _optionalChain([this, 'access', _6 => _6.options, 'access', _7 => _7.deviceMetrics, 'optionalCall', _8 => _8()]), async () => ( null)));
|
|
303
|
+
} catch (e3) {
|
|
217
304
|
metrics = void 0;
|
|
218
305
|
}
|
|
306
|
+
const metricsToSend = this.selectMetricsForHeartbeat(metrics);
|
|
307
|
+
let extras = {};
|
|
308
|
+
try {
|
|
309
|
+
extras = _nullishCoalesce(_optionalChain([this, 'access', _9 => _9.options, 'access', _10 => _10.heartbeatExtras, 'optionalCall', _11 => _11()]), () => ( {}));
|
|
310
|
+
} catch (e4) {
|
|
311
|
+
extras = {};
|
|
312
|
+
}
|
|
219
313
|
let result;
|
|
220
314
|
try {
|
|
221
315
|
result = await this.options.transport.heartbeat({
|
|
222
316
|
...this.options.bootstrapInput,
|
|
223
317
|
agentState: snapshot.state,
|
|
224
318
|
currentSessionId: snapshot.sessionId,
|
|
225
|
-
..._optionalChain([this, 'access',
|
|
226
|
-
...
|
|
319
|
+
..._optionalChain([this, 'access', _12 => _12.options, 'access', _13 => _13.deviceInfo, 'optionalAccess', _14 => _14.appVersion]) ? { appVersion: this.options.deviceInfo.appVersion } : {},
|
|
320
|
+
...metricsToSend ? { metrics: metricsToSend } : {},
|
|
321
|
+
...extras.recentExits && extras.recentExits.length > 0 ? { recentExits: extras.recentExits } : {},
|
|
322
|
+
...extras.powerState !== void 0 ? { powerState: extras.powerState } : {},
|
|
323
|
+
// Chỉ gửi khi TRUE. Server đọc doc tỉ số mỗi nhịp có cờ này, nên gửi
|
|
324
|
+
// `false` cũng vô hại nhưng phí một field trên mọi beat của mọi máy.
|
|
325
|
+
...extras.wantScore ? { wantScore: true } : {}
|
|
227
326
|
});
|
|
228
|
-
} catch (
|
|
327
|
+
} catch (e5) {
|
|
229
328
|
this.heartbeatFailures += 1;
|
|
230
329
|
if (this.heartbeatFailures >= 3) {
|
|
231
330
|
this.registered = false;
|
|
@@ -234,6 +333,7 @@ var DeviceAgent = (_class = class {
|
|
|
234
333
|
return;
|
|
235
334
|
}
|
|
236
335
|
this.heartbeatFailures = 0;
|
|
336
|
+
this.serverBeatHintMs = typeof result.beatIntervalMs === "number" && result.beatIntervalMs > 0 ? result.beatIntervalMs : void 0;
|
|
237
337
|
if (result.controlSession && !this.controlChannel) {
|
|
238
338
|
this.openControlChannel(result.controlSession);
|
|
239
339
|
}
|
|
@@ -244,7 +344,7 @@ var DeviceAgent = (_class = class {
|
|
|
244
344
|
channel = (_nullishCoalesce(this.options.createControlChannel, () => ( defaultCreateControlChannel)))(
|
|
245
345
|
grant.convexUrl
|
|
246
346
|
);
|
|
247
|
-
} catch (
|
|
347
|
+
} catch (e6) {
|
|
248
348
|
return;
|
|
249
349
|
}
|
|
250
350
|
this.controlChannel = channel;
|
|
@@ -269,13 +369,13 @@ var DeviceAgent = (_class = class {
|
|
|
269
369
|
for (const unsubscribe of this.controlUnsubscribers) {
|
|
270
370
|
try {
|
|
271
371
|
unsubscribe();
|
|
272
|
-
} catch (
|
|
372
|
+
} catch (e7) {
|
|
273
373
|
}
|
|
274
374
|
}
|
|
275
375
|
this.controlUnsubscribers = [];
|
|
276
376
|
try {
|
|
277
|
-
_optionalChain([this, 'access',
|
|
278
|
-
} catch (
|
|
377
|
+
_optionalChain([this, 'access', _15 => _15.controlChannel, 'optionalAccess', _16 => _16.close, 'call', _17 => _17()]);
|
|
378
|
+
} catch (e8) {
|
|
279
379
|
}
|
|
280
380
|
this.controlChannel = null;
|
|
281
381
|
this.controlSessionId = null;
|
|
@@ -316,7 +416,13 @@ var DeviceAgent = (_class = class {
|
|
|
316
416
|
void 0,
|
|
317
417
|
_nullishCoalesce(sessionId, () => ( void 0))
|
|
318
418
|
);
|
|
319
|
-
if (sessionId) _optionalChain([this, 'access',
|
|
419
|
+
if (sessionId) _optionalChain([this, 'access', _18 => _18.options, 'access', _19 => _19.onRemoteSessionStarted, 'optionalCall', _20 => _20(sessionId)]);
|
|
420
|
+
} else if (command.type === "publish_livestream") {
|
|
421
|
+
await this.runPublish();
|
|
422
|
+
await this.safeAck(channel, grant, command.id, "completed");
|
|
423
|
+
} else if (command.type === "unpublish_livestream") {
|
|
424
|
+
await this.runUnpublish();
|
|
425
|
+
await this.safeAck(channel, grant, command.id, "completed");
|
|
320
426
|
} else {
|
|
321
427
|
await this.runStopLivestream();
|
|
322
428
|
await this.safeAck(channel, grant, command.id, "completed");
|
|
@@ -327,7 +433,7 @@ var DeviceAgent = (_class = class {
|
|
|
327
433
|
grant,
|
|
328
434
|
command.id,
|
|
329
435
|
"failed",
|
|
330
|
-
|
|
436
|
+
_chunkIUIRVKJIcjs.normalizeError.call(void 0, error).message
|
|
331
437
|
);
|
|
332
438
|
}
|
|
333
439
|
}
|
|
@@ -341,13 +447,19 @@ var DeviceAgent = (_class = class {
|
|
|
341
447
|
await engine.prepare();
|
|
342
448
|
}
|
|
343
449
|
const remote = this.options.remoteStart;
|
|
450
|
+
const courtRef = _optionalChain([command, 'access', _21 => _21.payload, 'access', _22 => _22.courtRef, 'optionalAccess', _23 => _23.trim, 'call', _24 => _24()]);
|
|
344
451
|
await engine.start(
|
|
345
452
|
{
|
|
346
453
|
externalSessionId: `remote-${command.id}`,
|
|
347
|
-
title: _optionalChain([command, 'access',
|
|
454
|
+
title: _optionalChain([command, 'access', _25 => _25.payload, 'access', _26 => _26.title, 'optionalAccess', _27 => _27.trim, 'call', _28 => _28()]) || remote.defaultTitle || "Livestream",
|
|
348
455
|
visibility: _nullishCoalesce(_nullishCoalesce(command.payload.visibility, () => ( remote.visibility)), () => ( "public")),
|
|
349
456
|
consentVersion: remote.consentVersion,
|
|
350
|
-
metadata: {
|
|
457
|
+
metadata: {
|
|
458
|
+
...remote.metadata,
|
|
459
|
+
deviceCommandId: command.id,
|
|
460
|
+
// courtRef trong metadata → server đậu phiên standby vào sân
|
|
461
|
+
...courtRef ? { courtRef } : {}
|
|
462
|
+
},
|
|
351
463
|
standby: true
|
|
352
464
|
},
|
|
353
465
|
{ autoPublish: false }
|
|
@@ -359,6 +471,36 @@ var DeviceAgent = (_class = class {
|
|
|
359
471
|
await engine.stop("user");
|
|
360
472
|
}
|
|
361
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* standby → live theo lệnh dashboard. startPublishing() NUỐT lỗi (recordError
|
|
476
|
+
* vào snapshot thay vì throw) nên phải kiểm state sau khi gọi — không kiểm là
|
|
477
|
+
* lệnh được ack "completed" dù thiết bị chưa hề phát.
|
|
478
|
+
*/
|
|
479
|
+
async runPublish() {
|
|
480
|
+
const engine = this.options.engine;
|
|
481
|
+
const state = engine.getSnapshot().state;
|
|
482
|
+
if (state === "live") return;
|
|
483
|
+
if (state !== "standby") {
|
|
484
|
+
throw new Error(`Thi\u1EBFt b\u1ECB kh\xF4ng \u1EDF ch\u1EBF \u0111\u1ED9 ch\u1EDD (tr\u1EA1ng th\xE1i ${state})`);
|
|
485
|
+
}
|
|
486
|
+
await engine.startPublishing();
|
|
487
|
+
if (engine.getSnapshot().state !== "live") {
|
|
488
|
+
const message = _optionalChain([engine, 'access', _29 => _29.getSnapshot, 'call', _30 => _30(), 'access', _31 => _31.error, 'optionalAccess', _32 => _32.message]);
|
|
489
|
+
throw new Error(message ? `Kh\xF4ng ph\xE1t \u0111\u01B0\u1EE3c: ${message}` : "Kh\xF4ng ph\xE1t \u0111\u01B0\u1EE3c");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async runUnpublish() {
|
|
493
|
+
const engine = this.options.engine;
|
|
494
|
+
const state = engine.getSnapshot().state;
|
|
495
|
+
if (state === "standby") return;
|
|
496
|
+
if (state !== "live") {
|
|
497
|
+
throw new Error(`Thi\u1EBFt b\u1ECB kh\xF4ng \u0111ang ph\xE1t (tr\u1EA1ng th\xE1i ${state})`);
|
|
498
|
+
}
|
|
499
|
+
await engine.stopPublishing();
|
|
500
|
+
if (engine.getSnapshot().state !== "standby") {
|
|
501
|
+
throw new Error("Kh\xF4ng d\u1EEBng ph\xE1t \u0111\u01B0\u1EE3c");
|
|
502
|
+
}
|
|
503
|
+
}
|
|
362
504
|
async safeAck(channel, grant, commandId, status, errorMessage, resultSessionId) {
|
|
363
505
|
try {
|
|
364
506
|
await channel.ack({
|
|
@@ -369,7 +511,7 @@ var DeviceAgent = (_class = class {
|
|
|
369
511
|
...errorMessage !== void 0 ? { errorMessage } : {},
|
|
370
512
|
...resultSessionId !== void 0 ? { resultSessionId } : {}
|
|
371
513
|
});
|
|
372
|
-
} catch (
|
|
514
|
+
} catch (e9) {
|
|
373
515
|
}
|
|
374
516
|
}
|
|
375
517
|
}, _class);
|
|
@@ -382,11 +524,11 @@ var defaultPaths = {
|
|
|
382
524
|
function createHttpDeviceAgentTransport(options) {
|
|
383
525
|
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
384
526
|
if (baseUrl === "") {
|
|
385
|
-
throw new (0,
|
|
527
|
+
throw new (0, _chunkIUIRVKJIcjs.ExpoSdkConfigurationError)("baseUrl must be a non-empty string");
|
|
386
528
|
}
|
|
387
529
|
const fetchImplementation = _nullishCoalesce(options.fetch, () => ( globalThis.fetch));
|
|
388
530
|
if (typeof fetchImplementation !== "function") {
|
|
389
|
-
throw new (0,
|
|
531
|
+
throw new (0, _chunkIUIRVKJIcjs.ExpoSdkConfigurationError)("A Fetch API implementation is required");
|
|
390
532
|
}
|
|
391
533
|
const paths = { ...defaultPaths, ...options.paths };
|
|
392
534
|
async function post(path, body) {
|
|
@@ -405,18 +547,18 @@ function createHttpDeviceAgentTransport(options) {
|
|
|
405
547
|
}
|
|
406
548
|
);
|
|
407
549
|
const envelope = await response.json().catch(() => {
|
|
408
|
-
throw new (0,
|
|
550
|
+
throw new (0, _chunkIUIRVKJIcjs.InvalidResponseError)();
|
|
409
551
|
});
|
|
410
552
|
if (!response.ok) throw parseProxyError(envelope);
|
|
411
553
|
if (!isRecord(envelope) || !("data" in envelope)) {
|
|
412
|
-
throw new (0,
|
|
554
|
+
throw new (0, _chunkIUIRVKJIcjs.InvalidResponseError)();
|
|
413
555
|
}
|
|
414
556
|
return envelope.data;
|
|
415
557
|
} catch (error) {
|
|
416
|
-
if (controller.signal.aborted) throw new (0,
|
|
417
|
-
if (error instanceof
|
|
558
|
+
if (controller.signal.aborted) throw new (0, _chunkIUIRVKJIcjs.ExpoSdkTimeoutError)();
|
|
559
|
+
if (error instanceof _chunkIUIRVKJIcjs.PickleballExpoError) throw error;
|
|
418
560
|
if (error instanceof TypeError) {
|
|
419
|
-
throw new (0,
|
|
561
|
+
throw new (0, _chunkIUIRVKJIcjs.NetworkUnavailableError)(void 0, { cause: error });
|
|
420
562
|
}
|
|
421
563
|
throw error;
|
|
422
564
|
} finally {
|
|
@@ -427,8 +569,9 @@ function createHttpDeviceAgentTransport(options) {
|
|
|
427
569
|
async register(input) {
|
|
428
570
|
const data = await post(paths.register, input);
|
|
429
571
|
if (!isRecord(data) || typeof data.deviceId !== "string" || typeof data.heartbeatIntervalMs !== "number" || !Number.isFinite(data.heartbeatIntervalMs)) {
|
|
430
|
-
throw new (0,
|
|
572
|
+
throw new (0, _chunkIUIRVKJIcjs.InvalidResponseError)();
|
|
431
573
|
}
|
|
574
|
+
_optionalChain([options, 'access', _33 => _33.onData, 'optionalCall', _34 => _34("register", data)]);
|
|
432
575
|
return {
|
|
433
576
|
deviceId: data.deviceId,
|
|
434
577
|
heartbeatIntervalMs: data.heartbeatIntervalMs
|
|
@@ -436,42 +579,349 @@ function createHttpDeviceAgentTransport(options) {
|
|
|
436
579
|
},
|
|
437
580
|
async heartbeat(input) {
|
|
438
581
|
const data = await post(paths.heartbeat, input);
|
|
439
|
-
if (!isRecord(data)) throw new (0,
|
|
582
|
+
if (!isRecord(data)) throw new (0, _chunkIUIRVKJIcjs.InvalidResponseError)();
|
|
583
|
+
_optionalChain([options, 'access', _35 => _35.onData, 'optionalCall', _36 => _36("heartbeat", data)]);
|
|
584
|
+
const beatIntervalMs = typeof data.beatIntervalMs === "number" && Number.isFinite(data.beatIntervalMs) ? { beatIntervalMs: data.beatIntervalMs } : {};
|
|
440
585
|
const grant = data.controlSession;
|
|
441
|
-
if (grant === null || grant === void 0)
|
|
586
|
+
if (grant === null || grant === void 0) {
|
|
587
|
+
return { controlSession: null, ...beatIntervalMs };
|
|
588
|
+
}
|
|
442
589
|
if (!isRecord(grant) || typeof grant.id !== "string" || typeof grant.token !== "string" || typeof grant.convexUrl !== "string" || grant.convexUrl === "") {
|
|
443
|
-
throw new (0,
|
|
590
|
+
throw new (0, _chunkIUIRVKJIcjs.InvalidResponseError)();
|
|
444
591
|
}
|
|
445
592
|
return {
|
|
446
593
|
controlSession: {
|
|
447
594
|
id: grant.id,
|
|
448
595
|
token: grant.token,
|
|
449
596
|
convexUrl: grant.convexUrl
|
|
450
|
-
}
|
|
597
|
+
},
|
|
598
|
+
...beatIntervalMs
|
|
451
599
|
};
|
|
452
600
|
}
|
|
453
601
|
};
|
|
454
602
|
}
|
|
455
603
|
function parseProxyError(envelope) {
|
|
456
604
|
if (isRecord(envelope) && isRecord(envelope.error) && typeof envelope.error.code === "string") {
|
|
457
|
-
return
|
|
605
|
+
return _chunkIUIRVKJIcjs.errorFromCode.call(void 0,
|
|
458
606
|
envelope.error.code,
|
|
459
607
|
typeof envelope.error.message === "string" ? envelope.error.message : void 0
|
|
460
608
|
);
|
|
461
609
|
}
|
|
462
|
-
return new (0,
|
|
610
|
+
return new (0, _chunkIUIRVKJIcjs.InvalidResponseError)();
|
|
463
611
|
}
|
|
464
612
|
function isRecord(value) {
|
|
465
613
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
466
614
|
}
|
|
467
615
|
|
|
616
|
+
// src/device-agent/tournament.ts
|
|
617
|
+
var ACTIVE_ENGINE_STATES = /* @__PURE__ */ new Set([
|
|
618
|
+
"preparing",
|
|
619
|
+
"connecting",
|
|
620
|
+
"standby",
|
|
621
|
+
"live",
|
|
622
|
+
"reconnecting",
|
|
623
|
+
"ending"
|
|
624
|
+
]);
|
|
625
|
+
var RECOVERY_CHECK_MS = 5e3;
|
|
626
|
+
var DEFAULT_RECOVERY_GRACE_MS = 9e4;
|
|
627
|
+
var TournamentController = (_class2 = class {
|
|
628
|
+
constructor(options) {;_class2.prototype.__init16.call(this);_class2.prototype.__init17.call(this);_class2.prototype.__init18.call(this);_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);
|
|
629
|
+
this.options = options;
|
|
630
|
+
this.engine = options.engine;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
__init16() {this.armed = false}
|
|
634
|
+
__init17() {this.courtRef = null}
|
|
635
|
+
__init18() {this.lastError = null}
|
|
636
|
+
__init19() {this.attemptInFlight = false}
|
|
637
|
+
__init20() {this.retryAttempt = 0}
|
|
638
|
+
__init21() {this.retryTimer = null}
|
|
639
|
+
/** Mốc hết nhường cho engine tự cứu phiên; null = đang không trong pha đó. */
|
|
640
|
+
__init22() {this.recoveryDeadline = null}
|
|
641
|
+
__init23() {this.unsubscribe = null}
|
|
642
|
+
__init24() {this.disposed = false}
|
|
643
|
+
/** Tăng mỗi lần arm/disarm — huỷ attempt cũ đang chạy dở. */
|
|
644
|
+
__init25() {this.armGeneration = 0}
|
|
645
|
+
getState() {
|
|
646
|
+
const engineState = this.engine.getSnapshot().state;
|
|
647
|
+
let phase;
|
|
648
|
+
if (!this.armed) phase = "disarmed";
|
|
649
|
+
else if (engineState === "live") phase = "live";
|
|
650
|
+
else if (engineState === "standby") phase = "standby";
|
|
651
|
+
else if (this.retryTimer) phase = "retry_wait";
|
|
652
|
+
else phase = "arming";
|
|
653
|
+
return {
|
|
654
|
+
armed: this.armed,
|
|
655
|
+
courtRef: this.courtRef,
|
|
656
|
+
phase,
|
|
657
|
+
lastError: this.lastError
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
/** Khôi phục sau khi app mở lại (reboot giữa giải) — gọi sau engine.initialize(). */
|
|
661
|
+
async restore() {
|
|
662
|
+
if (this.disposed || this.armed) return;
|
|
663
|
+
let saved = null;
|
|
664
|
+
try {
|
|
665
|
+
saved = await _asyncNullishCoalesce(await _optionalChain([this, 'access', _37 => _37.options, 'access', _38 => _38.storage, 'optionalAccess', _39 => _39.load, 'call', _40 => _40()]), async () => ( null));
|
|
666
|
+
} catch (e10) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (_optionalChain([saved, 'optionalAccess', _41 => _41.courtRef])) await this.arm(saved.courtRef);
|
|
670
|
+
}
|
|
671
|
+
async arm(courtRef) {
|
|
672
|
+
if (this.disposed) return;
|
|
673
|
+
const normalized = courtRef.trim();
|
|
674
|
+
if (!normalized) throw new Error("courtRef kh\xF4ng \u0111\u01B0\u1EE3c \u0111\u1EC3 tr\u1ED1ng");
|
|
675
|
+
this.armed = true;
|
|
676
|
+
this.courtRef = normalized;
|
|
677
|
+
this.lastError = null;
|
|
678
|
+
this.retryAttempt = 0;
|
|
679
|
+
this.armGeneration += 1;
|
|
680
|
+
this.recoveryDeadline = null;
|
|
681
|
+
this.clearRetry();
|
|
682
|
+
try {
|
|
683
|
+
await _optionalChain([this, 'access', _42 => _42.options, 'access', _43 => _43.storage, 'optionalAccess', _44 => _44.save, 'call', _45 => _45({ courtRef: normalized })]);
|
|
684
|
+
} catch (e11) {
|
|
685
|
+
}
|
|
686
|
+
if (!this.unsubscribe) {
|
|
687
|
+
this.unsubscribe = this.engine.subscribe(() => this.handleEngineChange());
|
|
688
|
+
}
|
|
689
|
+
this.emit();
|
|
690
|
+
await this.ensureStandby();
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Tắt chế độ giải đấu. Phiên standby đang chờ được nhả (stop); phiên đang
|
|
694
|
+
* LIVE là trận do đối tác điều khiển — không cắt, chỉ ngừng re-arm.
|
|
695
|
+
*/
|
|
696
|
+
async disarm() {
|
|
697
|
+
this.armed = false;
|
|
698
|
+
this.courtRef = null;
|
|
699
|
+
this.armGeneration += 1;
|
|
700
|
+
this.recoveryDeadline = null;
|
|
701
|
+
this.clearRetry();
|
|
702
|
+
try {
|
|
703
|
+
await _optionalChain([this, 'access', _46 => _46.options, 'access', _47 => _47.storage, 'optionalAccess', _48 => _48.save, 'call', _49 => _49(null)]);
|
|
704
|
+
} catch (e12) {
|
|
705
|
+
}
|
|
706
|
+
const snapshot = this.engine.getSnapshot();
|
|
707
|
+
if (snapshot.sessionId && snapshot.state === "standby") {
|
|
708
|
+
try {
|
|
709
|
+
await this.engine.stop("user");
|
|
710
|
+
} catch (e13) {
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
this.emit();
|
|
714
|
+
}
|
|
715
|
+
dispose() {
|
|
716
|
+
this.disposed = true;
|
|
717
|
+
this.armed = false;
|
|
718
|
+
this.clearRetry();
|
|
719
|
+
_optionalChain([this, 'access', _50 => _50.unsubscribe, 'optionalCall', _51 => _51()]);
|
|
720
|
+
this.unsubscribe = null;
|
|
721
|
+
}
|
|
722
|
+
emit() {
|
|
723
|
+
_optionalChain([this, 'access', _52 => _52.options, 'access', _53 => _53.onStateChange, 'optionalCall', _54 => _54(this.getState())]);
|
|
724
|
+
}
|
|
725
|
+
clearRetry() {
|
|
726
|
+
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
727
|
+
this.retryTimer = null;
|
|
728
|
+
}
|
|
729
|
+
/** Engine còn đang tự cứu CHÍNH phiên này → nhường, đừng cắt để arm phiên mới. */
|
|
730
|
+
isEngineRecovering(state, sessionId) {
|
|
731
|
+
if (state !== "error" || !sessionId) return false;
|
|
732
|
+
const now = (_nullishCoalesce(this.options.now, () => ( Date.now)))();
|
|
733
|
+
if (this.recoveryDeadline === null) {
|
|
734
|
+
this.recoveryDeadline = now + (_nullishCoalesce(this.options.recoveryGraceMs, () => ( DEFAULT_RECOVERY_GRACE_MS)));
|
|
735
|
+
}
|
|
736
|
+
return now < this.recoveryDeadline;
|
|
737
|
+
}
|
|
738
|
+
scheduleRecoveryCheck() {
|
|
739
|
+
if (this.retryTimer || this.attemptInFlight || !this.armed) {
|
|
740
|
+
this.emit();
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
this.retryTimer = setTimeout(() => {
|
|
744
|
+
this.retryTimer = null;
|
|
745
|
+
void this.ensureStandby();
|
|
746
|
+
}, RECOVERY_CHECK_MS);
|
|
747
|
+
this.emit();
|
|
748
|
+
}
|
|
749
|
+
handleEngineChange() {
|
|
750
|
+
if (!this.armed || this.disposed) return;
|
|
751
|
+
const snapshot = this.engine.getSnapshot();
|
|
752
|
+
const state = snapshot.state;
|
|
753
|
+
if (this.isEngineRecovering(state, snapshot.sessionId)) {
|
|
754
|
+
this.scheduleRecoveryCheck();
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (state === "standby" || state === "live") this.recoveryDeadline = null;
|
|
758
|
+
if (state === "ended" || state === "idle" || state === "error") {
|
|
759
|
+
this.scheduleRearm();
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (state === "preview" && !snapshot.sessionId && !this.attemptInFlight) {
|
|
763
|
+
this.scheduleRearm();
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (state === "standby" || state === "live") {
|
|
767
|
+
this.retryAttempt = 0;
|
|
768
|
+
this.lastError = null;
|
|
769
|
+
}
|
|
770
|
+
this.emit();
|
|
771
|
+
}
|
|
772
|
+
scheduleRearm() {
|
|
773
|
+
if (this.retryTimer || this.attemptInFlight || !this.armed) {
|
|
774
|
+
this.emit();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
const base = _nullishCoalesce(this.options.rearmBaseDelayMs, () => ( 2e3));
|
|
778
|
+
const max = _nullishCoalesce(this.options.rearmMaxDelayMs, () => ( 6e4));
|
|
779
|
+
const delay = Math.min(base * 2 ** this.retryAttempt, max);
|
|
780
|
+
this.retryTimer = setTimeout(() => {
|
|
781
|
+
this.retryTimer = null;
|
|
782
|
+
void this.ensureStandby();
|
|
783
|
+
}, delay);
|
|
784
|
+
this.emit();
|
|
785
|
+
}
|
|
786
|
+
async ensureStandby() {
|
|
787
|
+
if (!this.armed || this.disposed || this.attemptInFlight) return;
|
|
788
|
+
const courtRef = this.courtRef;
|
|
789
|
+
if (!courtRef) return;
|
|
790
|
+
const generation = this.armGeneration;
|
|
791
|
+
const engineState = this.engine.getSnapshot().state;
|
|
792
|
+
if (engineState === "standby" || engineState === "live") {
|
|
793
|
+
this.recoveryDeadline = null;
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (ACTIVE_ENGINE_STATES.has(engineState)) {
|
|
797
|
+
this.scheduleRearm();
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (this.isEngineRecovering(engineState, this.engine.getSnapshot().sessionId)) {
|
|
801
|
+
this.scheduleRecoveryCheck();
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
this.attemptInFlight = true;
|
|
805
|
+
this.emit();
|
|
806
|
+
let failed = false;
|
|
807
|
+
try {
|
|
808
|
+
if (this.engine.getSnapshot().state === "error") {
|
|
809
|
+
try {
|
|
810
|
+
await this.engine.stop("error");
|
|
811
|
+
} catch (e14) {
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (this.engine.getSnapshot().state !== "preview") {
|
|
815
|
+
await this.engine.prepare();
|
|
816
|
+
}
|
|
817
|
+
if (!this.armed || this.armGeneration !== generation) return;
|
|
818
|
+
const start = this.options.standbyStart;
|
|
819
|
+
const now = _nullishCoalesce(this.options.now, () => ( Date.now));
|
|
820
|
+
await this.engine.start(
|
|
821
|
+
{
|
|
822
|
+
externalSessionId: `tournament-${courtRef}-${now()}`,
|
|
823
|
+
title: _nullishCoalesce(_optionalChain([start, 'access', _55 => _55.titleFor, 'optionalCall', _56 => _56(courtRef)]), () => ( `${courtRef} \u2014 ch\u1EDD tr\u1EADn`)),
|
|
824
|
+
visibility: _nullishCoalesce(start.visibility, () => ( "public")),
|
|
825
|
+
consentVersion: start.consentVersion,
|
|
826
|
+
metadata: { ...start.metadata, courtRef }
|
|
827
|
+
},
|
|
828
|
+
{ autoPublish: false }
|
|
829
|
+
);
|
|
830
|
+
this.retryAttempt = 0;
|
|
831
|
+
this.lastError = null;
|
|
832
|
+
} catch (error) {
|
|
833
|
+
this.lastError = error instanceof Error ? error.message : String(error);
|
|
834
|
+
this.retryAttempt += 1;
|
|
835
|
+
failed = true;
|
|
836
|
+
} finally {
|
|
837
|
+
this.attemptInFlight = false;
|
|
838
|
+
}
|
|
839
|
+
if (failed && this.armed && this.armGeneration === generation) {
|
|
840
|
+
this.scheduleRearm();
|
|
841
|
+
}
|
|
842
|
+
this.emit();
|
|
843
|
+
}
|
|
844
|
+
}, _class2);
|
|
845
|
+
|
|
468
846
|
// src/device-agent/index.ts
|
|
469
847
|
init_convex_control_channel();
|
|
470
848
|
|
|
849
|
+
// src/device-agent/convex-beat-transport.ts
|
|
850
|
+
var WS_BEAT_RETRY_MS = 6e4;
|
|
851
|
+
var WS_BEAT_TIMEOUT_MS = 1e4;
|
|
852
|
+
function withConvexBeat(base, realtime, options) {
|
|
853
|
+
const now = _nullishCoalesce(options.now, () => ( Date.now));
|
|
854
|
+
const timeoutMs = _nullishCoalesce(options.timeoutMs, () => ( WS_BEAT_TIMEOUT_MS));
|
|
855
|
+
let wsBrokenUntil = 0;
|
|
856
|
+
return {
|
|
857
|
+
register(input) {
|
|
858
|
+
return base.register(input);
|
|
859
|
+
},
|
|
860
|
+
async heartbeat(input) {
|
|
861
|
+
const deviceToken = options.getDeviceToken();
|
|
862
|
+
if (!deviceToken || now() < wsBrokenUntil) {
|
|
863
|
+
return base.heartbeat(input);
|
|
864
|
+
}
|
|
865
|
+
try {
|
|
866
|
+
const data = await withTimeout(
|
|
867
|
+
realtime.beat({
|
|
868
|
+
deviceToken,
|
|
869
|
+
// Server tra máy theo token — bootstrapInput (installationId…) chỉ
|
|
870
|
+
// đường HTTP cần. Chỉ gửi đúng phần mutation beat nhận.
|
|
871
|
+
agentState: input.agentState,
|
|
872
|
+
currentSessionId: input.currentSessionId,
|
|
873
|
+
...input.appVersion !== void 0 ? { appVersion: input.appVersion } : {},
|
|
874
|
+
...input.metrics !== void 0 ? { metrics: input.metrics } : {},
|
|
875
|
+
...input.recentExits !== void 0 && input.recentExits.length > 0 ? { recentExits: input.recentExits } : {},
|
|
876
|
+
...input.powerState !== void 0 ? { powerState: input.powerState } : {}
|
|
877
|
+
}),
|
|
878
|
+
timeoutMs
|
|
879
|
+
);
|
|
880
|
+
_optionalChain([options, 'access', _57 => _57.onData, 'optionalCall', _58 => _58("heartbeat", data)]);
|
|
881
|
+
return {
|
|
882
|
+
controlSession: parseControlSession(data.controlSession),
|
|
883
|
+
...typeof data.beatIntervalMs === "number" && Number.isFinite(data.beatIntervalMs) ? { beatIntervalMs: data.beatIntervalMs } : {}
|
|
884
|
+
};
|
|
885
|
+
} catch (e15) {
|
|
886
|
+
wsBrokenUntil = now() + WS_BEAT_RETRY_MS;
|
|
887
|
+
return base.heartbeat(input);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function parseControlSession(grant) {
|
|
893
|
+
if (grant === null || grant === void 0) return null;
|
|
894
|
+
if (grant === null || typeof grant !== "object" || typeof grant.id !== "string" || typeof grant.token !== "string" || typeof grant.convexUrl !== "string" || grant.convexUrl === "") {
|
|
895
|
+
throw new Error("Invalid controlSession in beat response");
|
|
896
|
+
}
|
|
897
|
+
const record = grant;
|
|
898
|
+
return {
|
|
899
|
+
id: record.id,
|
|
900
|
+
token: record.token,
|
|
901
|
+
convexUrl: record.convexUrl
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
function withTimeout(promise, ms) {
|
|
905
|
+
return new Promise((resolve, reject) => {
|
|
906
|
+
const timer = setTimeout(() => reject(new Error("beat timeout")), ms);
|
|
907
|
+
_optionalChain([timer, 'access', _59 => _59.unref, 'optionalCall', _60 => _60()]);
|
|
908
|
+
promise.then(
|
|
909
|
+
(value) => {
|
|
910
|
+
clearTimeout(timer);
|
|
911
|
+
resolve(value);
|
|
912
|
+
},
|
|
913
|
+
(error) => {
|
|
914
|
+
clearTimeout(timer);
|
|
915
|
+
reject(error);
|
|
916
|
+
}
|
|
917
|
+
);
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
471
921
|
// src/device-agent/component.tsx
|
|
472
922
|
var _react = require('react');
|
|
473
923
|
function PickleballDeviceAgent(props) {
|
|
474
|
-
const host =
|
|
924
|
+
const host = _chunkIUIRVKJIcjs.useLivestreamHost.call(void 0, );
|
|
475
925
|
const propsRef = _react.useRef.call(void 0, props);
|
|
476
926
|
propsRef.current = props;
|
|
477
927
|
_react.useEffect.call(void 0, () => {
|
|
@@ -483,7 +933,9 @@ function PickleballDeviceAgent(props) {
|
|
|
483
933
|
remoteStart: propsRef.current.remoteStart,
|
|
484
934
|
deviceInfo: propsRef.current.deviceInfo,
|
|
485
935
|
createControlChannel: propsRef.current.createControlChannel,
|
|
486
|
-
onRemoteSessionStarted: (sessionId) => _optionalChain([propsRef, 'access',
|
|
936
|
+
onRemoteSessionStarted: (sessionId) => _optionalChain([propsRef, 'access', _61 => _61.current, 'access', _62 => _62.onRemoteSessionStarted, 'optionalCall', _63 => _63(sessionId)]),
|
|
937
|
+
heartbeatExtras: () => _optionalChain([propsRef, 'access', _64 => _64.current, 'access', _65 => _65.heartbeatExtras, 'optionalCall', _66 => _66()]),
|
|
938
|
+
mqttHealth: () => _nullishCoalesce(_optionalChain([propsRef, 'access', _67 => _67.current, 'access', _68 => _68.mqttHealth, 'optionalCall', _69 => _69()]), () => ( null))
|
|
487
939
|
});
|
|
488
940
|
agent.start();
|
|
489
941
|
return () => agent.stop();
|
|
@@ -491,8 +943,485 @@ function PickleballDeviceAgent(props) {
|
|
|
491
943
|
return null;
|
|
492
944
|
}
|
|
493
945
|
|
|
946
|
+
// src/device-agent/mqtt-codec.ts
|
|
947
|
+
var PACKET = {
|
|
948
|
+
CONNECT: 1,
|
|
949
|
+
CONNACK: 2,
|
|
950
|
+
PUBLISH: 3,
|
|
951
|
+
PUBACK: 4,
|
|
952
|
+
SUBSCRIBE: 8,
|
|
953
|
+
SUBACK: 9,
|
|
954
|
+
PINGREQ: 12,
|
|
955
|
+
PINGRESP: 13,
|
|
956
|
+
DISCONNECT: 14
|
|
957
|
+
};
|
|
958
|
+
var CONNACK_REASON = {
|
|
959
|
+
0: "ch\u1EA5p nh\u1EADn",
|
|
960
|
+
1: "sai phi\xEAn b\u1EA3n giao th\u1EE9c",
|
|
961
|
+
2: "clientId b\u1ECB t\u1EEB ch\u1ED1i",
|
|
962
|
+
3: "broker kh\xF4ng s\u1EB5n s\xE0ng",
|
|
963
|
+
4: "sai username/password",
|
|
964
|
+
5: "kh\xF4ng \u0111\u01B0\u1EE3c ph\xE9p (auth hook t\u1EEB ch\u1ED1i)"
|
|
965
|
+
};
|
|
966
|
+
function utf8Encode(s) {
|
|
967
|
+
const out = [];
|
|
968
|
+
for (let i = 0; i < s.length; i++) {
|
|
969
|
+
let code = s.charCodeAt(i);
|
|
970
|
+
if (code >= 55296 && code <= 56319 && i + 1 < s.length) {
|
|
971
|
+
const next = s.charCodeAt(i + 1);
|
|
972
|
+
if (next >= 56320 && next <= 57343) {
|
|
973
|
+
code = (code - 55296 << 10) + (next - 56320) + 65536;
|
|
974
|
+
i++;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (code < 128) out.push(code);
|
|
978
|
+
else if (code < 2048) out.push(192 | code >> 6, 128 | code & 63);
|
|
979
|
+
else if (code < 65536)
|
|
980
|
+
out.push(224 | code >> 12, 128 | code >> 6 & 63, 128 | code & 63);
|
|
981
|
+
else
|
|
982
|
+
out.push(
|
|
983
|
+
240 | code >> 18,
|
|
984
|
+
128 | code >> 12 & 63,
|
|
985
|
+
128 | code >> 6 & 63,
|
|
986
|
+
128 | code & 63
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
return Uint8Array.from(out);
|
|
990
|
+
}
|
|
991
|
+
function utf8Decode(bytes) {
|
|
992
|
+
let out = "";
|
|
993
|
+
for (let i = 0; i < bytes.length; ) {
|
|
994
|
+
const b = bytes[i];
|
|
995
|
+
let code;
|
|
996
|
+
if (b < 128) {
|
|
997
|
+
code = b;
|
|
998
|
+
i += 1;
|
|
999
|
+
} else if (b < 224) {
|
|
1000
|
+
code = (b & 31) << 6 | bytes[i + 1] & 63;
|
|
1001
|
+
i += 2;
|
|
1002
|
+
} else if (b < 240) {
|
|
1003
|
+
code = (b & 15) << 12 | (bytes[i + 1] & 63) << 6 | bytes[i + 2] & 63;
|
|
1004
|
+
i += 3;
|
|
1005
|
+
} else {
|
|
1006
|
+
code = (b & 7) << 18 | (bytes[i + 1] & 63) << 12 | (bytes[i + 2] & 63) << 6 | bytes[i + 3] & 63;
|
|
1007
|
+
i += 4;
|
|
1008
|
+
}
|
|
1009
|
+
if (code > 65535) {
|
|
1010
|
+
code -= 65536;
|
|
1011
|
+
out += String.fromCharCode(55296 + (code >> 10), 56320 + (code & 1023));
|
|
1012
|
+
} else {
|
|
1013
|
+
out += String.fromCharCode(code);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return out;
|
|
1017
|
+
}
|
|
1018
|
+
function encodeString(s) {
|
|
1019
|
+
const bytes = utf8Encode(s);
|
|
1020
|
+
if (bytes.length > 65535) throw new Error("Chu\u1ED7i MQTT v\u01B0\u1EE3t 65535 byte");
|
|
1021
|
+
return [bytes.length >> 8, bytes.length & 255, ...bytes];
|
|
1022
|
+
}
|
|
1023
|
+
function encodeRemainingLength(n) {
|
|
1024
|
+
if (n < 0 || n > 268435455) throw new Error(`\u0110\u1ED9 d\xE0i MQTT kh\xF4ng h\u1EE3p l\u1EC7: ${n}`);
|
|
1025
|
+
const out = [];
|
|
1026
|
+
let value = n;
|
|
1027
|
+
do {
|
|
1028
|
+
let byte = value % 128;
|
|
1029
|
+
value = Math.floor(value / 128);
|
|
1030
|
+
if (value > 0) byte |= 128;
|
|
1031
|
+
out.push(byte);
|
|
1032
|
+
} while (value > 0);
|
|
1033
|
+
return out;
|
|
1034
|
+
}
|
|
1035
|
+
function decodeRemainingLength(bytes, offset) {
|
|
1036
|
+
let multiplier = 1;
|
|
1037
|
+
let value = 0;
|
|
1038
|
+
for (let i = 0; i < 4; i++) {
|
|
1039
|
+
const idx = offset + i;
|
|
1040
|
+
if (idx >= bytes.length) return null;
|
|
1041
|
+
const byte = bytes[idx];
|
|
1042
|
+
value += (byte & 127) * multiplier;
|
|
1043
|
+
if ((byte & 128) === 0) return { value, bytesUsed: i + 1 };
|
|
1044
|
+
multiplier *= 128;
|
|
1045
|
+
}
|
|
1046
|
+
throw new Error("Remaining Length h\u1ECFng (qu\xE1 4 byte)");
|
|
1047
|
+
}
|
|
1048
|
+
function frame(type, flags, body) {
|
|
1049
|
+
return Uint8Array.from([
|
|
1050
|
+
type << 4 | flags,
|
|
1051
|
+
...encodeRemainingLength(body.length),
|
|
1052
|
+
...body
|
|
1053
|
+
]);
|
|
1054
|
+
}
|
|
1055
|
+
function encodeConnect(o) {
|
|
1056
|
+
let flags = 0;
|
|
1057
|
+
if (o.cleanSession) flags |= 2;
|
|
1058
|
+
if (o.will) {
|
|
1059
|
+
flags |= 4;
|
|
1060
|
+
flags |= (o.will.qos & 3) << 3;
|
|
1061
|
+
if (o.will.retain) flags |= 32;
|
|
1062
|
+
}
|
|
1063
|
+
if (o.password !== void 0) flags |= 64;
|
|
1064
|
+
if (o.username !== void 0) flags |= 128;
|
|
1065
|
+
const body = [
|
|
1066
|
+
...encodeString("MQTT"),
|
|
1067
|
+
4,
|
|
1068
|
+
// protocol level 4 = MQTT 3.1.1
|
|
1069
|
+
flags,
|
|
1070
|
+
o.keepaliveS >> 8 & 255,
|
|
1071
|
+
o.keepaliveS & 255,
|
|
1072
|
+
...encodeString(o.clientId)
|
|
1073
|
+
];
|
|
1074
|
+
if (o.will) {
|
|
1075
|
+
body.push(...encodeString(o.will.topic));
|
|
1076
|
+
const payload = utf8Encode(o.will.payload);
|
|
1077
|
+
body.push(payload.length >> 8, payload.length & 255, ...payload);
|
|
1078
|
+
}
|
|
1079
|
+
if (o.username !== void 0) body.push(...encodeString(o.username));
|
|
1080
|
+
if (o.password !== void 0) body.push(...encodeString(o.password));
|
|
1081
|
+
return frame(PACKET.CONNECT, 0, body);
|
|
1082
|
+
}
|
|
1083
|
+
function encodeSubscribe(packetId, filters) {
|
|
1084
|
+
const body = [packetId >> 8, packetId & 255];
|
|
1085
|
+
for (const f of filters) {
|
|
1086
|
+
body.push(...encodeString(f.topic), f.qos);
|
|
1087
|
+
}
|
|
1088
|
+
return frame(PACKET.SUBSCRIBE, 2, body);
|
|
1089
|
+
}
|
|
1090
|
+
function encodePublish(o) {
|
|
1091
|
+
let flags = 0;
|
|
1092
|
+
if (o.retain) flags |= 1;
|
|
1093
|
+
flags |= (o.qos & 3) << 1;
|
|
1094
|
+
const body = [...encodeString(o.topic)];
|
|
1095
|
+
if (o.qos > 0) {
|
|
1096
|
+
if (o.packetId === void 0) throw new Error("PUBLISH QoS1 thi\u1EBFu packetId");
|
|
1097
|
+
body.push(o.packetId >> 8, o.packetId & 255);
|
|
1098
|
+
}
|
|
1099
|
+
body.push(...utf8Encode(o.payload));
|
|
1100
|
+
return frame(PACKET.PUBLISH, flags, body);
|
|
1101
|
+
}
|
|
1102
|
+
function encodePuback(packetId) {
|
|
1103
|
+
return frame(PACKET.PUBACK, 0, [packetId >> 8, packetId & 255]);
|
|
1104
|
+
}
|
|
1105
|
+
function encodePingReq() {
|
|
1106
|
+
return frame(PACKET.PINGREQ, 0, []);
|
|
1107
|
+
}
|
|
1108
|
+
function encodeDisconnect() {
|
|
1109
|
+
return frame(PACKET.DISCONNECT, 0, []);
|
|
1110
|
+
}
|
|
1111
|
+
function decodePackets(buffer) {
|
|
1112
|
+
const packets = [];
|
|
1113
|
+
let offset = 0;
|
|
1114
|
+
for (; ; ) {
|
|
1115
|
+
if (offset >= buffer.length) break;
|
|
1116
|
+
const header = buffer[offset];
|
|
1117
|
+
const len = decodeRemainingLength(buffer, offset + 1);
|
|
1118
|
+
if (!len) break;
|
|
1119
|
+
const start = offset + 1 + len.bytesUsed;
|
|
1120
|
+
const end = start + len.value;
|
|
1121
|
+
if (end > buffer.length) break;
|
|
1122
|
+
const type = header >> 4;
|
|
1123
|
+
const flags = header & 15;
|
|
1124
|
+
const body = buffer.subarray(start, end);
|
|
1125
|
+
packets.push(decodeBody(type, flags, body));
|
|
1126
|
+
offset = end;
|
|
1127
|
+
}
|
|
1128
|
+
return { packets, rest: buffer.subarray(offset) };
|
|
1129
|
+
}
|
|
1130
|
+
function readString(body, offset) {
|
|
1131
|
+
const len = body[offset] << 8 | body[offset + 1];
|
|
1132
|
+
const start = offset + 2;
|
|
1133
|
+
return { value: utf8Decode(body.subarray(start, start + len)), next: start + len };
|
|
1134
|
+
}
|
|
1135
|
+
function decodeBody(type, flags, body) {
|
|
1136
|
+
switch (type) {
|
|
1137
|
+
case PACKET.CONNACK:
|
|
1138
|
+
return {
|
|
1139
|
+
type: "connack",
|
|
1140
|
+
sessionPresent: (body[0] & 1) === 1,
|
|
1141
|
+
returnCode: _nullishCoalesce(body[1], () => ( 0))
|
|
1142
|
+
};
|
|
1143
|
+
case PACKET.SUBACK:
|
|
1144
|
+
return {
|
|
1145
|
+
type: "suback",
|
|
1146
|
+
packetId: body[0] << 8 | body[1],
|
|
1147
|
+
codes: Array.from(body.subarray(2))
|
|
1148
|
+
};
|
|
1149
|
+
case PACKET.PUBLISH: {
|
|
1150
|
+
const qos = flags >> 1 & 3;
|
|
1151
|
+
const topic = readString(body, 0);
|
|
1152
|
+
let next = topic.next;
|
|
1153
|
+
let packetId;
|
|
1154
|
+
if (qos > 0) {
|
|
1155
|
+
packetId = body[next] << 8 | body[next + 1];
|
|
1156
|
+
next += 2;
|
|
1157
|
+
}
|
|
1158
|
+
return {
|
|
1159
|
+
type: "publish",
|
|
1160
|
+
topic: topic.value,
|
|
1161
|
+
payload: utf8Decode(body.subarray(next)),
|
|
1162
|
+
qos,
|
|
1163
|
+
retain: (flags & 1) === 1,
|
|
1164
|
+
...packetId !== void 0 ? { packetId } : {}
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
case PACKET.PUBACK:
|
|
1168
|
+
return { type: "puback", packetId: body[0] << 8 | body[1] };
|
|
1169
|
+
case PACKET.PINGRESP:
|
|
1170
|
+
return { type: "pingresp" };
|
|
1171
|
+
default:
|
|
1172
|
+
return { type: "other", packetType: type };
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// src/device-agent/mqtt-channel.ts
|
|
1177
|
+
var AUTH_RETRY_MS = 5 * 6e4;
|
|
1178
|
+
function createMqttChannel(o, deps = {}) {
|
|
1179
|
+
const createSocket = _nullishCoalesce(deps.createSocket, () => ( ((url, protocols) => new WebSocket(url, protocols))));
|
|
1180
|
+
const setTimer = _nullishCoalesce(deps.setTimeout, () => ( ((fn, ms) => setTimeout(fn, ms))));
|
|
1181
|
+
const clearTimer = _nullishCoalesce(deps.clearTimeout, () => ( ((h) => clearTimeout(h))));
|
|
1182
|
+
const now = _nullishCoalesce(deps.now, () => ( (() => Date.now())));
|
|
1183
|
+
const random = _nullishCoalesce(deps.random, () => ( Math.random));
|
|
1184
|
+
let socket = null;
|
|
1185
|
+
let buffer = new Uint8Array(0);
|
|
1186
|
+
let connected = false;
|
|
1187
|
+
let stopped = true;
|
|
1188
|
+
let attempt = 0;
|
|
1189
|
+
let packetId = 0;
|
|
1190
|
+
let reconnectTimer = null;
|
|
1191
|
+
let pingTimer = null;
|
|
1192
|
+
let lastInboundAt = 0;
|
|
1193
|
+
let subscribed = false;
|
|
1194
|
+
let healthySince = 0;
|
|
1195
|
+
const nextPacketId = () => {
|
|
1196
|
+
packetId = packetId % 65535 + 1;
|
|
1197
|
+
return packetId;
|
|
1198
|
+
};
|
|
1199
|
+
const setConnected = (value) => {
|
|
1200
|
+
if (connected === value) return;
|
|
1201
|
+
connected = value;
|
|
1202
|
+
_optionalChain([o, 'access', _70 => _70.onConnectionChange, 'optionalCall', _71 => _71(value)]);
|
|
1203
|
+
};
|
|
1204
|
+
const clearTimers = () => {
|
|
1205
|
+
if (reconnectTimer !== null) {
|
|
1206
|
+
clearTimer(reconnectTimer);
|
|
1207
|
+
reconnectTimer = null;
|
|
1208
|
+
}
|
|
1209
|
+
if (pingTimer !== null) {
|
|
1210
|
+
clearTimer(pingTimer);
|
|
1211
|
+
pingTimer = null;
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
const send = (bytes) => {
|
|
1215
|
+
if (!socket) return false;
|
|
1216
|
+
try {
|
|
1217
|
+
socket.send(bytes);
|
|
1218
|
+
return true;
|
|
1219
|
+
} catch (e16) {
|
|
1220
|
+
teardown(true);
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
const scheduleReconnect = (delayMs) => {
|
|
1225
|
+
if (stopped || reconnectTimer !== null) return;
|
|
1226
|
+
const base = _nullishCoalesce(_nullishCoalesce(delayMs, () => ( o.reconnectDelaysMs[Math.min(attempt, o.reconnectDelaysMs.length - 1)])), () => ( 3e4));
|
|
1227
|
+
const jitter = base === 0 ? 0 : base * 0.2 * (random() * 2 - 1);
|
|
1228
|
+
attempt += 1;
|
|
1229
|
+
reconnectTimer = setTimer(() => {
|
|
1230
|
+
reconnectTimer = null;
|
|
1231
|
+
open();
|
|
1232
|
+
}, Math.max(0, Math.round(base + jitter)));
|
|
1233
|
+
};
|
|
1234
|
+
const teardown = (reconnect, delayMs) => {
|
|
1235
|
+
clearTimers();
|
|
1236
|
+
setConnected(false);
|
|
1237
|
+
subscribed = false;
|
|
1238
|
+
healthySince = 0;
|
|
1239
|
+
buffer = new Uint8Array(0);
|
|
1240
|
+
const dead = socket;
|
|
1241
|
+
socket = null;
|
|
1242
|
+
if (dead) {
|
|
1243
|
+
dead.onopen = null;
|
|
1244
|
+
dead.onmessage = null;
|
|
1245
|
+
dead.onerror = null;
|
|
1246
|
+
dead.onclose = null;
|
|
1247
|
+
try {
|
|
1248
|
+
dead.close();
|
|
1249
|
+
} catch (e17) {
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (reconnect) scheduleReconnect(delayMs);
|
|
1253
|
+
};
|
|
1254
|
+
const armPing = () => {
|
|
1255
|
+
if (pingTimer !== null) clearTimer(pingTimer);
|
|
1256
|
+
pingTimer = setTimer(() => {
|
|
1257
|
+
pingTimer = null;
|
|
1258
|
+
if (!socket) return;
|
|
1259
|
+
if (now() - lastInboundAt > o.keepaliveS * 1500) {
|
|
1260
|
+
teardown(true);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
send(encodePingReq());
|
|
1264
|
+
armPing();
|
|
1265
|
+
}, Math.max(1e3, Math.round(o.keepaliveS * 750)));
|
|
1266
|
+
};
|
|
1267
|
+
const handlePacket = (p) => {
|
|
1268
|
+
switch (p.type) {
|
|
1269
|
+
case "connack": {
|
|
1270
|
+
if (p.returnCode !== 0) {
|
|
1271
|
+
const reason = _nullishCoalesce(CONNACK_REASON[p.returnCode], () => ( `m\xE3 ${p.returnCode}`));
|
|
1272
|
+
_optionalChain([o, 'access', _72 => _72.onAuthRejected, 'optionalCall', _73 => _73(reason)]);
|
|
1273
|
+
teardown(true, p.returnCode === 4 || p.returnCode === 5 ? AUTH_RETRY_MS : void 0);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
attempt = 0;
|
|
1277
|
+
setConnected(true);
|
|
1278
|
+
send(
|
|
1279
|
+
encodePublish({
|
|
1280
|
+
topic: o.topics.status,
|
|
1281
|
+
payload: o.statusPayload(true),
|
|
1282
|
+
qos: 1,
|
|
1283
|
+
retain: true,
|
|
1284
|
+
packetId: nextPacketId()
|
|
1285
|
+
})
|
|
1286
|
+
);
|
|
1287
|
+
send(
|
|
1288
|
+
encodeSubscribe(nextPacketId(), [
|
|
1289
|
+
{ topic: o.topics.cmd, qos: 1 },
|
|
1290
|
+
{ topic: o.topics.cfg, qos: 1 },
|
|
1291
|
+
// Tỉ số trận đang ghi (retained) — reconnect là nhận ngay bản mới nhất.
|
|
1292
|
+
{ topic: o.topics.score, qos: 1 }
|
|
1293
|
+
])
|
|
1294
|
+
);
|
|
1295
|
+
armPing();
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
case "suback": {
|
|
1299
|
+
if (p.codes.every((c) => c < 128)) {
|
|
1300
|
+
subscribed = true;
|
|
1301
|
+
if (healthySince === 0) healthySince = now();
|
|
1302
|
+
}
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
case "publish": {
|
|
1306
|
+
if (p.qos === 1 && p.packetId !== void 0) send(encodePuback(p.packetId));
|
|
1307
|
+
_optionalChain([o, 'access', _74 => _74.onMessage, 'optionalCall', _75 => _75(p.topic, p.payload)]);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
default:
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
const open = () => {
|
|
1315
|
+
if (stopped || socket) return;
|
|
1316
|
+
let s;
|
|
1317
|
+
try {
|
|
1318
|
+
s = createSocket(o.url, ["mqtt"]);
|
|
1319
|
+
} catch (e18) {
|
|
1320
|
+
scheduleReconnect();
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
socket = s;
|
|
1324
|
+
s.binaryType = "arraybuffer";
|
|
1325
|
+
lastInboundAt = now();
|
|
1326
|
+
s.onopen = () => {
|
|
1327
|
+
if (socket !== s) return;
|
|
1328
|
+
send(
|
|
1329
|
+
encodeConnect({
|
|
1330
|
+
clientId: o.deviceId,
|
|
1331
|
+
username: o.deviceId,
|
|
1332
|
+
password: o.deviceToken,
|
|
1333
|
+
keepaliveS: o.keepaliveS,
|
|
1334
|
+
cleanSession: true,
|
|
1335
|
+
will: {
|
|
1336
|
+
topic: o.topics.status,
|
|
1337
|
+
payload: o.statusPayload(false),
|
|
1338
|
+
qos: 1,
|
|
1339
|
+
retain: true
|
|
1340
|
+
}
|
|
1341
|
+
})
|
|
1342
|
+
);
|
|
1343
|
+
};
|
|
1344
|
+
s.onmessage = (event) => {
|
|
1345
|
+
if (socket !== s) return;
|
|
1346
|
+
const data = event.data;
|
|
1347
|
+
let chunk = null;
|
|
1348
|
+
if (data instanceof ArrayBuffer) {
|
|
1349
|
+
chunk = new Uint8Array(data);
|
|
1350
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
1351
|
+
chunk = Uint8Array.from(data);
|
|
1352
|
+
}
|
|
1353
|
+
if (!chunk) return;
|
|
1354
|
+
lastInboundAt = now();
|
|
1355
|
+
const merged = new Uint8Array(buffer.length + chunk.length);
|
|
1356
|
+
merged.set(buffer, 0);
|
|
1357
|
+
merged.set(chunk, buffer.length);
|
|
1358
|
+
const { packets, rest } = decodePackets(merged);
|
|
1359
|
+
buffer = Uint8Array.from(rest);
|
|
1360
|
+
for (const p of packets) handlePacket(p);
|
|
1361
|
+
};
|
|
1362
|
+
s.onerror = () => {
|
|
1363
|
+
if (socket !== s) return;
|
|
1364
|
+
teardown(true);
|
|
1365
|
+
};
|
|
1366
|
+
s.onclose = () => {
|
|
1367
|
+
if (socket !== s) return;
|
|
1368
|
+
teardown(true);
|
|
1369
|
+
};
|
|
1370
|
+
};
|
|
1371
|
+
return {
|
|
1372
|
+
start() {
|
|
1373
|
+
if (!stopped) return;
|
|
1374
|
+
stopped = false;
|
|
1375
|
+
attempt = 0;
|
|
1376
|
+
open();
|
|
1377
|
+
},
|
|
1378
|
+
stop() {
|
|
1379
|
+
if (stopped) return;
|
|
1380
|
+
stopped = true;
|
|
1381
|
+
if (socket && connected) {
|
|
1382
|
+
send(
|
|
1383
|
+
encodePublish({
|
|
1384
|
+
topic: o.topics.status,
|
|
1385
|
+
payload: o.statusPayload(false),
|
|
1386
|
+
qos: 1,
|
|
1387
|
+
retain: true,
|
|
1388
|
+
packetId: nextPacketId()
|
|
1389
|
+
})
|
|
1390
|
+
);
|
|
1391
|
+
send(encodeDisconnect());
|
|
1392
|
+
}
|
|
1393
|
+
teardown(false);
|
|
1394
|
+
},
|
|
1395
|
+
isConnected: () => connected,
|
|
1396
|
+
health: () => ({ connected, subscribed, lastInboundAt, healthySince }),
|
|
1397
|
+
publish(topic, payload, opts) {
|
|
1398
|
+
if (!connected) return false;
|
|
1399
|
+
const qos = _nullishCoalesce(_optionalChain([opts, 'optionalAccess', _76 => _76.qos]), () => ( 0));
|
|
1400
|
+
return send(
|
|
1401
|
+
encodePublish({
|
|
1402
|
+
topic,
|
|
1403
|
+
payload,
|
|
1404
|
+
qos,
|
|
1405
|
+
retain: _nullishCoalesce(_optionalChain([opts, 'optionalAccess', _77 => _77.retain]), () => ( false)),
|
|
1406
|
+
...qos === 1 ? { packetId: nextPacketId() } : {}
|
|
1407
|
+
})
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
|
|
1422
|
+
|
|
494
1423
|
|
|
495
1424
|
|
|
496
1425
|
|
|
497
1426
|
|
|
498
|
-
exports.DeviceAgent = DeviceAgent; exports.PickleballDeviceAgent = PickleballDeviceAgent; exports.createConvexControlChannel = createConvexControlChannel; exports.createHttpDeviceAgentTransport = createHttpDeviceAgentTransport;
|
|
1427
|
+
exports.BEAT_FAST_MS = BEAT_FAST_MS; exports.BEAT_SLOW_MS = BEAT_SLOW_MS; exports.DeviceAgent = DeviceAgent; exports.PickleballDeviceAgent = PickleballDeviceAgent; exports.TournamentController = TournamentController; exports.createConvexCommandSubscription = _chunk6YE5BL3Acjs.createConvexCommandSubscription; exports.createConvexControlChannel = createConvexControlChannel; exports.createConvexDeviceRealtime = _chunk6YE5BL3Acjs.createConvexDeviceRealtime; exports.createHttpDeviceAgentTransport = createHttpDeviceAgentTransport; exports.createMqttChannel = createMqttChannel; exports.metricsCrossedThreshold = metricsCrossedThreshold; exports.mqttBeatHealthy = mqttBeatHealthy; exports.nextBeatDelayMs = nextBeatDelayMs; exports.withConvexBeat = withConvexBeat;
|