react-native-debug-toolkit 3.0.0 → 3.1.3

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 (83) hide show
  1. package/README.md +115 -97
  2. package/README.zh-CN.md +113 -95
  3. package/lib/commonjs/core/initialize.js +5 -0
  4. package/lib/commonjs/core/initialize.js.map +1 -1
  5. package/lib/commonjs/index.js +23 -26
  6. package/lib/commonjs/index.js.map +1 -1
  7. package/lib/commonjs/ui/panel/StreamingSettingsModal.js +24 -58
  8. package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +1 -1
  9. package/lib/commonjs/utils/DaemonClient.js +721 -0
  10. package/lib/commonjs/utils/DaemonClient.js.map +1 -0
  11. package/lib/commonjs/utils/{sessionReport.js → deviceReport.js} +3 -3
  12. package/lib/commonjs/utils/deviceReport.js.map +1 -0
  13. package/lib/module/core/initialize.js +6 -0
  14. package/lib/module/core/initialize.js.map +1 -1
  15. package/lib/module/index.js +3 -5
  16. package/lib/module/index.js.map +1 -1
  17. package/lib/module/ui/panel/StreamingSettingsModal.js +21 -55
  18. package/lib/module/ui/panel/StreamingSettingsModal.js.map +1 -1
  19. package/lib/module/utils/DaemonClient.js +703 -0
  20. package/lib/module/utils/DaemonClient.js.map +1 -0
  21. package/lib/module/utils/{sessionReport.js → deviceReport.js} +2 -2
  22. package/lib/module/utils/deviceReport.js.map +1 -0
  23. package/lib/typescript/src/core/initialize.d.ts.map +1 -1
  24. package/lib/typescript/src/index.d.ts +5 -10
  25. package/lib/typescript/src/index.d.ts.map +1 -1
  26. package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +1 -1
  27. package/lib/typescript/src/utils/DaemonClient.d.ts +141 -0
  28. package/lib/typescript/src/utils/DaemonClient.d.ts.map +1 -0
  29. package/lib/typescript/src/utils/{sessionReport.d.ts → deviceReport.d.ts} +4 -4
  30. package/lib/typescript/src/utils/deviceReport.d.ts.map +1 -0
  31. package/node/daemon/src/cli.js +9 -2
  32. package/node/daemon/src/console/console.html +1052 -249
  33. package/node/daemon/src/constants.js +6 -0
  34. package/node/daemon/src/server.js +205 -123
  35. package/node/daemon/src/store.js +122 -45
  36. package/node/mcp/src/daemonClient.js +6 -6
  37. package/node/mcp/src/index.js +2 -2
  38. package/node/mcp/src/logs.js +5 -4
  39. package/node/mcp/src/tools.js +16 -16
  40. package/package.json +2 -2
  41. package/src/core/initialize.ts +8 -0
  42. package/src/index.ts +18 -10
  43. package/src/ui/panel/StreamingSettingsModal.tsx +25 -63
  44. package/src/utils/DaemonClient.ts +887 -0
  45. package/src/utils/{sessionReport.ts → deviceReport.ts} +6 -6
  46. package/lib/commonjs/utils/autoDetectDaemon.js +0 -141
  47. package/lib/commonjs/utils/autoDetectDaemon.js.map +0 -1
  48. package/lib/commonjs/utils/daemonConnection.js +0 -81
  49. package/lib/commonjs/utils/daemonConnection.js.map +0 -1
  50. package/lib/commonjs/utils/daemonSettings.js +0 -110
  51. package/lib/commonjs/utils/daemonSettings.js.map +0 -1
  52. package/lib/commonjs/utils/reportToDaemon.js +0 -112
  53. package/lib/commonjs/utils/reportToDaemon.js.map +0 -1
  54. package/lib/commonjs/utils/sessionReport.js.map +0 -1
  55. package/lib/commonjs/utils/streamToDaemon.js +0 -334
  56. package/lib/commonjs/utils/streamToDaemon.js.map +0 -1
  57. package/lib/module/utils/autoDetectDaemon.js +0 -136
  58. package/lib/module/utils/autoDetectDaemon.js.map +0 -1
  59. package/lib/module/utils/daemonConnection.js +0 -77
  60. package/lib/module/utils/daemonConnection.js.map +0 -1
  61. package/lib/module/utils/daemonSettings.js +0 -102
  62. package/lib/module/utils/daemonSettings.js.map +0 -1
  63. package/lib/module/utils/reportToDaemon.js +0 -105
  64. package/lib/module/utils/reportToDaemon.js.map +0 -1
  65. package/lib/module/utils/sessionReport.js.map +0 -1
  66. package/lib/module/utils/streamToDaemon.js +0 -328
  67. package/lib/module/utils/streamToDaemon.js.map +0 -1
  68. package/lib/typescript/src/utils/autoDetectDaemon.d.ts +0 -15
  69. package/lib/typescript/src/utils/autoDetectDaemon.d.ts.map +0 -1
  70. package/lib/typescript/src/utils/daemonConnection.d.ts +0 -18
  71. package/lib/typescript/src/utils/daemonConnection.d.ts.map +0 -1
  72. package/lib/typescript/src/utils/daemonSettings.d.ts +0 -19
  73. package/lib/typescript/src/utils/daemonSettings.d.ts.map +0 -1
  74. package/lib/typescript/src/utils/reportToDaemon.d.ts +0 -34
  75. package/lib/typescript/src/utils/reportToDaemon.d.ts.map +0 -1
  76. package/lib/typescript/src/utils/sessionReport.d.ts.map +0 -1
  77. package/lib/typescript/src/utils/streamToDaemon.d.ts +0 -23
  78. package/lib/typescript/src/utils/streamToDaemon.d.ts.map +0 -1
  79. package/src/utils/autoDetectDaemon.ts +0 -175
  80. package/src/utils/daemonConnection.ts +0 -133
  81. package/src/utils/daemonSettings.ts +0 -134
  82. package/src/utils/reportToDaemon.ts +0 -172
  83. package/src/utils/streamToDaemon.ts +0 -419
