react-native-debug-toolkit 2.3.0 → 3.1.2

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 (93) hide show
  1. package/README.md +115 -97
  2. package/README.zh-CN.md +113 -95
  3. package/bin/debug-toolkit.js +114 -0
  4. package/lib/commonjs/core/initialize.js +5 -0
  5. package/lib/commonjs/core/initialize.js.map +1 -1
  6. package/lib/commonjs/features/network/index.js +28 -2
  7. package/lib/commonjs/features/network/index.js.map +1 -1
  8. package/lib/commonjs/features/network/networkInterceptor.js +14 -6
  9. package/lib/commonjs/features/network/networkInterceptor.js.map +1 -1
  10. package/lib/commonjs/index.js +56 -0
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/ui/panel/DebugPanel.js +25 -0
  13. package/lib/commonjs/ui/panel/DebugPanel.js.map +1 -1
  14. package/lib/commonjs/ui/panel/FloatPanelView.js +15 -62
  15. package/lib/commonjs/ui/panel/FloatPanelView.js.map +1 -1
  16. package/lib/commonjs/ui/panel/StreamingSettingsModal.js +495 -0
  17. package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +1 -0
  18. package/lib/commonjs/ui/panel/useTabAnimation.js +71 -0
  19. package/lib/commonjs/ui/panel/useTabAnimation.js.map +1 -0
  20. package/lib/commonjs/utils/DaemonClient.js +721 -0
  21. package/lib/commonjs/utils/DaemonClient.js.map +1 -0
  22. package/lib/commonjs/utils/createPersistedObservableStore.js +23 -3
  23. package/lib/commonjs/utils/createPersistedObservableStore.js.map +1 -1
  24. package/lib/commonjs/utils/deviceReport.js +132 -0
  25. package/lib/commonjs/utils/deviceReport.js.map +1 -0
  26. package/lib/module/core/initialize.js +6 -0
  27. package/lib/module/core/initialize.js.map +1 -1
  28. package/lib/module/features/network/index.js +25 -1
  29. package/lib/module/features/network/index.js.map +1 -1
  30. package/lib/module/features/network/networkInterceptor.js +14 -6
  31. package/lib/module/features/network/networkInterceptor.js.map +1 -1
  32. package/lib/module/index.js +3 -0
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/ui/panel/DebugPanel.js +26 -1
  35. package/lib/module/ui/panel/DebugPanel.js.map +1 -1
  36. package/lib/module/ui/panel/FloatPanelView.js +16 -63
  37. package/lib/module/ui/panel/FloatPanelView.js.map +1 -1
  38. package/lib/module/ui/panel/StreamingSettingsModal.js +490 -0
  39. package/lib/module/ui/panel/StreamingSettingsModal.js.map +1 -0
  40. package/lib/module/ui/panel/useTabAnimation.js +67 -0
  41. package/lib/module/ui/panel/useTabAnimation.js.map +1 -0
  42. package/lib/module/utils/DaemonClient.js +703 -0
  43. package/lib/module/utils/DaemonClient.js.map +1 -0
  44. package/lib/module/utils/createPersistedObservableStore.js +23 -3
  45. package/lib/module/utils/createPersistedObservableStore.js.map +1 -1
  46. package/lib/module/utils/deviceReport.js +128 -0
  47. package/lib/module/utils/deviceReport.js.map +1 -0
  48. package/lib/typescript/src/core/initialize.d.ts.map +1 -1
  49. package/lib/typescript/src/features/network/index.d.ts +2 -0
  50. package/lib/typescript/src/features/network/index.d.ts.map +1 -1
  51. package/lib/typescript/src/features/network/networkInterceptor.d.ts +1 -1
  52. package/lib/typescript/src/features/network/networkInterceptor.d.ts.map +1 -1
  53. package/lib/typescript/src/index.d.ts +5 -0
  54. package/lib/typescript/src/index.d.ts.map +1 -1
  55. package/lib/typescript/src/ui/panel/DebugPanel.d.ts.map +1 -1
  56. package/lib/typescript/src/ui/panel/FloatPanelView.d.ts.map +1 -1
  57. package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts +8 -0
  58. package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +1 -0
  59. package/lib/typescript/src/ui/panel/useTabAnimation.d.ts +14 -0
  60. package/lib/typescript/src/ui/panel/useTabAnimation.d.ts.map +1 -0
  61. package/lib/typescript/src/utils/DaemonClient.d.ts +141 -0
  62. package/lib/typescript/src/utils/DaemonClient.d.ts.map +1 -0
  63. package/lib/typescript/src/utils/createPersistedObservableStore.d.ts +2 -1
  64. package/lib/typescript/src/utils/createPersistedObservableStore.d.ts.map +1 -1
  65. package/lib/typescript/src/utils/deviceReport.d.ts +18 -0
  66. package/lib/typescript/src/utils/deviceReport.d.ts.map +1 -0
  67. package/node/daemon/src/cli.js +82 -0
  68. package/node/daemon/src/console/console.html +1662 -0
  69. package/node/daemon/src/console/index.js +47 -0
  70. package/node/daemon/src/constants.js +38 -0
  71. package/node/daemon/src/index.js +11 -0
  72. package/node/daemon/src/server.js +447 -0
  73. package/node/daemon/src/store.js +187 -0
  74. package/node/mcp/src/cli.js +31 -0
  75. package/node/mcp/src/constants.js +13 -0
  76. package/node/mcp/src/daemonClient.js +132 -0
  77. package/node/mcp/src/httpClient.js +49 -0
  78. package/node/mcp/src/index.js +15 -0
  79. package/node/mcp/src/logs.js +96 -0
  80. package/node/mcp/src/server.js +144 -0
  81. package/node/mcp/src/tools.js +84 -0
  82. package/package.json +8 -3
  83. package/src/core/initialize.ts +8 -0
  84. package/src/features/network/index.ts +30 -3
  85. package/src/features/network/networkInterceptor.ts +19 -6
  86. package/src/index.ts +22 -0
  87. package/src/ui/panel/DebugPanel.tsx +23 -1
  88. package/src/ui/panel/FloatPanelView.tsx +10 -68
  89. package/src/ui/panel/StreamingSettingsModal.tsx +528 -0
  90. package/src/ui/panel/useTabAnimation.ts +77 -0
  91. package/src/utils/DaemonClient.ts +887 -0
  92. package/src/utils/createPersistedObservableStore.ts +16 -3
  93. package/src/utils/deviceReport.ts +203 -0
