2025-01-05 22:23:52 +05:30
|
|
|
import React, { useCallback, useEffect, useRef, useMemo } from 'react';
|
2025-01-05 23:31:11 +05:30
|
|
|
import { unstable_batchedUpdates } from 'react-dom';
|
2024-06-14 21:56:50 +05:30
|
|
|
import { useSocketStore } from '../../context/socket';
|
|
|
|
|
import { useGlobalInfoStore } from "../../context/globalInfo";
|
2024-07-25 22:44:18 +05:30
|
|
|
import { useActionContext } from '../../context/browserActions';
|
2024-12-18 18:22:51 +05:30
|
|
|
import DatePicker from './DatePicker';
|
2024-12-19 14:04:05 +05:30
|
|
|
import Dropdown from './Dropdown';
|
2024-12-19 16:26:23 +05:30
|
|
|
import TimePicker from './TimePicker';
|
2024-12-20 19:54:05 +05:30
|
|
|
import DateTimeLocalPicker from './DateTimeLocalPicker';
|
2025-01-04 15:57:18 +05:30
|
|
|
import { FrontendPerformanceMonitor } from '../../../perf/performance';
|
2024-06-14 21:56:50 +05:30
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
// Optimized throttle with RAF
|
|
|
|
|
const rafThrottle = <T extends (...args: any[]) => any>(callback: T) => {
|
|
|
|
|
let requestId: number | null = null;
|
|
|
|
|
let lastArgs: Parameters<T>;
|
|
|
|
|
|
|
|
|
|
const later = () => {
|
|
|
|
|
requestId = null;
|
|
|
|
|
callback.apply(null, lastArgs);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (...args: Parameters<T>) => {
|
|
|
|
|
lastArgs = args;
|
|
|
|
|
if (requestId === null) {
|
|
|
|
|
requestId = requestAnimationFrame(later);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Cache DOM measurements
|
|
|
|
|
let measurementCache = new WeakMap<HTMLElement, DOMRect>();
|
|
|
|
|
const getBoundingClientRectCached = (element: HTMLElement) => {
|
|
|
|
|
let rect = measurementCache.get(element);
|
|
|
|
|
if (!rect) {
|
|
|
|
|
rect = element.getBoundingClientRect();
|
|
|
|
|
measurementCache.set(element, rect);
|
|
|
|
|
}
|
|
|
|
|
return rect;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Types (kept the same)
|
2024-06-14 21:56:50 +05:30
|
|
|
interface CreateRefCallback {
|
|
|
|
|
(ref: React.RefObject<HTMLCanvasElement>): void;
|
|
|
|
|
}
|
2024-07-23 22:28:32 +05:30
|
|
|
|
2024-06-14 21:56:50 +05:30
|
|
|
interface CanvasProps {
|
|
|
|
|
width: number;
|
|
|
|
|
height: number;
|
|
|
|
|
onCreateRef: CreateRefCallback;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface Coordinates {
|
|
|
|
|
x: number;
|
|
|
|
|
y: number;
|
2025-01-05 22:23:52 +05:30
|
|
|
}
|
|
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
// Batch updates helper
|
|
|
|
|
const batchedUpdates = (updates: Array<() => void>) => {
|
|
|
|
|
unstable_batchedUpdates(() => {
|
|
|
|
|
updates.forEach(update => update());
|
|
|
|
|
});
|
2025-01-05 22:23:52 +05:30
|
|
|
};
|
2024-07-23 22:28:32 +05:30
|
|
|
|
2025-01-05 22:23:52 +05:30
|
|
|
const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
|
2025-01-04 15:57:18 +05:30
|
|
|
const performanceMonitor = useRef(new FrontendPerformanceMonitor());
|
2024-06-14 21:56:50 +05:30
|
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
|
const { socket } = useSocketStore();
|
2024-07-23 20:08:40 +05:30
|
|
|
const { setLastAction, lastAction } = useGlobalInfoStore();
|
2024-08-08 00:41:32 +05:30
|
|
|
const { getText, getList } = useActionContext();
|
2025-01-05 23:31:11 +05:30
|
|
|
|
|
|
|
|
// Use a single ref object to reduce memory allocations
|
|
|
|
|
const refs = useRef({
|
|
|
|
|
getText,
|
|
|
|
|
getList,
|
|
|
|
|
lastMousePosition: { x: 0, y: 0 },
|
|
|
|
|
frameRequest: 0,
|
|
|
|
|
eventQueue: [] as Array<() => void>,
|
|
|
|
|
isProcessing: false
|
|
|
|
|
});
|
2024-06-14 21:56:50 +05:30
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
// Consolidated state using a single reducer
|
|
|
|
|
const [state, dispatch] = React.useReducer((state: any, action: any) => {
|
|
|
|
|
switch (action.type) {
|
|
|
|
|
case 'BATCH_UPDATE':
|
|
|
|
|
return { ...state, ...action.payload };
|
|
|
|
|
default:
|
|
|
|
|
return state;
|
|
|
|
|
}
|
|
|
|
|
}, {
|
2025-01-05 22:23:52 +05:30
|
|
|
datePickerInfo: null,
|
|
|
|
|
dropdownInfo: null,
|
|
|
|
|
timePickerInfo: null,
|
|
|
|
|
dateTimeLocalInfo: null
|
|
|
|
|
});
|
2024-12-19 16:26:23 +05:30
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
// Process events in batches
|
|
|
|
|
const processEventQueue = useCallback(() => {
|
|
|
|
|
if (refs.current.isProcessing || refs.current.eventQueue.length === 0) return;
|
|
|
|
|
|
|
|
|
|
refs.current.isProcessing = true;
|
|
|
|
|
const events = [...refs.current.eventQueue];
|
|
|
|
|
refs.current.eventQueue = [];
|
2024-12-20 19:54:05 +05:30
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
batchedUpdates(events.map(event => () => event()));
|
|
|
|
|
|
|
|
|
|
refs.current.isProcessing = false;
|
|
|
|
|
|
|
|
|
|
if (refs.current.eventQueue.length > 0) {
|
|
|
|
|
requestAnimationFrame(processEventQueue);
|
2025-01-05 22:23:52 +05:30
|
|
|
}
|
2025-01-05 23:31:11 +05:30
|
|
|
}, []);
|
2024-07-25 22:44:50 +05:30
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
// Optimized mouse move handler using RAF throttle
|
|
|
|
|
const handleMouseMove = useMemo(
|
|
|
|
|
() => rafThrottle((coordinates: Coordinates) => {
|
2025-01-05 22:23:52 +05:30
|
|
|
if (!socket) return;
|
2024-07-26 04:19:19 +05:30
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
const current = refs.current.lastMousePosition;
|
|
|
|
|
if (current.x !== coordinates.x || current.y !== coordinates.y) {
|
|
|
|
|
refs.current.lastMousePosition = coordinates;
|
2025-01-05 22:23:52 +05:30
|
|
|
socket.emit('input:mousemove', coordinates);
|
2025-01-05 23:31:11 +05:30
|
|
|
refs.current.eventQueue.push(() => setLastAction('move'));
|
|
|
|
|
requestAnimationFrame(processEventQueue);
|
2025-01-05 22:23:52 +05:30
|
|
|
}
|
2025-01-05 23:31:11 +05:30
|
|
|
}),
|
|
|
|
|
[socket, processEventQueue]
|
2025-01-05 22:23:52 +05:30
|
|
|
);
|
2024-12-18 18:22:51 +05:30
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
// Optimized event handler with better performance characteristics
|
2025-01-05 22:23:52 +05:30
|
|
|
const onMouseEvent = useCallback((event: MouseEvent) => {
|
|
|
|
|
if (!socket || !canvasRef.current) return;
|
2024-12-19 14:04:05 +05:30
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
performanceMonitor.current.measureEventLatency(event);
|
|
|
|
|
const rect = getBoundingClientRectCached(canvasRef.current);
|
|
|
|
|
const coordinates = {
|
2025-01-05 22:23:52 +05:30
|
|
|
x: event.clientX - rect.left,
|
|
|
|
|
y: event.clientY - rect.top,
|
|
|
|
|
};
|
2024-12-19 16:26:23 +05:30
|
|
|
|
2025-01-05 22:23:52 +05:30
|
|
|
switch (event.type) {
|
|
|
|
|
case 'mousedown':
|
2025-01-05 23:31:11 +05:30
|
|
|
refs.current.eventQueue.push(() => {
|
|
|
|
|
if (refs.current.getText) {
|
|
|
|
|
console.log('Capturing Text...');
|
|
|
|
|
} else if (refs.current.getList) {
|
|
|
|
|
console.log('Capturing List...');
|
|
|
|
|
} else {
|
|
|
|
|
socket.emit('input:mousedown', coordinates);
|
|
|
|
|
}
|
|
|
|
|
setLastAction('click');
|
|
|
|
|
});
|
2025-01-05 22:23:52 +05:30
|
|
|
break;
|
2024-12-20 19:54:05 +05:30
|
|
|
|
2025-01-05 22:23:52 +05:30
|
|
|
case 'mousemove':
|
2025-01-05 23:31:11 +05:30
|
|
|
handleMouseMove(coordinates);
|
2025-01-05 22:23:52 +05:30
|
|
|
break;
|
2024-12-18 18:22:51 +05:30
|
|
|
|
2025-01-05 22:23:52 +05:30
|
|
|
case 'wheel':
|
2025-01-05 23:31:11 +05:30
|
|
|
if (refs.current.frameRequest) {
|
|
|
|
|
cancelAnimationFrame(refs.current.frameRequest);
|
2025-01-05 22:23:52 +05:30
|
|
|
}
|
2025-01-05 23:31:11 +05:30
|
|
|
refs.current.frameRequest = requestAnimationFrame(() => {
|
2024-06-14 21:56:50 +05:30
|
|
|
const wheelEvent = event as WheelEvent;
|
2025-01-05 22:23:52 +05:30
|
|
|
socket.emit('input:wheel', {
|
2024-06-14 21:56:50 +05:30
|
|
|
deltaX: Math.round(wheelEvent.deltaX),
|
2025-01-05 23:31:11 +05:30
|
|
|
deltaY: Math.round(wheelEvent.deltaY)
|
2025-01-05 22:23:52 +05:30
|
|
|
});
|
2025-01-05 23:31:11 +05:30
|
|
|
refs.current.eventQueue.push(() => setLastAction('scroll'));
|
2025-01-05 22:23:52 +05:30
|
|
|
});
|
|
|
|
|
break;
|
2024-06-14 21:56:50 +05:30
|
|
|
}
|
|
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
requestAnimationFrame(processEventQueue);
|
|
|
|
|
}, [socket, handleMouseMove, processEventQueue]);
|
2025-01-05 22:23:52 +05:30
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
// Optimized keyboard handler
|
|
|
|
|
const onKeyboardEvent = useMemo(
|
|
|
|
|
() => rafThrottle((event: KeyboardEvent) => {
|
|
|
|
|
if (!socket) return;
|
2025-01-05 22:23:52 +05:30
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
refs.current.eventQueue.push(() => {
|
|
|
|
|
switch (event.type) {
|
|
|
|
|
case 'keydown':
|
|
|
|
|
socket.emit('input:keydown', {
|
|
|
|
|
key: event.key,
|
|
|
|
|
coordinates: refs.current.lastMousePosition
|
|
|
|
|
});
|
|
|
|
|
setLastAction(`${event.key} pressed`);
|
|
|
|
|
break;
|
|
|
|
|
case 'keyup':
|
|
|
|
|
socket.emit('input:keyup', event.key);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
requestAnimationFrame(processEventQueue);
|
|
|
|
|
}),
|
|
|
|
|
[socket, processEventQueue]
|
|
|
|
|
);
|
2024-07-23 10:19:11 +05:30
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
// Update refs
|
2025-01-04 15:57:18 +05:30
|
|
|
useEffect(() => {
|
2025-01-05 23:31:11 +05:30
|
|
|
refs.current.getText = getText;
|
|
|
|
|
refs.current.getList = getList;
|
2025-01-05 22:23:52 +05:30
|
|
|
}, [getText, getList]);
|
|
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
// Socket event setup with optimized cleanup
|
2025-01-05 22:23:52 +05:30
|
|
|
useEffect(() => {
|
|
|
|
|
if (!socket) return;
|
|
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
const handlers = {
|
|
|
|
|
showDatePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { datePickerInfo: info } }),
|
|
|
|
|
showDropdown: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { dropdownInfo: info } }),
|
|
|
|
|
showTimePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { timePickerInfo: info } }),
|
|
|
|
|
showDateTimePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { dateTimeLocalInfo: info } })
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Object.entries(handlers).forEach(([event, handler]) => {
|
2025-01-05 22:23:52 +05:30
|
|
|
socket.on(event, handler);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
2025-01-05 23:31:11 +05:30
|
|
|
Object.keys(handlers).forEach(event => {
|
2025-01-05 22:23:52 +05:30
|
|
|
socket.off(event);
|
|
|
|
|
});
|
|
|
|
|
};
|
2025-01-05 23:31:11 +05:30
|
|
|
}, [socket]);
|
2025-01-05 22:23:52 +05:30
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const monitor = performanceMonitor.current;
|
2025-01-04 15:57:18 +05:30
|
|
|
const intervalId = setInterval(() => {
|
2025-01-05 23:31:11 +05:30
|
|
|
console.log('Frontend Performance Report:', monitor.getPerformanceReport());
|
|
|
|
|
}, 15000); // Increased to 15 seconds
|
|
|
|
|
|
2025-01-05 22:23:52 +05:30
|
|
|
return () => {
|
|
|
|
|
clearInterval(intervalId);
|
2025-01-05 23:31:11 +05:30
|
|
|
if (refs.current.frameRequest) {
|
|
|
|
|
cancelAnimationFrame(refs.current.frameRequest);
|
2025-01-05 22:23:52 +05:30
|
|
|
}
|
2025-01-05 23:31:11 +05:30
|
|
|
|
|
|
|
|
// Clear measurement cache on unmount
|
|
|
|
|
measurementCache = new WeakMap(); // Reset the WeakMap
|
2025-01-05 22:23:52 +05:30
|
|
|
};
|
2025-01-04 15:57:18 +05:30
|
|
|
}, []);
|
2025-01-05 23:31:11 +05:30
|
|
|
|
2024-06-14 21:56:50 +05:30
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
// Canvas setup with optimized event binding
|
2024-06-14 21:56:50 +05:30
|
|
|
useEffect(() => {
|
2025-01-05 22:23:52 +05:30
|
|
|
if (!canvasRef.current) return;
|
2024-07-23 22:28:32 +05:30
|
|
|
|
2025-01-05 22:23:52 +05:30
|
|
|
onCreateRef(canvasRef);
|
|
|
|
|
const canvas = canvasRef.current;
|
2025-01-05 23:31:11 +05:30
|
|
|
|
|
|
|
|
const options = { passive: true };
|
|
|
|
|
canvas.addEventListener('mousedown', onMouseEvent, options);
|
|
|
|
|
canvas.addEventListener('mousemove', onMouseEvent, options);
|
|
|
|
|
canvas.addEventListener('wheel', onMouseEvent, options);
|
|
|
|
|
canvas.addEventListener('keydown', onKeyboardEvent, options);
|
|
|
|
|
canvas.addEventListener('keyup', onKeyboardEvent, options);
|
2024-07-23 22:28:32 +05:30
|
|
|
|
2025-01-05 22:23:52 +05:30
|
|
|
return () => {
|
|
|
|
|
canvas.removeEventListener('mousedown', onMouseEvent);
|
|
|
|
|
canvas.removeEventListener('mousemove', onMouseEvent);
|
|
|
|
|
canvas.removeEventListener('wheel', onMouseEvent);
|
|
|
|
|
canvas.removeEventListener('keydown', onKeyboardEvent);
|
|
|
|
|
canvas.removeEventListener('keyup', onKeyboardEvent);
|
|
|
|
|
};
|
|
|
|
|
}, [onMouseEvent, onKeyboardEvent, onCreateRef]);
|
2024-06-14 21:56:50 +05:30
|
|
|
|
2025-01-05 23:31:11 +05:30
|
|
|
const memoizedSize = useMemo(() => ({
|
|
|
|
|
width: width || 900,
|
|
|
|
|
height: height || 400
|
|
|
|
|
}), [width, height]);
|
|
|
|
|
|
2024-06-14 21:56:50 +05:30
|
|
|
return (
|
2025-01-05 22:23:52 +05:30
|
|
|
<div className="relative bg-white rounded-b-md overflow-hidden">
|
2024-10-23 08:01:38 +05:30
|
|
|
<canvas
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
ref={canvasRef}
|
2025-01-05 23:31:11 +05:30
|
|
|
height={memoizedSize.height}
|
|
|
|
|
width={memoizedSize.width}
|
2025-01-05 22:23:52 +05:30
|
|
|
className="block"
|
2024-10-23 08:01:21 +05:30
|
|
|
/>
|
2025-01-05 22:23:52 +05:30
|
|
|
{state.datePickerInfo && (
|
2024-12-18 18:22:51 +05:30
|
|
|
<DatePicker
|
2025-01-05 22:23:52 +05:30
|
|
|
coordinates={state.datePickerInfo.coordinates}
|
|
|
|
|
selector={state.datePickerInfo.selector}
|
2025-01-05 23:31:11 +05:30
|
|
|
onClose={() => dispatch({
|
|
|
|
|
type: 'BATCH_UPDATE',
|
|
|
|
|
payload: { datePickerInfo: null }
|
|
|
|
|
})}
|
2024-12-19 14:04:05 +05:30
|
|
|
/>
|
|
|
|
|
)}
|
2025-01-05 22:23:52 +05:30
|
|
|
{state.timePickerInfo && (
|
2024-12-19 16:26:23 +05:30
|
|
|
<TimePicker
|
2025-01-05 22:23:52 +05:30
|
|
|
coordinates={state.timePickerInfo.coordinates}
|
|
|
|
|
selector={state.timePickerInfo.selector}
|
|
|
|
|
onClose={() => dispatch({ type: 'SET_TIME_PICKER', payload: null })}
|
2024-12-19 16:26:23 +05:30
|
|
|
/>
|
|
|
|
|
)}
|
2025-01-05 22:23:52 +05:30
|
|
|
{state.dateTimeLocalInfo && (
|
2024-12-20 19:54:05 +05:30
|
|
|
<DateTimeLocalPicker
|
2025-01-05 22:23:52 +05:30
|
|
|
coordinates={state.dateTimeLocalInfo.coordinates}
|
|
|
|
|
selector={state.dateTimeLocalInfo.selector}
|
|
|
|
|
onClose={() => dispatch({ type: 'SET_DATETIME_PICKER', payload: null })}
|
2024-12-20 19:54:05 +05:30
|
|
|
/>
|
|
|
|
|
)}
|
2024-10-23 08:01:38 +05:30
|
|
|
</div>
|
2024-06-14 21:56:50 +05:30
|
|
|
);
|
2025-01-05 22:23:52 +05:30
|
|
|
});
|
2024-07-23 22:28:32 +05:30
|
|
|
|
2025-01-05 22:23:52 +05:30
|
|
|
Canvas.displayName = 'Canvas';
|
2024-07-23 22:28:32 +05:30
|
|
|
|
2024-07-13 22:21:42 +05:30
|
|
|
export default Canvas;
|