@@ -1,419 +0,0 @@
1
- import { AppState, type AppStateStatus } from 'react-native';
2
-
3
- import { DebugToolkit } from '../core/DebugToolkit';
4
- import { _addDaemonEndpointToNetworkBlacklist } from '../features/network';
5
- import { createDebugSessionReport } from './sessionReport';
6
- import {
7
- buildDaemonUrl,
8
- getDefaultDaemonEndpoint,
9
- getGlobalFetch,
10
- } from './reportToDaemon';
11
- import { safeStringify } from './safeStringify';
12
-
13
- export interface StreamToDaemonOptions {
14
- endpoint?: string;
15
- token?: string;
16
- debounceMs?: number;
17
- timeoutMs?: number;
18
- onStatus?: (status: StreamStatus) => void;
19
- }
20
-
21
- export type StreamStatus =
22
- | { state: 'connecting' }
23
- | { state: 'connected'; sessionId: string }
24
- | { state: 'retrying'; retryInMs: number }
25
- | { state: 'failed'; reason: 'auth' | 'retry_limit' };
26
-
27
- const DEFAULT_DEBOUNCE_MS = 200;
28
- const DEFAULT_TIMEOUT_MS = 3000;
29
- const RETRY_BASE_MS = 1000;
30
- const MAX_RETRY_DELAY_MS = 30000;
31
- const MAX_RETRY_ATTEMPTS = 10;
32
- const BACKGROUND_RESYNC_THRESHOLD_MS = 5 * 60 * 1000;
33
- type SendResult = 'ok' | 'retry' | 'auth_failed';
34
-
35
- type FetchHeaders = Record<string, string>;
36
-
37
- interface StreamState {
38
- endpoint: string;
39
- reportUrl: string;
40
- ingestUrl: string;
41
- token: string | undefined;
42
- debounceMs: number;
43
- timeoutMs: number;
44
- sessionId: string | null;
45
- sending: boolean;
46
- debounceTimer: ReturnType<typeof setTimeout> | null;
47
- retryTimer: ReturnType<typeof setTimeout> | null;
48
- retryAttempt: number;
49
- dirtyFeatures: Set<string>;
50
- lastSentIds: Map<string, Set<string | number>>;
51
- featureUnsubscribes: Array<() => void>;
52
- appStateUnsubscribe: (() => void) | null;
53
- backgroundedAt: number | null;
54
- onStatus: ((status: StreamStatus) => void) | undefined;
55
- }
56
-
57
- let active: StreamState | null = null;
58
-
59
- type AbortControllerLike = {
60
- signal: unknown;
61
- abort: () => void;
62
- };
63
-
64
- function createAbortController(): AbortControllerLike | undefined {
65
- const AbortControllerCtor = (globalThis as {
66
- AbortController?: new () => AbortControllerLike;
67
- }).AbortController;
68
- return AbortControllerCtor ? new AbortControllerCtor() : undefined;
69
- }
70
-
71
- function getEntryId(entry: unknown): string | number | null {
72
- if (!entry || typeof entry !== 'object') {
73
- return null;
74
- }
75
-
76
- const id = (entry as Record<string, unknown>).id;
77
- return typeof id === 'string' || typeof id === 'number' ? id : null;
78
- }
79
-
80
- function snapshotToIds(snapshot: unknown[]): Set<string | number> {
81
- return new Set(
82
- snapshot
83
- .map(getEntryId)
84
- .filter((id): id is string | number => id != null),
85
- );
86
- }
87
-
88
- function fetchHeaders(state: StreamState): FetchHeaders {
89
- const headers: FetchHeaders = { 'Content-Type': 'application/json' };
90
- if (state.token) headers.Authorization = `Bearer ${state.token}`;
91
- return headers;
92
- }
93
-
94
- function emitStatus(state: StreamState, status: StreamStatus): void {
95
- try {
96
- state.onStatus?.(status);
97
- } catch {
98
- // Consumer status callbacks should not affect log delivery.
99
- }
100
- }
101
-
102
- function isAuthFailure(status: number): boolean {
103
- return status === 401 || status === 403;
104
- }
105
-
106
- function failStreaming(state: StreamState, reason: 'auth' | 'retry_limit'): void {
107
- if (active !== state) return;
108
- emitStatus(state, { state: 'failed', reason });
109
- stopStreaming();
110
- }
111
-
112
- async function doPost(
113
- url: string,
114
- headers: FetchHeaders,
115
- body: unknown,
116
- timeoutMs: number,
117
- ): Promise<{ status: number; json?: () => Promise<unknown> } | null> {
118
- const fetchImpl = getGlobalFetch();
119
- if (!fetchImpl) {
120
- return null;
121
- }
122
- const controller = createAbortController();
123
- const timeout = controller && timeoutMs > 0
124
- ? setTimeout(() => controller.abort(), timeoutMs)
125
- : undefined;
126
- try {
127
- return await fetchImpl(url, {
128
- method: 'POST',
129
- headers,
130
- body: safeStringify(body),
131
- signal: controller?.signal,
132
- });
133
- } catch {
134
- return null;
135
- } finally {
136
- if (timeout) {
137
- clearTimeout(timeout);
138
- }
139
- }
140
- }
141
-
142
- function resetRetry(state: StreamState): void {
143
- state.retryAttempt = 0;
144
- if (state.retryTimer) {
145
- clearTimeout(state.retryTimer);
146
- state.retryTimer = null;
147
- }
148
- }
149
-
150
- function scheduleRetry(state: StreamState): void {
151
- if (state.retryTimer) return;
152
- if (state.retryAttempt >= MAX_RETRY_ATTEMPTS) {
153
- failStreaming(state, 'retry_limit');
154
- return;
155
- }
156
- const delay = Math.min(
157
- RETRY_BASE_MS * (2 ** state.retryAttempt),
158
- MAX_RETRY_DELAY_MS,
159
- );
160
- state.retryAttempt += 1;
161
- emitStatus(state, { state: 'retrying', retryInMs: delay });
162
- state.retryTimer = setTimeout(() => {
163
- state.retryTimer = null;
164
- if (active !== state) return;
165
- if (state.sessionId) {
166
- sendDelta(state);
167
- } else {
168
- sendFullReport(state);
169
- }
170
- }, delay);
171
- }
172
-
173
- async function sendFullReport(state: StreamState): Promise<void> {
174
- if (state.sending) return;
175
- state.sending = true;
176
- let result: SendResult = 'ok';
177
- try {
178
- result = await doSendFullReport(state);
179
- if (result === 'ok') resetRetry(state);
180
- } finally {
181
- state.sending = false;
182
- if (active !== state) return;
183
- if (result === 'auth_failed') {
184
- failStreaming(state, 'auth');
185
- } else if (result === 'retry') {
186
- scheduleRetry(state);
187
- } else if (state.dirtyFeatures.size > 0 && !state.debounceTimer) {
188
- scheduleDelta(state);
189
- }
190
- }
191
- }
192
-
193
- async function doSendFullReport(state: StreamState): Promise<SendResult> {
194
- const report = createDebugSessionReport();
195
- const response = await doPost(state.reportUrl, fetchHeaders(state), report, state.timeoutMs);
196
- if (!response) return 'retry';
197
- if (isAuthFailure(response.status)) return 'auth_failed';
198
- if (response.status < 200 || response.status >= 300) return 'retry';
199
-
200
- try {
201
- const body = response.json
202
- ? (await response.json()) as Record<string, unknown> | null
203
- : null;
204
- if (body?.ok !== true || typeof body.sessionId !== 'string') return 'retry';
205
- state.sessionId = body.sessionId;
206
- emitStatus(state, { state: 'connected', sessionId: body.sessionId });
207
- } catch {
208
- return 'retry';
209
- }
210
-
211
- state.lastSentIds.clear();
212
- for (const feature of DebugToolkit.features) {
213
- try {
214
- const snapshot = feature.getSnapshot();
215
- if (Array.isArray(snapshot)) state.lastSentIds.set(feature.name, snapshotToIds(snapshot));
216
- } catch {
217
- // skip
218
- }
219
- }
220
- return 'ok';
221
- }
222
-
223
- async function sendDelta(state: StreamState): Promise<void> {
224
- if (state.sending || state.dirtyFeatures.size === 0) return;
225
- state.sending = true;
226
- let retry = false;
227
- try {
228
- const delta: Record<string, unknown[]> = {};
229
- const nextSentIds = new Map<string, Set<string | number>>();
230
- const features = DebugToolkit.features;
231
-
232
- for (const featureName of state.dirtyFeatures) {
233
- const feature = features.find((f) => f.name === featureName);
234
- if (!feature) continue;
235
-
236
- let snapshot: unknown;
237
- try {
238
- snapshot = feature.getSnapshot();
239
- } catch {
240
- continue;
241
- }
242
-
243
- if (!Array.isArray(snapshot)) continue;
244
-
245
- const prevIds = state.lastSentIds.get(featureName) || new Set<string | number>();
246
- const newEntries = snapshot.filter(
247
- (entry) => {
248
- const id = getEntryId(entry);
249
- return id != null && !prevIds.has(id);
250
- },
251
- );
252
-
253
- if (newEntries.length > 0) {
254
- delta[featureName] = newEntries;
255
- nextSentIds.set(featureName, snapshotToIds(snapshot));
256
- }
257
- }
258
-
259
- state.dirtyFeatures.clear();
260
- state.debounceTimer = null;
261
-
262
- if (Object.keys(delta).length === 0) return;
263
-
264
- if (!state.sessionId) {
265
- const result = await doSendFullReport(state);
266
- retry = result === 'retry';
267
- if (result !== 'ok') Object.keys(delta).forEach((featureName) => state.dirtyFeatures.add(featureName));
268
- if (result === 'auth_failed') failStreaming(state, 'auth');
269
- if (result === 'ok') resetRetry(state);
270
- return;
271
- }
272
-
273
- const response = await doPost(
274
- state.ingestUrl,
275
- fetchHeaders(state),
276
- { sessionId: state.sessionId, delta: { logs: delta } },
277
- state.timeoutMs,
278
- );
279
- if (!response) {
280
- Object.keys(delta).forEach((featureName) => state.dirtyFeatures.add(featureName));
281
- retry = true;
282
- return;
283
- }
284
-
285
- if (response.status === 404) {
286
- state.sessionId = null;
287
- state.lastSentIds.clear();
288
- const result = await doSendFullReport(state);
289
- retry = result === 'retry';
290
- if (result !== 'ok') Object.keys(delta).forEach((featureName) => state.dirtyFeatures.add(featureName));
291
- if (result === 'auth_failed') failStreaming(state, 'auth');
292
- if (result === 'ok') resetRetry(state);
293
- return;
294
- }
295
-
296
- if (isAuthFailure(response.status)) {
297
- Object.keys(delta).forEach((featureName) => state.dirtyFeatures.add(featureName));
298
- failStreaming(state, 'auth');
299
- return;
300
- }
301
-
302
- if (response.status < 200 || response.status >= 300) {
303
- Object.keys(delta).forEach((featureName) => state.dirtyFeatures.add(featureName));
304
- retry = true;
305
- return;
306
- }
307
-
308
- nextSentIds.forEach((ids, featureName) => {
309
- state.lastSentIds.set(featureName, ids);
310
- });
311
- resetRetry(state);
312
- if (state.sessionId) {
313
- emitStatus(state, { state: 'connected', sessionId: state.sessionId });
314
- }
315
- } finally {
316
- state.sending = false;
317
- if (active !== state) return;
318
- if (retry && state.dirtyFeatures.size > 0) {
319
- scheduleRetry(state);
320
- } else if (state.dirtyFeatures.size > 0 && !state.debounceTimer) {
321
- scheduleDelta(state);
322
- }
323
- }
324
- }
325
-
326
- function scheduleDelta(state: StreamState): void {
327
- if (state.debounceTimer) clearTimeout(state.debounceTimer);
328
- state.debounceTimer = setTimeout(() => {
329
- state.debounceTimer = null;
330
- if (active === state) sendDelta(state);
331
- }, state.debounceMs);
332
- }
333
-
334
- function onFeatureChange(featureName: string): void {
335
- if (!active) return;
336
- active.dirtyFeatures.add(featureName);
337
- if (active.retryTimer) return;
338
- scheduleDelta(active);
339
- }
340
-
341
- function handleAppStateChange(nextState: AppStateStatus): void {
342
- if (!active) return;
343
- if (nextState === 'background') {
344
- active.backgroundedAt = Date.now();
345
- if (active.debounceTimer) {
346
- clearTimeout(active.debounceTimer);
347
- active.debounceTimer = null;
348
- }
349
- sendDelta(active);
350
- } else if (nextState === 'active') {
351
- const wasAway = active.backgroundedAt ? Date.now() - active.backgroundedAt : 0;
352
- active.backgroundedAt = null;
353
-
354
- if (wasAway > BACKGROUND_RESYNC_THRESHOLD_MS || !active.sessionId) {
355
- active.sessionId = null;
356
- active.lastSentIds.clear();
357
- sendFullReport(active);
358
- }
359
- }
360
- }
361
-
362
- export function startStreaming(options: StreamToDaemonOptions = {}): void {
363
- if (active) return;
364
-
365
- const endpoint = options.endpoint || getDefaultDaemonEndpoint();
366
- const reportUrl = buildDaemonUrl(endpoint, '/report');
367
- const ingestUrl = buildDaemonUrl(endpoint, '/ingest');
368
-
369
- _addDaemonEndpointToNetworkBlacklist(endpoint);
370
- _addDaemonEndpointToNetworkBlacklist(reportUrl);
371
- _addDaemonEndpointToNetworkBlacklist(ingestUrl);
372
-
373
- const state: StreamState = {
374
- endpoint,
375
- reportUrl,
376
- ingestUrl,
377
- token: options.token,
378
- debounceMs: options.debounceMs || DEFAULT_DEBOUNCE_MS,
379
- timeoutMs: Math.max(0, options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
380
- sessionId: null,
381
- sending: false,
382
- debounceTimer: null,
383
- retryTimer: null,
384
- retryAttempt: 0,
385
- dirtyFeatures: new Set(),
386
- lastSentIds: new Map(),
387
- featureUnsubscribes: [],
388
- appStateUnsubscribe: null,
389
- backgroundedAt: null,
390
- onStatus: options.onStatus,
391
- };
392
-
393
- for (const feature of DebugToolkit.features) {
394
- if (!feature.subscribe) continue;
395
- const unsub = feature.subscribe(() => { onFeatureChange(feature.name); });
396
- state.featureUnsubscribes.push(unsub);
397
- }
398
-
399
- state.appStateUnsubscribe = AppState.addEventListener('change', handleAppStateChange).remove;
400
- active = state;
401
-
402
- emitStatus(active, { state: 'connecting' });
403
- sendFullReport(active);
404
- }
405
-
406
- export function stopStreaming(): void {
407
- if (!active) return;
408
- const state = active;
409
- active = null;
410
-
411
- if (state.debounceTimer) clearTimeout(state.debounceTimer);
412
- if (state.retryTimer) clearTimeout(state.retryTimer);
413
- state.featureUnsubscribes.forEach((fn) => fn());
414
- state.appStateUnsubscribe?.();
415
- }
416
-
417
- export function isStreaming(): boolean {
418
- return active !== null;
419
- }