+
🔧 WebSocket Debugger
+
Snapshots in store: {snapshots.length}
+
+ Send Test Snapshot
+
+
dispatch(wsConnect())} style={{ padding: 8, margin: 4 }}>
+ Reconnect WebSocket
+
+
+ {JSON.stringify(snapshots, null, 2)}
+
+
+ );
+};
+
+export default WebSocketDebugger;
\ No newline at end of file
diff --git a/client/src/containers/MainContainer.tsx b/client/src/containers/MainContainer.tsx
index 199c4ee..5830f38 100644
--- a/client/src/containers/MainContainer.tsx
+++ b/client/src/containers/MainContainer.tsx
@@ -1,16 +1,221 @@
+// src/containers/MainContainer.tsx
import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import type { RootState } from '../store/store';
+import { wsSend } from '../transport/socket';
import SnapshotView from '../components/SnapshotView';
-import TimelineSlider from '../components/TimelineSlider'
+import TimelineSlider from '../components/TimelineSlider';
+import MetricsPanel from '../components/MetricsPanel';
+//import SimpleHeader from '../components/SimpleHeader'; this needs styling and .svg
-const MainContainer = (): React.JSX.Element => {
+// Inline debug component (can move to separate file later)
+const ConnectionDebugger: React.FC = () => {
+ const dispatch = useDispatch();
+ const { snapshots, currentIndex } = useSelector((s: RootState) => s.snapshot);
+ const { commits, lags, firstRenders } = useSelector((s: RootState) => s.metric);
+
+ const sendTestSnapshot = () => {
+ const testSnapshot = {
+ channel: 'snapshot',
+ type: 'add',
+ payload: {
+ id: `manual-test-${Date.now()}`,
+ timestamp: Date.now(),
+ component: 'TestComponent',
+ props: { name: 'Manual Test' },
+ state: { count: Math.floor(Math.random() * 100) }
+ }
+ };
+
+ console.log('📤 Sending test snapshot:', testSnapshot);
+ dispatch(wsSend(testSnapshot));
+ };
+
+ const sendTestCommitMetric = () => {
+ const testMetric = {
+ channel: 'metrics',
+ type: 'commit',
+ payload: {
+ ts: Date.now(),
+ durationMs: Math.random() * 50 + 10,
+ fibersUpdated: Math.floor(Math.random() * 10) + 1,
+ appId: 'debug-test'
+ }
+ };
+
+ console.log('📊 Sending test metric:', testMetric);
+ dispatch(wsSend(testMetric));
+ };
+
+//added next two to generate different types of metrics for testing
+const sendTestLagMetric = () => {
+ const testMetric = {
+ channel: 'metrics',
+ type: 'lag', // Different type
+ payload: {
+ ts: Date.now(),
+ lagMs: Math.random() * 100 + 5,
+ appId: 'debug-test'
+ }
+ };
+ dispatch(wsSend(testMetric));
+};
+
+const sendTestFirstRenderMetric = () => {
+ const testMetric = {
+ channel: 'metrics',
+ type: 'firstRender', // Different type
+ payload: {
+ ts: Date.now(),
+ firstRenderMs: Math.random() * 2000 + 500,
+ appId: 'debug-test'
+ }
+ };
+ dispatch(wsSend(testMetric));
+};
return (
-
-
Reactime Native DevTool (MVP)
-
-
+
+
🔧 Debug Panel
+
+
+ 📦 Snapshots: {snapshots.length}
+ 📍 Current: {currentIndex}
+ 📊 Commits: {commits.length}
+ 📈 Lags: {lags.length}
+ 🎯 First Renders: {firstRenders.length}
+
+
+
+ 📤 Send Test Snapshot
+
+
+
+ 📊 Send Test Commit Metric
+
+
+
+ ⏱️ Send Test Lag Metric
+
+
+
+ 🚀 Send Test First Render Metric
+
);
};
+const MainContainer: React.FC = () => {
+ // Read once from Redux
+ const { snapshots, currentIndex } = useSelector((s: RootState) => s.snapshot);
+
+ // Derive current snapshot (guard against empty arrays)
+ const current = snapshots[currentIndex] ?? null;
+
+ console.log('📦 snapshots:', snapshots);
+ console.log('📍 currentIndex:', currentIndex);
+ console.log('📸 current:', current);
+
+ return (
+ <>
+ {/* Debug panel - remove this later when everything works */}
+
+
+ {/* Main layout - this is your original working layout */}
+
+
+
+
+
+ {/* Snapshot View This is a duplicate */}
+
+ {/* Pass plain props to presentational component */}
+
+
+
+
+ {/* Performance Metrics here is a duplicate */}
+
+
+
+
+ >
+ );
+};
+
export default MainContainer;
+
diff --git a/client/src/hooks/useSnapshotRecorder.ts b/client/src/hooks/useSnapshotRecorder.ts
new file mode 100644
index 0000000..92e3fbf
--- /dev/null
+++ b/client/src/hooks/useSnapshotRecorder.ts
@@ -0,0 +1,25 @@
+import { useEffect } from 'react';
+import { useDispatch } from 'react-redux';
+//import { addSnapshot } from '../slices/snapshotSlice';
+//import deepClone from '../utils/deepClone'; // optional, if needed
+
+// Jam disabled below.
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const useSnapshotRecorder = (fiberTree: any): void => { // could type more strictly if desired
+ const dispatch = useDispatch();
+
+console.log('Recording snapshot:', fiberTree);
+
+ useEffect(() => {
+ if (fiberTree) {
+ // Pseudocode:
+ // Whenever the fiberTree changes:
+ // - deep clone it if necessary
+ // - dispatch the new snapshot to Redux
+
+ //dispatch(addSnapshot(deepClone(fiberTree)));
+ }
+ }, [fiberTree, dispatch]);
+};
+
+export default useSnapshotRecorder;
diff --git a/client/src/slices/metricSlice.ts b/client/src/slices/metricSlice.ts
new file mode 100644
index 0000000..1c96e59
--- /dev/null
+++ b/client/src/slices/metricSlice.ts
@@ -0,0 +1,58 @@
+import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
+
+export type CommitMetric = {
+ ts: number; //timestamp in ms from RN device
+ durationMs: number; //duration in ms of commit
+ fibersUpdated?: number; //optinal number of fibers updated during commit
+ appId?: string; //optional appId if multiple RN apps are being profiled
+};
+
+export type LagMetric = {
+ ts: number; //timestamp in ms from RN device
+ lagMs: number; //lag in ms aka event loop stall measured on RN device - will's Q: how would RN app provide duration and lag in ms?
+ appId?: string; //optional appId if multiple RN apps are being profiled
+};
+
+export type FirstRenderMetric = {
+ ts: number,
+ firstRenderMs: number; //time in ms for first screen render
+ appId?: string; //optional appId if multiple RN apps are being profiled
+}
+
+interface MetricState { //shape of state for metrics in slice
+ commits: CommitMetric[];
+ lags: LagMetric[];
+ firstRenders: FirstRenderMetric[]; //
+
+}
+
+const initialState: MetricState = { //initial state for this slice
+ commits: [],
+ lags: [],
+ firstRenders: [],
+};
+
+const metricSlice = createSlice({ //create the reducer slice
+ name: 'metric', //name of slice
+ initialState, //initial state
+ reducers: { //pure functions to update state
+ pushCommitMetric: (state, action: PayloadAction
) => {
+ state.commits.push(action.payload);
+ },
+ pushLagMetric: (state, action: PayloadAction) => {
+ state.lags.push(action.payload);
+ },
+ pushFirstRenderMetric: (state, action: PayloadAction) => { // sent only once per app launch
+ state.firstRenders.push(action.payload);
+ },
+ clearMetrics: (state) => {
+ state.commits = [];
+ state.lags = [];
+ },
+ },
+});
+
+export const { pushCommitMetric, pushLagMetric, pushFirstRenderMetric, clearMetrics } = metricSlice.actions; //
+
+export default metricSlice.reducer; //export reducer to include in store
+
diff --git a/client/src/store/store.ts b/client/src/store/store.ts
index 2e94718..ada26e4 100644
--- a/client/src/store/store.ts
+++ b/client/src/store/store.ts
@@ -1,16 +1,16 @@
import { configureStore } from '@reduxjs/toolkit';
-
//no need for reducers/main reducer b/c Redux Toolkit's createSlice handles that for us
-import snapshotReducer from '../slices/snapshotSlice';
+import snapshotReducer from '../slices/snapshotSlice'; // since it's the default export, we can name our import whatever we want here
+import metricReducer from '../slices/metricSlice';
import { wsListener } from '../transport/socket';
-//import more slices here later?
-
export const store = configureStore({
- //export store
+//export store
reducer: {
- snapshot: snapshotReducer,
+ snapshot: snapshotReducer, //should this be snapshotSlice.reducer? NO b/c default export in snapshotSlice.ts is the reducer
+ metric: metricReducer,
},
+
middleware: (
getDefaultMiddleware //Redux Toolkit's default middleware to customize
) =>
@@ -19,5 +19,6 @@ export const store = configureStore({
}).prepend(wsListener.middleware),
});
+//shoulde these be exported?
export type RootState = ReturnType; //export utility type to reflect Redux state tree
export type AppDispatch = typeof store.dispatch; //export AppDispatch type used in async thunks or custom hooks (may not need this) -> a thunk handles async logic within a Redux app
diff --git a/client/src/transport/socket.ts b/client/src/transport/socket.ts
index de55086..f88340e 100644
--- a/client/src/transport/socket.ts
+++ b/client/src/transport/socket.ts
@@ -1,28 +1,34 @@
import { createListenerMiddleware } from '@reduxjs/toolkit';
-import { addSnapshot } from '../slices/snapshotSlice';
+import { addSnapshot, jumpToSnapshot } from '../slices/snapshotSlice';
import { wsConnect, wsDisconnect, wsSend } from './wsActions';
+import { pushCommitMetric, pushLagMetric, pushFirstRenderMetric } from '../slices/metricSlice';
// TODO: figure out why state sometimes persists on websockets disconnect/connect and sometimes doesn't
-export const wsListener = createListenerMiddleware();
+const parseData = async (d: unknown) => { // parse incoming WS data (string, Blob, ArrayBuffer) to JSON
+ if (typeof d === 'string') return JSON.parse(d); // most common case
+ if (d instanceof Blob) return JSON.parse(await d.text()); // unlikely with React Native, but just in case
+ if (d instanceof ArrayBuffer) return JSON.parse(new TextDecoder().decode(d)); // also unlikely
+ throw new Error('Unknown WS data type'); // shouldn't happen
+};
-const parseData = async (d: unknown) => {
- if (typeof d === 'string') return JSON.parse(d);
- if (d instanceof Blob) return JSON.parse(await d.text());
- if (d instanceof ArrayBuffer) return JSON.parse(new TextDecoder().decode(d));
- throw new Error('Unknown WS data type');
+const defaultWsUrl = () => { // default WS URL based on current location
+ const proto = location.protocol === 'https:' ? 'wss' : 'ws'; // secure if page is secure
+ return `${proto}://${location.hostname}:8080`; // default port 8080
};
-let socket: WebSocket | null = null;
-let closedByUser = false;
+export const wsListener = createListenerMiddleware(); // create the listener middleware instance
-// OPEN on ws/connect
-wsListener.startListening({
- actionCreator: wsConnect,
- effect: async (action, api) => {
- const url = action.payload;
+let socket: WebSocket | null = null; // track current socket instance
+let closedByUser = false; // track if close was user-initiated (vs. unexpected)
+
+
+wsListener.startListening({ // CONNECT
+ actionCreator: wsConnect, // when this action is dispatched, run the effect
+ effect: async (action, api) => { // effect is async to allow waiting for socket close
+ const url = action.payload || defaultWsUrl(); //
- // prevent duplicate connects (StrictMode, remounts, etc.)
+ // Avoid duplicates (StrictMode double-invoke, remounts, etc.)
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
console.log('[ws] already connecting/open');
return;
@@ -34,65 +40,103 @@ wsListener.startListening({
socket.onopen = () => {
console.log('[ws] OPEN', url);
- if (closedByUser) {
- console.log('[ws] closing immediately after OPEN (disconnect requested during CONNECTING)');
- socket?.close();
- }
+ // optional health ping
+ socket?.send(JSON.stringify({ channel: 'control', type: 'ping' }));
+ if (closedByUser) socket?.close(); // if a disconnect was requested during CONNECTING
};
- socket.onerror = (e) => console.log('[ws] ERROR', e);
+ socket.onerror = (e) => console.log('[ws] ERROR', e); // close event will follow
- const onMessage = async (e: MessageEvent) => {
+ // Handle incoming messages
+ // Note: messages may arrive before onopen completes, so this must be set before that
+ const onMessage = async (e: MessageEvent) => {
+ console.log( 'e data:', e.data, e);
try {
- const snap = await parseData(e.data);
- console.log('[ws] <-', snap);
- api.dispatch(addSnapshot(snap));
+ const msg = (await parseData(e.data)) as Envelope;
+
+ // Route by channel/type
+ switch (msg.channel) {
+ case 'snapshot': {
+ if (msg.type === 'add') api.dispatch(addSnapshot(msg.payload));
+ if (msg.type === 'jumpTo') api.dispatch(jumpToSnapshot(msg.payload.index));
+ break;
+ }
+ case 'metrics': {
+ if (msg.type === 'commit') api.dispatch(pushCommitMetric(msg.payload));
+ if (msg.type === 'lag') api.dispatch(pushLagMetric(msg.payload));
+ if (msg.type === 'firstRender') api.dispatch(pushFirstRenderMetric(msg.payload));
+ break;
+ }
+ case 'control': {
+ if (msg.type === 'pong') console.log('[ws] pong');
+ if (msg.type === 'error') console.warn('[ws] remote error:', (msg.payload)); // (msg as any).payload caused TS error
+ break;
+ }
+ }
} catch (err) {
console.error('[ws] parse error:', err);
}
};
+
socket.addEventListener('message', onMessage);
- // Wait until either the socket closes OR this listener is aborted
+ // Wait until socket closes OR this listener is aborted
await new Promise((resolve) => {
const onClose = (e: CloseEvent) => {
console.log('[ws] CLOSE', e.code, e.reason || '', closedByUser ? '(by user)' : '(unexpected)');
+ console.log('[ws] Close event details:', { code: e.code, reason: e.reason, wasClean: e.wasClean });
+ console.log('[ws] CLOSE', e.code, e.reason || '', closedByUser ? '(by user)' : '(unexpected)');
socket?.removeEventListener('message', onMessage);
socket = null;
resolve();
};
socket!.addEventListener('close', onClose);
- const onAbort = () => {
- closedByUser = true;
- // If still CONNECTING, let onopen close it; otherwise close now.
- if (socket && socket.readyState === WebSocket.OPEN) {
- socket.close();
- }
- // If CONNECTING, we’ll close in onopen; in both cases, resolve after close fires.
- };
- api.signal.addEventListener('abort', onAbort, { once: true });
+ api.signal.addEventListener(
+ 'abort',
+ () => {
+ closedByUser = true;
+ if (socket?.readyState === WebSocket.OPEN) socket.close();
+ // if CONNECTING, onopen will close immediately
+ },
+ { once: true }
+ );
});
},
});
-// CLOSE on ws/disconnect
+// DISCONNECT
wsListener.startListening({
actionCreator: wsDisconnect,
- effect: () => {
- if (!socket) return;
- closedByUser = true;
+ effect: () => { // no need for async here because not waiting for close to complete
+ if (!socket) return; //
+ closedByUser = true;
console.log('[ws] manual DISCONNECT (state:', socket.readyState, ')');
if (socket.readyState === WebSocket.OPEN) socket.close();
- // If CONNECTING, we let onopen immediately close it.
},
});
// SEND
wsListener.startListening({
actionCreator: wsSend,
- effect: async (action) => {
- if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify(action.payload));
- else console.log('[ws] SEND skipped (not open)');
+ effect: (action) => {
+ console.log('🚀 Attempting to send:', action.payload); // Add this line
+ console.log('📡 Socket state check:', {
+ socketExists: !!socket,
+ readyState: socket?.readyState,
+ readyStateText: socket?.readyState === 0 ? 'CONNECTING' :
+ socket?.readyState === 1 ? 'OPEN' :
+ socket?.readyState === 2 ? 'CLOSING' :
+ socket?.readyState === 3 ? 'CLOSED' : 'UNKNOWN'
+ });
+
+ if (socket?.readyState === WebSocket.OPEN) { // only send if open
+ socket.send(JSON.stringify(action.payload));
+ console.log('✅ Message sent successfully'); // Add this line
+ } else {
+ console.log('[ws] SEND skipped (not open), readyState:', socket?.readyState); // Update this line
+ }
},
});
+
+export { wsConnect, wsDisconnect, wsSend };
diff --git a/package.json b/package.json
index 896f457..c3545d0 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,8 @@
"description": "Will add description later",
"main": "index.js",
"scripts": {
- "test": "npm run test"
+ "test": "npm run test",
+ "dev": "vite"
},
"repository": {
"type": "git",
diff --git a/project-structure.txt b/project-structure.txt
new file mode 100644
index 0000000..49bae4c
--- /dev/null
+++ b/project-structure.txt
@@ -0,0 +1,779 @@
+.
+├── LICENSE
+├── README.md
+├── client
+│ ├── eslint.config.js
+│ ├── index.html
+│ ├── node_modules
+│ │ ├── @adobe
+│ │ ├── @ampproject
+│ │ ├── @asamuzakjp
+│ │ ├── @babel
+│ │ ├── @bufbuild
+│ │ ├── @csstools
+│ │ ├── @esbuild
+│ │ ├── @eslint
+│ │ ├── @eslint-community
+│ │ ├── @humanfs
+│ │ ├── @humanwhocodes
+│ │ ├── @jridgewell
+│ │ ├── @nodelib
+│ │ ├── @parcel
+│ │ ├── @rc-component
+│ │ ├── @rolldown
+│ │ ├── @rollup
+│ │ ├── @testing-library
+│ │ ├── @types
+│ │ ├── @typescript-eslint
+│ │ ├── @vitejs
+│ │ ├── @vitest
+│ │ ├── acorn
+│ │ ├── acorn-jsx
+│ │ ├── agent-base
+│ │ ├── ajv
+│ │ ├── ansi-regex
+│ │ ├── ansi-styles
+│ │ ├── argparse
+│ │ ├── aria-query
+│ │ ├── assertion-error
+│ │ ├── balanced-match
+│ │ ├── brace-expansion
+│ │ ├── braces
+│ │ ├── browserslist
+│ │ ├── buffer-builder
+│ │ ├── cac
+│ │ ├── callsites
+│ │ ├── caniuse-lite
+│ │ ├── chai
+│ │ ├── chalk
+│ │ ├── check-error
+│ │ ├── chokidar
+│ │ ├── classnames
+│ │ ├── clone
+│ │ ├── color
+│ │ ├── color-convert
+│ │ ├── color-name
+│ │ ├── color-string
+│ │ ├── colorjs.io
+│ │ ├── concat-map
+│ │ ├── contains
+│ │ ├── convert-source-map
+│ │ ├── cross-spawn
+│ │ ├── css.escape
+│ │ ├── cssstyle
+│ │ ├── csstype
+│ │ ├── data-urls
+│ │ ├── debug
+│ │ ├── decimal.js
+│ │ ├── deep-eql
+│ │ ├── deep-is
+│ │ ├── dequal
+│ │ ├── detect-libc
+│ │ ├── dom-accessibility-api
+│ │ ├── electron-to-chromium
+│ │ ├── entities
+│ │ ├── es-module-lexer
+│ │ ├── esbuild
+│ │ ├── escalade
+│ │ ├── escape-html
+│ │ ├── escape-string-regexp
+│ │ ├── eslint
+│ │ ├── eslint-plugin-react-hooks
+│ │ ├── eslint-plugin-react-refresh
+│ │ ├── eslint-scope
+│ │ ├── eslint-visitor-keys
+│ │ ├── espree
+│ │ ├── esquery
+│ │ ├── esrecurse
+│ │ ├── estraverse
+│ │ ├── estree-walker
+│ │ ├── esutils
+│ │ ├── expect-type
+│ │ ├── fast-deep-equal
+│ │ ├── fast-glob
+│ │ ├── fast-json-stable-stringify
+│ │ ├── fast-levenshtein
+│ │ ├── fastq
+│ │ ├── file-entry-cache
+│ │ ├── fill-range
+│ │ ├── find-up
+│ │ ├── flat-cache
+│ │ ├── flatted
+│ │ ├── fsevents
+│ │ ├── gensync
+│ │ ├── glob-parent
+│ │ ├── globals
+│ │ ├── graphemer
+│ │ ├── has-flag
+│ │ ├── hasown
+│ │ ├── html-encoding-sniffer
+│ │ ├── http-proxy-agent
+│ │ ├── https-proxy-agent
+│ │ ├── iconv-lite
+│ │ ├── ignore
+│ │ ├── immutable
+│ │ ├── import-fresh
+│ │ ├── imurmurhash
+│ │ ├── indent-string
+│ │ ├── is-arrayish
+│ │ ├── is-extglob
+│ │ ├── is-glob
+│ │ ├── is-number
+│ │ ├── is-potential-custom-element-name
+│ │ ├── isexe
+│ │ ├── js-tokens
+│ │ ├── js-yaml
+│ │ ├── jsdom
+│ │ ├── jsesc
+│ │ ├── json-buffer
+│ │ ├── json-schema-traverse
+│ │ ├── json-stable-stringify-without-jsonify
+│ │ ├── json5
+│ │ ├── keyv
+│ │ ├── levn
+│ │ ├── locate-path
+│ │ ├── lodash-es
+│ │ ├── lodash.merge
+│ │ ├── loupe
+│ │ ├── lru-cache
+│ │ ├── lz-string
+│ │ ├── magic-string
+│ │ ├── matches-selector
+│ │ ├── merge2
+│ │ ├── micromatch
+│ │ ├── min-indent
+│ │ ├── minimatch
+│ │ ├── ms
+│ │ ├── nanoid
+│ │ ├── natural-compare
+│ │ ├── newify
+│ │ ├── node-addon-api
+│ │ ├── node-releases
+│ │ ├── nwsapi
+│ │ ├── object-assign
+│ │ ├── optionator
+│ │ ├── p-limit
+│ │ ├── p-locate
+│ │ ├── parent-module
+│ │ ├── parse5
+│ │ ├── path-exists
+│ │ ├── path-key
+│ │ ├── pathe
+│ │ ├── pathval
+│ │ ├── picocolors
+│ │ ├── picomatch
+│ │ ├── postcss
+│ │ ├── prelude-ls
+│ │ ├── pretty-format
+│ │ ├── punycode
+│ │ ├── queue-microtask
+│ │ ├── rc-motion
+│ │ ├── rc-resize-observer
+│ │ ├── rc-slider
+│ │ ├── rc-tooltip
+│ │ ├── rc-util
+│ │ ├── react
+│ │ ├── react-base16-styling
+│ │ ├── react-dom
+│ │ ├── react-is
+│ │ ├── react-json-tree
+│ │ ├── react-refresh
+│ │ ├── react-style-normalizer
+│ │ ├── readdirp
+│ │ ├── redent
+│ │ ├── region
+│ │ ├── region-align
+│ │ ├── resize-observer-polyfill
+│ │ ├── resolve-from
+│ │ ├── reusify
+│ │ ├── rollup
+│ │ ├── rrweb-cssom
+│ │ ├── run-parallel
+│ │ ├── rxjs
+│ │ ├── safer-buffer
+│ │ ├── sass
+│ │ ├── sass-embedded
+│ │ ├── sass-embedded-darwin-x64
+│ │ ├── saxes
+│ │ ├── scheduler
+│ │ ├── semver
+│ │ ├── shebang-command
+│ │ ├── shebang-regex
+│ │ ├── siginfo
+│ │ ├── simple-swizzle
+│ │ ├── source-map-js
+│ │ ├── stackback
+│ │ ├── std-env
+│ │ ├── strip-indent
+│ │ ├── strip-json-comments
+│ │ ├── strip-literal
+│ │ ├── supports-color
+│ │ ├── symbol-tree
+│ │ ├── sync-child-process
+│ │ ├── sync-message-port
+│ │ ├── tinybench
+│ │ ├── tinyexec
+│ │ ├── tinyglobby
+│ │ ├── tinypool
+│ │ ├── tinyrainbow
+│ │ ├── tinyspy
+│ │ ├── tldts
+│ │ ├── tldts-core
+│ │ ├── to-regex-range
+│ │ ├── to-style
+│ │ ├── tooltip
+│ │ ├── tough-cookie
+│ │ ├── tr46
+│ │ ├── ts-api-utils
+│ │ ├── tslib
+│ │ ├── type-check
+│ │ ├── typescript
+│ │ ├── typescript-eslint
+│ │ ├── update-browserslist-db
+│ │ ├── uri-js
+│ │ ├── varint
+│ │ ├── vite
+│ │ ├── vite-node
+│ │ ├── vitest
+│ │ ├── w3c-xmlserializer
+│ │ ├── webidl-conversions
+│ │ ├── whatwg-encoding
+│ │ ├── whatwg-mimetype
+│ │ ├── whatwg-url
+│ │ ├── which
+│ │ ├── why-is-node-running
+│ │ ├── word-wrap
+│ │ ├── ws
+│ │ ├── xml-name-validator
+│ │ ├── xmlchars
+│ │ ├── yallist
+│ │ └── yocto-queue
+│ ├── package-lock.json
+│ ├── package.json
+│ ├── src
+│ │ ├── App.tsx
+│ │ ├── components
+│ │ ├── containers
+│ │ ├── example
+│ │ ├── features
+│ │ ├── hooks
+│ │ ├── main.tsx
+│ │ ├── slices
+│ │ ├── store
+│ │ ├── styles
+│ │ ├── tests
+│ │ ├── transport
+│ │ ├── utils
+│ │ └── vite-env.d.ts
+│ ├── tsconfig.app.json
+│ ├── tsconfig.json
+│ ├── tsconfig.node.json
+│ ├── vite.config.ts
+│ └── vitest.config.ts
+├── client copy
+│ └── src
+│ ├── features
+│ └── test
+├── node_modules
+│ ├── @adobe
+│ ├── @babel
+│ ├── @esbuild
+│ ├── @jridgewell
+│ ├── @rc-component
+│ ├── @reduxjs
+│ │ └── toolkit
+│ ├── @rollup
+│ ├── @standard-schema
+│ │ ├── spec
+│ │ └── utils
+│ ├── @testing-library
+│ ├── @types
+│ │ ├── react
+│ │ ├── react-dom
+│ │ └── use-sync-external-store
+│ ├── @vitest
+│ ├── csstype
+│ │ ├── LICENSE
+│ │ ├── README.md
+│ │ ├── index.d.ts
+│ │ ├── index.js.flow
+│ │ └── package.json
+│ ├── immer
+│ │ ├── LICENSE
+│ │ ├── dist
+│ │ ├── package.json
+│ │ ├── readme.md
+│ │ └── src
+│ ├── react
+│ │ ├── LICENSE
+│ │ ├── README.md
+│ │ ├── cjs
+│ │ ├── compiler-runtime.js
+│ │ ├── index.js
+│ │ ├── jsx-dev-runtime.js
+│ │ ├── jsx-dev-runtime.react-server.js
+│ │ ├── jsx-runtime.js
+│ │ ├── jsx-runtime.react-server.js
+│ │ ├── package.json
+│ │ └── react.react-server.js
+│ ├── react-redux
+│ │ ├── LICENSE.md
+│ │ ├── README.md
+│ │ ├── dist
+│ │ ├── package.json
+│ │ └── src
+│ ├── redux
+│ │ ├── LICENSE.md
+│ │ ├── README.md
+│ │ ├── dist
+│ │ ├── package.json
+│ │ └── src
+│ ├── redux-thunk
+│ │ ├── LICENSE.md
+│ │ ├── README.md
+│ │ ├── dist
+│ │ ├── package.json
+│ │ └── src
+│ ├── reselect
+│ │ ├── LICENSE
+│ │ ├── README.md
+│ │ ├── dist
+│ │ ├── package.json
+│ │ └── src
+│ └── use-sync-external-store
+│ ├── LICENSE
+│ ├── README.md
+│ ├── cjs
+│ ├── index.js
+│ ├── package.json
+│ ├── shim
+│ └── with-selector.js
+├── package-lock.json
+├── package.json
+├── project-structure.txt
+├── sample-RN-app
+│ ├── MobileSample.tsx
+│ ├── app.json
+│ ├── assets
+│ │ ├── adaptive-icon.png
+│ │ ├── favicon.png
+│ │ ├── icon.png
+│ │ └── splash-icon.png
+│ ├── index.ts
+│ ├── node_modules
+│ │ ├── @0no-co
+│ │ ├── @ampproject
+│ │ ├── @babel
+│ │ ├── @expo
+│ │ ├── @isaacs
+│ │ ├── @istanbuljs
+│ │ ├── @jest
+│ │ ├── @jridgewell
+│ │ ├── @pkgjs
+│ │ ├── @react-native
+│ │ ├── @sinclair
+│ │ ├── @sinonjs
+│ │ ├── @types
+│ │ ├── @urql
+│ │ ├── @xmldom
+│ │ ├── abort-controller
+│ │ ├── accepts
+│ │ ├── acorn
+│ │ ├── agent-base
+│ │ ├── anser
+│ │ ├── ansi-escapes
+│ │ ├── ansi-regex
+│ │ ├── ansi-styles
+│ │ ├── any-promise
+│ │ ├── anymatch
+│ │ ├── arg
+│ │ ├── argparse
+│ │ ├── asap
+│ │ ├── async-limiter
+│ │ ├── babel-jest
+│ │ ├── babel-plugin-istanbul
+│ │ ├── babel-plugin-jest-hoist
+│ │ ├── babel-plugin-polyfill-corejs2
+│ │ ├── babel-plugin-polyfill-corejs3
+│ │ ├── babel-plugin-polyfill-regenerator
+│ │ ├── babel-plugin-react-native-web
+│ │ ├── babel-plugin-syntax-hermes-parser
+│ │ ├── babel-plugin-transform-flow-enums
+│ │ ├── babel-preset-current-node-syntax
+│ │ ├── babel-preset-expo
+│ │ ├── babel-preset-jest
+│ │ ├── balanced-match
+│ │ ├── base64-js
+│ │ ├── better-opn
+│ │ ├── big-integer
+│ │ ├── bplist-creator
+│ │ ├── bplist-parser
+│ │ ├── brace-expansion
+│ │ ├── braces
+│ │ ├── browserslist
+│ │ ├── bser
+│ │ ├── buffer
+│ │ ├── buffer-from
+│ │ ├── bytes
+│ │ ├── caller-callsite
+│ │ ├── caller-path
+│ │ ├── callsites
+│ │ ├── camelcase
+│ │ ├── caniuse-lite
+│ │ ├── chalk
+│ │ ├── chownr
+│ │ ├── chrome-launcher
+│ │ ├── chromium-edge-launcher
+│ │ ├── ci-info
+│ │ ├── cli-cursor
+│ │ ├── cli-spinners
+│ │ ├── cliui
+│ │ ├── clone
+│ │ ├── color-convert
+│ │ ├── color-name
+│ │ ├── commander
+│ │ ├── compressible
+│ │ ├── compression
+│ │ ├── concat-map
+│ │ ├── connect
+│ │ ├── convert-source-map
+│ │ ├── core-js-compat
+│ │ ├── cosmiconfig
+│ │ ├── cross-spawn
+│ │ ├── crypto-random-string
+│ │ ├── csstype
+│ │ ├── debug
+│ │ ├── deep-extend
+│ │ ├── deepmerge
+│ │ ├── defaults
+│ │ ├── define-lazy-prop
+│ │ ├── depd
+│ │ ├── destroy
+│ │ ├── detect-libc
+│ │ ├── dotenv
+│ │ ├── dotenv-expand
+│ │ ├── eastasianwidth
+│ │ ├── ee-first
+│ │ ├── electron-to-chromium
+│ │ ├── emoji-regex
+│ │ ├── encodeurl
+│ │ ├── env-editor
+│ │ ├── error-ex
+│ │ ├── error-stack-parser
+│ │ ├── escalade
+│ │ ├── escape-html
+│ │ ├── escape-string-regexp
+│ │ ├── esprima
+│ │ ├── etag
+│ │ ├── event-target-shim
+│ │ ├── exec-async
+│ │ ├── expo
+│ │ ├── expo-asset
+│ │ ├── expo-constants
+│ │ ├── expo-file-system
+│ │ ├── expo-font
+│ │ ├── expo-keep-awake
+│ │ ├── expo-modules-autolinking
+│ │ ├── expo-modules-core
+│ │ ├── expo-status-bar
+│ │ ├── exponential-backoff
+│ │ ├── fast-json-stable-stringify
+│ │ ├── fb-watchman
+│ │ ├── fill-range
+│ │ ├── finalhandler
+│ │ ├── find-up
+│ │ ├── flow-enums-runtime
+│ │ ├── fontfaceobserver
+│ │ ├── foreground-child
+│ │ ├── freeport-async
+│ │ ├── fresh
+│ │ ├── fs.realpath
+│ │ ├── fsevents
+│ │ ├── function-bind
+│ │ ├── gensync
+│ │ ├── get-caller-file
+│ │ ├── get-package-type
+│ │ ├── getenv
+│ │ ├── glob
+│ │ ├── graceful-fs
+│ │ ├── has-flag
+│ │ ├── hasown
+│ │ ├── hermes-estree
+│ │ ├── hermes-parser
+│ │ ├── hosted-git-info
+│ │ ├── http-errors
+│ │ ├── https-proxy-agent
+│ │ ├── ieee754
+│ │ ├── ignore
+│ │ ├── image-size
+│ │ ├── import-fresh
+│ │ ├── imurmurhash
+│ │ ├── inflight
+│ │ ├── inherits
+│ │ ├── ini
+│ │ ├── invariant
+│ │ ├── is-arrayish
+│ │ ├── is-core-module
+│ │ ├── is-directory
+│ │ ├── is-docker
+│ │ ├── is-fullwidth-code-point
+│ │ ├── is-number
+│ │ ├── is-wsl
+│ │ ├── isexe
+│ │ ├── istanbul-lib-coverage
+│ │ ├── istanbul-lib-instrument
+│ │ ├── jackspeak
+│ │ ├── jest-environment-node
+│ │ ├── jest-get-type
+│ │ ├── jest-haste-map
+│ │ ├── jest-message-util
+│ │ ├── jest-mock
+│ │ ├── jest-regex-util
+│ │ ├── jest-util
+│ │ ├── jest-validate
+│ │ ├── jest-worker
+│ │ ├── jimp-compact
+│ │ ├── js-tokens
+│ │ ├── js-yaml
+│ │ ├── jsc-safe-url
+│ │ ├── jsesc
+│ │ ├── json-parse-better-errors
+│ │ ├── json5
+│ │ ├── kleur
+│ │ ├── lan-network
+│ │ ├── leven
+│ │ ├── lighthouse-logger
+│ │ ├── lightningcss
+│ │ ├── lightningcss-darwin-x64
+│ │ ├── lines-and-columns
+│ │ ├── locate-path
+│ │ ├── lodash.debounce
+│ │ ├── lodash.throttle
+│ │ ├── log-symbols
+│ │ ├── loose-envify
+│ │ ├── lru-cache
+│ │ ├── makeerror
+│ │ ├── marky
+│ │ ├── memoize-one
+│ │ ├── merge-stream
+│ │ ├── metro
+│ │ ├── metro-babel-transformer
+│ │ ├── metro-cache
+│ │ ├── metro-cache-key
+│ │ ├── metro-config
+│ │ ├── metro-core
+│ │ ├── metro-file-map
+│ │ ├── metro-minify-terser
+│ │ ├── metro-resolver
+│ │ ├── metro-runtime
+│ │ ├── metro-source-map
+│ │ ├── metro-symbolicate
+│ │ ├── metro-transform-plugins
+│ │ ├── metro-transform-worker
+│ │ ├── micromatch
+│ │ ├── mime
+│ │ ├── mime-db
+│ │ ├── mime-types
+│ │ ├── mimic-fn
+│ │ ├── minimatch
+│ │ ├── minimist
+│ │ ├── minipass
+│ │ ├── minizlib
+│ │ ├── mkdirp
+│ │ ├── ms
+│ │ ├── mz
+│ │ ├── nanoid
+│ │ ├── negotiator
+│ │ ├── nested-error-stacks
+│ │ ├── node-forge
+│ │ ├── node-int64
+│ │ ├── node-releases
+│ │ ├── normalize-path
+│ │ ├── npm-package-arg
+│ │ ├── nullthrows
+│ │ ├── ob1
+│ │ ├── object-assign
+│ │ ├── on-finished
+│ │ ├── on-headers
+│ │ ├── once
+│ │ ├── onetime
+│ │ ├── open
+│ │ ├── ora
+│ │ ├── p-limit
+│ │ ├── p-locate
+│ │ ├── p-try
+│ │ ├── package-json-from-dist
+│ │ ├── parse-json
+│ │ ├── parse-png
+│ │ ├── parseurl
+│ │ ├── path-exists
+│ │ ├── path-is-absolute
+│ │ ├── path-key
+│ │ ├── path-parse
+│ │ ├── path-scurry
+│ │ ├── picocolors
+│ │ ├── picomatch
+│ │ ├── pirates
+│ │ ├── plist
+│ │ ├── pngjs
+│ │ ├── postcss
+│ │ ├── pretty-bytes
+│ │ ├── pretty-format
+│ │ ├── proc-log
+│ │ ├── progress
+│ │ ├── promise
+│ │ ├── prompts
+│ │ ├── punycode
+│ │ ├── qrcode-terminal
+│ │ ├── queue
+│ │ ├── range-parser
+│ │ ├── rc
+│ │ ├── react
+│ │ ├── react-devtools-core
+│ │ ├── react-is
+│ │ ├── react-native
+│ │ ├── react-native-edge-to-edge
+│ │ ├── react-native-is-edge-to-edge
+│ │ ├── react-refresh
+│ │ ├── regenerate
+│ │ ├── regenerate-unicode-properties
+│ │ ├── regenerator-runtime
+│ │ ├── regexpu-core
+│ │ ├── regjsgen
+│ │ ├── regjsparser
+│ │ ├── require-directory
+│ │ ├── require-from-string
+│ │ ├── requireg
+│ │ ├── resolve
+│ │ ├── resolve-from
+│ │ ├── resolve-workspace-root
+│ │ ├── resolve.exports
+│ │ ├── restore-cursor
+│ │ ├── rimraf
+│ │ ├── safe-buffer
+│ │ ├── sax
+│ │ ├── scheduler
+│ │ ├── semver
+│ │ ├── send
+│ │ ├── serialize-error
+│ │ ├── serve-static
+│ │ ├── setprototypeof
+│ │ ├── shebang-command
+│ │ ├── shebang-regex
+│ │ ├── shell-quote
+│ │ ├── signal-exit
+│ │ ├── simple-plist
+│ │ ├── sisteransi
+│ │ ├── slash
+│ │ ├── slugify
+│ │ ├── source-map
+│ │ ├── source-map-js
+│ │ ├── source-map-support
+│ │ ├── sprintf-js
+│ │ ├── stack-utils
+│ │ ├── stackframe
+│ │ ├── stacktrace-parser
+│ │ ├── statuses
+│ │ ├── stream-buffers
+│ │ ├── string-width
+│ │ ├── string-width-cjs
+│ │ ├── strip-ansi
+│ │ ├── strip-ansi-cjs
+│ │ ├── strip-json-comments
+│ │ ├── structured-headers
+│ │ ├── sucrase
+│ │ ├── supports-color
+│ │ ├── supports-hyperlinks
+│ │ ├── supports-preserve-symlinks-flag
+│ │ ├── tar
+│ │ ├── temp-dir
+│ │ ├── terminal-link
+│ │ ├── terser
+│ │ ├── test-exclude
+│ │ ├── thenify
+│ │ ├── thenify-all
+│ │ ├── throat
+│ │ ├── tmpl
+│ │ ├── to-regex-range
+│ │ ├── toidentifier
+│ │ ├── ts-interface-checker
+│ │ ├── type-detect
+│ │ ├── type-fest
+│ │ ├── typescript
+│ │ ├── undici
+│ │ ├── undici-types
+│ │ ├── unicode-canonical-property-names-ecmascript
+│ │ ├── unicode-match-property-ecmascript
+│ │ ├── unicode-match-property-value-ecmascript
+│ │ ├── unicode-property-aliases-ecmascript
+│ │ ├── unique-string
+│ │ ├── unpipe
+│ │ ├── update-browserslist-db
+│ │ ├── utils-merge
+│ │ ├── uuid
+│ │ ├── validate-npm-package-name
+│ │ ├── vary
+│ │ ├── vlq
+│ │ ├── walker
+│ │ ├── wcwidth
+│ │ ├── webidl-conversions
+│ │ ├── whatwg-fetch
+│ │ ├── whatwg-url-without-unicode
+│ │ ├── which
+│ │ ├── wonka
+│ │ ├── wrap-ansi
+│ │ ├── wrap-ansi-cjs
+│ │ ├── wrappy
+│ │ ├── write-file-atomic
+│ │ ├── ws
+│ │ ├── xcode
+│ │ ├── xml2js
+│ │ ├── xmlbuilder
+│ │ ├── y18n
+│ │ ├── yallist
+│ │ ├── yargs
+│ │ ├── yargs-parser
+│ │ └── yocto-queue
+│ ├── package-lock.json
+│ ├── package.json
+│ ├── server.js
+│ ├── tsconfig.json
+│ └── useFiberTree.ts
+└── server
+ ├── node_modules
+ │ ├── anymatch
+ │ ├── balanced-match
+ │ ├── binary-extensions
+ │ ├── brace-expansion
+ │ ├── braces
+ │ ├── chokidar
+ │ ├── concat-map
+ │ ├── debug
+ │ ├── fill-range
+ │ ├── fsevents
+ │ ├── glob-parent
+ │ ├── has-flag
+ │ ├── ignore-by-default
+ │ ├── is-binary-path
+ │ ├── is-extglob
+ │ ├── is-glob
+ │ ├── is-number
+ │ ├── minimatch
+ │ ├── ms
+ │ ├── nodemon
+ │ ├── normalize-path
+ │ ├── picomatch
+ │ ├── pstree.remy
+ │ ├── readdirp
+ │ ├── semver
+ │ ├── simple-update-notifier
+ │ ├── supports-color
+ │ ├── to-regex-range
+ │ ├── touch
+ │ ├── undefsafe
+ │ └── ws
+ ├── package-lock.json
+ ├── package.json
+ └── server.js
+
+710 directories, 67 files
diff --git a/sample-RN-app/.expo/settings.json b/sample-RN-app/.expo/settings.json
new file mode 100644
index 0000000..d3973e9
--- /dev/null
+++ b/sample-RN-app/.expo/settings.json
@@ -0,0 +1,3 @@
+{
+ "urlRandomness": "V23EBeg"
+}
diff --git a/sample-RN-app/MobileSample.tsx b/sample-RN-app/MobileSample.tsx
index 28c7cd6..e49755b 100644
--- a/sample-RN-app/MobileSample.tsx
+++ b/sample-RN-app/MobileSample.tsx
@@ -9,7 +9,56 @@ import { logFiber, traverse } from './useFiberTree';
// Constants.manifest?.debuggerHost?.split(':')[0] || // older SDK
// 'localhost'; // fallback
-const devHost = '10.0.0.157'; // Will's laptop IP since the above code wasn't working
+const devHost = '192.168.1.67'; //
+
+// ADDED: Helper function to extract component data for Reactime
+function extractComponentSnapshot(fiberRoot: any): any {
+ const components: any[] = [];
+
+ function walkFiber(node: any) {
+ if (!node) return;
+
+ const tag = node.tag;
+ if (tag === 0 || tag === 1 || tag === 2 || tag === 10) { // Function/Class components
+ const name = node.elementType?.name || node.elementType?.displayName || 'Anonymous';
+
+ // Extract state if available
+ let state = {};
+ if (node.memoizedState) {
+ // Simple state extraction - you can make this more sophisticated
+ try {
+ state = {
+ hasState: true,
+ memoizedState: node.memoizedState.memoizedState || 'no memoized state'
+ };
+ } catch (e) {
+ state = { hasState: true, error: 'Could not serialize state' };
+ }
+ }
+
+ components.push({
+ name,
+ tag,
+ key: node.key,
+ state,
+ props: node.memoizedProps || {}
+ });
+ }
+
+ walkFiber(node.child);
+ walkFiber(node.sibling);
+ }
+
+ walkFiber(fiberRoot);
+ return {
+ timestamp: Date.now(),
+ componentTree: components,
+ rootInfo: {
+ type: 'ReactNativeApp',
+ children: components.length
+ }
+ };
+}
export default function App() {
const [count, setCount] = useState(0);
@@ -24,13 +73,11 @@ export default function App() {
socket.onopen = () => {
console.log('🔌 WS connected');
- socket.send(
- JSON.stringify({
- count: count,
- letter: letter,
- timestamp: new Date().toISOString(),
- })
- );
+ // CHANGED: Send proper control message instead of raw state
+ socket.send(JSON.stringify({
+ channel: 'control',
+ type: 'ping'
+ }));
};
socket.onerror = (e) => {
console.log('WS error', (e as any).message ?? e);
@@ -40,18 +87,65 @@ export default function App() {
return () => socket.close();
}, []);
- /* helper to broadcast the current state */
- const emit = (nextCount: number, nextLetter: string) => {
+ /* CHANGED: helper to send snapshot data to Reactime */
+ const sendSnapshot = (currentFiberRoot?: any) => {
const socket = ws.current;
- if (socket?.readyState === WebSocket.OPEN) {
- socket.send(
- JSON.stringify({
- count: nextCount,
- letter: nextLetter,
- timestamp: new Date().toISOString(),
- })
- );
+ if (socket?.readyState !== WebSocket.OPEN) return;
+
+ // If we have a fiber root, extract component data
+ let snapshotData;
+ if (currentFiberRoot) {
+ snapshotData = extractComponentSnapshot(currentFiberRoot);
+ } else {
+ // Fallback: send current component state
+ snapshotData = {
+ timestamp: Date.now(),
+ appState: { count, letter },
+ components: [
+ {
+ name: 'App',
+ state: { count, letter },
+ type: 'FunctionComponent'
+ }
+ ]
+ };
}
+
+ // CHANGED: Send in the format your browser client expects
+ const message = {
+ channel: 'snapshot',
+ type: 'add',
+ payload: snapshotData
+ };
+
+ socket.send(JSON.stringify(message));
+ console.log('📤 Sent snapshot to Reactime');
+ };
+
+ /* ADDED: helper to send performance metrics */
+ const sendMetric = () => {
+ const socket = ws.current;
+ if (socket?.readyState !== WebSocket.OPEN) return;
+
+ const metric = {
+ channel: 'metrics',
+ type: 'commit',
+ payload: {
+ ts: Date.now(),
+ durationMs: Math.random() * 20 + 5, // Simulated commit duration
+ fibersUpdated: Math.floor(Math.random() * 5) + 1,
+ appId: 'react-native-sample'
+ }
+ };
+
+ socket.send(JSON.stringify(metric));
+ console.log('📊 Sent metric to Reactime');
+ };
+
+ // CHANGED: helper to broadcast the current state (now sends proper snapshot format)
+ const emit = (nextCount: number, nextLetter: string) => {
+ // Send snapshot after state update
+ setTimeout(() => sendSnapshot(), 10);
};
const incCount = () =>
@@ -77,9 +171,11 @@ export default function App() {
console.log('DevTools hook found!', Object.keys(hook));
- /* 1. Stream every commit through logFiber */
+ /* CHANGED: Send snapshot on every React commit */
hook.onCommitFiberRoot = (_id: any, root: any) => {
- logFiber(root.current);
+ logFiber(root.current); // Keep your existing logging
+ sendSnapshot(root.current); // ADDED: Send snapshot to Reactime
+ sendMetric(); // ADDED: Send performance metric
};
/* 2. Walk existing trees once for an initial dump */
@@ -98,7 +194,11 @@ export default function App() {
continue;
}
- roots.forEach((r: any) => traverse(r.current));
+ // ADDED: Send initial snapshot
+ roots.forEach((r: any) => {
+ traverse(r.current); // Keep your existing traversal
+ sendSnapshot(r.current); // ADDED: Send initial snapshot
+ });
}
}, []);
@@ -124,3 +224,131 @@ const styles = StyleSheet.create({
btn: { borderWidth: 2, borderRadius: 8, padding: 12, borderColor: '#fff' },
btnText: { fontSize: 24, fontWeight: '600', color: '#fff' },
});
+
+
+// import React, { useEffect, useRef, useState } from 'react';
+// import { View, Button, Text, StyleSheet, Pressable } from 'react-native';
+// import Constants from 'expo-constants';
+// import { logFiber, traverse } from './useFiberTree';
+
+// /** Compute laptop IP so the phone can reach ws://:8080 */
+// // const devHost =
+// // (Constants.manifest2 as any)?.extra?.expoGo?.developerHostname || // SDK 50
+// // Constants.manifest?.debuggerHost?.split(':')[0] || // older SDK
+// // 'localhost'; // fallback
+
+// const devHost = '192.168.1.67'; //
+
+// export default function App() {
+// const [count, setCount] = useState(0);
+// const [letter, setLetter] = useState('a');
+
+// const ws = useRef(null);
+
+// /* open WebSocket once */
+// useEffect(() => {
+// const socket = new WebSocket(`ws://${devHost}:8080`);
+// ws.current = socket;
+
+// socket.onopen = () => {
+// console.log('🔌 WS connected');
+// socket.send(
+// JSON.stringify({
+// count: count,
+// letter: letter,
+// timestamp: new Date().toISOString(),
+// })
+// );
+// };
+// socket.onerror = (e) => {
+// console.log('WS error', (e as any).message ?? e);
+// };
+// socket.onclose = () => console.log('WS closed');
+
+// return () => socket.close();
+// }, []);
+
+// /* helper to broadcast the current state */
+// const emit = (nextCount: number, nextLetter: string) => {
+// const socket = ws.current;
+// if (socket?.readyState === WebSocket.OPEN) {
+// socket.send(
+// JSON.stringify({
+// count: nextCount,
+// letter: nextLetter,
+// timestamp: new Date().toISOString(),
+// })
+// );
+// }
+// };
+
+// const incCount = () =>
+// setCount((e) => {
+// const n = e + 1;
+// emit(n, letter);
+// return n;
+// });
+
+// const incLetter = () =>
+// setLetter((e) => {
+// const n = String.fromCharCode(((e.charCodeAt(0) - 97 + 1) % 26) + 97);
+// emit(count, n);
+// return n;
+// });
+
+// useEffect(() => {
+// const hook = (globalThis as any).__REACT_DEVTOOLS_GLOBAL_HOOK__;
+// if (!hook) {
+// console.log('❌ DevTools hook not found');
+// return;
+// }
+
+// console.log('✅ DevTools hook found!', Object.keys(hook));
+
+// /* 1. Stream every commit through logFiber */
+// hook.onCommitFiberRoot = (_id: any, root: any) => {
+// logFiber(root.current);
+// };
+
+// /* 2. Walk existing trees once for an initial dump */
+// const renderers = hook.renderers;
+// if (!renderers || renderers.size === 0) {
+// console.log('⚠️ No renderers found.');
+// return;
+// }
+
+// for (const [rendererId, renderer] of renderers) {
+// console.log(`🔧 Renderer ${rendererId}:`, renderer.rendererPackageName ?? 'unknown');
+
+// const roots = hook.getFiberRoots?.(rendererId);
+// if (!roots || roots.size === 0) {
+// console.log(`⚠️ No fiber roots found for renderer ${rendererId}`);
+// continue;
+// }
+
+// roots.forEach((r: any) => traverse(r.current));
+// }
+// }, []);
+
+// return (
+//
+//
+// {count} : {letter}
+//
+//
+// +1
+//
+
+//
+// next letter
+//
+//
+// );
+// }
+
+// const styles = StyleSheet.create({
+// container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#3c3c3c' },
+// display: { fontSize: 100, marginBottom: 40, color: '#fff' },
+// btn: { borderWidth: 2, borderRadius: 8, padding: 12, borderColor: '#fff' },
+// btnText: { fontSize: 24, fontWeight: '600', color: '#fff' },
+// });
diff --git a/server/server.js b/server/server.js
index 0f90de0..286a5df 100644
--- a/server/server.js
+++ b/server/server.js
@@ -1,3 +1,4 @@
+//doesn't appear that this file is causing bugs
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
@@ -5,24 +6,45 @@ const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
console.log('Client connected');
- ws.on('message', (data, isBinary) => {
- const message = isBinary ? data.toString() : String(data); // normalize our message to a String
+ ws.on('message', (data, isBinary) => {// could remove isBinary and just do data.toString() directly? J
+ const message = isBinary ? data.toString() : String(data); // normalize our message to a String //J question: do we expect binary files?
try {
console.log('received:', message);
- console.log('JSON parsed msg:', JSON.parse(message));
- } catch {
- console.log('unable to JSON parse message. Message in string format is:', message.toString());
- }
- ws.send(`Echo: ${message}`); // Send a message back to the client
-
- // I think we need to send to other clients? the above line only sends the message back to the same client apparently?
+ const parsed = JSON.parse(message); // ADDED: Parse message for proper handling
+ console.log('JSON parsed msg:', parsed);
+
+ // CHANGED: Handle different message types properly
+ if (parsed.channel === 'control' && parsed.type === 'ping') {
+ // Respond to ping with pong
+ ws.send(JSON.stringify({
+ channel: 'control',
+ type: 'pong'
+ }));
+ console.log('Sent pong response');
+ } else {
+ // For debugging with single client, send back to sender
+ if (wss.clients.size === 1) {
+ ws.send(JSON.stringify(parsed));
+ console.log('Sent back to sender (single client debug mode)');
+ } else {
+ // Multiple clients - forward to others
for (const client of wss.clients) {
- // we're just checking that client !== ws, but we ws.send anyways earlier... what does that mean? Also where does ws.send(echo message) go?
if (client !== ws && client.readyState === WebSocket.OPEN) {
- // why were we not getting here before but we are now ...?
- client.send(message); // parse on receive I think
+ client.send(JSON.stringify(parsed));
+ console.log('Forwarded message to client');
}
}
+ }
+}
+ } catch {
+ console.log('unable to JSON parse message. Message in string format is:', message.toString());
+ // ADDED: Send error back to client in proper format
+ ws.send(JSON.stringify({
+ channel: 'control',
+ type: 'error',
+ payload: { message: 'Failed to parse JSON', raw: message }
+ }));
+ }
});
ws.on('close', () => {
@@ -35,3 +57,66 @@ wss.on('connection', (ws) => {
});
console.log('WebSocket server started on port 8080');
+
+
+// // Send periodic test data to all connected clients
+// setInterval(() => {
+// if (wss.clients.size > 0) {
+// const testMetric = {
+// channel: 'metrics',
+// type: 'commit',
+// payload: {
+// ts: Date.now(),
+// durationMs: Math.random() * 100 + 10, // 10-110ms
+// fibersUpdated: Math.floor(Math.random() * 20) + 1,
+// appId: 'test-app'
+// }
+// };
+
+// wss.clients.forEach(client => {
+// if (client.readyState === WebSocket.OPEN) {
+// client.send(JSON.stringify(testMetric));
+// }
+// });
+
+// console.log(`📊 Sent test metric to ${wss.clients.size} clients`);
+// }
+// }, 10000); // Every 10 seconds
+// //doesn't appear that this file is causing bugs
+// const WebSocket = require('ws');
+
+// const wss = new WebSocket.Server({ port: 8080 });
+
+// wss.on('connection', (ws) => {
+// console.log('Client connected');
+
+// ws.on('message', (data, isBinary) => {// could remove isBinary and just do data.toString() directly? J
+// const message = isBinary ? data.toString() : String(data); // normalize our message to a String //J question: do we expect binary files?
+// try {
+// console.log('received:', message);
+// console.log('JSON parsed msg:', JSON.parse(message));
+// } catch {
+// console.log('unable to JSON parse message. Message in string format is:', message.toString());
+// }
+// ws.send(`Echo: ${message}`); // Send a message back to the client //J Question: Test? do we need this in production with real data?
+
+// // I think we need to send to other clients? the above line only sends the message back to the same client apparently?
+// for (const client of wss.clients) {
+// // we're just checking that client !== ws, but we ws.send anyways earlier... what does that mean? Also where does ws.send(echo message) go?
+// if (client !== ws && client.readyState === WebSocket.OPEN) {
+// // why were we not getting here before but we are now ...?
+// client.send(message); // parse on receive I think
+// }
+// }
+// });
+
+// ws.on('close', () => {
+// console.log('Client disconnected');
+// });
+
+// ws.on('error', (err) => {
+// console.error('WebSocket error:', err);
+// });
+// });
+
+// console.log('WebSocket server started on port 8080');