@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.
Files changed (89) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +155 -101
  3. package/dist/advanced-DcbHBCGb.d.ts +300 -0
  4. package/dist/advanced-DeQwg7MK.d.cts +300 -0
  5. package/dist/advanced.cjs +14 -2
  6. package/dist/advanced.d.cts +2 -2
  7. package/dist/advanced.d.ts +2 -2
  8. package/dist/advanced.js +17 -5
  9. package/dist/camera-controls-By8tHigY.d.cts +97 -0
  10. package/dist/camera-controls-rCBUGGPC.d.ts +97 -0
  11. package/dist/chunk-62YV3PCD.cjs +139 -0
  12. package/dist/chunk-6F3HN6US.js +50 -0
  13. package/dist/chunk-6YE5BL3A.cjs +50 -0
  14. package/dist/chunk-7Z7TN5N3.js +318 -0
  15. package/dist/{chunk-TRLASNPQ.cjs → chunk-GXQYMPHE.cjs} +1 -1
  16. package/dist/{chunk-TBPR2QND.cjs → chunk-IUIRVKJI.cjs} +736 -399
  17. package/dist/chunk-TBUEXRFQ.cjs +318 -0
  18. package/dist/{chunk-FODE2W2H.js → chunk-XGP2MN5R.js} +688 -351
  19. package/dist/{chunk-6JOMESNT.js → chunk-XL4CISVQ.js} +1 -1
  20. package/dist/chunk-ZSKSXOCZ.js +139 -0
  21. package/dist/convex-command-subscription-Cabh9tND.d.ts +41 -0
  22. package/dist/convex-command-subscription-DUgpu75l.d.cts +41 -0
  23. package/dist/device-agent/index.cjs +973 -44
  24. package/dist/device-agent/index.d.cts +339 -3
  25. package/dist/device-agent/index.d.ts +339 -3
  26. package/dist/device-agent/index.js +940 -11
  27. package/dist/{engine-CCSSqPbZ.d.cts → engine-BSvSdCSE.d.cts} +227 -2
  28. package/dist/{engine-CCSSqPbZ.d.ts → engine-BSvSdCSE.d.ts} +227 -2
  29. package/dist/index.cjs +419 -937
  30. package/dist/index.d.cts +315 -32
  31. package/dist/index.d.ts +315 -32
  32. package/dist/index.js +423 -941
  33. package/dist/plugin.cjs +11 -13
  34. package/dist/plugin.d.cts +15 -1
  35. package/dist/plugin.d.ts +15 -1
  36. package/dist/plugin.js +7 -9
  37. package/dist/realtime/index.cjs +31 -0
  38. package/dist/realtime/index.d.cts +15 -0
  39. package/dist/realtime/index.d.ts +15 -0
  40. package/dist/realtime/index.js +31 -0
  41. package/dist/ui/index.cjs +1297 -0
  42. package/dist/ui/index.d.cts +493 -0
  43. package/dist/ui/index.d.ts +493 -0
  44. package/dist/ui/index.js +1297 -0
  45. package/dist/version.cjs +2 -2
  46. package/dist/version.js +1 -1
  47. package/package.json +31 -15
  48. package/src/advanced.ts +11 -9
  49. package/src/auto-resume.tsx +259 -0
  50. package/src/camera-controls.ts +99 -0
  51. package/src/camera-grant-provider.ts +392 -0
  52. package/src/camera-settings.ts +133 -0
  53. package/src/contracts.ts +73 -0
  54. package/src/device-agent/agent.ts +262 -9
  55. package/src/device-agent/beat-policy.ts +46 -0
  56. package/src/device-agent/component.tsx +11 -0
  57. package/src/device-agent/convex-beat-transport.ts +140 -0
  58. package/src/device-agent/convex-command-subscription.ts +101 -0
  59. package/src/device-agent/convex-control-channel.ts +7 -1
  60. package/src/device-agent/http-transport.ts +22 -1
  61. package/src/device-agent/index.ts +22 -0
  62. package/src/device-agent/mqtt-channel.ts +379 -0
  63. package/src/device-agent/mqtt-codec.ts +347 -0
  64. package/src/device-agent/tournament.ts +352 -0
  65. package/src/engine.ts +580 -15
  66. package/src/http-session-provider.ts +21 -189
  67. package/src/index.ts +84 -5
  68. package/src/local-video-quality.ts +43 -0
  69. package/src/native-runtime.ts +234 -78
  70. package/src/plugin.ts +19 -6
  71. package/src/provider.tsx +94 -18
  72. package/src/realtime/index.ts +56 -0
  73. package/src/resume-policy.ts +85 -0
  74. package/src/resume.ts +132 -0
  75. package/src/rtmp-room-adapter.ts +164 -12
  76. package/src/session-grant.ts +256 -0
  77. package/src/ui/camera-screen.tsx +484 -0
  78. package/src/ui/controls-sheet.tsx +445 -0
  79. package/src/ui/copy.ts +326 -0
  80. package/src/ui/helpers.ts +105 -0
  81. package/src/ui/hooks.ts +59 -0
  82. package/src/ui/index.ts +42 -0
  83. package/src/ui/preview-view.tsx +39 -0
  84. package/src/ui/primitives.tsx +304 -0
  85. package/src/uplink-check.ts +173 -0
  86. package/dist/advanced-BXDrDl1h.d.ts +0 -316
  87. package/dist/advanced-BdAa2Erv.d.cts +0 -316
  88. package/src/livekit-room-adapter.ts +0 -390
  89. 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
