Added back Remote Cursors

This commit is contained in:
ExilProductions
2026-01-16 17:06:43 +01:00
parent fa0762643e
commit 8eed0a0a92
10 changed files with 307 additions and 34 deletions

View File

@@ -1,14 +1,14 @@
import { io, Socket } from 'socket.io-client';
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { createContext, useContext, useEffect, useState, ReactNode, useCallback, useRef } from 'react';
import { useAuth } from './AuthContext';
import { Pixel, Cursor } from '../types';
import { Pixel, Cursor, PaintAck, PendingPixel } from '../types';
interface SocketContextType {
socket: Socket | null;
isConnected: boolean;
cursors: Cursor[];
sendCursorMove: (x: number, y: number) => void;
sendPaintPixels: (pixels: Pixel[]) => void;
sendPaintPixels: (pixels: Pixel[], onAck?: (ack: PaintAck) => void) => void;
}
const SocketContext = createContext<SocketContextType | undefined>(undefined);
@@ -18,16 +18,15 @@ export function SocketProvider({ children }: { children: ReactNode }) {
const [isConnected, setIsConnected] = useState(false);
const [cursors, setCursors] = useState<Cursor[]>([]);
const { user } = useAuth();
const pendingPixelsRef = useRef<Map<string, PendingPixel>>(new Map());
useEffect(() => {
// Only connect sock if user is authed
if (!user) {
setSocket(null);
setIsConnected(false);
return;
}
// Get socket auth token first
fetch('/api/socket-token', {
credentials: 'include'
})
@@ -39,14 +38,12 @@ export function SocketProvider({ children }: { children: ReactNode }) {
})
.then(data => {
if (data.token) {
// Conn with token
const newSocket = io('', {
transports: ['websocket']
});
newSocket.on('connect', () => {
setIsConnected(true);
// auth when logged in
newSocket.emit('join-canvas', data.token);
});
@@ -68,11 +65,9 @@ export function SocketProvider({ children }: { children: ReactNode }) {
newSocket.on('error', (message: string) => {
console.error('Socket error:', message);
// If auth fails we disconnect and retry after delay
if (message.includes('Authentication required') || message.includes('expired')) {
newSocket.disconnect();
setTimeout(() => {
// Trigger reconn
}, 2000);
}
});
@@ -94,15 +89,74 @@ export function SocketProvider({ children }: { children: ReactNode }) {
});
}, [user]);
const resendPendingPixels = useCallback((socket: Socket) => {
const now = Date.now();
const maxRetries = 3;
const retryDelay = 1000;
for (const [key, pending] of pendingPixelsRef.current) {
if (pending.retries >= maxRetries) {
pendingPixelsRef.current.delete(key);
window.dispatchEvent(new CustomEvent('pixel-sync-failed', { detail: pending.pixel }));
continue;
}
if (now - pending.timestamp > retryDelay * (pending.retries + 1)) {
pending.retries++;
pending.timestamp = now;
socket.emit('paint-pixels', {
pixels: [pending.pixel],
timestamp: pending.timestamp
}, (ack: PaintAck) => {
if (ack.success) {
pendingPixelsRef.current.delete(key);
}
});
}
}
}, []);
useEffect(() => {
if (!socket || !isConnected) return;
const interval = setInterval(() => {
if (pendingPixelsRef.current.size > 0) {
resendPendingPixels(socket);
}
}, 500);
return () => clearInterval(interval);
}, [socket, isConnected, resendPendingPixels]);
const sendCursorMove = (x: number, y: number) => {
if (socket && isConnected) {
socket.emit('cursor-move', { x, y });
}
};
const sendPaintPixels = (pixels: Pixel[]) => {
const sendPaintPixels = (pixels: Pixel[], onAck?: (ack: PaintAck) => void) => {
if (socket && isConnected) {
socket.emit('paint-pixels', pixels);
const timestamp = Date.now();
const key = `${pixels[0]?.x},${pixels[0]?.y}`;
socket.emit('paint-pixels', { pixels, timestamp }, (ack: PaintAck) => {
if (onAck) {
onAck(ack);
}
if (ack.success) {
pendingPixelsRef.current.delete(key);
} else if (ack.conflicts > 0 && ack.pixels) {
for (const pixel of ack.pixels) {
const pixelKey = `${pixel.x},${pixel.y}`;
pendingPixelsRef.current.set(pixelKey, {
pixel,
timestamp: Date.now(),
retries: 0
});
}
}
});
}
};