react-native-ai-devtools 1.1.4

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 (147) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +1250 -0
  3. package/build/__tests__/helpers/fake-cdp-server.d.ts +56 -0
  4. package/build/__tests__/helpers/fake-cdp-server.d.ts.map +1 -0
  5. package/build/__tests__/helpers/fake-cdp-server.js +108 -0
  6. package/build/__tests__/helpers/fake-cdp-server.js.map +1 -0
  7. package/build/__tests__/integration/connection-health.test.d.ts +2 -0
  8. package/build/__tests__/integration/connection-health.test.d.ts.map +1 -0
  9. package/build/__tests__/integration/connection-health.test.js +151 -0
  10. package/build/__tests__/integration/connection-health.test.js.map +1 -0
  11. package/build/__tests__/integration/execute-in-app.test.d.ts +2 -0
  12. package/build/__tests__/integration/execute-in-app.test.d.ts.map +1 -0
  13. package/build/__tests__/integration/execute-in-app.test.js +115 -0
  14. package/build/__tests__/integration/execute-in-app.test.js.map +1 -0
  15. package/build/__tests__/integration/tools.test.d.ts +2 -0
  16. package/build/__tests__/integration/tools.test.d.ts.map +1 -0
  17. package/build/__tests__/integration/tools.test.js +228 -0
  18. package/build/__tests__/integration/tools.test.js.map +1 -0
  19. package/build/__tests__/setup.d.ts +2 -0
  20. package/build/__tests__/setup.d.ts.map +1 -0
  21. package/build/__tests__/setup.js +11 -0
  22. package/build/__tests__/setup.js.map +1 -0
  23. package/build/__tests__/unit/bundle.test.d.ts +2 -0
  24. package/build/__tests__/unit/bundle.test.d.ts.map +1 -0
  25. package/build/__tests__/unit/bundle.test.js +53 -0
  26. package/build/__tests__/unit/bundle.test.js.map +1 -0
  27. package/build/__tests__/unit/connection-health.test.d.ts +2 -0
  28. package/build/__tests__/unit/connection-health.test.d.ts.map +1 -0
  29. package/build/__tests__/unit/connection-health.test.js +28 -0
  30. package/build/__tests__/unit/connection-health.test.js.map +1 -0
  31. package/build/__tests__/unit/executor.test.d.ts +2 -0
  32. package/build/__tests__/unit/executor.test.d.ts.map +1 -0
  33. package/build/__tests__/unit/executor.test.js +79 -0
  34. package/build/__tests__/unit/executor.test.js.map +1 -0
  35. package/build/__tests__/unit/logs.test.d.ts +2 -0
  36. package/build/__tests__/unit/logs.test.d.ts.map +1 -0
  37. package/build/__tests__/unit/logs.test.js +81 -0
  38. package/build/__tests__/unit/logs.test.js.map +1 -0
  39. package/build/__tests__/unit/metro.test.d.ts +2 -0
  40. package/build/__tests__/unit/metro.test.d.ts.map +1 -0
  41. package/build/__tests__/unit/metro.test.js +61 -0
  42. package/build/__tests__/unit/metro.test.js.map +1 -0
  43. package/build/__tests__/unit/network.test.d.ts +2 -0
  44. package/build/__tests__/unit/network.test.d.ts.map +1 -0
  45. package/build/__tests__/unit/network.test.js +102 -0
  46. package/build/__tests__/unit/network.test.js.map +1 -0
  47. package/build/__tests__/unit/tap.test.d.ts +2 -0
  48. package/build/__tests__/unit/tap.test.d.ts.map +1 -0
  49. package/build/__tests__/unit/tap.test.js +157 -0
  50. package/build/__tests__/unit/tap.test.js.map +1 -0
  51. package/build/core/android.d.ts +265 -0
  52. package/build/core/android.d.ts.map +1 -0
  53. package/build/core/android.js +1413 -0
  54. package/build/core/android.js.map +1 -0
  55. package/build/core/bundle.d.ts +49 -0
  56. package/build/core/bundle.d.ts.map +1 -0
  57. package/build/core/bundle.js +368 -0
  58. package/build/core/bundle.js.map +1 -0
  59. package/build/core/connection.d.ts +43 -0
  60. package/build/core/connection.d.ts.map +1 -0
  61. package/build/core/connection.js +963 -0
  62. package/build/core/connection.js.map +1 -0
  63. package/build/core/connectionState.d.ts +108 -0
  64. package/build/core/connectionState.d.ts.map +1 -0
  65. package/build/core/connectionState.js +284 -0
  66. package/build/core/connectionState.js.map +1 -0
  67. package/build/core/errorScreenParser.d.ts +30 -0
  68. package/build/core/errorScreenParser.d.ts.map +1 -0
  69. package/build/core/errorScreenParser.js +198 -0
  70. package/build/core/errorScreenParser.js.map +1 -0
  71. package/build/core/executor.d.ts +113 -0
  72. package/build/core/executor.d.ts.map +1 -0
  73. package/build/core/executor.js +1877 -0
  74. package/build/core/executor.js.map +1 -0
  75. package/build/core/format.d.ts +8 -0
  76. package/build/core/format.d.ts.map +1 -0
  77. package/build/core/format.js +34 -0
  78. package/build/core/format.js.map +1 -0
  79. package/build/core/guides.d.ts +14 -0
  80. package/build/core/guides.d.ts.map +1 -0
  81. package/build/core/guides.js +261 -0
  82. package/build/core/guides.js.map +1 -0
  83. package/build/core/httpServer.d.ts +14 -0
  84. package/build/core/httpServer.d.ts.map +1 -0
  85. package/build/core/httpServer.js +2459 -0
  86. package/build/core/httpServer.js.map +1 -0
  87. package/build/core/httpServerProcess.d.ts +25 -0
  88. package/build/core/httpServerProcess.d.ts.map +1 -0
  89. package/build/core/httpServerProcess.js +153 -0
  90. package/build/core/httpServerProcess.js.map +1 -0
  91. package/build/core/index.d.ts +25 -0
  92. package/build/core/index.d.ts.map +1 -0
  93. package/build/core/index.js +53 -0
  94. package/build/core/index.js.map +1 -0
  95. package/build/core/ios.d.ts +214 -0
  96. package/build/core/ios.d.ts.map +1 -0
  97. package/build/core/ios.js +1232 -0
  98. package/build/core/ios.js.map +1 -0
  99. package/build/core/logs.d.ts +43 -0
  100. package/build/core/logs.d.ts.map +1 -0
  101. package/build/core/logs.js +144 -0
  102. package/build/core/logs.js.map +1 -0
  103. package/build/core/metro.d.ts +23 -0
  104. package/build/core/metro.d.ts.map +1 -0
  105. package/build/core/metro.js +96 -0
  106. package/build/core/metro.js.map +1 -0
  107. package/build/core/network.d.ts +43 -0
  108. package/build/core/network.d.ts.map +1 -0
  109. package/build/core/network.js +217 -0
  110. package/build/core/network.js.map +1 -0
  111. package/build/core/networkInterceptor.d.ts +3 -0
  112. package/build/core/networkInterceptor.d.ts.map +1 -0
  113. package/build/core/networkInterceptor.js +203 -0
  114. package/build/core/networkInterceptor.js.map +1 -0
  115. package/build/core/ocr.d.ts +69 -0
  116. package/build/core/ocr.d.ts.map +1 -0
  117. package/build/core/ocr.js +212 -0
  118. package/build/core/ocr.js.map +1 -0
  119. package/build/core/state.d.ts +17 -0
  120. package/build/core/state.d.ts.map +1 -0
  121. package/build/core/state.js +50 -0
  122. package/build/core/state.js.map +1 -0
  123. package/build/core/tap.d.ts +91 -0
  124. package/build/core/tap.d.ts.map +1 -0
  125. package/build/core/tap.js +542 -0
  126. package/build/core/tap.js.map +1 -0
  127. package/build/core/telemetry.d.ts +4 -0
  128. package/build/core/telemetry.d.ts.map +1 -0
  129. package/build/core/telemetry.js +289 -0
  130. package/build/core/telemetry.js.map +1 -0
  131. package/build/core/types.d.ts +134 -0
  132. package/build/core/types.d.ts.map +1 -0
  133. package/build/core/types.js +2 -0
  134. package/build/core/types.js.map +1 -0
  135. package/build/httpServerStandalone.d.ts +7 -0
  136. package/build/httpServerStandalone.d.ts.map +1 -0
  137. package/build/httpServerStandalone.js +31 -0
  138. package/build/httpServerStandalone.js.map +1 -0
  139. package/build/index.d.ts +3 -0
  140. package/build/index.d.ts.map +1 -0
  141. package/build/index.js +3012 -0
  142. package/build/index.js.map +1 -0
  143. package/build/pro/tap.d.ts +91 -0
  144. package/build/pro/tap.d.ts.map +1 -0
  145. package/build/pro/tap.js +542 -0
  146. package/build/pro/tap.js.map +1 -0
  147. package/package.json +63 -0