@@ -0,0 +1,721 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.DaemonClient = void 0;
7
+ exports._resetDaemonClientForTesting = _resetDaemonClientForTesting;
8
+ exports.buildDeviceDaemonEndpoint = buildDeviceDaemonEndpoint;
9
+ exports.checkDaemonConnection = checkDaemonConnection;
10
+ exports.daemonClient = void 0;
11
+ exports.getDefaultDaemonEndpoint = getDefaultDaemonEndpoint;
12
+ exports.isStreaming = isStreaming;
13
+ exports.loadDaemonSettings = loadDaemonSettings;
14
+ exports.loadDaemonStreamingEnabled = loadDaemonStreamingEnabled;
15
+ exports.normalizeDaemonSettings = normalizeDaemonSettings;
16
+ exports.reportDebugDeviceToDaemon = reportDebugDeviceToDaemon;
17
+ exports.restoreDaemonStreaming = restoreDaemonStreaming;
18
+ exports.saveDaemonSettings = saveDaemonSettings;
19
+ exports.saveDaemonStreamingEnabled = saveDaemonStreamingEnabled;
20
+ exports.startStreaming = startStreaming;
21
+ exports.stopStreaming = stopStreaming;
22
+ var _reactNative = require("react-native");
23
+ var _DebugToolkit = require("../core/DebugToolkit");
24
+ var _deviceReport = require("./deviceReport");
25
+ var _safeStringify = require("./safeStringify");
26
+ // ---- Public Types ----
27
+
28
+ // ---- Internal Transport Types ----
29
+
30
+ // ---- Constants ----
31
+
32
+ const DEFAULT_HEALTH_TIMEOUT_MS = 2000;
33
+ const DEFAULT_DEBOUNCE_MS = 200;
34
+ const DEFAULT_TIMEOUT_MS = 3000;
35
+ const DEFAULT_REPORT_TIMEOUT_MS = 3000;
36
+ const RETRY_BASE_MS = 1000;
37
+ const MAX_RETRY_DELAY_MS = 30000;
38
+ const BACKGROUND_RESYNC_THRESHOLD_MS = 5 * 60 * 1000;
39
+
40
+ // ---- Standalone Utilities (also used internally) ----
41
+
42
+ function getDefaultDaemonEndpoint() {
43
+ if (_reactNative.Platform.OS === 'android') {
44
+ return 'http://10.0.2.2:3799';
45
+ }
46
+ return 'http://localhost:3799';
47
+ }
48
+ function buildDeviceDaemonEndpoint(host) {
49
+ const trimmed = host.trim().replace(/\/+$/, '');
50
+ if (!trimmed) return '';
51
+ const withProtocol = /^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(trimmed) ? trimmed : `http://${trimmed}`;
52
+ try {
53
+ const url = new URL(withProtocol);
54
+ if (!url.port) url.port = '3799';
55
+ return url.toString().replace(/\/$/, '');
56
+ } catch {
57
+ return withProtocol;
58
+ }
59
+ }
60
+ function normalizeDaemonSettings(settings) {
61
+ const endpoint = settings.mode === 'device' ? buildDeviceDaemonEndpoint(settings.deviceHost) : '';
62
+ const token = settings.token.trim();
63
+ return {
64
+ endpoint: endpoint || undefined,
65
+ token: token || undefined
66
+ };
67
+ }
68
+
69
+ // ---- Stream State (internal) ----
70
+
71
+ // ---- DaemonClient ----
72
+
73
+ class DaemonClient {
74
+ _settings = {
75
+ mode: 'simulator',
76
+ endpoint: '',
77
+ deviceHost: '',
78
+ token: ''
79
+ };
80
+ _streamingEnabled = null;
81
+ _stream = null;
82
+ _restorePromise = null;
83
+ constructor(options) {
84
+ this._fetch = options?.fetch;
85
+ this._AbortController = options?.AbortController;
86
+ this._onEndpointDetected = options?.onEndpointDetected;
87
+ }
88
+
89
+ // --- Settings ---
90
+
91
+ getSettings() {
92
+ return {
93
+ ...this._settings
94
+ };
95
+ }
96
+ configure(settings) {
97
+ const normalized = normalizeDaemonSettings(settings);
98
+ this._settings = {
99
+ mode: settings.mode,
100
+ deviceHost: settings.deviceHost.trim(),
101
+ endpoint: normalized.endpoint || '',
102
+ token: settings.token.trim()
103
+ };
104
+ }
105
+
106
+ // --- Connection Health Check ---
107
+
108
+ async checkConnection(options = {}) {
109
+ const endpoint = options.endpoint ?? getDefaultDaemonEndpoint();
110
+ const healthUrl = buildDaemonUrl(endpoint, '/health');
111
+ const fetchImpl = this.resolveFetch();
112
+ if (!fetchImpl) {
113
+ return {
114
+ ok: false,
115
+ endpoint,
116
+ reason: 'fetch_unavailable',
117
+ error: 'global fetch is not available'
118
+ };
119
+ }
120
+ const timeoutMs = Math.max(0, options.timeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS);
121
+ const controller = this.createAbortController();
122
+ let timedOut = false;
123
+ const timeout = controller && timeoutMs > 0 ? setTimeout(() => {
124
+ timedOut = true;
125
+ controller.abort();
126
+ }, timeoutMs) : undefined;
127
+ try {
128
+ const response = await fetchImpl(healthUrl, {
129
+ method: 'GET',
130
+ headers: {},
131
+ signal: controller?.signal
132
+ });
133
+ const body = await readHealthBody(response);
134
+ if (!response.ok) {
135
+ return {
136
+ ok: false,
137
+ endpoint,
138
+ reason: 'http',
139
+ status: response.status
140
+ };
141
+ }
142
+ if (body?.ok !== true) {
143
+ return {
144
+ ok: false,
145
+ endpoint,
146
+ reason: 'invalid_response',
147
+ status: response.status
148
+ };
149
+ }
150
+ return {
151
+ ok: true,
152
+ endpoint,
153
+ status: response.status
154
+ };
155
+ } catch (error) {
156
+ return {
157
+ ok: false,
158
+ endpoint,
159
+ reason: timedOut ? 'timeout' : 'network',
160
+ error: error instanceof Error ? error.message : String(error)
161
+ };
162
+ } finally {
163
+ if (timeout) clearTimeout(timeout);
164
+ }
165
+ }
166
+
167
+ // --- Streaming ---
168
+
169
+ connect(options = {}) {
170
+ if (this._stream) return;
171
+ const endpoint = options.endpoint || this.resolveEndpoint();
172
+ const reportUrl = buildDaemonUrl(endpoint, '/report');
173
+ const ingestUrl = buildDaemonUrl(endpoint, '/ingest');
174
+ this.notifyEndpoint(endpoint);
175
+ this.notifyEndpoint(reportUrl);
176
+ this.notifyEndpoint(ingestUrl);
177
+ const state = {
178
+ endpoint,
179
+ reportUrl,
180
+ ingestUrl,
181
+ token: options.token ?? (this._settings.token.trim() || undefined),
182
+ debounceMs: options.debounceMs || DEFAULT_DEBOUNCE_MS,
183
+ timeoutMs: Math.max(0, options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
184
+ deviceId: null,
185
+ sending: false,
186
+ debounceTimer: null,
187
+ retryTimer: null,
188
+ retryAttempt: 0,
189
+ maxRetryAttempts: typeof options.maxRetryAttempts === 'number' ? Math.max(0, Math.floor(options.maxRetryAttempts)) : null,
190
+ dirtyFeatures: new Set(),
191
+ lastSentIds: new Map(),
192
+ featureUnsubscribes: [],
193
+ appStateUnsubscribe: null,
194
+ backgroundedAt: null,
195
+ onStatus: options.onStatus
196
+ };
197
+ for (const feature of _DebugToolkit.DebugToolkit.features) {
198
+ if (!feature.subscribe) continue;
199
+ const unsub = feature.subscribe(() => {
200
+ this.onFeatureChange(feature.name);
201
+ });
202
+ state.featureUnsubscribes.push(unsub);
203
+ }
204
+ state.appStateUnsubscribe = _reactNative.AppState.addEventListener('change', nextState => {
205
+ this.handleAppStateChange(nextState);
206
+ }).remove;
207
+ this._stream = state;
208
+ this.emitStatus({
209
+ state: 'connecting'
210
+ });
211
+ this.enqueueSendFullReport();
212
+ }
213
+ disconnect() {
214
+ if (!this._stream) return;
215
+ const state = this._stream;
216
+ this._stream = null;
217
+ if (state.debounceTimer) clearTimeout(state.debounceTimer);
218
+ if (state.retryTimer) clearTimeout(state.retryTimer);
219
+ state.featureUnsubscribes.forEach(fn => fn());
220
+ state.appStateUnsubscribe?.();
221
+ }
222
+ isConnected() {
223
+ return this._stream !== null;
224
+ }
225
+ getStatus() {
226
+ if (!this._stream) return null;
227
+ if (this._stream.deviceId) {
228
+ return {
229
+ state: 'connected',
230
+ deviceId: this._stream.deviceId
231
+ };
232
+ }
233
+ if (this._stream.retryTimer) {
234
+ return {
235
+ state: 'retrying',
236
+ retryInMs: Math.min(RETRY_BASE_MS * 2 ** this._stream.retryAttempt, MAX_RETRY_DELAY_MS)
237
+ };
238
+ }
239
+ return {
240
+ state: 'connecting'
241
+ };
242
+ }
243
+ setStreamingEnabled(enabled) {
244
+ this._streamingEnabled = enabled;
245
+ }
246
+ setEndpointDetector(callback) {
247
+ this._onEndpointDetected = callback;
248
+ }
249
+
250
+ // --- Restore (init-time reconnect) ---
251
+
252
+ async restore() {
253
+ if (this._restorePromise) return this._restorePromise;
254
+ this._restorePromise = this.doRestore().finally(() => {
255
+ this._restorePromise = null;
256
+ });
257
+ return this._restorePromise;
258
+ }
259
+ async doRestore() {
260
+ if (this.isConnected()) return;
261
+ const enabled = this._streamingEnabled;
262
+ const options = normalizeDaemonSettings(this._settings);
263
+ if (enabled === false) return;
264
+ if (enabled === true) {
265
+ this.connect();
266
+ return;
267
+ }
268
+ const canProbe = this._settings.mode === 'simulator' || Boolean(this._settings.deviceHost.trim());
269
+ if (!canProbe) return;
270
+ const endpoint = options.endpoint || this.resolveEndpoint();
271
+ const connection = await this.checkConnection({
272
+ ...options,
273
+ endpoint,
274
+ timeoutMs: 1000
275
+ });
276
+ if (!connection.ok || this.isConnected()) return;
277
+ this._streamingEnabled = true;
278
+ this.connect();
279
+ }
280
+
281
+ // --- One-shot Report ---
282
+
283
+ async reportOnce(options = {}) {
284
+ const endpoint = options.endpoint ?? this.resolveEndpoint();
285
+ const reportUrl = buildDaemonUrl(endpoint, '/report');
286
+ const report = (0, _deviceReport.createDebugDeviceReport)(options);
287
+ const fetchImpl = this.resolveFetch();
288
+ this.notifyEndpoint(endpoint);
289
+ this.notifyEndpoint(reportUrl);
290
+ if (!fetchImpl) {
291
+ return {
292
+ ok: false,
293
+ endpoint,
294
+ report,
295
+ error: 'global fetch is not available'
296
+ };
297
+ }
298
+ const timeoutMs = Math.max(0, options.timeoutMs ?? DEFAULT_REPORT_TIMEOUT_MS);
299
+ const controller = this.createAbortController();
300
+ const timeout = controller && timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
301
+ try {
302
+ const headers = {
303
+ 'Content-Type': 'application/json'
304
+ };
305
+ if (options.token) headers.Authorization = `Bearer ${options.token}`;
306
+ const response = await fetchImpl(reportUrl, {
307
+ method: 'POST',
308
+ headers,
309
+ body: JSON.stringify(report),
310
+ signal: controller?.signal
311
+ });
312
+ const bodyObject = await readReportResponseBody(response);
313
+ const logCount = readLogCount(bodyObject.logCount);
314
+ return {
315
+ ok: response.ok && bodyObject.ok === true,
316
+ endpoint,
317
+ report,
318
+ status: response.status,
319
+ deviceId: typeof bodyObject.deviceId === 'string' ? bodyObject.deviceId : undefined,
320
+ receivedAt: typeof bodyObject.receivedAt === 'string' ? bodyObject.receivedAt : undefined,
321
+ logCount,
322
+ error: response.ok ? undefined : typeof bodyObject.error === 'string' ? bodyObject.error : 'Report failed'
323
+ };
324
+ } catch (error) {
325
+ return {
326
+ ok: false,
327
+ endpoint,
328
+ report,
329
+ error: error instanceof Error ? error.message : String(error)
330
+ };
331
+ } finally {
332
+ if (timeout) clearTimeout(timeout);
333
+ }
334
+ }
335
+
336
+ // --- Test Helpers ---
337
+
338
+ _resetForTesting() {
339
+ this.disconnect();
340
+ this._settings = {
341
+ mode: 'simulator',
342
+ endpoint: '',
343
+ deviceHost: '',
344
+ token: ''
345
+ };
346
+ this._streamingEnabled = null;
347
+ this._restorePromise = null;
348
+ }
349
+
350
+ // ---- Private: Transport ----
351
+
352
+ resolveEndpoint() {
353
+ const normalized = normalizeDaemonSettings(this._settings);
354
+ return normalized.endpoint || getDefaultDaemonEndpoint();
355
+ }
356
+ resolveFetch() {
357
+ return this._fetch ?? globalThis.fetch;
358
+ }
359
+ createAbortController() {
360
+ if (this._AbortController) return new this._AbortController();
361
+ const Ctor = globalThis.AbortController;
362
+ return Ctor ? new Ctor() : undefined;
363
+ }
364
+ notifyEndpoint(url) {
365
+ this._onEndpointDetected?.(url);
366
+ }
367
+ emitStatus(status) {
368
+ try {
369
+ this._stream?.onStatus?.(status);
370
+ } catch {
371
+ // Consumer callbacks must not affect delivery.
372
+ }
373
+ }
374
+ async doPost(url, headers, body, timeoutMs) {
375
+ const fetchImpl = this.resolveFetch();
376
+ if (!fetchImpl) return null;
377
+ const controller = this.createAbortController();
378
+ const timeout = controller && timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
379
+ try {
380
+ return await fetchImpl(url, {
381
+ method: 'POST',
382
+ headers,
383
+ body: (0, _safeStringify.safeStringify)(body),
384
+ signal: controller?.signal
385
+ });
386
+ } catch {
387
+ return null;
388
+ } finally {
389
+ if (timeout) clearTimeout(timeout);
390
+ }
391
+ }
392
+
393
+ // ---- Private: Streaming State Machine ----
394
+
395
+ emitStreamStatus(state, status) {
396
+ try {
397
+ state.onStatus?.(status);
398
+ } catch {
399
+ // Consumer callbacks must not affect delivery.
400
+ }
401
+ }
402
+ resetRetry(state) {
403
+ state.retryAttempt = 0;
404
+ if (state.retryTimer) {
405
+ clearTimeout(state.retryTimer);
406
+ state.retryTimer = null;
407
+ }
408
+ }
409
+ failStreaming(state, reason) {
410
+ if (this._stream !== state) return;
411
+ this.emitStreamStatus(state, {
412
+ state: 'failed',
413
+ reason
414
+ });
415
+ this.disconnect();
416
+ }
417
+ scheduleRetry(state) {
418
+ if (state.retryTimer) return;
419
+ if (state.maxRetryAttempts !== null && state.retryAttempt >= state.maxRetryAttempts) {
420
+ this.failStreaming(state, 'retry_limit');
421
+ return;
422
+ }
423
+ const delay = Math.min(RETRY_BASE_MS * 2 ** state.retryAttempt, MAX_RETRY_DELAY_MS);
424
+ state.retryAttempt += 1;
425
+ this.emitStreamStatus(state, {
426
+ state: 'retrying',
427
+ retryInMs: delay
428
+ });
429
+ state.retryTimer = setTimeout(() => {
430
+ state.retryTimer = null;
431
+ if (this._stream !== state) return;
432
+ if (state.deviceId) {
433
+ this.enqueueSendDelta();
434
+ } else {
435
+ this.enqueueSendFullReport();
436
+ }
437
+ }, delay);
438
+ }
439
+ scheduleDelta(state) {
440
+ if (state.debounceTimer) clearTimeout(state.debounceTimer);
441
+ state.debounceTimer = setTimeout(() => {
442
+ state.debounceTimer = null;
443
+ if (this._stream === state) this.enqueueSendDelta();
444
+ }, state.debounceMs);
445
+ }
446
+ onFeatureChange(featureName) {
447
+ if (!this._stream) return;
448
+ this._stream.dirtyFeatures.add(featureName);
449
+ if (this._stream.retryTimer) return;
450
+ this.scheduleDelta(this._stream);
451
+ }
452
+ handleAppStateChange(nextState) {
453
+ if (!this._stream) return;
454
+ const state = this._stream;
455
+ if (nextState === 'background') {
456
+ state.backgroundedAt = Date.now();
457
+ if (state.debounceTimer) {
458
+ clearTimeout(state.debounceTimer);
459
+ state.debounceTimer = null;
460
+ }
461
+ this.enqueueSendDelta();
462
+ } else if (nextState === 'active') {
463
+ const wasAway = state.backgroundedAt ? Date.now() - state.backgroundedAt : 0;
464
+ state.backgroundedAt = null;
465
+ if (wasAway > BACKGROUND_RESYNC_THRESHOLD_MS || !state.deviceId) {
466
+ state.deviceId = null;
467
+ state.lastSentIds.clear();
468
+ this.enqueueSendFullReport();
469
+ }
470
+ }
471
+ }
472
+ fetchHeaders(state) {
473
+ const headers = {
474
+ 'Content-Type': 'application/json'
475
+ };
476
+ if (state.token) headers.Authorization = `Bearer ${state.token}`;
477
+ return headers;
478
+ }
479
+
480
+ // ---- Private: Full Report ----
481
+
482
+ enqueueSendFullReport() {
483
+ const state = this._stream;
484
+ if (!state || state.sending) return;
485
+ state.sending = true;
486
+ (async () => {
487
+ let result = 'ok';
488
+ try {
489
+ result = await this.doSendFullReport(state);
490
+ if (result === 'ok') this.resetRetry(state);
491
+ } finally {
492
+ state.sending = false;
493
+ if (this._stream !== state) return;
494
+ if (result === 'auth_failed') {
495
+ this.failStreaming(state, 'auth');
496
+ } else if (result === 'retry') {
497
+ this.scheduleRetry(state);
498
+ } else if (state.dirtyFeatures.size > 0 && !state.debounceTimer) {
499
+ this.scheduleDelta(state);
500
+ }
501
+ }
502
+ })();
503
+ }
504
+ async doSendFullReport(state) {
505
+ const report = (0, _deviceReport.createDebugDeviceReport)();
506
+ const response = await this.doPost(state.reportUrl, this.fetchHeaders(state), report, state.timeoutMs);
507
+ if (!response) return 'retry';
508
+ if (isAuthFailure(response.status)) return 'auth_failed';
509
+ if (response.status < 200 || response.status >= 300) return 'retry';
510
+ try {
511
+ const body = response.json ? await response.json() : null;
512
+ if (body?.ok !== true || typeof body.deviceId !== 'string') return 'retry';
513
+ state.deviceId = body.deviceId;
514
+ this.emitStreamStatus(state, {
515
+ state: 'connected',
516
+ deviceId: body.deviceId
517
+ });
518
+ } catch {
519
+ return 'retry';
520
+ }
521
+ state.lastSentIds.clear();
522
+ for (const feature of _DebugToolkit.DebugToolkit.features) {
523
+ try {
524
+ const snapshot = feature.getSnapshot();
525
+ if (Array.isArray(snapshot)) {
526
+ state.lastSentIds.set(feature.name, snapshotToIds(snapshot));
527
+ }
528
+ } catch {
529
+ // skip
530
+ }
531
+ }
532
+ return 'ok';
533
+ }
534
+
535
+ // ---- Private: Delta ----
536
+
537
+ enqueueSendDelta() {
538
+ const state = this._stream;
539
+ if (!state || state.sending || state.dirtyFeatures.size === 0) return;
540
+ state.sending = true;
541
+ (async () => {
542
+ let retry = false;
543
+ try {
544
+ const delta = {};
545
+ const nextSentIds = new Map();
546
+ const features = _DebugToolkit.DebugToolkit.features;
547
+ for (const featureName of state.dirtyFeatures) {
548
+ const feature = features.find(f => f.name === featureName);
549
+ if (!feature) continue;
550
+ let snapshot;
551
+ try {
552
+ snapshot = feature.getSnapshot();
553
+ } catch {
554
+ continue;
555
+ }
556
+ if (!Array.isArray(snapshot)) continue;
557
+ const prevIds = state.lastSentIds.get(featureName) || new Set();
558
+ const newEntries = snapshot.filter(entry => {
559
+ const id = getEntryId(entry);
560
+ return id != null && !prevIds.has(id);
561
+ });
562
+ if (newEntries.length > 0) {
563
+ delta[featureName] = newEntries;
564
+ nextSentIds.set(featureName, snapshotToIds(snapshot));
565
+ }
566
+ }
567
+ state.dirtyFeatures.clear();
568
+ state.debounceTimer = null;
569
+ if (Object.keys(delta).length === 0) return;
570
+ if (!state.deviceId) {
571
+ const result = await this.doSendFullReport(state);
572
+ retry = result === 'retry';
573
+ if (result !== 'ok') {
574
+ Object.keys(delta).forEach(n => state.dirtyFeatures.add(n));
575
+ }
576
+ if (result === 'auth_failed') this.failStreaming(state, 'auth');
577
+ if (result === 'ok') this.resetRetry(state);
578
+ return;
579
+ }
580
+ const response = await this.doPost(state.ingestUrl, this.fetchHeaders(state), {
581
+ deviceId: state.deviceId,
582
+ delta: {
583
+ logs: delta
584
+ }
585
+ }, state.timeoutMs);
586
+ if (!response) {
587
+ Object.keys(delta).forEach(n => state.dirtyFeatures.add(n));
588
+ retry = true;
589
+ return;
590
+ }
591
+ if (response.status === 404) {
592
+ state.deviceId = null;
593
+ state.lastSentIds.clear();
594
+ const result = await this.doSendFullReport(state);
595
+ retry = result === 'retry';
596
+ if (result !== 'ok') {
597
+ Object.keys(delta).forEach(n => state.dirtyFeatures.add(n));
598
+ }
599
+ if (result === 'auth_failed') this.failStreaming(state, 'auth');
600
+ if (result === 'ok') this.resetRetry(state);
601
+ return;
602
+ }
603
+ if (isAuthFailure(response.status)) {
604
+ Object.keys(delta).forEach(n => state.dirtyFeatures.add(n));
605
+ this.failStreaming(state, 'auth');
606
+ return;
607
+ }
608
+ if (response.status < 200 || response.status >= 300) {
609
+ Object.keys(delta).forEach(n => state.dirtyFeatures.add(n));
610
+ retry = true;
611
+ return;
612
+ }
613
+ nextSentIds.forEach((ids, featureName) => {
614
+ state.lastSentIds.set(featureName, ids);
615
+ });
616
+ this.resetRetry(state);
617
+ if (state.deviceId) {
618
+ this.emitStreamStatus(state, {
619
+ state: 'connected',
620
+ deviceId: state.deviceId
621
+ });
622
+ }
623
+ } finally {
624
+ state.sending = false;
625
+ if (this._stream !== state) return;
626
+ if (retry && state.dirtyFeatures.size > 0) {
627
+ this.scheduleRetry(state);
628
+ } else if (state.dirtyFeatures.size > 0 && !state.debounceTimer) {
629
+ this.scheduleDelta(state);
630
+ }
631
+ }
632
+ })();
633
+ }
634
+ }
635
+
636
+ // ---- Module-level Singleton ----
637
+ exports.DaemonClient = DaemonClient;
638
+ const daemonClient = exports.daemonClient = new DaemonClient();
639
+
640
+ // ---- Backward-compatible Function Exports ----
641
+
642
+ async function loadDaemonSettings() {
643
+ return daemonClient.getSettings();
644
+ }
645
+ async function saveDaemonSettings(settings) {
646
+ daemonClient.configure(settings);
647
+ }
648
+ async function loadDaemonStreamingEnabled() {
649
+ return null;
650
+ }
651
+ async function saveDaemonStreamingEnabled(enabled) {
652
+ daemonClient.setStreamingEnabled(enabled);
653
+ }
654
+ function startStreaming(options = {}) {
655
+ daemonClient.connect(options);
656
+ }
657
+ function stopStreaming() {
658
+ daemonClient.disconnect();
659
+ }
660
+ function isStreaming() {
661
+ return daemonClient.isConnected();
662
+ }
663
+ function checkDaemonConnection(options = {}) {
664
+ return daemonClient.checkConnection(options);
665
+ }
666
+ function reportDebugDeviceToDaemon(options = {}) {
667
+ return daemonClient.reportOnce(options);
668
+ }
669
+ function restoreDaemonStreaming() {
670
+ return daemonClient.restore();
671
+ }
672
+
673
+ // ---- Internal Helpers ----
674
+
675
+ function buildDaemonUrl(endpoint, path) {
676
+ const trimmed = endpoint.replace(/\/+$/, '');
677
+ return trimmed.endsWith(path) ? trimmed : `${trimmed}${path}`;
678
+ }
679
+ function isAuthFailure(status) {
680
+ return status === 401 || status === 403;
681
+ }
682
+ function getEntryId(entry) {
683
+ if (!entry || typeof entry !== 'object') return null;
684
+ const id = entry.id;
685
+ return typeof id === 'string' || typeof id === 'number' ? id : null;
686
+ }
687
+ function snapshotToIds(snapshot) {
688
+ return new Set(snapshot.map(getEntryId).filter(id => id != null));
689
+ }
690
+ async function readHealthBody(response) {
691
+ try {
692
+ const body = response.json ? await response.json() : null;
693
+ return body && typeof body === 'object' && !Array.isArray(body) ? body : null;
694
+ } catch {
695
+ return null;
696
+ }
697
+ }
698
+ async function readReportResponseBody(response) {
699
+ try {
700
+ if (response.json) {
701
+ const raw = await response.json();
702
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
703
+ return raw;
704
+ }
705
+ }
706
+ } catch {
707
+ // ignore
708
+ }
709
+ return {};
710
+ }
711
+ function readLogCount(value) {
712
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
713
+ return Object.entries(value).reduce((acc, [key, count]) => {
714
+ if (typeof count === 'number') acc[key] = count;
715
+ return acc;
716
+ }, {});
717
+ }
718
+ function _resetDaemonClientForTesting() {
719
+ daemonClient._resetForTesting();
720
+ }
721
+ //# sourceMappingURL=DaemonClient.js.map