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.
- package/README.md +115 -97
- package/README.zh-CN.md +113 -95
- package/bin/debug-toolkit.js +114 -0
- package/lib/commonjs/core/initialize.js +5 -0
- package/lib/commonjs/core/initialize.js.map +1 -1
- package/lib/commonjs/features/network/index.js +28 -2
- package/lib/commonjs/features/network/index.js.map +1 -1
- package/lib/commonjs/features/network/networkInterceptor.js +14 -6
- package/lib/commonjs/features/network/networkInterceptor.js.map +1 -1
- package/lib/commonjs/index.js +56 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/panel/DebugPanel.js +25 -0
- package/lib/commonjs/ui/panel/DebugPanel.js.map +1 -1
- package/lib/commonjs/ui/panel/FloatPanelView.js +15 -62
- package/lib/commonjs/ui/panel/FloatPanelView.js.map +1 -1
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js +495 -0
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +1 -0
- package/lib/commonjs/ui/panel/useTabAnimation.js +71 -0
- package/lib/commonjs/ui/panel/useTabAnimation.js.map +1 -0
- package/lib/commonjs/utils/DaemonClient.js +721 -0
- package/lib/commonjs/utils/DaemonClient.js.map +1 -0
- package/lib/commonjs/utils/createPersistedObservableStore.js +23 -3
- package/lib/commonjs/utils/createPersistedObservableStore.js.map +1 -1
- package/lib/commonjs/utils/deviceReport.js +132 -0
- package/lib/commonjs/utils/deviceReport.js.map +1 -0
- package/lib/module/core/initialize.js +6 -0
- package/lib/module/core/initialize.js.map +1 -1
- package/lib/module/features/network/index.js +25 -1
- package/lib/module/features/network/index.js.map +1 -1
- package/lib/module/features/network/networkInterceptor.js +14 -6
- package/lib/module/features/network/networkInterceptor.js.map +1 -1
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/panel/DebugPanel.js +26 -1
- package/lib/module/ui/panel/DebugPanel.js.map +1 -1
- package/lib/module/ui/panel/FloatPanelView.js +16 -63
- package/lib/module/ui/panel/FloatPanelView.js.map +1 -1
- package/lib/module/ui/panel/StreamingSettingsModal.js +490 -0
- package/lib/module/ui/panel/StreamingSettingsModal.js.map +1 -0
- package/lib/module/ui/panel/useTabAnimation.js +67 -0
- package/lib/module/ui/panel/useTabAnimation.js.map +1 -0
- package/lib/module/utils/DaemonClient.js +703 -0
- package/lib/module/utils/DaemonClient.js.map +1 -0
- package/lib/module/utils/createPersistedObservableStore.js +23 -3
- package/lib/module/utils/createPersistedObservableStore.js.map +1 -1
- package/lib/module/utils/deviceReport.js +128 -0
- package/lib/module/utils/deviceReport.js.map +1 -0
- package/lib/typescript/src/core/initialize.d.ts.map +1 -1
- package/lib/typescript/src/features/network/index.d.ts +2 -0
- package/lib/typescript/src/features/network/index.d.ts.map +1 -1
- package/lib/typescript/src/features/network/networkInterceptor.d.ts +1 -1
- package/lib/typescript/src/features/network/networkInterceptor.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/DebugPanel.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/FloatPanelView.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts +8 -0
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +1 -0
- package/lib/typescript/src/ui/panel/useTabAnimation.d.ts +14 -0
- package/lib/typescript/src/ui/panel/useTabAnimation.d.ts.map +1 -0
- package/lib/typescript/src/utils/DaemonClient.d.ts +141 -0
- package/lib/typescript/src/utils/DaemonClient.d.ts.map +1 -0
- package/lib/typescript/src/utils/createPersistedObservableStore.d.ts +2 -1
- package/lib/typescript/src/utils/createPersistedObservableStore.d.ts.map +1 -1
- package/lib/typescript/src/utils/deviceReport.d.ts +18 -0
- package/lib/typescript/src/utils/deviceReport.d.ts.map +1 -0
- package/node/daemon/src/cli.js +82 -0
- package/node/daemon/src/console/console.html +1662 -0
- package/node/daemon/src/console/index.js +47 -0
- package/node/daemon/src/constants.js +38 -0
- package/node/daemon/src/index.js +11 -0
- package/node/daemon/src/server.js +447 -0
- package/node/daemon/src/store.js +187 -0
- package/node/mcp/src/cli.js +31 -0
- package/node/mcp/src/constants.js +13 -0
- package/node/mcp/src/daemonClient.js +132 -0
- package/node/mcp/src/httpClient.js +49 -0
- package/node/mcp/src/index.js +15 -0
- package/node/mcp/src/logs.js +96 -0
- package/node/mcp/src/server.js +144 -0
- package/node/mcp/src/tools.js +84 -0
- package/package.json +8 -3
- package/src/core/initialize.ts +8 -0
- package/src/features/network/index.ts +30 -3
- package/src/features/network/networkInterceptor.ts +19 -6
- package/src/index.ts +22 -0
- package/src/ui/panel/DebugPanel.tsx +23 -1
- package/src/ui/panel/FloatPanelView.tsx +10 -68
- package/src/ui/panel/StreamingSettingsModal.tsx +528 -0
- package/src/ui/panel/useTabAnimation.ts +77 -0
- package/src/utils/DaemonClient.ts +887 -0
- package/src/utils/createPersistedObservableStore.ts +16 -3
- 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
|