@@ -0,0 +1,963 @@
1
+ import WebSocket from "ws";
2
+ import { connectedApps, pendingExecutions, getNextMessageId, logBuffer, networkBuffer, setActiveSimulatorUdid, clearActiveSimulatorIfSource, updateLastCDPMessageTime, getLastCDPMessageTime } from "./state.js";
3
+ import { mapConsoleType } from "./logs.js";
4
+ import { findSimulatorByName } from "./ios.js";
5
+ import { fetchDevices, selectMainDevice, scanMetroPorts } from "./metro.js";
6
+ import { DEFAULT_RECONNECTION_CONFIG, MIN_STABLE_CONNECTION_MS, initConnectionState, updateConnectionState, getConnectionState, recordConnectionGap, closeConnectionGap, saveConnectionMetadata, getConnectionMetadata, saveReconnectionTimer, cancelReconnectionTimer, calculateBackoffDelay, initContextHealth, markContextHealthy, markContextStale, getContextHealth, updateContextHealth, formatDuration, } from "./connectionState.js";
7
+ // Connection locks to prevent concurrent connection attempts to the same device
8
+ const connectionLocks = new Set();
9
+ // Suppress auto-reconnection for intentionally disconnected devices
10
+ const reconnectionSuppressed = new Set();
11
+ /**
12
+ * Suppress auto-reconnection for all current connections.
13
+ * Used by disconnect_metro to prevent close handlers from re-connecting.
14
+ */
15
+ export function suppressReconnection() {
16
+ for (const key of connectedApps.keys()) {
17
+ reconnectionSuppressed.add(key);
18
+ }
19
+ }
20
+ /**
21
+ * Clear reconnection suppression (called when user explicitly reconnects via scan_metro).
22
+ */
23
+ export function clearReconnectionSuppression() {
24
+ reconnectionSuppressed.clear();
25
+ }
26
+ const STALE_ACTIVITY_THRESHOLD_MS = 30_000;
27
+ const RECONNECT_SETTLE_MS = 500;
28
+ // Helper to find appKey from device info by searching connectedApps
29
+ function findAppKeyForDevice(device) {
30
+ for (const [key, app] of connectedApps.entries()) {
31
+ if (app.deviceInfo.id === device.id) {
32
+ return key;
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ // Helper to convert WebSocket readyState to readable name
38
+ function getWebSocketStateName(state) {
39
+ switch (state) {
40
+ case WebSocket.CONNECTING: return "CONNECTING";
41
+ case WebSocket.OPEN: return "OPEN";
42
+ case WebSocket.CLOSING: return "CLOSING";
43
+ case WebSocket.CLOSED: return "CLOSED";
44
+ default: return `UNKNOWN(${state})`;
45
+ }
46
+ }
47
+ // Format CDP RemoteObject to readable string
48
+ export function formatRemoteObject(result) {
49
+ if (result.type === "undefined") {
50
+ return "undefined";
51
+ }
52
+ if (result.subtype === "null") {
53
+ return "null";
54
+ }
55
+ // For objects/arrays with a value, stringify it
56
+ if (result.value !== undefined) {
57
+ if (typeof result.value === "object") {
58
+ return JSON.stringify(result.value, null, 2);
59
+ }
60
+ return String(result.value);
61
+ }
62
+ // Use description for complex objects
63
+ if (result.description) {
64
+ return result.description;
65
+ }
66
+ // Handle unserializable values (NaN, Infinity, etc.)
67
+ if (result.unserializableValue) {
68
+ return result.unserializableValue;
69
+ }
70
+ return `[${result.type}${result.subtype ? ` ${result.subtype}` : ""}]`;
71
+ }
72
+ /**
73
+ * Extract a clean, informative error message from CDP exception details
74
+ * Handles various error formats from Hermes and other JS engines
75
+ */
76
+ function extractExceptionMessage(exceptionDetails) {
77
+ const parts = [];
78
+ // Get the exception object if available
79
+ const exc = exceptionDetails.exception;
80
+ if (exc) {
81
+ // For error objects, className tells us the error type (ReferenceError, TypeError, etc.)
82
+ const errorType = exc.className || (exc.subtype === 'error' ? 'Error' : '');
83
+ // The description usually contains "ErrorType: message" or full stack trace
84
+ // We want to extract just the first line (the actual error message)
85
+ if (exc.description) {
86
+ const firstLine = exc.description.split('\n')[0].trim();
87
+ // If description already includes the error type, use it directly
88
+ if (firstLine.includes(':')) {
89
+ parts.push(firstLine);
90
+ }
91
+ else if (errorType) {
92
+ // Combine error type with description
93
+ parts.push(`${errorType}: ${firstLine}`);
94
+ }
95
+ else {
96
+ parts.push(firstLine);
97
+ }
98
+ }
99
+ else if (exc.value !== undefined) {
100
+ // For primitive exceptions (throw "string" or throw 123)
101
+ const valueStr = typeof exc.value === 'string' ? exc.value : JSON.stringify(exc.value);
102
+ if (errorType) {
103
+ parts.push(`${errorType}: ${valueStr}`);
104
+ }
105
+ else {
106
+ parts.push(valueStr);
107
+ }
108
+ }
109
+ else if (errorType) {
110
+ // Just the error type, no message
111
+ parts.push(errorType);
112
+ }
113
+ }
114
+ // Fall back to exceptionDetails.text if we couldn't extract from exception object
115
+ // But avoid just "Uncaught" which is not helpful
116
+ if (parts.length === 0) {
117
+ const text = exceptionDetails.text;
118
+ if (text && text.toLowerCase() !== 'uncaught') {
119
+ parts.push(text);
120
+ }
121
+ }
122
+ // Add location info for syntax/compilation errors (helps identify the problem)
123
+ if (exceptionDetails.lineNumber !== undefined && exceptionDetails.columnNumber !== undefined) {
124
+ // Only add location if it's meaningful (not 0:0 which is often just wrapper)
125
+ if (exceptionDetails.lineNumber > 0 || exceptionDetails.columnNumber > 0) {
126
+ parts.push(`at line ${exceptionDetails.lineNumber}:${exceptionDetails.columnNumber}`);
127
+ }
128
+ }
129
+ // If we still have nothing, provide a generic message
130
+ if (parts.length === 0) {
131
+ return 'JavaScript execution failed (no error details available)';
132
+ }
133
+ return parts.join(' ');
134
+ }
135
+ // Format a CDP object preview recursively
136
+ function formatPreview(preview) {
137
+ const isArray = preview.subtype === "array";
138
+ const props = preview.properties || [];
139
+ const formatted = props.map((p) => {
140
+ let value;
141
+ if (p.valuePreview) {
142
+ value = formatPreview(p.valuePreview);
143
+ }
144
+ else if (p.subtype === "null") {
145
+ value = "null";
146
+ }
147
+ else if (p.type === "string") {
148
+ value = `"${p.value}"`;
149
+ }
150
+ else {
151
+ value = p.value ?? "undefined";
152
+ }
153
+ return isArray ? value : `${p.name}: ${value}`;
154
+ });
155
+ const overflow = preview.overflow ? ", ..." : "";
156
+ return isArray
157
+ ? `[${formatted.join(", ")}${overflow}]`
158
+ : `{${formatted.join(", ")}${overflow}}`;
159
+ }
160
+ // Format a single CDP console argument (sync — without object resolution)
161
+ function formatConsoleArg(arg) {
162
+ // Primitives
163
+ if (arg.type === "string" || arg.type === "number" || arg.type === "boolean") {
164
+ return String(arg.value);
165
+ }
166
+ // Objects/arrays with preview — expand inline
167
+ if (arg.preview?.properties) {
168
+ return formatPreview(arg.preview);
169
+ }
170
+ // Raw value (e.g. null sent as value)
171
+ if (arg.value !== undefined) {
172
+ return JSON.stringify(arg.value);
173
+ }
174
+ // Description fallback (functions, symbols, errors without preview)
175
+ if (arg.description) {
176
+ return arg.description;
177
+ }
178
+ return "[object]";
179
+ }
180
+ // Fetch object properties via CDP Runtime.getProperties
181
+ function fetchObjectProperties(ws, objectId, depth = 2) {
182
+ return new Promise((resolve) => {
183
+ const msgId = getNextMessageId();
184
+ const timeout = setTimeout(() => {
185
+ resolve("Object"); // Fallback on timeout
186
+ }, 3000);
187
+ const handler = (data) => {
188
+ try {
189
+ const response = JSON.parse(data.toString());
190
+ if (response.id === msgId) {
191
+ clearTimeout(timeout);
192
+ ws.removeListener("message", handler);
193
+ if (response.result?.result) {
194
+ resolve(formatCDPProperties(ws, response.result.result, depth));
195
+ }
196
+ else {
197
+ resolve("Object");
198
+ }
199
+ }
200
+ }
201
+ catch {
202
+ // Ignore parse errors
203
+ }
204
+ };
205
+ ws.on("message", handler);
206
+ ws.send(JSON.stringify({
207
+ id: msgId,
208
+ method: "Runtime.getProperties",
209
+ params: { objectId, ownProperties: true, generatePreview: true }
210
+ }));
211
+ });
212
+ }
213
+ // Format CDP property descriptors into a readable string
214
+ async function formatCDPProperties(ws, properties, depth) {
215
+ const parts = [];
216
+ let isArrayLike = false;
217
+ // Detect arrays by checking for numeric keys and "length" property
218
+ const propNames = properties.filter((p) => !p.isAccessor && p.name !== "__proto__").map((p) => p.name);
219
+ const hasLength = propNames.includes("length");
220
+ const numericKeys = propNames.filter((n) => /^\d+$/.test(n));
221
+ if (hasLength && numericKeys.length > 0 && numericKeys.length >= propNames.length - 1) {
222
+ isArrayLike = true;
223
+ }
224
+ const filteredProps = properties.filter((p) => !p.isAccessor && p.name !== "__proto__" && (!isArrayLike || p.name !== "length"));
225
+ for (const prop of filteredProps) {
226
+ const val = prop.value;
227
+ if (!val)
228
+ continue;
229
+ const formatted = await formatPropertyValue(ws, val, depth - 1);
230
+ if (isArrayLike) {
231
+ parts.push(formatted);
232
+ }
233
+ else {
234
+ parts.push(`${prop.name}: ${formatted}`);
235
+ }
236
+ }
237
+ return isArrayLike
238
+ ? `[${parts.join(", ")}]`
239
+ : `{${parts.join(", ")}}`;
240
+ }
241
+ // Format a single property value
242
+ async function formatPropertyValue(ws, val, remainingDepth) {
243
+ const type = val.type;
244
+ const subtype = val.subtype;
245
+ if (subtype === "null")
246
+ return "null";
247
+ if (type === "undefined")
248
+ return "undefined";
249
+ if (type === "string")
250
+ return `"${val.value}"`;
251
+ if (type === "number" || type === "boolean")
252
+ return String(val.value);
253
+ if (type === "function")
254
+ return "[Function]";
255
+ // Nested object — recurse if depth allows
256
+ if (type === "object" && val.objectId && remainingDepth > 0) {
257
+ return fetchObjectProperties(ws, val.objectId, remainingDepth);
258
+ }
259
+ // Object but no depth left — use description
260
+ if (type === "object") {
261
+ return val.description || "[Object]";
262
+ }
263
+ if (val.value !== undefined)
264
+ return JSON.stringify(val.value);
265
+ if (val.description)
266
+ return val.description;
267
+ return `[${type}]`;
268
+ }
269
+ // Resolve all object args in a console message, returning formatted text
270
+ async function resolveConsoleArgs(ws, args) {
271
+ const parts = await Promise.all(args.map(async (arg) => {
272
+ // Primitives — format synchronously
273
+ if (arg.type === "string" || arg.type === "number" || arg.type === "boolean") {
274
+ return String(arg.value);
275
+ }
276
+ // Objects/arrays with preview — expand inline
277
+ if (arg.preview?.properties) {
278
+ return formatPreview(arg.preview);
279
+ }
280
+ // Object with objectId — fetch properties via CDP
281
+ if (arg.type === "object" && arg.objectId) {
282
+ return fetchObjectProperties(ws, arg.objectId);
283
+ }
284
+ // Raw value
285
+ if (arg.value !== undefined) {
286
+ return JSON.stringify(arg.value);
287
+ }
288
+ // Description fallback
289
+ if (arg.description) {
290
+ return arg.description;
291
+ }
292
+ return "[object]";
293
+ }));
294
+ return parts.join(" ");
295
+ }
296
+ // Handle CDP messages
297
+ export function handleCDPMessage(message, _device, ws) {
298
+ // Track last CDP activity for connection liveness detection
299
+ updateLastCDPMessageTime(new Date());
300
+ // Handle responses to our requests (e.g., Runtime.evaluate)
301
+ if (typeof message.id === "number") {
302
+ const pending = pendingExecutions.get(message.id);
303
+ if (pending) {
304
+ clearTimeout(pending.timeoutId);
305
+ pendingExecutions.delete(message.id);
306
+ // Check for CDP-level error (protocol error, not JS exception)
307
+ if (message.error) {
308
+ const error = message.error;
309
+ // Build comprehensive error message including code and data if available
310
+ const parts = [];
311
+ if (error.message)
312
+ parts.push(error.message);
313
+ if (error.code !== undefined)
314
+ parts.push(`(code: ${error.code})`);
315
+ if (error.data)
316
+ parts.push(`- ${error.data}`);
317
+ const errorMessage = parts.length > 0 ? parts.join(' ') : 'Unknown CDP protocol error';
318
+ pending.resolve({ success: false, error: errorMessage });
319
+ return;
320
+ }
321
+ // Check for JavaScript exception in result
322
+ const result = message.result;
323
+ if (result?.exceptionDetails) {
324
+ const errorMessage = extractExceptionMessage(result.exceptionDetails);
325
+ pending.resolve({ success: false, error: errorMessage });
326
+ return;
327
+ }
328
+ // Success - format the result
329
+ if (result?.result) {
330
+ pending.resolve({ success: true, result: formatRemoteObject(result.result) });
331
+ return;
332
+ }
333
+ pending.resolve({ success: true, result: "undefined" });
334
+ }
335
+ return;
336
+ }
337
+ const method = message.method;
338
+ // Handle Runtime.consoleAPICalled
339
+ if (method === "Runtime.consoleAPICalled") {
340
+ const params = message.params;
341
+ const type = params.type || "log";
342
+ const level = mapConsoleType(type);
343
+ const args = params.args || [];
344
+ // Check if any args need async object resolution
345
+ const hasObjectArgs = ws && args.some((a) => a.type === "object" && a.objectId && !a.preview?.properties);
346
+ if (hasObjectArgs) {
347
+ // Resolve object args asynchronously via CDP
348
+ resolveConsoleArgs(ws, args).then((messageText) => {
349
+ if (messageText.trim()) {
350
+ logBuffer.add({
351
+ timestamp: new Date(),
352
+ level,
353
+ message: messageText,
354
+ args: args.map((a) => a.value)
355
+ });
356
+ }
357
+ }).catch(() => {
358
+ // Fallback to sync formatting on error
359
+ const messageText = args.map(formatConsoleArg).join(" ");
360
+ if (messageText.trim()) {
361
+ logBuffer.add({
362
+ timestamp: new Date(),
363
+ level,
364
+ message: messageText,
365
+ args: args.map((a) => a.value)
366
+ });
367
+ }
368
+ });
369
+ }
370
+ else {
371
+ const messageText = args.map(formatConsoleArg).join(" ");
372
+ if (messageText.trim()) {
373
+ logBuffer.add({
374
+ timestamp: new Date(),
375
+ level,
376
+ message: messageText,
377
+ args: args.map((a) => a.value)
378
+ });
379
+ }
380
+ }
381
+ }
382
+ // Handle Log.entryAdded
383
+ if (method === "Log.entryAdded") {
384
+ const params = message.params;
385
+ if (params.entry) {
386
+ const level = mapConsoleType(params.entry.level || "log");
387
+ logBuffer.add({
388
+ timestamp: new Date(),
389
+ level,
390
+ message: params.entry.text || ""
391
+ });
392
+ }
393
+ }
394
+ // Handle Network.requestWillBeSent
395
+ if (method === "Network.requestWillBeSent") {
396
+ const params = message.params;
397
+ const request = {
398
+ requestId: params.requestId,
399
+ timestamp: new Date(),
400
+ method: params.request.method,
401
+ url: params.request.url,
402
+ headers: params.request.headers || {},
403
+ postData: params.request.postData,
404
+ timing: {
405
+ requestTime: params.timestamp
406
+ },
407
+ completed: false
408
+ };
409
+ networkBuffer.set(params.requestId, request);
410
+ }
411
+ // Handle Network.responseReceived
412
+ if (method === "Network.responseReceived") {
413
+ const params = message.params;
414
+ const existing = networkBuffer.get(params.requestId);
415
+ if (existing) {
416
+ existing.status = params.response.status;
417
+ existing.statusText = params.response.statusText;
418
+ existing.responseHeaders = params.response.headers || {};
419
+ existing.mimeType = params.response.mimeType;
420
+ if (params.timestamp && existing.timing?.requestTime) {
421
+ existing.timing.responseTime = params.timestamp;
422
+ }
423
+ networkBuffer.set(params.requestId, existing);
424
+ }
425
+ }
426
+ // Handle Network.loadingFinished
427
+ if (method === "Network.loadingFinished") {
428
+ const params = message.params;
429
+ const existing = networkBuffer.get(params.requestId);
430
+ if (existing) {
431
+ existing.completed = true;
432
+ existing.contentLength = params.encodedDataLength;
433
+ if (params.timestamp && existing.timing?.requestTime) {
434
+ existing.timing.duration = Math.round((params.timestamp - existing.timing.requestTime) * 1000);
435
+ }
436
+ networkBuffer.set(params.requestId, existing);
437
+ }
438
+ }
439
+ // Handle Network.loadingFailed
440
+ if (method === "Network.loadingFailed") {
441
+ const params = message.params;
442
+ const existing = networkBuffer.get(params.requestId);
443
+ if (existing) {
444
+ existing.completed = true;
445
+ existing.error = params.canceled ? "Canceled" : (params.errorText || "Request failed");
446
+ networkBuffer.set(params.requestId, existing);
447
+ }
448
+ }
449
+ // Handle Runtime context lifecycle events for health tracking
450
+ const appKey = findAppKeyForDevice(_device);
451
+ if (appKey) {
452
+ // Handle Runtime.executionContextCreated
453
+ if (method === "Runtime.executionContextCreated") {
454
+ const params = message.params;
455
+ markContextHealthy(appKey, params.context.id);
456
+ console.error(`[rn-ai-debugger] Context created: ${params.context.id}`);
457
+ }
458
+ // Handle Runtime.executionContextDestroyed
459
+ if (method === "Runtime.executionContextDestroyed") {
460
+ markContextStale(appKey);
461
+ console.error(`[rn-ai-debugger] Context destroyed`);
462
+ }
463
+ // Handle Runtime.executionContextsCleared
464
+ if (method === "Runtime.executionContextsCleared") {
465
+ markContextStale(appKey);
466
+ console.error(`[rn-ai-debugger] All contexts cleared`);
467
+ }
468
+ }
469
+ }
470
+ // Connect to a device via CDP WebSocket
471
+ export async function connectToDevice(device, port, options = {}) {
472
+ const { isReconnection = false, reconnectionConfig = DEFAULT_RECONNECTION_CONFIG } = options;
473
+ return new Promise((resolve, reject) => {
474
+ const appKey = `${port}-${device.id}`;
475
+ // Check if already connected with a valid WebSocket
476
+ const existingApp = connectedApps.get(appKey);
477
+ if (existingApp) {
478
+ if (existingApp.ws.readyState === WebSocket.OPEN) {
479
+ resolve(`Already connected to ${device.title}`);
480
+ return;
481
+ }
482
+ // WebSocket exists but not OPEN - clean up stale entry
483
+ console.error(`[rn-ai-debugger] Cleaning up stale connection for ${device.title} (state: ${getWebSocketStateName(existingApp.ws.readyState)})`);
484
+ connectedApps.delete(appKey);
485
+ }
486
+ // Prevent concurrent connection attempts to the same device
487
+ if (connectionLocks.has(appKey)) {
488
+ resolve(`Connection already in progress for ${device.title}`);
489
+ return;
490
+ }
491
+ connectionLocks.add(appKey);
492
+ // Cancel any pending reconnection timer for this appKey
493
+ cancelReconnectionTimer(appKey);
494
+ // Save connection metadata for potential reconnection
495
+ saveConnectionMetadata(appKey, {
496
+ port,
497
+ deviceInfo: device,
498
+ webSocketUrl: device.webSocketDebuggerUrl
499
+ });
500
+ try {
501
+ const ws = new WebSocket(device.webSocketDebuggerUrl);
502
+ ws.on("open", async () => {
503
+ // Release connection lock
504
+ connectionLocks.delete(appKey);
505
+ connectedApps.set(appKey, { ws, deviceInfo: device, port, platform: "android" });
506
+ // Initialize or update connection state
507
+ // Note: We do NOT reset reconnectionAttempts here - that happens
508
+ // only when connection has been stable for MIN_STABLE_CONNECTION_MS
509
+ if (isReconnection) {
510
+ closeConnectionGap(appKey);
511
+ updateConnectionState(appKey, {
512
+ status: "connected",
513
+ lastConnectedTime: new Date()
514
+ // reconnectionAttempts NOT reset here - see ws.on("close") for stable connection check
515
+ });
516
+ // Reset context health for reconnection
517
+ initContextHealth(appKey);
518
+ console.error(`[rn-ai-debugger] Reconnected to ${device.title}`);
519
+ }
520
+ else {
521
+ initConnectionState(appKey);
522
+ initContextHealth(appKey);
523
+ console.error(`[rn-ai-debugger] Connected to ${device.title}`);
524
+ }
525
+ // Enable Runtime domain to receive console messages
526
+ ws.send(JSON.stringify({
527
+ id: getNextMessageId(),
528
+ method: "Runtime.enable"
529
+ }));
530
+ // Also enable Log domain
531
+ ws.send(JSON.stringify({
532
+ id: getNextMessageId(),
533
+ method: "Log.enable"
534
+ }));
535
+ // Enable Network domain to track requests
536
+ ws.send(JSON.stringify({
537
+ id: getNextMessageId(),
538
+ method: "Network.enable"
539
+ }));
540
+ // Try to resolve iOS simulator UDID from device name
541
+ // This enables automatic device scoping for iOS tools
542
+ if (device.deviceName) {
543
+ const simulatorUdid = await findSimulatorByName(device.deviceName);
544
+ if (simulatorUdid) {
545
+ setActiveSimulatorUdid(simulatorUdid, appKey);
546
+ // Update platform to ios now that simulator is confirmed
547
+ const connectedApp = connectedApps.get(appKey);
548
+ if (connectedApp) {
549
+ connectedApp.platform = "ios";
550
+ }
551
+ console.error(`[rn-ai-debugger] Linked to iOS simulator: ${simulatorUdid}`);
552
+ }
553
+ }
554
+ resolve(`Connected to ${device.title} (${device.deviceName})`);
555
+ });
556
+ ws.on("message", (data) => {
557
+ try {
558
+ const message = JSON.parse(data.toString());
559
+ handleCDPMessage(message, device, ws);
560
+ }
561
+ catch {
562
+ // Ignore non-JSON messages
563
+ }
564
+ });
565
+ ws.on("close", () => {
566
+ // Release connection lock if still held
567
+ connectionLocks.delete(appKey);
568
+ connectedApps.delete(appKey);
569
+ // Clear active simulator UDID if this connection set it
570
+ clearActiveSimulatorIfSource(appKey);
571
+ // Check if connection was stable before resetting attempts
572
+ const state = getConnectionState(appKey);
573
+ let wasStable = false;
574
+ if (state?.lastConnectedTime) {
575
+ const connectionDuration = Date.now() - state.lastConnectedTime.getTime();
576
+ wasStable = connectionDuration >= MIN_STABLE_CONNECTION_MS;
577
+ if (wasStable) {
578
+ // Connection was stable - reset attempts for fresh start
579
+ updateConnectionState(appKey, { reconnectionAttempts: 0 });
580
+ console.error(`[rn-ai-debugger] Connection was stable for ${Math.round(connectionDuration / 1000)}s, resetting reconnection attempts`);
581
+ }
582
+ }
583
+ // Record the gap and trigger reconnection
584
+ recordConnectionGap(appKey, "Connection closed");
585
+ updateConnectionState(appKey, {
586
+ status: "disconnected",
587
+ lastDisconnectTime: new Date()
588
+ });
589
+ console.error(`[rn-ai-debugger] Disconnected from ${device.title}`);
590
+ // Schedule auto-reconnection if enabled (skip if intentionally disconnected)
591
+ if (reconnectionConfig.enabled && !reconnectionSuppressed.has(appKey)) {
592
+ scheduleReconnection(appKey, reconnectionConfig);
593
+ }
594
+ else if (reconnectionSuppressed.has(appKey)) {
595
+ reconnectionSuppressed.delete(appKey);
596
+ console.error(`[rn-ai-debugger] Reconnection suppressed for ${device.title} (intentional disconnect)`);
597
+ }
598
+ });
599
+ ws.on("error", (error) => {
600
+ // Release connection lock
601
+ connectionLocks.delete(appKey);
602
+ // Cancel any pending reconnection timer to prevent orphaned loops
603
+ cancelReconnectionTimer(appKey);
604
+ connectedApps.delete(appKey);
605
+ // Clear active simulator UDID if this connection set it
606
+ clearActiveSimulatorIfSource(appKey);
607
+ // Extract error message safely - some WebSocket errors may not have a message
608
+ const errorMsg = error?.message || error?.toString() || 'Unknown WebSocket error';
609
+ // Only reject if this is initial connection, not reconnection attempt
610
+ if (!isReconnection) {
611
+ reject(`Failed to connect to ${device.title}: ${errorMsg}`);
612
+ }
613
+ else {
614
+ console.error(`[rn-ai-debugger] Reconnection error: ${errorMsg}`);
615
+ }
616
+ });
617
+ // Timeout after 5 seconds
618
+ setTimeout(() => {
619
+ if (ws.readyState !== WebSocket.OPEN) {
620
+ // Release connection lock on timeout
621
+ connectionLocks.delete(appKey);
622
+ ws.terminate();
623
+ if (!isReconnection) {
624
+ reject(`Connection to ${device.title} timed out`);
625
+ }
626
+ }
627
+ }, 5000);
628
+ }
629
+ catch (error) {
630
+ // Release connection lock on exception
631
+ connectionLocks.delete(appKey);
632
+ if (!isReconnection) {
633
+ const errorMessage = error instanceof Error ? error.message : (error ? String(error) : "Unknown error");
634
+ reject(`Failed to create WebSocket connection: ${errorMessage}`);
635
+ }
636
+ }
637
+ });
638
+ }
639
+ /**
640
+ * Schedule a reconnection attempt with exponential backoff
641
+ */
642
+ function scheduleReconnection(appKey, config = DEFAULT_RECONNECTION_CONFIG) {
643
+ const state = getConnectionState(appKey);
644
+ if (!state)
645
+ return;
646
+ const attempts = state.reconnectionAttempts;
647
+ if (attempts >= config.maxAttempts) {
648
+ console.error(`[rn-ai-debugger] Max reconnection attempts (${config.maxAttempts}) reached for ${appKey}`);
649
+ updateConnectionState(appKey, { status: "disconnected" });
650
+ return;
651
+ }
652
+ const delay = calculateBackoffDelay(attempts, config);
653
+ console.error(`[rn-ai-debugger] Scheduling reconnection attempt ${attempts + 1}/${config.maxAttempts} in ${delay}ms`);
654
+ updateConnectionState(appKey, {
655
+ status: "reconnecting",
656
+ reconnectionAttempts: attempts + 1
657
+ });
658
+ const timer = setTimeout(() => {
659
+ attemptReconnection(appKey, config);
660
+ }, delay);
661
+ saveReconnectionTimer(appKey, timer);
662
+ }
663
+ /**
664
+ * Attempt to reconnect to a previously connected device
665
+ */
666
+ async function attemptReconnection(appKey, config = DEFAULT_RECONNECTION_CONFIG) {
667
+ const metadata = getConnectionMetadata(appKey);
668
+ if (!metadata) {
669
+ console.error(`[rn-ai-debugger] No metadata for reconnection: ${appKey}`);
670
+ return false;
671
+ }
672
+ try {
673
+ // Re-fetch devices to get fresh WebSocket URL (may have changed)
674
+ const devices = await fetchDevices(metadata.port);
675
+ // Try to find the same device first, otherwise select main device
676
+ const device = devices.find(d => d.id === metadata.deviceInfo.id)
677
+ || selectMainDevice(devices);
678
+ if (!device) {
679
+ console.error(`[rn-ai-debugger] Device no longer available for ${appKey}`);
680
+ // Schedule next attempt
681
+ scheduleReconnection(appKey, config);
682
+ return false;
683
+ }
684
+ await connectToDevice(device, metadata.port, { isReconnection: true, reconnectionConfig: config });
685
+ return true;
686
+ }
687
+ catch (error) {
688
+ console.error(`[rn-ai-debugger] Reconnection failed: ${error}`);
689
+ // Schedule next attempt
690
+ scheduleReconnection(appKey, config);
691
+ return false;
692
+ }
693
+ }
694
+ // Get list of connected apps
695
+ export function getConnectedApps() {
696
+ return Array.from(connectedApps.entries()).map(([key, app]) => ({
697
+ key,
698
+ app,
699
+ isConnected: app.ws.readyState === WebSocket.OPEN
700
+ }));
701
+ }
702
+ // Get first connected app with an OPEN WebSocket (or null if none)
703
+ export function getFirstConnectedApp() {
704
+ // Find first app with OPEN WebSocket, cleaning up stale entries
705
+ for (const [key, app] of connectedApps.entries()) {
706
+ if (app.ws.readyState === WebSocket.OPEN) {
707
+ return app;
708
+ }
709
+ // Clean up stale entry
710
+ console.error(`[rn-ai-debugger] Cleaning up stale connection in getFirstConnectedApp: ${key} (state: ${getWebSocketStateName(app.ws.readyState)})`);
711
+ connectedApps.delete(key);
712
+ }
713
+ return null;
714
+ }
715
+ // Check if any app is connected with an OPEN WebSocket
716
+ export function hasConnectedApp() {
717
+ for (const [, app] of connectedApps.entries()) {
718
+ if (app.ws.readyState === WebSocket.OPEN) {
719
+ return true;
720
+ }
721
+ }
722
+ return false;
723
+ }
724
+ /**
725
+ * Run a quick health check to verify the page context is responsive
726
+ * Returns true if the context can execute code, false otherwise
727
+ */
728
+ export async function runQuickHealthCheck(app) {
729
+ const HEALTH_CHECK_TIMEOUT = 2000;
730
+ const messageId = getNextMessageId();
731
+ return new Promise((resolve) => {
732
+ const timeoutId = setTimeout(() => {
733
+ pendingExecutions.delete(messageId);
734
+ resolve(false);
735
+ }, HEALTH_CHECK_TIMEOUT);
736
+ pendingExecutions.set(messageId, {
737
+ resolve: (result) => {
738
+ clearTimeout(timeoutId);
739
+ pendingExecutions.delete(messageId);
740
+ // Update context health tracking
741
+ const appKey = findAppKeyForDevice(app.deviceInfo);
742
+ if (appKey) {
743
+ updateContextHealth(appKey, {
744
+ lastHealthCheck: new Date(),
745
+ lastHealthCheckSuccess: result.success,
746
+ isStale: !result.success,
747
+ });
748
+ }
749
+ resolve(result.success);
750
+ },
751
+ timeoutId,
752
+ });
753
+ try {
754
+ app.ws.send(JSON.stringify({
755
+ id: messageId,
756
+ method: "Runtime.evaluate",
757
+ params: { expression: "1+1", returnByValue: true },
758
+ }));
759
+ }
760
+ catch {
761
+ clearTimeout(timeoutId);
762
+ pendingExecutions.delete(messageId);
763
+ resolve(false);
764
+ }
765
+ });
766
+ }
767
+ /**
768
+ * Find the first available Metro port
769
+ */
770
+ async function findFirstMetroPort() {
771
+ const ports = await scanMetroPorts();
772
+ return ports.length > 0 ? ports[0] : null;
773
+ }
774
+ /**
775
+ * Ensure a healthy connection to a React Native app
776
+ * This will verify or establish a connection, optionally running a health check
777
+ */
778
+ export async function ensureConnection(options = {}) {
779
+ const { port, healthCheck = true, forceRefresh = false } = options;
780
+ let app = getFirstConnectedApp();
781
+ let wasReconnected = false;
782
+ // Force refresh if requested - close existing connection
783
+ if (forceRefresh && app) {
784
+ const appKey = `${app.port}-${app.deviceInfo.id}`;
785
+ cancelReconnectionTimer(appKey);
786
+ try {
787
+ app.ws.close();
788
+ }
789
+ catch {
790
+ // Ignore close errors
791
+ }
792
+ connectedApps.delete(appKey);
793
+ app = null;
794
+ }
795
+ // Attempt connection if not connected
796
+ if (!app) {
797
+ const targetPort = port ?? await findFirstMetroPort();
798
+ if (!targetPort) {
799
+ return {
800
+ connected: false,
801
+ wasReconnected: false,
802
+ healthCheckPassed: false,
803
+ connectionInfo: null,
804
+ error: "No Metro server found. Make sure Metro bundler is running.",
805
+ };
806
+ }
807
+ const devices = await fetchDevices(targetPort);
808
+ const mainDevice = selectMainDevice(devices);
809
+ if (!mainDevice) {
810
+ return {
811
+ connected: false,
812
+ wasReconnected: false,
813
+ healthCheckPassed: false,
814
+ connectionInfo: null,
815
+ error: `No debuggable devices found on port ${targetPort}. Make sure the app is running.`,
816
+ };
817
+ }
818
+ try {
819
+ await connectToDevice(mainDevice, targetPort);
820
+ app = getFirstConnectedApp();
821
+ wasReconnected = true;
822
+ }
823
+ catch (error) {
824
+ // Ensure we always have a meaningful error message
825
+ let errorMessage;
826
+ if (error instanceof Error) {
827
+ errorMessage = error.message;
828
+ }
829
+ else if (error !== undefined && error !== null) {
830
+ errorMessage = String(error);
831
+ }
832
+ else {
833
+ errorMessage = "WebSocket connection failed with no error details";
834
+ }
835
+ return {
836
+ connected: false,
837
+ wasReconnected: false,
838
+ healthCheckPassed: false,
839
+ connectionInfo: null,
840
+ error: `Connection failed: ${errorMessage}`,
841
+ };
842
+ }
843
+ }
844
+ if (!app) {
845
+ return {
846
+ connected: false,
847
+ wasReconnected: false,
848
+ healthCheckPassed: false,
849
+ connectionInfo: null,
850
+ error: "Connection succeeded but app is not available",
851
+ };
852
+ }
853
+ // Run health check if requested
854
+ let healthCheckPassed = true;
855
+ if (healthCheck) {
856
+ healthCheckPassed = await runQuickHealthCheck(app);
857
+ // If health check failed and we haven't just reconnected, try reconnecting
858
+ if (!healthCheckPassed && !wasReconnected) {
859
+ console.error(`[rn-ai-debugger] Health check failed, attempting reconnection...`);
860
+ // Close and reconnect
861
+ const appKey = `${app.port}-${app.deviceInfo.id}`;
862
+ const targetPort = app.port;
863
+ cancelReconnectionTimer(appKey);
864
+ try {
865
+ app.ws.close();
866
+ }
867
+ catch {
868
+ // Ignore
869
+ }
870
+ connectedApps.delete(appKey);
871
+ // Re-fetch devices and reconnect
872
+ const devices = await fetchDevices(targetPort);
873
+ const mainDevice = selectMainDevice(devices);
874
+ if (mainDevice) {
875
+ try {
876
+ await connectToDevice(mainDevice, targetPort);
877
+ app = getFirstConnectedApp();
878
+ wasReconnected = true;
879
+ // Re-run health check after reconnection
880
+ if (app) {
881
+ healthCheckPassed = await runQuickHealthCheck(app);
882
+ }
883
+ }
884
+ catch {
885
+ // Failed to reconnect
886
+ healthCheckPassed = false;
887
+ }
888
+ }
889
+ }
890
+ }
891
+ // Build connection info
892
+ const appKey = app ? `${app.port}-${app.deviceInfo.id}` : null;
893
+ const connectionState = appKey ? getConnectionState(appKey) : null;
894
+ const contextHealth = appKey ? getContextHealth(appKey) : null;
895
+ let uptime = "unknown";
896
+ if (connectionState?.lastConnectedTime) {
897
+ const uptimeMs = Date.now() - connectionState.lastConnectedTime.getTime();
898
+ uptime = formatDuration(uptimeMs);
899
+ }
900
+ return {
901
+ connected: app !== null && app.ws.readyState === WebSocket.OPEN,
902
+ wasReconnected,
903
+ healthCheckPassed,
904
+ connectionInfo: app ? {
905
+ deviceTitle: app.deviceInfo.title,
906
+ port: app.port,
907
+ uptime,
908
+ contextId: contextHealth?.contextId ?? null,
909
+ } : null,
910
+ };
911
+ }
912
+ export function getPassiveConnectionStatus() {
913
+ if (!hasConnectedApp()) {
914
+ return { connected: false, needsPing: false, reason: "no_connection" };
915
+ }
916
+ const app = getFirstConnectedApp();
917
+ if (app) {
918
+ const appKey = `${app.port}-${app.deviceInfo.id}`;
919
+ const health = getContextHealth(appKey);
920
+ if (health?.isStale) {
921
+ return { connected: false, needsPing: false, reason: "context_stale" };
922
+ }
923
+ }
924
+ const lastMessage = getLastCDPMessageTime();
925
+ if (!lastMessage) {
926
+ return { connected: false, needsPing: false, reason: "no_activity" };
927
+ }
928
+ const elapsed = Date.now() - lastMessage.getTime();
929
+ if (elapsed > STALE_ACTIVITY_THRESHOLD_MS) {
930
+ return { connected: true, needsPing: true, reason: "activity_stale" };
931
+ }
932
+ return { connected: true, needsPing: false, reason: "ok" };
933
+ }
934
+ export async function checkAndEnsureConnection() {
935
+ const passive = getPassiveConnectionStatus();
936
+ if (passive.connected && !passive.needsPing) {
937
+ return { connected: true, wasReconnected: false, message: null };
938
+ }
939
+ if (passive.connected && passive.needsPing) {
940
+ const app = getFirstConnectedApp();
941
+ if (app) {
942
+ const healthy = await runQuickHealthCheck(app);
943
+ if (healthy) {
944
+ return { connected: true, wasReconnected: false, message: null };
945
+ }
946
+ }
947
+ }
948
+ const result = await ensureConnection({ forceRefresh: true, healthCheck: true });
949
+ if (result.connected && result.healthCheckPassed) {
950
+ await new Promise(resolve => setTimeout(resolve, RECONNECT_SETTLE_MS));
951
+ return {
952
+ connected: true,
953
+ wasReconnected: true,
954
+ message: "[CONNECTION] Was stale, re-established. Earlier data may be incomplete; new data will appear on next call.",
955
+ };
956
+ }
957
+ return {
958
+ connected: false,
959
+ wasReconnected: false,
960
+ message: "[CONNECTION] No active connection. Could not reconnect. Ensure Metro and the app are running, then call scan_metro.",
961
+ };
962
+ }
963
+ //# sourceMappingURL=connection.js.map