react-native-debug-toolkit 2.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/debug-toolkit.js +114 -0
- 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 +59 -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 +529 -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/autoDetectDaemon.js +141 -0
- package/lib/commonjs/utils/autoDetectDaemon.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/daemonConnection.js +81 -0
- package/lib/commonjs/utils/daemonConnection.js.map +1 -0
- package/lib/commonjs/utils/daemonSettings.js +110 -0
- package/lib/commonjs/utils/daemonSettings.js.map +1 -0
- package/lib/commonjs/utils/reportToDaemon.js +112 -0
- package/lib/commonjs/utils/reportToDaemon.js.map +1 -0
- package/lib/commonjs/utils/sessionReport.js +132 -0
- package/lib/commonjs/utils/sessionReport.js.map +1 -0
- package/lib/commonjs/utils/streamToDaemon.js +334 -0
- package/lib/commonjs/utils/streamToDaemon.js.map +1 -0
- 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 +5 -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 +524 -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/autoDetectDaemon.js +136 -0
- package/lib/module/utils/autoDetectDaemon.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/daemonConnection.js +77 -0
- package/lib/module/utils/daemonConnection.js.map +1 -0
- package/lib/module/utils/daemonSettings.js +102 -0
- package/lib/module/utils/daemonSettings.js.map +1 -0
- package/lib/module/utils/reportToDaemon.js +105 -0
- package/lib/module/utils/reportToDaemon.js.map +1 -0
- package/lib/module/utils/sessionReport.js +128 -0
- package/lib/module/utils/sessionReport.js.map +1 -0
- package/lib/module/utils/streamToDaemon.js +328 -0
- package/lib/module/utils/streamToDaemon.js.map +1 -0
- 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 +10 -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/autoDetectDaemon.d.ts +15 -0
- package/lib/typescript/src/utils/autoDetectDaemon.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/daemonConnection.d.ts +18 -0
- package/lib/typescript/src/utils/daemonConnection.d.ts.map +1 -0
- package/lib/typescript/src/utils/daemonSettings.d.ts +19 -0
- package/lib/typescript/src/utils/daemonSettings.d.ts.map +1 -0
- package/lib/typescript/src/utils/reportToDaemon.d.ts +34 -0
- package/lib/typescript/src/utils/reportToDaemon.d.ts.map +1 -0
- package/lib/typescript/src/utils/sessionReport.d.ts +18 -0
- package/lib/typescript/src/utils/sessionReport.d.ts.map +1 -0
- package/lib/typescript/src/utils/streamToDaemon.d.ts +23 -0
- package/lib/typescript/src/utils/streamToDaemon.d.ts.map +1 -0
- package/node/daemon/src/cli.js +75 -0
- package/node/daemon/src/console/console.html +936 -0
- package/node/daemon/src/console/index.js +47 -0
- package/node/daemon/src/constants.js +32 -0
- package/node/daemon/src/index.js +11 -0
- package/node/daemon/src/server.js +365 -0
- package/node/daemon/src/store.js +110 -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 +95 -0
- package/node/mcp/src/server.js +144 -0
- package/node/mcp/src/tools.js +84 -0
- package/package.json +7 -2
- package/src/features/network/index.ts +30 -3
- package/src/features/network/networkInterceptor.ts +19 -6
- package/src/index.ts +14 -0
- package/src/ui/panel/DebugPanel.tsx +23 -1
- package/src/ui/panel/FloatPanelView.tsx +10 -68
- package/src/ui/panel/StreamingSettingsModal.tsx +566 -0
- package/src/ui/panel/useTabAnimation.ts +77 -0
- package/src/utils/autoDetectDaemon.ts +175 -0
- package/src/utils/createPersistedObservableStore.ts +16 -3
- package/src/utils/daemonConnection.ts +133 -0
- package/src/utils/daemonSettings.ts +134 -0
- package/src/utils/reportToDaemon.ts +172 -0
- package/src/utils/sessionReport.ts +203 -0
- package/src/utils/streamToDaemon.ts +419 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { NativeModules } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export interface AutoDetectOptions {
|
|
4
|
+
port?: number;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
batchSize?: number;
|
|
7
|
+
scanSubnets?: boolean;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type AutoDetectMethod = 'metro-bundle' | 'network-info' | 'subnet-common' | 'none';
|
|
12
|
+
|
|
13
|
+
export interface AutoDetectResult {
|
|
14
|
+
ip: string | null;
|
|
15
|
+
method: AutoDetectMethod;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULT_PORT = 3799;
|
|
19
|
+
const PROBE_TIMEOUT_MS = 1500;
|
|
20
|
+
const DEFAULT_BATCH_SIZE = 20;
|
|
21
|
+
|
|
22
|
+
const COMMON_SUBNETS = [
|
|
23
|
+
'192.168.0',
|
|
24
|
+
'192.168.1',
|
|
25
|
+
'192.168.2',
|
|
26
|
+
'192.168.3',
|
|
27
|
+
'192.168.4',
|
|
28
|
+
'192.168.31',
|
|
29
|
+
'192.168.43',
|
|
30
|
+
'192.168.50',
|
|
31
|
+
'192.168.68',
|
|
32
|
+
'192.168.100',
|
|
33
|
+
'10.0.0',
|
|
34
|
+
'10.0.1',
|
|
35
|
+
'10.0.2',
|
|
36
|
+
'172.16.0',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const SIMULATOR_HOSTS = ['localhost', '127.0.0.1', '10.0.2.2'];
|
|
40
|
+
|
|
41
|
+
export function getMetroHost(): string | null {
|
|
42
|
+
try {
|
|
43
|
+
const scriptURL: unknown = NativeModules?.SourceCode?.scriptURL;
|
|
44
|
+
if (typeof scriptURL !== 'string') return null;
|
|
45
|
+
const url = new URL(scriptURL);
|
|
46
|
+
if (SIMULATOR_HOSTS.includes(url.hostname)) return null;
|
|
47
|
+
return url.hostname;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface NetworkInfoLike {
|
|
54
|
+
getIPAddress: () => Promise<string>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getNetworkInfo(): NetworkInfoLike | null {
|
|
58
|
+
try {
|
|
59
|
+
const mod = require('react-native-network-info');
|
|
60
|
+
return mod.default || mod;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getSubnetPrefix(ip: string): string | null {
|
|
67
|
+
const parts = ip.split('.');
|
|
68
|
+
if (parts.length !== 4) return null;
|
|
69
|
+
return parts.slice(0, 3).join('.');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildCandidates(prefix: string): string[] {
|
|
73
|
+
const candidates: string[] = [];
|
|
74
|
+
for (let i = 1; i <= 255; i++) {
|
|
75
|
+
candidates.push(`${prefix}.${i}`);
|
|
76
|
+
}
|
|
77
|
+
return candidates;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function probeIp(ip: string, port: number, timeoutMs: number, signal?: AbortSignal): Promise<boolean> {
|
|
81
|
+
if (signal?.aborted) return false;
|
|
82
|
+
|
|
83
|
+
type GlobalFetch = typeof globalThis.fetch;
|
|
84
|
+
const fetchImpl = (globalThis as { fetch?: GlobalFetch }).fetch;
|
|
85
|
+
if (!fetchImpl) return false;
|
|
86
|
+
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
89
|
+
|
|
90
|
+
const onAbort = () => controller.abort();
|
|
91
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetchImpl(`http://${ip}:${port}/health`, {
|
|
95
|
+
method: 'GET',
|
|
96
|
+
signal: controller.signal,
|
|
97
|
+
});
|
|
98
|
+
if (!response.ok) return false;
|
|
99
|
+
try {
|
|
100
|
+
const body = await response.json();
|
|
101
|
+
return body != null && typeof body === 'object' && (body as Record<string, unknown>).ok === true;
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
} finally {
|
|
108
|
+
clearTimeout(timeout);
|
|
109
|
+
signal?.removeEventListener('abort', onAbort);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function probeBatch(
|
|
114
|
+
candidates: string[],
|
|
115
|
+
port: number,
|
|
116
|
+
timeoutMs: number,
|
|
117
|
+
batchSize: number,
|
|
118
|
+
signal?: AbortSignal,
|
|
119
|
+
): Promise<string | null> {
|
|
120
|
+
for (let i = 0; i < candidates.length; i += batchSize) {
|
|
121
|
+
if (signal?.aborted) return null;
|
|
122
|
+
const batch = candidates.slice(i, i + batchSize);
|
|
123
|
+
const results = await Promise.all(
|
|
124
|
+
batch.map((ip) => probeIp(ip, port, timeoutMs, signal)),
|
|
125
|
+
);
|
|
126
|
+
const idx = results.findIndex(Boolean);
|
|
127
|
+
if (idx !== -1) return batch[idx] ?? null;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function autoDetectDaemonIp(
|
|
133
|
+
options: AutoDetectOptions = {},
|
|
134
|
+
): Promise<AutoDetectResult> {
|
|
135
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
136
|
+
const timeoutMs = options.timeoutMs ?? PROBE_TIMEOUT_MS;
|
|
137
|
+
const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
138
|
+
const scanSubnets = options.scanSubnets ?? true;
|
|
139
|
+
const { signal } = options;
|
|
140
|
+
|
|
141
|
+
// Strategy 0: extract Mac IP from Metro bundle URL (zero-dep, instant)
|
|
142
|
+
const metroHost = getMetroHost();
|
|
143
|
+
if (metroHost) {
|
|
144
|
+
const alive = await probeIp(metroHost, port, timeoutMs, signal);
|
|
145
|
+
if (alive) return { ip: metroHost, method: 'metro-bundle' };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!scanSubnets) {
|
|
149
|
+
return { ip: null, method: 'none' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Strategy 1: react-native-network-info → device IP → subnet scan
|
|
153
|
+
const networkInfo = getNetworkInfo();
|
|
154
|
+
if (networkInfo?.getIPAddress) {
|
|
155
|
+
try {
|
|
156
|
+
const deviceIp = await networkInfo.getIPAddress();
|
|
157
|
+
const prefix = getSubnetPrefix(deviceIp);
|
|
158
|
+
if (prefix) {
|
|
159
|
+
const found = await probeBatch(buildCandidates(prefix), port, timeoutMs, batchSize, signal);
|
|
160
|
+
if (found) return { ip: found, method: 'network-info' };
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// fall through
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Strategy 2: common subnet probe
|
|
168
|
+
for (const prefix of COMMON_SUBNETS) {
|
|
169
|
+
if (signal?.aborted) break;
|
|
170
|
+
const found = await probeBatch(buildCandidates(prefix), port, timeoutMs, batchSize, signal);
|
|
171
|
+
if (found) return { ip: found, method: 'subnet-common' };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { ip: null, method: 'none' };
|
|
175
|
+
}
|
|
@@ -9,8 +9,9 @@ export interface PersistedStoreOptions<T> {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export interface PersistedObservableStore<T> extends ObservableStore<T> {
|
|
12
|
-
/** Returns the next auto-incrementing ID and advances the counter. */
|
|
13
12
|
nextId: () => string;
|
|
13
|
+
ready: Promise<void>;
|
|
14
|
+
destroy: () => void;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export function createPersistedObservableStore<T extends { id?: string }>(
|
|
@@ -20,13 +21,15 @@ export function createPersistedObservableStore<T extends { id?: string }>(
|
|
|
20
21
|
const store = createObservableStore<T>();
|
|
21
22
|
let writeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
22
23
|
let idCounter = 0;
|
|
24
|
+
let resolveReady: () => void;
|
|
25
|
+
const ready = new Promise<void>((resolve) => { resolveReady = resolve; });
|
|
23
26
|
|
|
24
27
|
// Restore from storage (single notify via pushBatch)
|
|
25
28
|
getPreference(storageKey).then((raw) => {
|
|
26
|
-
if (!raw) return;
|
|
29
|
+
if (!raw) { resolveReady(); return; }
|
|
27
30
|
try {
|
|
28
31
|
const entries = JSON.parse(raw) as T[];
|
|
29
|
-
if (!Array.isArray(entries)) return;
|
|
32
|
+
if (!Array.isArray(entries)) { resolveReady(); return; }
|
|
30
33
|
const restored = entries.slice(-maxPersist);
|
|
31
34
|
store.pushBatch(restored);
|
|
32
35
|
// Fix ID counter to avoid collision with restored entries
|
|
@@ -39,6 +42,7 @@ export function createPersistedObservableStore<T extends { id?: string }>(
|
|
|
39
42
|
} catch {
|
|
40
43
|
// ignore corrupt data
|
|
41
44
|
}
|
|
45
|
+
resolveReady();
|
|
42
46
|
});
|
|
43
47
|
|
|
44
48
|
function scheduleWrite(): void {
|
|
@@ -72,5 +76,14 @@ export function createPersistedObservableStore<T extends { id?: string }>(
|
|
|
72
76
|
},
|
|
73
77
|
subscribe: store.subscribe,
|
|
74
78
|
nextId: () => String(idCounter++),
|
|
79
|
+
ready,
|
|
80
|
+
destroy: () => {
|
|
81
|
+
if (writeTimer !== null) {
|
|
82
|
+
clearTimeout(writeTimer);
|
|
83
|
+
writeTimer = null;
|
|
84
|
+
}
|
|
85
|
+
store.clear();
|
|
86
|
+
setPreference(storageKey, '[]');
|
|
87
|
+
},
|
|
75
88
|
};
|
|
76
89
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildDaemonUrl,
|
|
3
|
+
getDefaultDaemonEndpoint,
|
|
4
|
+
getGlobalFetch,
|
|
5
|
+
} from './reportToDaemon';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_HEALTH_TIMEOUT_MS = 2000;
|
|
8
|
+
|
|
9
|
+
type FetchResponseLike = {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
status: number;
|
|
12
|
+
json?: () => Promise<unknown>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type AbortControllerLike = {
|
|
16
|
+
signal: unknown;
|
|
17
|
+
abort: () => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type DaemonConnectionFailureReason =
|
|
21
|
+
| 'fetch_unavailable'
|
|
22
|
+
| 'timeout'
|
|
23
|
+
| 'http'
|
|
24
|
+
| 'invalid_response'
|
|
25
|
+
| 'network';
|
|
26
|
+
|
|
27
|
+
export type DaemonConnectionResult =
|
|
28
|
+
| {
|
|
29
|
+
ok: true;
|
|
30
|
+
endpoint: string;
|
|
31
|
+
status: number;
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
ok: false;
|
|
35
|
+
endpoint: string;
|
|
36
|
+
reason: DaemonConnectionFailureReason;
|
|
37
|
+
status?: number;
|
|
38
|
+
error?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export interface DaemonConnectionOptions {
|
|
42
|
+
endpoint?: string;
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createAbortController(): AbortControllerLike | undefined {
|
|
47
|
+
const AbortControllerCtor = (globalThis as {
|
|
48
|
+
AbortController?: new () => AbortControllerLike;
|
|
49
|
+
}).AbortController;
|
|
50
|
+
return AbortControllerCtor ? new AbortControllerCtor() : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function readHealthBody(response: FetchResponseLike): Promise<Record<string, unknown> | null> {
|
|
54
|
+
try {
|
|
55
|
+
const body = response.json ? await response.json() : null;
|
|
56
|
+
return body && typeof body === 'object' && !Array.isArray(body)
|
|
57
|
+
? body as Record<string, unknown>
|
|
58
|
+
: null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function checkDaemonConnection(
|
|
65
|
+
options: DaemonConnectionOptions = {},
|
|
66
|
+
): Promise<DaemonConnectionResult> {
|
|
67
|
+
const endpoint = options.endpoint ?? getDefaultDaemonEndpoint();
|
|
68
|
+
const healthUrl = buildDaemonUrl(endpoint, '/health');
|
|
69
|
+
const fetchImpl = getGlobalFetch();
|
|
70
|
+
|
|
71
|
+
if (!fetchImpl) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
endpoint,
|
|
75
|
+
reason: 'fetch_unavailable',
|
|
76
|
+
error: 'global fetch is not available',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const timeoutMs = Math.max(0, options.timeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS);
|
|
81
|
+
const controller = createAbortController();
|
|
82
|
+
let timedOut = false;
|
|
83
|
+
const timeout = controller && timeoutMs > 0
|
|
84
|
+
? setTimeout(() => {
|
|
85
|
+
timedOut = true;
|
|
86
|
+
controller.abort();
|
|
87
|
+
}, timeoutMs)
|
|
88
|
+
: undefined;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const response = await fetchImpl(healthUrl, {
|
|
92
|
+
method: 'GET',
|
|
93
|
+
headers: {},
|
|
94
|
+
signal: controller?.signal,
|
|
95
|
+
});
|
|
96
|
+
const body = await readHealthBody(response);
|
|
97
|
+
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
endpoint,
|
|
102
|
+
reason: 'http',
|
|
103
|
+
status: response.status,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (body?.ok !== true) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
endpoint,
|
|
111
|
+
reason: 'invalid_response',
|
|
112
|
+
status: response.status,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
ok: true,
|
|
118
|
+
endpoint,
|
|
119
|
+
status: response.status,
|
|
120
|
+
};
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
endpoint,
|
|
125
|
+
reason: timedOut ? 'timeout' : 'network',
|
|
126
|
+
error: error instanceof Error ? error.message : String(error),
|
|
127
|
+
};
|
|
128
|
+
} finally {
|
|
129
|
+
if (timeout) {
|
|
130
|
+
clearTimeout(timeout);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export const DAEMON_ENDPOINT_STORAGE_KEY = 'debugToolkit_streamEndpoint';
|
|
2
|
+
export const DAEMON_TOKEN_STORAGE_KEY = 'debugToolkit_streamToken';
|
|
3
|
+
export const DAEMON_CONNECTION_MODE_STORAGE_KEY = 'debugToolkit_connectionMode';
|
|
4
|
+
export const DAEMON_DEVICE_HOST_STORAGE_KEY = 'debugToolkit_deviceHost';
|
|
5
|
+
|
|
6
|
+
export type DaemonConnectionMode = 'simulator' | 'device';
|
|
7
|
+
|
|
8
|
+
export interface DaemonSettings {
|
|
9
|
+
mode: DaemonConnectionMode;
|
|
10
|
+
endpoint: string;
|
|
11
|
+
deviceHost: string;
|
|
12
|
+
token: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface AsyncStorageLike {
|
|
16
|
+
getItem: (key: string) => Promise<string | null>;
|
|
17
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getAsyncStorage(): AsyncStorageLike | null {
|
|
21
|
+
try {
|
|
22
|
+
const asyncStorageModule = require('@react-native-async-storage/async-storage');
|
|
23
|
+
return asyncStorageModule.default || asyncStorageModule;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function loadDaemonSettings(): Promise<DaemonSettings> {
|
|
30
|
+
const AsyncStorage = getAsyncStorage();
|
|
31
|
+
if (!AsyncStorage) {
|
|
32
|
+
return { mode: 'simulator', endpoint: '', deviceHost: '', token: '' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const [mode, endpoint, deviceHost, token] = await Promise.all([
|
|
37
|
+
AsyncStorage.getItem(DAEMON_CONNECTION_MODE_STORAGE_KEY),
|
|
38
|
+
AsyncStorage.getItem(DAEMON_ENDPOINT_STORAGE_KEY),
|
|
39
|
+
AsyncStorage.getItem(DAEMON_DEVICE_HOST_STORAGE_KEY),
|
|
40
|
+
AsyncStorage.getItem(DAEMON_TOKEN_STORAGE_KEY),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const inferredMode = readMode(mode, endpoint || '');
|
|
44
|
+
const inferredHost = deviceHost || extractDeviceHost(endpoint || '');
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
mode: inferredMode,
|
|
48
|
+
endpoint: endpoint || '',
|
|
49
|
+
deviceHost: inferredMode === 'device' ? inferredHost : '',
|
|
50
|
+
token: token || '',
|
|
51
|
+
};
|
|
52
|
+
} catch {
|
|
53
|
+
return { mode: 'simulator', endpoint: '', deviceHost: '', token: '' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function saveDaemonSettings(settings: DaemonSettings): Promise<void> {
|
|
58
|
+
const AsyncStorage = getAsyncStorage();
|
|
59
|
+
if (!AsyncStorage) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const normalized = normalizeDaemonSettings(settings);
|
|
64
|
+
try {
|
|
65
|
+
await Promise.all([
|
|
66
|
+
AsyncStorage.setItem(DAEMON_CONNECTION_MODE_STORAGE_KEY, settings.mode),
|
|
67
|
+
AsyncStorage.setItem(DAEMON_DEVICE_HOST_STORAGE_KEY, settings.deviceHost.trim()),
|
|
68
|
+
AsyncStorage.setItem(DAEMON_ENDPOINT_STORAGE_KEY, normalized.endpoint || ''),
|
|
69
|
+
AsyncStorage.setItem(DAEMON_TOKEN_STORAGE_KEY, settings.token.trim()),
|
|
70
|
+
]);
|
|
71
|
+
} catch {
|
|
72
|
+
// AsyncStorage unavailable or rejected: keep runtime behavior unchanged.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readMode(value: string | null, endpoint: string): DaemonConnectionMode {
|
|
77
|
+
if (value === 'device' || value === 'simulator') {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
return endpoint && !isSimulatorEndpoint(endpoint) ? 'device' : 'simulator';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isSimulatorEndpoint(endpoint: string): boolean {
|
|
84
|
+
try {
|
|
85
|
+
const url = new URL(endpoint);
|
|
86
|
+
return ['localhost', '127.0.0.1', '10.0.2.2'].includes(url.hostname);
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function extractDeviceHost(endpoint: string): string {
|
|
93
|
+
try {
|
|
94
|
+
const url = new URL(endpoint);
|
|
95
|
+
return url.port && url.port !== '3799' ? `${url.hostname}:${url.port}` : url.hostname;
|
|
96
|
+
} catch {
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildDeviceDaemonEndpoint(host: string): string {
|
|
102
|
+
const trimmed = host.trim().replace(/\/+$/, '');
|
|
103
|
+
if (!trimmed) {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const withProtocol = /^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(trimmed)
|
|
108
|
+
? trimmed
|
|
109
|
+
: `http://${trimmed}`;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const url = new URL(withProtocol);
|
|
113
|
+
if (!url.port) {
|
|
114
|
+
url.port = '3799';
|
|
115
|
+
}
|
|
116
|
+
return url.toString().replace(/\/$/, '');
|
|
117
|
+
} catch {
|
|
118
|
+
return withProtocol;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function normalizeDaemonSettings(settings: DaemonSettings): {
|
|
123
|
+
endpoint?: string;
|
|
124
|
+
token?: string;
|
|
125
|
+
} {
|
|
126
|
+
const endpoint = settings.mode === 'device'
|
|
127
|
+
? buildDeviceDaemonEndpoint(settings.deviceHost)
|
|
128
|
+
: '';
|
|
129
|
+
const token = settings.token.trim();
|
|
130
|
+
return {
|
|
131
|
+
endpoint: endpoint || undefined,
|
|
132
|
+
token: token || undefined,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { _addDaemonEndpointToNetworkBlacklist } from '../features/network';
|
|
4
|
+
import {
|
|
5
|
+
createDebugSessionReport,
|
|
6
|
+
type DebugSessionReport,
|
|
7
|
+
type DebugSessionReportOptions,
|
|
8
|
+
} from './sessionReport';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 3000;
|
|
11
|
+
|
|
12
|
+
type FetchResponseLike = {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
status: number;
|
|
15
|
+
json?: () => Promise<unknown>;
|
|
16
|
+
text?: () => Promise<string>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type FetchLike = (
|
|
20
|
+
url: string,
|
|
21
|
+
init: {
|
|
22
|
+
method: string;
|
|
23
|
+
headers: Record<string, string>;
|
|
24
|
+
body?: string;
|
|
25
|
+
signal?: unknown;
|
|
26
|
+
},
|
|
27
|
+
) => Promise<FetchResponseLike>;
|
|
28
|
+
|
|
29
|
+
type AbortControllerLike = {
|
|
30
|
+
signal: unknown;
|
|
31
|
+
abort: () => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface ReportToDaemonOptions extends DebugSessionReportOptions {
|
|
35
|
+
endpoint?: string;
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
token?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ReportResult {
|
|
41
|
+
ok: boolean;
|
|
42
|
+
endpoint: string;
|
|
43
|
+
report: DebugSessionReport;
|
|
44
|
+
status?: number;
|
|
45
|
+
sessionId?: string;
|
|
46
|
+
receivedAt?: string;
|
|
47
|
+
logCount?: Record<string, number>;
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getGlobalFetch(): FetchLike | undefined {
|
|
52
|
+
return (globalThis as { fetch?: FetchLike }).fetch;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createAbortController(): AbortControllerLike | undefined {
|
|
56
|
+
const AbortControllerCtor = (globalThis as {
|
|
57
|
+
AbortController?: new () => AbortControllerLike;
|
|
58
|
+
}).AbortController;
|
|
59
|
+
return AbortControllerCtor ? new AbortControllerCtor() : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getDefaultDaemonEndpoint(): string {
|
|
63
|
+
if (Platform.OS === 'android') {
|
|
64
|
+
return 'http://10.0.2.2:3799';
|
|
65
|
+
}
|
|
66
|
+
return 'http://localhost:3799';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildDaemonUrl(endpoint: string, path: string): string {
|
|
70
|
+
const trimmed = endpoint.replace(/\/+$/, '');
|
|
71
|
+
return trimmed.endsWith(path) ? trimmed : `${trimmed}${path}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function readResponseBody(response: FetchResponseLike): Promise<unknown> {
|
|
75
|
+
try {
|
|
76
|
+
if (response.json) {
|
|
77
|
+
return await response.json();
|
|
78
|
+
}
|
|
79
|
+
if (response.text) {
|
|
80
|
+
const text = await response.text();
|
|
81
|
+
return text ? JSON.parse(text) : null;
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readLogCount(value: unknown): Record<string, number> | undefined {
|
|
90
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return Object.entries(value as Record<string, unknown>).reduce<Record<string, number>>(
|
|
95
|
+
(acc, [key, count]) => {
|
|
96
|
+
if (typeof count === 'number') {
|
|
97
|
+
acc[key] = count;
|
|
98
|
+
}
|
|
99
|
+
return acc;
|
|
100
|
+
},
|
|
101
|
+
{},
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function reportDebugSessionToDaemon(
|
|
106
|
+
options: ReportToDaemonOptions = {},
|
|
107
|
+
): Promise<ReportResult> {
|
|
108
|
+
const endpoint = options.endpoint ?? getDefaultDaemonEndpoint();
|
|
109
|
+
const reportUrl = buildDaemonUrl(endpoint, '/report');
|
|
110
|
+
const report = createDebugSessionReport(options);
|
|
111
|
+
const fetchImpl = getGlobalFetch();
|
|
112
|
+
|
|
113
|
+
_addDaemonEndpointToNetworkBlacklist(endpoint);
|
|
114
|
+
_addDaemonEndpointToNetworkBlacklist(reportUrl);
|
|
115
|
+
|
|
116
|
+
if (!fetchImpl) {
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
endpoint,
|
|
120
|
+
report,
|
|
121
|
+
error: 'global fetch is not available',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const timeoutMs = Math.max(0, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
126
|
+
const controller = createAbortController();
|
|
127
|
+
const timeout = controller && timeoutMs > 0
|
|
128
|
+
? setTimeout(() => controller.abort(), timeoutMs)
|
|
129
|
+
: undefined;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const headers: Record<string, string> = {
|
|
133
|
+
'Content-Type': 'application/json',
|
|
134
|
+
};
|
|
135
|
+
if (options.token) {
|
|
136
|
+
headers.Authorization = `Bearer ${options.token}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const response = await fetchImpl(reportUrl, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers,
|
|
142
|
+
body: JSON.stringify(report),
|
|
143
|
+
signal: controller?.signal,
|
|
144
|
+
});
|
|
145
|
+
const responseBody = await readResponseBody(response);
|
|
146
|
+
const bodyObject = responseBody && typeof responseBody === 'object'
|
|
147
|
+
? responseBody as Record<string, unknown>
|
|
148
|
+
: {};
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
ok: response.ok && bodyObject.ok === true,
|
|
152
|
+
endpoint,
|
|
153
|
+
report,
|
|
154
|
+
status: response.status,
|
|
155
|
+
sessionId: typeof bodyObject.sessionId === 'string' ? bodyObject.sessionId : undefined,
|
|
156
|
+
receivedAt: typeof bodyObject.receivedAt === 'string' ? bodyObject.receivedAt : undefined,
|
|
157
|
+
logCount: readLogCount(bodyObject.logCount),
|
|
158
|
+
error: response.ok ? undefined : typeof bodyObject.error === 'string' ? bodyObject.error : 'Report failed',
|
|
159
|
+
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
endpoint,
|
|
164
|
+
report,
|
|
165
|
+
error: error instanceof Error ? error.message : String(error),
|
|
166
|
+
};
|
|
167
|
+
} finally {
|
|
168
|
+
if (timeout) {
|
|
169
|
+
clearTimeout(timeout);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|