- var _chunkTBPR2QNDcjs = require('../chunk-TBPR2QND.cjs');
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
- __init10() {this.controlChannel = null}
136
- __init11() {this.controlSessionId = null}
137
- __init12() {this.controlUnsubscribers = []}
138
- __init13() {this.executing = false}
139
- __init14() {this.handledCommandIds = /* @__PURE__ */ new Set()}
140
- 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);
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.heartbeatIntervalMs);
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 (e) {
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', _3 => _3.options, 'access', _4 => _4.deviceMetrics, 'optionalCall', _5 => _5()]), async () => ( null)));
216
- } catch (e2) {
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', _6 => _6.options, 'access', _7 => _7.deviceInfo, 'optionalAccess', _8 => _8.appVersion]) ? { appVersion: this.options.deviceInfo.appVersion } : {},
226
- ...metrics ? { metrics } : {}
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 (e3) {
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 (e4) {
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 (e5) {
372
+ } catch (e7) {
273
373
  }
274
374
  }
275
375
  this.controlUnsubscribers = [];
276
376
  try {
277
- _optionalChain([this, 'access', _9 => _9.controlChannel, 'optionalAccess', _10 => _10.close, 'call', _11 => _11()]);
278
- } catch (e6) {
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', _12 => _12.options, 'access', _13 => _13.onRemoteSessionStarted, 'optionalCall', _14 => _14(sessionId)]);
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
- _chunkTBPR2QNDcjs.normalizeError.call(void 0, error).message
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', _15 => _15.payload, 'access', _16 => _16.title, 'optionalAccess', _17 => _17.trim, 'call', _18 => _18()]) || remote.defaultTitle || "Livestream",
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: { ...remote.metadata, deviceCommandId: command.id },
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 (e7) {
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, _chunkTBPR2QNDcjs.ExpoSdkConfigurationError)("baseUrl must be a non-empty string");
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, _chunkTBPR2QNDcjs.ExpoSdkConfigurationError)("A Fetch API implementation is required");
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, _chunkTBPR2QNDcjs.InvalidResponseError)();
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, _chunkTBPR2QNDcjs.InvalidResponseError)();
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, _chunkTBPR2QNDcjs.ExpoSdkTimeoutError)();
417
- if (error instanceof _chunkTBPR2QNDcjs.PickleballExpoError) throw error;
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, _chunkTBPR2QNDcjs.NetworkUnavailableError)(void 0, { cause: error });
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, _chunkTBPR2QNDcjs.InvalidResponseError)();
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, _chunkTBPR2QNDcjs.InvalidResponseError)();
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) return { controlSession: null };
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, _chunkTBPR2QNDcjs.InvalidResponseError)();
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 _chunkTBPR2QNDcjs.errorFromCode.call(void 0,
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, _chunkTBPR2QNDcjs.InvalidResponseError)();
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 = _chunkTBPR2QNDcjs.useLivestreamHost.call(void 0, );
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', _19 => _19.current, 'access', _20 => _20.onRemoteSessionStarted, 'optionalCall', _21 => _21(sessionId)])
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;