-
-
Notifications
You must be signed in to change notification settings - Fork 35
feat: add real-time latency indicator and websocket heartbeat #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
faa9f4b
5f6a160
68ab574
786813e
94066b0
34b69ce
9b457da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,83 +1,115 @@ | ||
| import { useState, useEffect, useCallback, useRef } from 'react'; | ||
| import { useState, useEffect, useCallback, useRef } from "react"; | ||
| import type { RemoteMessage } from "@/types"; | ||
|
|
||
| export const useRemoteConnection = () => { | ||
| const wsRef = useRef<WebSocket | null>(null); | ||
| const [status, setStatus] = useState<'connecting' | 'connected' | 'disconnected'>('disconnected'); | ||
|
|
||
| useEffect(() => { | ||
| let isMounted = true; | ||
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | ||
| const host = window.location.host; | ||
| const wsUrl = `${protocol}//${host}/ws`; | ||
|
|
||
| let reconnectTimer: NodeJS.Timeout; | ||
|
|
||
| const connect = () => { | ||
| if (!isMounted) return; | ||
|
|
||
| // Close any existing socket before creating a new one | ||
| if (wsRef.current) { | ||
| wsRef.current.onopen = null; | ||
| wsRef.current.onclose = null; | ||
| wsRef.current.onerror = null; | ||
| wsRef.current.close(); | ||
| wsRef.current = null; | ||
| } | ||
|
|
||
| console.log(`Connecting to ${wsUrl}`); | ||
| setStatus('connecting'); | ||
| const socket = new WebSocket(wsUrl); | ||
|
|
||
| socket.onopen = () => { | ||
| if (isMounted) setStatus('connected'); | ||
| }; | ||
| socket.onclose = () => { | ||
| if (isMounted) { | ||
| setStatus('disconnected'); | ||
| reconnectTimer = setTimeout(connect, 3000); | ||
| } | ||
| }; | ||
| socket.onerror = (e) => { | ||
| console.error("WS Error", e); | ||
| socket.close(); | ||
| }; | ||
|
|
||
| wsRef.current = socket; | ||
| }; | ||
|
|
||
| // Defer to next tick so React Strict Mode's immediate unmount | ||
| // sets isMounted=false before any socket is created | ||
| const initialTimer = setTimeout(connect, 0); | ||
|
|
||
| return () => { | ||
| isMounted = false; | ||
| clearTimeout(initialTimer); | ||
| clearTimeout(reconnectTimer); | ||
| if (wsRef.current) { | ||
| // Nullify handlers to prevent cascading error/close events | ||
| wsRef.current.onopen = null; | ||
| wsRef.current.onclose = null; | ||
| wsRef.current.onerror = null; | ||
| wsRef.current.close(); | ||
| wsRef.current = null; | ||
| } | ||
| }; | ||
| }, []); | ||
|
|
||
| const send = useCallback((msg: any) => { | ||
| if (wsRef.current?.readyState === WebSocket.OPEN) { | ||
| wsRef.current.send(JSON.stringify(msg)); | ||
| } | ||
| }, []); | ||
|
|
||
| const sendCombo = useCallback((msg: string[]) => { | ||
| if (wsRef.current?.readyState === WebSocket.OPEN) { | ||
| wsRef.current.send(JSON.stringify({ | ||
| type: "combo", | ||
| keys: msg, | ||
| })); | ||
| } | ||
| }, []); | ||
|
|
||
| return { status, send, sendCombo }; | ||
| const wsRef = useRef<WebSocket | null>(null); | ||
| const [status, setStatus] = useState<"connecting" | "connected" | "disconnected">("disconnected"); | ||
| const [latency, setLatency] = useState<number | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| let isMounted = true; | ||
| const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; | ||
| const host = window.location.host; | ||
| const wsUrl = `${protocol}//${host}/ws`; | ||
|
|
||
| let reconnectTimer: NodeJS.Timeout; | ||
| let heartbeatTimer: NodeJS.Timeout; | ||
| let reconnectDelay = 1000; | ||
| const MAX_RECONNECT_DELAY = 30000; | ||
|
|
||
| const connect = () => { | ||
| if (!isMounted) return; | ||
|
|
||
| if (wsRef.current) { | ||
| wsRef.current.onopen = null; | ||
| wsRef.current.onclose = null; | ||
| wsRef.current.onerror = null; | ||
| wsRef.current.close(); | ||
| wsRef.current = null; | ||
| } | ||
|
|
||
| console.log(`Connecting to ${wsUrl}`); | ||
| setStatus("connecting"); | ||
| const socket = new WebSocket(wsUrl); | ||
| wsRef.current = socket; | ||
|
|
||
| socket.onopen = () => { | ||
| if (!isMounted) return; | ||
| setStatus("connected"); | ||
| reconnectDelay = 1000; | ||
| heartbeatTimer = setInterval(() => { | ||
| if (socket.readyState === WebSocket.OPEN) { | ||
| socket.send(JSON.stringify({ type: "ping", timestamp: Date.now() })); | ||
| } | ||
| }, 3000); | ||
| }; | ||
|
|
||
| socket.onmessage = (event) => { | ||
| try { | ||
| const msg = JSON.parse(event.data); | ||
| if (msg.type === "pong") { | ||
| const ts = Number(msg.timestamp); | ||
| const rtt = Date.now() - ts; | ||
| if (Number.isFinite(ts) && Number.isFinite(rtt) && rtt >= 0 && rtt < 60000) { | ||
| setLatency(rtt); | ||
| } | ||
| } | ||
| } catch (e) { | ||
| console.error("Error parsing WS message", e); | ||
| } | ||
| }; | ||
|
|
||
| socket.onclose = () => { | ||
| if (!isMounted) return; | ||
| setStatus("disconnected"); | ||
| setLatency(null); | ||
| wsRef.current = null; | ||
| clearInterval(heartbeatTimer); | ||
| const delay = reconnectDelay; | ||
| reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY); | ||
| reconnectTimer = setTimeout(connect, delay); | ||
| }; | ||
|
|
||
| socket.onerror = (e) => { | ||
| console.error("WS Error", e); | ||
| socket.close(); | ||
| }; | ||
| }; | ||
|
|
||
| const initialTimer = setTimeout(connect, 0); | ||
|
|
||
| return () => { | ||
| isMounted = false; | ||
| clearTimeout(initialTimer); | ||
| clearTimeout(reconnectTimer); | ||
| clearInterval(heartbeatTimer); | ||
|
|
||
| if (wsRef.current) { | ||
| wsRef.current.onopen = null; | ||
| wsRef.current.onclose = null; | ||
| wsRef.current.onerror = null; | ||
| wsRef.current.close(); | ||
| wsRef.current = null; | ||
| } | ||
| }; | ||
| }, []); | ||
|
|
||
| const send = useCallback((msg: RemoteMessage) => { | ||
| if (wsRef.current?.readyState === WebSocket.OPEN) { | ||
| wsRef.current.send(JSON.stringify(msg)); | ||
| } | ||
| }, []); | ||
|
|
||
| const sendCombo = useCallback((msg: string[]) => { | ||
| if (wsRef.current?.readyState === WebSocket.OPEN) { | ||
| wsRef.current.send( | ||
| JSON.stringify({ | ||
| type: "combo", | ||
| keys: msg, | ||
| }), | ||
| ); | ||
| } | ||
| }, []); | ||
|
Comment on lines
+103
to
+112
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# First, let's find and examine the RemoteMessage type definition
echo "=== Looking for RemoteMessage type definition ==="
rg -n --type=ts --type=tsx "type RemoteMessage|export type RemoteMessage" -A 10
echo -e "\n=== Checking sendCombo implementation in useRemoteConnection.ts ==="
sed -n '103,112p' src/hooks/useRemoteConnection.ts
echo -e "\n=== Searching for all 'combo' references in the codebase ==="
rg -n '"combo"' --type=ts --type=tsx
echo -e "\n=== Searching for RemoteMessage usage context ==="
rg -n "RemoteMessage" --type=ts --type=tsx -B 2 -A 2Repository: AOSSIE-Org/Rein Length of output: 604 🏁 Script executed: #!/bin/bash
# Find the types file
echo "=== Locating types file ==="
find . -name "types.*" -type f | grep -E "\.(ts|tsx|js|jsx)$"
echo -e "\n=== Searching for RemoteMessage definition ==="
rg -n "type RemoteMessage|RemoteMessage =" -A 15
echo -e "\n=== Searching for combo references ==="
rg -n "combo" -C 2
echo -e "\n=== Examining src/types.tsx or similar ==="
if [ -f src/types.tsx ]; then
cat -n src/types.tsx
elif [ -f src/types.ts ]; then
cat -n src/types.ts
fiRepository: AOSSIE-Org/Rein Length of output: 3073
The Add Suggested type additionexport type RemoteMessage =
| { type: "move"; dx: number; dy: number }
| { type: "scroll"; dx?: number; dy?: number }
| { type: "click"; button: "left" | "right" | "middle"; press: boolean }
| { type: "key"; key: string }
| { type: "text"; text: string }
| { type: "zoom"; delta: number }
| { type: "combo"; keys: string[] };🤖 Prompt for AI Agents |
||
|
|
||
| return { status, latency, send, sendCombo }; | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.