Files
parcer/src/components/atoms/canvas.tsx

380 lines
12 KiB
TypeScript
Raw Normal View History

import React, { useCallback, useEffect, useRef, useMemo, Suspense } from 'react';
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';
const DatePicker = React.lazy(() => import('./DatePicker'));
const Dropdown = React.lazy(() => import('./Dropdown'));
const TimePicker = React.lazy(() => import('./TimePicker'));
const DateTimeLocalPicker = React.lazy(() => import('./DateTimeLocalPicker'));
class RAFScheduler {
private queue: Set<() => void> = new Set();
private isProcessing: boolean = false;
private frameId: number | null = null;
schedule(callback: () => void): void {
this.queue.add(callback);
if (!this.isProcessing) {
this.process();
}
}
private process = (): void => {
this.isProcessing = true;
this.frameId = requestAnimationFrame(() => {
const callbacks = Array.from(this.queue);
this.queue.clear();
2025-01-06 02:01:20 +05:30
callbacks.forEach(callback => {
try {
callback();
} catch (error) {
console.error('RAF Scheduler error:', error);
}
});
this.isProcessing = false;
this.frameId = null;
2025-01-06 02:01:20 +05:30
if (this.queue.size > 0) {
this.process();
}
});
}
clear(): void {
this.queue.clear();
if (this.frameId !== null) {
cancelAnimationFrame(this.frameId);
this.frameId = null;
}
this.isProcessing = false;
}
}
class EventDebouncer {
private highPriorityQueue: Array<() => void> = [];
private lowPriorityQueue: Array<() => void> = [];
private processing: boolean = false;
private scheduler: RAFScheduler;
constructor(scheduler: RAFScheduler) {
this.scheduler = scheduler;
}
add(callback: () => void, highPriority: boolean = false): void {
if (highPriority) {
this.highPriorityQueue.push(callback);
} else {
this.lowPriorityQueue.push(callback);
}
if (!this.processing) {
this.process();
}
}
private process(): void {
this.processing = true;
this.scheduler.schedule(() => {
while (this.highPriorityQueue.length > 0) {
const callback = this.highPriorityQueue.shift();
callback?.();
}
if (this.lowPriorityQueue.length > 0) {
const callback = this.lowPriorityQueue.shift();
callback?.();
2025-01-06 02:01:20 +05:30
if (this.lowPriorityQueue.length > 0) {
this.process();
}
}
2025-01-06 02:01:20 +05:30
this.processing = false;
});
}
clear(): void {
this.highPriorityQueue = [];
this.lowPriorityQueue = [];
this.processing = false;
}
}
// Optimized measurement cache with LRU
class MeasurementCache {
private cache: Map<HTMLElement, DOMRect>;
private maxSize: number;
constructor(maxSize: number = 100) {
this.cache = new Map();
this.maxSize = maxSize;
}
get(element: HTMLElement): DOMRect | undefined {
const cached = this.cache.get(element);
if (cached) {
// Refresh the entry
this.cache.delete(element);
this.cache.set(element, cached);
}
return cached;
}
set(element: HTMLElement, rect: DOMRect): void {
if (this.cache.size >= this.maxSize) {
// Remove oldest entry
const firstKey = this.cache.keys().next().value;
if (firstKey !== undefined) {
this.cache.delete(firstKey);
}
}
this.cache.set(element, rect);
}
clear(): void {
this.cache.clear();
}
2024-06-14 21:56:50 +05:30
}
2024-06-14 21:56:50 +05:30
interface CanvasProps {
width: number;
height: number;
onCreateRef: (ref: React.RefObject<HTMLCanvasElement>) => void;
2024-06-14 21:56:50 +05:30
}
/**
* Interface for mouse's x,y coordinates
*/
export interface Coordinates {
x: number;
y: number;
};
2025-01-05 22:23:52 +05:30
const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
2024-06-14 21:56:50 +05:30
const canvasRef = useRef<HTMLCanvasElement>(null);
const { socket } = useSocketStore();
2025-01-06 20:18:27 +05:30
const { setLastAction, lastAction } = useGlobalInfoStore();
2024-08-08 00:41:32 +05:30
const { getText, getList } = useActionContext();
const scheduler = useRef(new RAFScheduler());
const debouncer = useRef(new EventDebouncer(scheduler.current));
const measurementCache = useRef(new MeasurementCache(50));
//const performanceMonitor = useRef(new FrontendPerformanceMonitor());
const refs = useRef({
getText,
getList,
lastMousePosition: { x: 0, y: 0 },
lastFrameTime: 0,
context: null as CanvasRenderingContext2D | null,
});
2024-06-14 21:56:50 +05:30
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
});
const getEventCoordinates = useCallback((event: MouseEvent): { x: number; y: number } => {
if (!canvasRef.current) return { x: 0, y: 0 };
let rect = measurementCache.current.get(canvasRef.current);
if (!rect) {
rect = canvasRef.current.getBoundingClientRect();
measurementCache.current.set(canvasRef.current, rect);
2025-01-05 22:23:52 +05:30
}
2024-07-25 22:44:50 +05:30
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}, []);
const handleMouseEvent = useCallback((event: MouseEvent) => {
2025-01-05 22:23:52 +05:30
if (!socket || !canvasRef.current) return;
//performanceMonitor.current.measureEventLatency(event);
const coordinates = getEventCoordinates(event);
2025-01-05 22:23:52 +05:30
switch (event.type) {
case 'mousedown':
debouncer.current.add(() => {
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');
}, true); // High priority
2025-01-05 22:23:52 +05:30
break;
2025-01-05 22:23:52 +05:30
case 'mousemove':
if (refs.current.lastMousePosition.x !== coordinates.x ||
refs.current.lastMousePosition.y !== coordinates.y) {
debouncer.current.add(() => {
refs.current.lastMousePosition = coordinates;
socket.emit('input:mousemove', coordinates);
setLastAction('move');
});
}
2025-01-05 22:23:52 +05:30
break;
2025-01-05 22:23:52 +05:30
case 'wheel':
const wheelEvent = event as WheelEvent;
debouncer.current.add(() => {
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),
deltaY: Math.round(wheelEvent.deltaY)
2025-01-05 22:23:52 +05:30
});
setLastAction('scroll');
2025-01-05 22:23:52 +05:30
});
break;
2024-06-14 21:56:50 +05:30
}
}, [socket, getEventCoordinates]);
2025-01-05 22:23:52 +05:30
const handleKeyboardEvent = useCallback((event: KeyboardEvent) => {
if (!socket) return;
2024-07-23 10:19:11 +05:30
debouncer.current.add(() => {
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;
}
}, event.type === 'keydown'); // High priority for keydown
}, [socket]);
2025-01-05 22:23:52 +05:30
// Setup and cleanup
2025-01-05 22:23:52 +05:30
useEffect(() => {
if (!canvasRef.current) return;
const canvas = canvasRef.current;
refs.current.context = canvas.getContext('2d', {
alpha: false,
desynchronized: true
2025-01-05 22:23:52 +05:30
});
onCreateRef(canvasRef);
const options = { passive: true };
canvas.addEventListener('mousedown', handleMouseEvent, options);
canvas.addEventListener('mousemove', handleMouseEvent, options);
canvas.addEventListener('wheel', handleMouseEvent, options);
canvas.addEventListener('keydown', handleKeyboardEvent, options);
canvas.addEventListener('keyup', handleKeyboardEvent, options);
2025-01-05 22:23:52 +05:30
return () => {
canvas.removeEventListener('mousedown', handleMouseEvent);
canvas.removeEventListener('mousemove', handleMouseEvent);
canvas.removeEventListener('wheel', handleMouseEvent);
canvas.removeEventListener('keydown', handleKeyboardEvent);
canvas.removeEventListener('keyup', handleKeyboardEvent);
2025-01-06 02:01:20 +05:30
scheduler.current.clear();
debouncer.current.clear();
measurementCache.current.clear();
2025-01-05 22:23:52 +05:30
};
}, [handleMouseEvent, handleKeyboardEvent, onCreateRef]);
2025-01-05 22:23:52 +05:30
// Performance monitoring
// useEffect(() => {
// const intervalId = setInterval(() => {
// console.log('Performance Report:', performanceMonitor.current.getPerformanceReport());
// }, 20000);
// return () => clearInterval(intervalId);
// }, []);
2024-06-14 21:56:50 +05:30
useEffect(() => {
if (!socket) return;
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]) => socket.on(event, handler));
2025-01-05 22:23:52 +05:30
return () => {
Object.keys(handlers).forEach(event => socket.off(event));
2025-01-05 22:23:52 +05:30
};
}, [socket]);
2024-06-14 21:56:50 +05:30
const memoizedDimensions = 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}
height={memoizedDimensions.height}
width={memoizedDimensions.width}
2025-01-05 22:23:52 +05:30
className="block"
/>
<Suspense fallback={null}>
2025-01-06 02:01:20 +05:30
{state.datePickerInfo && (
<DatePicker
coordinates={state.datePickerInfo.coordinates}
selector={state.datePickerInfo.selector}
onClose={() => dispatch({
type: 'BATCH_UPDATE',
payload: { datePickerInfo: null }
})}
/>
)}
2025-01-06 20:18:27 +05:30
{state.dropdownInfo && (
<Dropdown
coordinates={state.dropdownInfo.coordinates}
selector={state.dropdownInfo.selector}
options={state.dropdownInfo.options}
onClose={() => dispatch({
type: 'BATCH_UPDATE',
payload: { dropdownInfo: null }
})}
/>
)}
2025-01-06 02:01:20 +05:30
{state.timePickerInfo && (
<TimePicker
coordinates={state.timePickerInfo.coordinates}
selector={state.timePickerInfo.selector}
onClose={() => dispatch({ type: 'SET_TIME_PICKER', payload: null })}
/>
)}
{state.dateTimeLocalInfo && (
<DateTimeLocalPicker
coordinates={state.dateTimeLocalInfo.coordinates}
selector={state.dateTimeLocalInfo.selector}
onClose={() => dispatch({ type: 'SET_DATETIME_PICKER', payload: null })}
/>
)}
</Suspense>
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
});
2025-01-05 22:23:52 +05:30
Canvas.displayName = 'Canvas';
2024-07-13 22:21:42 +05:30
export default Canvas;