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