reactoradar 1.2.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 (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +366 -0
  3. package/app.js +2450 -0
  4. package/assets/icon.svg +54 -0
  5. package/bin/cli.js +79 -0
  6. package/bin/open-debugger.sh +9 -0
  7. package/bin/setup.js +473 -0
  8. package/index.html +82 -0
  9. package/main.js +528 -0
  10. package/package.json +76 -0
  11. package/preload.js +31 -0
  12. package/sdk/RNDebugSDK.js +540 -0
  13. package/src/main/main.js +396 -0
  14. package/src/main/preload.js +28 -0
  15. package/src/renderer/app.js +221 -0
  16. package/src/renderer/components/object-tree.js +245 -0
  17. package/src/renderer/index.html +111 -0
  18. package/src/renderer/panels/console.js +248 -0
  19. package/src/renderer/panels/memory.js +60 -0
  20. package/src/renderer/panels/network.js +559 -0
  21. package/src/renderer/panels/performance.js +144 -0
  22. package/src/renderer/panels/react.js +31 -0
  23. package/src/renderer/panels/redux.js +159 -0
  24. package/src/renderer/panels/settings.js +93 -0
  25. package/src/renderer/panels/sources.js +189 -0
  26. package/src/renderer/panels/storage.js +134 -0
  27. package/src/renderer/state.js +132 -0
  28. package/src/renderer/styles/components.css +145 -0
  29. package/src/renderer/styles/console.css +73 -0
  30. package/src/renderer/styles/main.css +229 -0
  31. package/src/renderer/styles/network.css +242 -0
  32. package/src/renderer/styles/performance.css +45 -0
  33. package/src/renderer/styles/redux.css +77 -0
  34. package/src/renderer/styles/settings.css +63 -0
  35. package/src/renderer/styles/sources.css +48 -0
  36. package/src/renderer/styles/storage.css +28 -0
  37. package/src/renderer/styles/theme-light.css +57 -0
  38. package/styles.css +1308 -0
package/preload.js ADDED
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ const { contextBridge, ipcRenderer } = require('electron');
4
+
5
+ const registeredChannels = new Set();
6
+
7
+ contextBridge.exposeInMainWorld('electronAPI', {
8
+ // Listen from main (idempotent — only one listener per channel)
9
+ on: (channel, cb) => {
10
+ const allowed = [
11
+ 'ports', 'cdp-targets', 'redux-event', 'storage-event', 'network-event',
12
+ 'console-event', 'perf-event', 'redux-connected', 'storage-connected', 'network-connected',
13
+ 'react-dt-status', 'trigger-open-cdp', 'clear-all-ui', 'theme-changed', 'update-available',
14
+ ];
15
+ if (allowed.includes(channel)) {
16
+ ipcRenderer.removeAllListeners(channel);
17
+ registeredChannels.add(channel);
18
+ ipcRenderer.on(channel, (_, ...args) => cb(...args));
19
+ }
20
+ },
21
+ // Send to main
22
+ openCDPTarget: (wsUrl) => ipcRenderer.send('open-cdp-target', wsUrl),
23
+ openReactDevTools: () => ipcRenderer.send('open-react-devtools'),
24
+ clearAll: () => ipcRenderer.send('clear-all'),
25
+ setTheme: (theme) => ipcRenderer.send('set-theme', theme),
26
+ setNetworkCapture: (enabled) => ipcRenderer.send('set-network-capture', enabled),
27
+ setStackTraceCapture: (enabled) => ipcRenderer.send('set-stack-trace-capture', enabled),
28
+ setNetworkThrottle: (profile) => ipcRenderer.send('set-network-throttle', profile),
29
+ readSourceFile: (filepath) => ipcRenderer.invoke('read-source-file', filepath),
30
+ openExternal: (url) => ipcRenderer.send('open-external', url),
31
+ });
@@ -0,0 +1,540 @@
1
+ /**
2
+ * RNDebugSDK.js
3
+ * Place in: src/debug/RNDebugSDK.js
4
+ *
5
+ * Usage in index.js (MUST be the very first import):
6
+ * if (__DEV__) require('./src/debug/RNDebugSDK');
7
+ *
8
+ * For Redux, use the exported enhancer:
9
+ * import { reduxEnhancer } from './src/debug/RNDebugSDK';
10
+ * const store = configureStore({ ..., enhancers: [reduxEnhancer] });
11
+ *
12
+ * For AsyncStorage monitoring, wrap your AsyncStorage calls:
13
+ * import { watchAsyncStorage } from './src/debug/RNDebugSDK';
14
+ * watchAsyncStorage(); // call once early in app
15
+ */
16
+
17
+ if (!__DEV__) {
18
+ module.exports = { reduxEnhancer: x => x, watchAsyncStorage: () => {} };
19
+ } else {
20
+
21
+ // ─── Config ───────────────────────────────────────────────────────────────────
22
+ // Android emulator → 10.0.2.2 | iOS sim → 127.0.0.1 | Device → your LAN IP
23
+ const HOST = '10.0.2.2';
24
+
25
+ const PORTS = {
26
+ NETWORK_AND_CONSOLE: 9092, // unified feed for network + console
27
+ REDUX: 9090, // Redux state + actions
28
+ STORAGE: 9091, // AsyncStorage snapshots
29
+ };
30
+
31
+ // ─── Feature Flags (can be toggled by debugger app) ──────────────────────────
32
+ let _networkCaptureEnabled = true;
33
+ let _stackTraceEnabled = false; // Disabled by default for performance
34
+ let _throttleProfile = 'none'; // 'none', 'fast3g', 'slow3g', 'offline'
35
+ const THROTTLE_DELAYS = { none: 0, fast3g: 500, slow3g: 2000, offline: -1 };
36
+
37
+ // ─── WebSocket Factory ────────────────────────────────────────────────────────
38
+ function makeChannel(port, name, onMessage) {
39
+ let ws = null, queue = [], connected = false;
40
+
41
+ function connect() {
42
+ try {
43
+ ws = new WebSocket(`ws://${HOST}:${port}`);
44
+ ws.onopen = () => {
45
+ connected = true;
46
+ queue.forEach(m => ws.send(m));
47
+ queue = [];
48
+ };
49
+ ws.onmessage = (evt) => {
50
+ if (onMessage) {
51
+ try { onMessage(JSON.parse(evt.data)); } catch {}
52
+ }
53
+ };
54
+ ws.onclose = () => { connected = false; setTimeout(connect, 2000); };
55
+ ws.onerror = () => {};
56
+ } catch { setTimeout(connect, 2000); }
57
+ }
58
+
59
+ function send(obj) {
60
+ const msg = JSON.stringify({ ...obj, ts: Date.now() });
61
+ if (connected && ws?.readyState === WebSocket.OPEN) ws.send(msg);
62
+ else { queue.push(msg); if (queue.length > 300) queue.shift(); }
63
+ }
64
+
65
+ connect();
66
+ return { send };
67
+ }
68
+
69
+ // The main channel (console + network) listens for control messages from the debugger
70
+ const mainCh = makeChannel(PORTS.NETWORK_AND_CONSOLE, 'main', (msg) => {
71
+ if (msg.type === 'control') {
72
+ if (msg.action === 'set-network-capture') _networkCaptureEnabled = !!msg.enabled;
73
+ if (msg.action === 'set-throttle') _throttleProfile = msg.profile || 'none';
74
+ if (msg.action === 'set-stack-trace') _stackTraceEnabled = !!msg.enabled;
75
+ }
76
+ });
77
+ const reduxCh = makeChannel(PORTS.REDUX, 'redux');
78
+ const storageCh = makeChannel(PORTS.STORAGE, 'storage');
79
+
80
+ // ─── Console Intercept ────────────────────────────────────────────────────────
81
+ function serializeArg(a) {
82
+ if (a === null) return { t: 'null', v: null };
83
+ if (a === undefined) return { t: 'undefined', v: undefined };
84
+ if (typeof a === 'string') return { t: 'string', v: a };
85
+ if (typeof a === 'number') return { t: 'number', v: a };
86
+ if (typeof a === 'boolean') return { t: 'boolean', v: a };
87
+ if (typeof a === 'symbol') return { t: 'string', v: a.toString() };
88
+ if (typeof a === 'function') return { t: 'string', v: `[Function: ${a.name || 'anonymous'}]` };
89
+ if (a instanceof Error) return { t: 'object', v: { name: a.name, message: a.message, stack: a.stack } };
90
+ if (Array.isArray(a)) {
91
+ try { const j = JSON.parse(JSON.stringify(a)); return { t: 'array', v: j }; }
92
+ catch { return { t: 'string', v: String(a) }; }
93
+ }
94
+ if (typeof a === 'object') {
95
+ try { const j = JSON.parse(JSON.stringify(a)); return { t: 'object', v: j }; }
96
+ catch { return { t: 'string', v: String(a) }; }
97
+ }
98
+ return { t: 'string', v: String(a) };
99
+ }
100
+
101
+ const LEVELS = ['log','info','warn','error','debug'];
102
+ const _console = {};
103
+
104
+ // Pre-compiled regexes for stack parsing (avoid creating per call)
105
+ const _skipRe = /RNDebugSDK|apply \(native\)|call \(native\)|anonymous \(native\)|MessageQueue|__callFunction|__guard|callFunctionReturn|processTicksAndRejections/;
106
+ const _frameRe = /at\s+(.+?)(?:\s+\((.+?):(\d+):\d+\)|(?:\s+)?(.+?):(\d+):\d+)/;
107
+
108
+ function _extractCaller() {
109
+ const stack = (new Error().stack || '').split('\n');
110
+ for (let i = 2; i < Math.min(stack.length, 15); i++) {
111
+ const frame = stack[i]?.trim() || '';
112
+ if (!frame || _skipRe.test(frame)) continue;
113
+ const m = frame.match(_frameRe);
114
+ if (!m) continue;
115
+ const fn = m[1] || '', src = m[2] || m[4] || '', ln = m[3] || m[5] || '';
116
+ // Skip console internals and single-char minified names from Hermes
117
+ if (/^console|^_console|^overrideMethod|^reactConsoleError|^anonymous$/.test(fn)) continue;
118
+ if (fn.length <= 2) continue; // Skip minified single/double-char names like "a", "b", "Oa"
119
+ // Real source file
120
+ if (src && !src.includes('index.bundle') && /\.[jt]sx?$/.test(src)) {
121
+ return `${src.split('/').pop()}:${ln}` + (fn.length > 2 ? ` (${fn})` : '');
122
+ }
123
+ // Named function from bundle — must be meaningful (3+ chars, starts with uppercase = component)
124
+ if (fn.length >= 3 && fn !== 'Object' && fn !== 'Function') return fn;
125
+ }
126
+ return '';
127
+ }
128
+
129
+ LEVELS.forEach(level => {
130
+ _console[level] = console[level].bind(console);
131
+ console[level] = (...args) => {
132
+ _console[level](...args);
133
+ const structuredArgs = args.map(serializeArg);
134
+ const message = args.map(a => {
135
+ if (typeof a === 'string') return a;
136
+ try { return JSON.stringify(a, null, 2); } catch { return String(a); }
137
+ }).join(' ');
138
+ // Stack trace capture controlled by toggle (disabled by default for performance)
139
+ // When enabled: captures for all levels. When disabled: skips entirely.
140
+ const caller = _stackTraceEnabled ? _extractCaller() : '';
141
+ mainCh.send({ type: 'console', level, message, args: structuredArgs, caller });
142
+ };
143
+ });
144
+
145
+ // ─── Header Flattener (ensures all values are strings) ───────────────────────
146
+ function _flattenHeaders(h) {
147
+ if (!h) return {};
148
+ const flat = {};
149
+ try {
150
+ // Handle Headers object (has forEach)
151
+ if (typeof h.forEach === 'function') {
152
+ h.forEach((v, k) => { flat[k] = String(v); });
153
+ return flat;
154
+ }
155
+ // Handle plain object — stringify nested objects
156
+ if (typeof h === 'object') {
157
+ Object.entries(h).forEach(([k, v]) => {
158
+ if (v == null) return;
159
+ flat[k] = (typeof v === 'object') ? JSON.stringify(v) : String(v);
160
+ });
161
+ return flat;
162
+ }
163
+ } catch {}
164
+ return flat;
165
+ }
166
+
167
+ // ─── Fetch Intercept ─────────────────────────────────────────────────────────
168
+ const _fetch = global.fetch;
169
+ global.fetch = async (input, init = {}) => {
170
+ // Throttle: simulate slow network or offline
171
+ const delay = THROTTLE_DELAYS[_throttleProfile] || 0;
172
+ if (delay === -1) return Promise.reject(new TypeError('Network request failed (offline throttle)'));
173
+ if (delay > 0) await new Promise(r => setTimeout(r, delay));
174
+
175
+ if (!_networkCaptureEnabled) return _fetch(input, init);
176
+
177
+ const url = typeof input === 'string' ? input : input?.url || '';
178
+ const method = (init.method || 'GET').toUpperCase();
179
+ const id = `f-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
180
+
181
+ mainCh.send({ type: 'network', phase: 'request', id, url, method,
182
+ requestHeaders: _flattenHeaders(init.headers), requestBody: init.body || null });
183
+
184
+ const t0 = Date.now();
185
+ try {
186
+ const resp = await _fetch(input, init);
187
+ const clone = resp.clone();
188
+ clone.text().then(body => {
189
+ if (!_networkCaptureEnabled) return;
190
+ let parsed = body;
191
+ try { parsed = JSON.parse(body); } catch {}
192
+ const rHeaders = {};
193
+ clone.headers?.forEach?.((v, k) => { rHeaders[k] = v; });
194
+ mainCh.send({ type: 'network', phase: 'response', id, url, method,
195
+ status: resp.status, statusText: resp.statusText,
196
+ duration: Date.now() - t0, responseHeaders: rHeaders, responseBody: parsed });
197
+ }).catch(() => {});
198
+ return resp;
199
+ } catch (err) {
200
+ mainCh.send({ type: 'network', phase: 'error', id, url, method,
201
+ duration: Date.now() - t0, error: err?.message || String(err) });
202
+ throw err;
203
+ }
204
+ };
205
+
206
+ // ─── Network Intercept via XHR readystatechange (RN 0.81 compatible) ─────────
207
+ // RN 0.81 + Reactotron both fight over XMLHttpRequest.prototype. Instead of
208
+ // patching prototype methods (which get overwritten), we use a non-invasive
209
+ // approach: wrap XMLHttpRequest constructor to add a readystatechange listener
210
+ // on every NEW instance. This works regardless of who patches the prototype.
211
+ (function setupXHRNetworkCapture() {
212
+ const _xhrTracker = new WeakMap();
213
+
214
+ function wrapXHR() {
215
+ const OrigXHR = global.XMLHttpRequest;
216
+ if (!OrigXHR || OrigXHR.__dbgWrapped) return;
217
+
218
+ function WrappedXHR() {
219
+ const xhr = new OrigXHR();
220
+ const meta = { id: `x-${Date.now()}-${Math.random().toString(36).slice(2,6)}`, method: 'GET', url: '', t0: 0, headers: {}, sent: false };
221
+ _xhrTracker.set(xhr, meta);
222
+
223
+ // Wrap open
224
+ const _open = xhr.open.bind(xhr);
225
+ xhr.open = function(method, url) {
226
+ meta.method = (method || 'GET').toUpperCase();
227
+ meta.url = String(url);
228
+ meta.t0 = Date.now();
229
+ return _open.apply(xhr, arguments);
230
+ };
231
+
232
+ // Wrap setRequestHeader
233
+ const _setHeader = xhr.setRequestHeader.bind(xhr);
234
+ xhr.setRequestHeader = function(key, value) {
235
+ meta.headers[key] = value;
236
+ return _setHeader.apply(xhr, arguments);
237
+ };
238
+
239
+ // Wrap send
240
+ const _send = xhr.send.bind(xhr);
241
+ xhr.send = function(body) {
242
+ if (_networkCaptureEnabled && !meta.sent) {
243
+ meta.sent = true;
244
+ let reqBody = null;
245
+ if (body != null) {
246
+ try { reqBody = typeof body === 'string' ? body : JSON.parse(JSON.stringify(body)); } catch { reqBody = String(body); }
247
+ }
248
+ mainCh.send({ type: 'network', phase: 'request', id: meta.id, url: meta.url,
249
+ method: meta.method, requestHeaders: meta.headers, requestBody: reqBody });
250
+ }
251
+ return _send.apply(xhr, arguments);
252
+ };
253
+
254
+ // Listen for completion
255
+ xhr.addEventListener('readystatechange', function() {
256
+ if (xhr.readyState !== 4 || !meta.sent || !_networkCaptureEnabled) return;
257
+ try {
258
+ const duration = Date.now() - meta.t0;
259
+ if (xhr.status > 0) {
260
+ // Safely read response body — responseText throws if responseType is blob/arraybuffer
261
+ let respBody = null;
262
+ const rType = xhr.responseType || '';
263
+ if (rType === '' || rType === 'text') {
264
+ try { respBody = xhr.responseText || ''; } catch { respBody = ''; }
265
+ try { respBody = JSON.parse(respBody); } catch {}
266
+ } else if (rType === 'json') {
267
+ respBody = xhr.response;
268
+ } else {
269
+ // blob, arraybuffer, document — can't serialize, show type info
270
+ respBody = `[${rType} response — ${xhr.response?.size || xhr.response?.byteLength || '?'} bytes]`;
271
+ }
272
+ const respHeaders = {};
273
+ try {
274
+ const raw = xhr.getAllResponseHeaders() || '';
275
+ raw.split('\r\n').forEach(line => {
276
+ const idx = line.indexOf(':');
277
+ if (idx > 0) respHeaders[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
278
+ });
279
+ } catch {}
280
+ mainCh.send({ type: 'network', phase: 'response', id: meta.id, url: meta.url,
281
+ method: meta.method, status: xhr.status, statusText: xhr.statusText,
282
+ duration, responseHeaders: respHeaders, responseBody: respBody });
283
+ } else {
284
+ mainCh.send({ type: 'network', phase: 'error', id: meta.id, url: meta.url,
285
+ method: meta.method, duration: Date.now() - meta.t0, error: 'Request failed (status 0)' });
286
+ }
287
+ } catch (e) {
288
+ // Safety net — never let our interceptor crash the app
289
+ mainCh.send({ type: 'network', phase: 'response', id: meta.id, url: meta.url,
290
+ method: meta.method, status: xhr.status || 0, duration: Date.now() - meta.t0,
291
+ responseBody: `[Error reading response: ${e.message}]` });
292
+ }
293
+ });
294
+
295
+ return xhr;
296
+ }
297
+
298
+ // Copy static properties and prototype
299
+ WrappedXHR.prototype = OrigXHR.prototype;
300
+ WrappedXHR.UNSENT = 0;
301
+ WrappedXHR.OPENED = 1;
302
+ WrappedXHR.HEADERS_RECEIVED = 2;
303
+ WrappedXHR.LOADING = 3;
304
+ WrappedXHR.DONE = 4;
305
+ WrappedXHR.__dbgWrapped = true;
306
+ // Keep reference to original for Reactotron etc
307
+ WrappedXHR.__original = OrigXHR;
308
+
309
+ global.XMLHttpRequest = WrappedXHR;
310
+ _console.log('[RNDebugSDK] XHR constructor wrapped for network capture');
311
+ }
312
+
313
+ // Wrap immediately if available
314
+ if (global.XMLHttpRequest) wrapXHR();
315
+
316
+ // Also wrap after RN polyfills set up (they replace global.XMLHttpRequest)
317
+ [0, 50, 200, 500].forEach(delay => {
318
+ setTimeout(() => {
319
+ if (global.XMLHttpRequest && !global.XMLHttpRequest.__dbgWrapped) {
320
+ wrapXHR();
321
+ }
322
+ }, delay);
323
+ });
324
+ })();
325
+
326
+ // ─── Axios Interceptor (belt-and-suspenders with XHR patch) ──────────────────
327
+ // Patches axios.create after a tick so import hoisting has resolved.
328
+ setTimeout(() => {
329
+ try {
330
+ const axios = require('axios');
331
+ if (!axios || axios.__dbgPatched) return;
332
+ axios.__dbgPatched = true;
333
+
334
+ function addDbgInterceptors(instance) {
335
+ if (!instance || !instance.interceptors || instance.__dbgInt) return;
336
+ instance.__dbgInt = true;
337
+ instance.interceptors.request.use(config => {
338
+ if (!_networkCaptureEnabled) return config;
339
+ const id = `ax-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
340
+ config._dbgId = id;
341
+ config._dbgT0 = Date.now();
342
+ const url = config.baseURL
343
+ ? config.baseURL.replace(/\/+$/, '') + '/' + (config.url || '').replace(/^\/+/, '')
344
+ : (config.url || '');
345
+ const h = _flattenHeaders(typeof config.headers?.toJSON === 'function' ? config.headers.toJSON() : config.headers);
346
+ let body = null;
347
+ if (config.data != null) { try { body = typeof config.data === 'string' ? config.data : JSON.parse(JSON.stringify(config.data)); } catch { body = String(config.data); } }
348
+ mainCh.send({ type:'network', phase:'request', id, url, method:(config.method||'GET').toUpperCase(), requestHeaders:h, requestBody:body });
349
+ return config;
350
+ }, e => Promise.reject(e));
351
+ instance.interceptors.response.use(resp => {
352
+ const c = resp.config || {};
353
+ if (!c._dbgId) return resp;
354
+ const url = c.baseURL ? c.baseURL.replace(/\/+$/,'') + '/' + (c.url||'').replace(/^\/+/,'') : (c.url||'');
355
+ const dur = c._dbgT0 ? Date.now() - c._dbgT0 : 0;
356
+ const rh = {};
357
+ try { const h = typeof resp.headers?.toJSON === 'function' ? resp.headers.toJSON() : resp.headers;
358
+ if (h) Object.entries(h).forEach(([k,v]) => { if (typeof v === 'string') rh[k] = v; }); } catch {}
359
+ let body = resp.data;
360
+ if (body && typeof body === 'object') { try { body = JSON.parse(JSON.stringify(body)); } catch {} }
361
+ mainCh.send({ type:'network', phase:'response', id:c._dbgId, url, method:(c.method||'GET').toUpperCase(),
362
+ status:resp.status, statusText:resp.statusText, duration:dur, responseHeaders:rh, responseBody:body });
363
+ return resp;
364
+ }, err => {
365
+ const c = err?.config || {};
366
+ if (c._dbgId) {
367
+ const url = c.baseURL ? c.baseURL.replace(/\/+$/,'') + '/' + (c.url||'').replace(/^\/+/,'') : (c.url||'');
368
+ const dur = c._dbgT0 ? Date.now() - c._dbgT0 : 0;
369
+ const r = err?.response;
370
+ if (r) { let b = r.data; if (b && typeof b === 'object') { try { b = JSON.parse(JSON.stringify(b)); } catch {} }
371
+ mainCh.send({ type:'network', phase:'response', id:c._dbgId, url, method:(c.method||'GET').toUpperCase(), status:r.status, statusText:r.statusText, duration:dur, responseBody:b });
372
+ } else { mainCh.send({ type:'network', phase:'error', id:c._dbgId, url, method:(c.method||'GET').toUpperCase(), duration:dur, error:err?.message||String(err) }); }
373
+ }
374
+ return Promise.reject(err);
375
+ });
376
+ }
377
+
378
+ addDbgInterceptors(axios);
379
+ const _create = axios.create.bind(axios);
380
+ axios.create = function(...args) {
381
+ const inst = _create(...args);
382
+ addDbgInterceptors(inst);
383
+ return inst;
384
+ };
385
+ _console.log('[RNDebugSDK] Axios interceptor active (global + create)');
386
+ } catch {}
387
+ }, 0);
388
+
389
+ // ─── Redux Enhancer ──────────────────────────────────────────────────────────
390
+ function reduxEnhancer(createStore) {
391
+ return (reducer, preloadedState, enhancer) => {
392
+ const store = createStore(reducer, preloadedState, enhancer);
393
+ let actionCount = 0;
394
+
395
+ // Send initial state
396
+ reduxCh.send({ type: 'redux', action: { type: '@@INIT' }, nextState: store.getState(), index: actionCount++ });
397
+
398
+ const origDispatch = store.dispatch;
399
+ store.dispatch = (action) => {
400
+ const result = origDispatch(action);
401
+ const nextState = store.getState();
402
+ reduxCh.send({ type: 'redux', action, nextState, index: actionCount++ });
403
+ return result;
404
+ };
405
+ return store;
406
+ };
407
+ }
408
+
409
+ // ─── Redux Toolkit middleware (alternative) ───────────────────────────────────
410
+ // If you use RTK configureStore, add this to middleware array instead:
411
+ const reduxMiddleware = store => next => action => {
412
+ const result = next(action);
413
+ reduxCh.send({ type: 'redux', action, nextState: store.getState() });
414
+ return result;
415
+ };
416
+
417
+ // ─── AsyncStorage Monitor ─────────────────────────────────────────────────────
418
+ let _asyncStoragePatched = false;
419
+ function watchAsyncStorage() {
420
+ if (_asyncStoragePatched) return; // Only patch once
421
+ _asyncStoragePatched = true;
422
+ try {
423
+ const RNAsyncStorage = require('@react-native-async-storage/async-storage').default;
424
+ if (!RNAsyncStorage) return;
425
+
426
+ // Send full snapshot once on first connect
427
+ RNAsyncStorage.getAllKeys().then(keys => {
428
+ if (!keys?.length) return;
429
+ RNAsyncStorage.multiGet(keys).then(pairs => {
430
+ const snapshot = Object.fromEntries(pairs);
431
+ storageCh.send({ type: 'storage', action: 'snapshot', key: snapshot });
432
+ }).catch(() => {});
433
+ }).catch(() => {});
434
+
435
+ // Patch individual methods
436
+ const _setItem = RNAsyncStorage.setItem.bind(RNAsyncStorage);
437
+ RNAsyncStorage.setItem = async (key, value, ...rest) => {
438
+ const result = await _setItem(key, value, ...rest);
439
+ storageCh.send({ type: 'storage', action: 'set', key, value });
440
+ return result;
441
+ };
442
+
443
+ const _removeItem = RNAsyncStorage.removeItem.bind(RNAsyncStorage);
444
+ RNAsyncStorage.removeItem = async (key, ...rest) => {
445
+ const result = await _removeItem(key, ...rest);
446
+ storageCh.send({ type: 'storage', action: 'remove', key });
447
+ return result;
448
+ };
449
+
450
+ const _mergeItem = RNAsyncStorage.mergeItem.bind(RNAsyncStorage);
451
+ RNAsyncStorage.mergeItem = async (key, value, ...rest) => {
452
+ const result = await _mergeItem(key, value, ...rest);
453
+ // Read back merged value
454
+ RNAsyncStorage.getItem(key).then(v => storageCh.send({ type: 'storage', action: 'set', key, value: v }));
455
+ return result;
456
+ };
457
+
458
+ const _clear = RNAsyncStorage.clear.bind(RNAsyncStorage);
459
+ RNAsyncStorage.clear = async (...rest) => {
460
+ const result = await _clear(...rest);
461
+ storageCh.send({ type: 'storage', action: 'snapshot', key: {} });
462
+ return result;
463
+ };
464
+
465
+ console.log('[RNDebugSDK] AsyncStorage monitoring active');
466
+ } catch (e) {
467
+ console.warn('[RNDebugSDK] AsyncStorage not available:', e.message);
468
+ }
469
+ }
470
+
471
+ // ─── Fix: Guard against "Debug JS Remotely" crash on Hermes/New Arch ─────────
472
+ // RN 0.74+ with Hermes removed DevSettings.setIsDebuggingRemotely.
473
+ // Some packages (react-native-devsettings, etc.) still call it and crash.
474
+ // We patch it as a no-op to prevent the crash.
475
+ try {
476
+ const { NativeModules } = require('react-native');
477
+ const DevSettings = NativeModules?.DevSettings;
478
+ if (DevSettings && typeof DevSettings.setIsDebuggingRemotely !== 'function') {
479
+ DevSettings.setIsDebuggingRemotely = () => {
480
+ _console.warn('[RNDebugSDK] "Debug JS Remotely" is not available on Hermes. Use "Open DevTools" instead — it will open in the ReactoRadar app.');
481
+ };
482
+ }
483
+ } catch {}
484
+
485
+ // ─── Performance + Memory Metrics ────────────────────────────────────────────
486
+ // Sends FPS, JS thread time, and memory stats every 2 seconds
487
+ (function startPerfMetrics() {
488
+ let frameCount = 0;
489
+ let lastTime = Date.now();
490
+
491
+ // FPS counter using requestAnimationFrame
492
+ function countFrame() {
493
+ frameCount++;
494
+ if (typeof requestAnimationFrame === 'function') {
495
+ requestAnimationFrame(countFrame);
496
+ }
497
+ }
498
+ if (typeof requestAnimationFrame === 'function') {
499
+ requestAnimationFrame(countFrame);
500
+ }
501
+
502
+ setInterval(() => {
503
+ const now = Date.now();
504
+ const elapsed = (now - lastTime) / 1000;
505
+ const fps = elapsed > 0 ? Math.round(frameCount / elapsed) : 0;
506
+ frameCount = 0;
507
+ lastTime = now;
508
+
509
+ const perfData = { type: 'perf', fps };
510
+
511
+ // Hermes memory stats
512
+ try {
513
+ if (global.HermesInternal && typeof global.HermesInternal.getRuntimeProperties === 'function') {
514
+ const props = global.HermesInternal.getRuntimeProperties();
515
+ perfData.heapUsed = props['js_heapSize'] || 0;
516
+ perfData.heapTotal = props['js_totalHeapSize'] || 0;
517
+ perfData.native = props['js_nativeHeapSize'] || 0;
518
+ }
519
+ } catch {}
520
+
521
+ // Try Performance API for thread timing
522
+ try {
523
+ if (global.performance && typeof global.performance.now === 'function') {
524
+ perfData.jsThread = global.performance.now() % 16.67; // approximate frame time
525
+ }
526
+ } catch {}
527
+
528
+ mainCh.send(perfData);
529
+ }, 2000);
530
+ })();
531
+
532
+ // Note: "Open DevTools" in the simulator dev menu opens Chrome/Metro's built-in debugger.
533
+ // To debug JS in the ReactoRadar app instead, press Cmd+D in the debugger app
534
+ // or click the "JS Debugger" button. Both can coexist — they connect to the
535
+ // same Hermes CDP target independently.
536
+
537
+ console.log(`[RNDebugSDK] Connected to ${HOST} | Console+Network:${PORTS.NETWORK_AND_CONSOLE} Redux:${PORTS.REDUX} Storage:${PORTS.STORAGE}`);
538
+
539
+ module.exports = { reduxEnhancer, reduxMiddleware, watchAsyncStorage };
540
+ }