Merge pull request #313 from getmaxun/performance
feat: recorder performance improvements
This commit is contained in:
@@ -44,6 +44,7 @@
|
|||||||
"joi": "^17.6.0",
|
"joi": "^17.6.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"loglevel": "^1.8.0",
|
"loglevel": "^1.8.0",
|
||||||
"loglevel-plugin-remote": "^0.6.8",
|
"loglevel-plugin-remote": "^0.6.8",
|
||||||
"maxun-core": "^0.0.7",
|
"maxun-core": "^0.0.7",
|
||||||
@@ -66,6 +67,7 @@
|
|||||||
"react-transition-group": "^4.4.2",
|
"react-transition-group": "^4.4.2",
|
||||||
"sequelize": "^6.37.3",
|
"sequelize": "^6.37.3",
|
||||||
"sequelize-typescript": "^2.1.6",
|
"sequelize-typescript": "^2.1.6",
|
||||||
|
"sharp": "^0.33.5",
|
||||||
"socket.io": "^4.4.1",
|
"socket.io": "^4.4.1",
|
||||||
"socket.io-client": "^4.4.1",
|
"socket.io-client": "^4.4.1",
|
||||||
"styled-components": "^5.3.3",
|
"styled-components": "^5.3.3",
|
||||||
@@ -97,6 +99,7 @@
|
|||||||
"@types/cookie-parser": "^1.4.7",
|
"@types/cookie-parser": "^1.4.7",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
"@types/lodash": "^4.17.14",
|
||||||
"@types/loglevel": "^1.6.3",
|
"@types/loglevel": "^1.6.3",
|
||||||
"@types/node": "22.7.9",
|
"@types/node": "22.7.9",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
|
|||||||
181
perf/performance.ts
Normal file
181
perf/performance.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
// Frontend Performance Monitoring
|
||||||
|
export class FrontendPerformanceMonitor {
|
||||||
|
private metrics: {
|
||||||
|
fps: number[];
|
||||||
|
memoryUsage: MemoryInfo[];
|
||||||
|
renderTime: number[];
|
||||||
|
eventLatency: number[];
|
||||||
|
};
|
||||||
|
private lastFrameTime: number;
|
||||||
|
private frameCount: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.metrics = {
|
||||||
|
fps: [],
|
||||||
|
memoryUsage: [],
|
||||||
|
renderTime: [],
|
||||||
|
eventLatency: [],
|
||||||
|
};
|
||||||
|
this.lastFrameTime = performance.now();
|
||||||
|
this.frameCount = 0;
|
||||||
|
|
||||||
|
// Start monitoring
|
||||||
|
this.startMonitoring();
|
||||||
|
}
|
||||||
|
|
||||||
|
private startMonitoring(): void {
|
||||||
|
// Monitor FPS
|
||||||
|
const measureFPS = () => {
|
||||||
|
const currentTime = performance.now();
|
||||||
|
const elapsed = currentTime - this.lastFrameTime;
|
||||||
|
this.frameCount++;
|
||||||
|
|
||||||
|
if (elapsed >= 1000) { // Calculate FPS every second
|
||||||
|
const fps = Math.round((this.frameCount * 1000) / elapsed);
|
||||||
|
this.metrics.fps.push(fps);
|
||||||
|
this.frameCount = 0;
|
||||||
|
this.lastFrameTime = currentTime;
|
||||||
|
}
|
||||||
|
requestAnimationFrame(measureFPS);
|
||||||
|
};
|
||||||
|
requestAnimationFrame(measureFPS);
|
||||||
|
|
||||||
|
// Monitor Memory Usage
|
||||||
|
if (window.performance && (performance as any).memory) {
|
||||||
|
setInterval(() => {
|
||||||
|
const memory = (performance as any).memory;
|
||||||
|
this.metrics.memoryUsage.push({
|
||||||
|
usedJSHeapSize: memory.usedJSHeapSize,
|
||||||
|
totalJSHeapSize: memory.totalJSHeapSize,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor Canvas Render Time
|
||||||
|
public measureRenderTime(renderFunction: () => void): void {
|
||||||
|
const startTime = performance.now();
|
||||||
|
renderFunction();
|
||||||
|
const endTime = performance.now();
|
||||||
|
this.metrics.renderTime.push(endTime - startTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor Event Latency
|
||||||
|
public measureEventLatency(event: MouseEvent | KeyboardEvent): void {
|
||||||
|
const latency = performance.now() - event.timeStamp;
|
||||||
|
this.metrics.eventLatency.push(latency);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Performance Report
|
||||||
|
public getPerformanceReport(): PerformanceReport {
|
||||||
|
return {
|
||||||
|
averageFPS: this.calculateAverage(this.metrics.fps),
|
||||||
|
averageRenderTime: this.calculateAverage(this.metrics.renderTime),
|
||||||
|
averageEventLatency: this.calculateAverage(this.metrics.eventLatency),
|
||||||
|
memoryTrend: this.getMemoryTrend(),
|
||||||
|
lastMemoryUsage: this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateAverage(array: number[]): number {
|
||||||
|
return array.length ? array.reduce((a, b) => a + b) / array.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMemoryTrend(): MemoryTrend {
|
||||||
|
if (this.metrics.memoryUsage.length < 2) return 'stable';
|
||||||
|
const latest = this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1];
|
||||||
|
const previous = this.metrics.memoryUsage[this.metrics.memoryUsage.length - 2];
|
||||||
|
const change = latest.usedJSHeapSize - previous.usedJSHeapSize;
|
||||||
|
if (change > 1000000) return 'increasing'; // 1MB threshold
|
||||||
|
if (change < -1000000) return 'decreasing';
|
||||||
|
return 'stable';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend Performance Monitoring
|
||||||
|
export class BackendPerformanceMonitor {
|
||||||
|
private metrics: {
|
||||||
|
screenshotTimes: number[];
|
||||||
|
emitTimes: number[];
|
||||||
|
memoryUsage: NodeJS.MemoryUsage[];
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.metrics = {
|
||||||
|
screenshotTimes: [],
|
||||||
|
emitTimes: [],
|
||||||
|
memoryUsage: []
|
||||||
|
};
|
||||||
|
this.startMonitoring();
|
||||||
|
}
|
||||||
|
|
||||||
|
private startMonitoring(): void {
|
||||||
|
// Monitor Memory Usage
|
||||||
|
setInterval(() => {
|
||||||
|
this.metrics.memoryUsage.push(process.memoryUsage());
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async measureScreenshotPerformance(
|
||||||
|
makeScreenshot: () => Promise<void>
|
||||||
|
): Promise<void> {
|
||||||
|
const startTime = process.hrtime();
|
||||||
|
await makeScreenshot();
|
||||||
|
const [seconds, nanoseconds] = process.hrtime(startTime);
|
||||||
|
this.metrics.screenshotTimes.push(seconds * 1000 + nanoseconds / 1000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public measureEmitPerformance(emitFunction: () => void): void {
|
||||||
|
const startTime = process.hrtime();
|
||||||
|
emitFunction();
|
||||||
|
const [seconds, nanoseconds] = process.hrtime(startTime);
|
||||||
|
this.metrics.emitTimes.push(seconds * 1000 + nanoseconds / 1000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPerformanceReport(): BackendPerformanceReport {
|
||||||
|
return {
|
||||||
|
averageScreenshotTime: this.calculateAverage(this.metrics.screenshotTimes),
|
||||||
|
averageEmitTime: this.calculateAverage(this.metrics.emitTimes),
|
||||||
|
currentMemoryUsage: this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1],
|
||||||
|
memoryTrend: this.getMemoryTrend()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateAverage(array: number[]): number {
|
||||||
|
return array.length ? array.reduce((a, b) => a + b) / array.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMemoryTrend(): MemoryTrend {
|
||||||
|
if (this.metrics.memoryUsage.length < 2) return 'stable';
|
||||||
|
const latest = this.metrics.memoryUsage[this.metrics.memoryUsage.length - 1];
|
||||||
|
const previous = this.metrics.memoryUsage[this.metrics.memoryUsage.length - 2];
|
||||||
|
const change = latest.heapUsed - previous.heapUsed;
|
||||||
|
if (change > 1000000) return 'increasing';
|
||||||
|
if (change < -1000000) return 'decreasing';
|
||||||
|
return 'stable';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MemoryInfo {
|
||||||
|
usedJSHeapSize: number;
|
||||||
|
totalJSHeapSize: number;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemoryTrend = 'increasing' | 'decreasing' | 'stable';
|
||||||
|
|
||||||
|
interface PerformanceReport {
|
||||||
|
averageFPS: number;
|
||||||
|
averageRenderTime: number;
|
||||||
|
averageEventLatency: number;
|
||||||
|
memoryTrend: MemoryTrend;
|
||||||
|
lastMemoryUsage: MemoryInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BackendPerformanceReport {
|
||||||
|
averageScreenshotTime: number;
|
||||||
|
averageEmitTime: number;
|
||||||
|
currentMemoryUsage: NodeJS.MemoryUsage;
|
||||||
|
memoryTrend: MemoryTrend;
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import { chromium } from 'playwright-extra';
|
|||||||
import stealthPlugin from 'puppeteer-extra-plugin-stealth';
|
import stealthPlugin from 'puppeteer-extra-plugin-stealth';
|
||||||
import { PlaywrightBlocker } from '@cliqz/adblocker-playwright';
|
import { PlaywrightBlocker } from '@cliqz/adblocker-playwright';
|
||||||
import fetch from 'cross-fetch';
|
import fetch from 'cross-fetch';
|
||||||
|
import { throttle } from 'lodash';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
import { InterpreterSettings, RemoteBrowserOptions } from "../../types";
|
import { InterpreterSettings, RemoteBrowserOptions } from "../../types";
|
||||||
@@ -16,8 +18,30 @@ import { WorkflowGenerator } from "../../workflow-management/classes/Generator";
|
|||||||
import { WorkflowInterpreter } from "../../workflow-management/classes/Interpreter";
|
import { WorkflowInterpreter } from "../../workflow-management/classes/Interpreter";
|
||||||
import { getDecryptedProxyConfig } from '../../routes/proxy';
|
import { getDecryptedProxyConfig } from '../../routes/proxy';
|
||||||
import { getInjectableScript } from 'idcac-playwright';
|
import { getInjectableScript } from 'idcac-playwright';
|
||||||
|
|
||||||
chromium.use(stealthPlugin());
|
chromium.use(stealthPlugin());
|
||||||
|
|
||||||
|
const MEMORY_CONFIG = {
|
||||||
|
gcInterval: 60000, // 1 minute
|
||||||
|
maxHeapSize: 2048 * 1024 * 1024, // 2GB
|
||||||
|
heapUsageThreshold: 0.85 // 85%
|
||||||
|
};
|
||||||
|
|
||||||
|
const SCREENCAST_CONFIG: {
|
||||||
|
format: "jpeg" | "png";
|
||||||
|
maxWidth: number;
|
||||||
|
maxHeight: number;
|
||||||
|
targetFPS: number;
|
||||||
|
compressionQuality: number;
|
||||||
|
maxQueueSize: number;
|
||||||
|
} = {
|
||||||
|
format: 'jpeg',
|
||||||
|
maxWidth: 900,
|
||||||
|
maxHeight: 400,
|
||||||
|
targetFPS: 30,
|
||||||
|
compressionQuality: 0.8,
|
||||||
|
maxQueueSize: 2
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class represents a remote browser instance.
|
* This class represents a remote browser instance.
|
||||||
@@ -78,6 +102,11 @@ export class RemoteBrowser {
|
|||||||
*/
|
*/
|
||||||
public interpreter: WorkflowInterpreter;
|
public interpreter: WorkflowInterpreter;
|
||||||
|
|
||||||
|
|
||||||
|
private screenshotQueue: Buffer[] = [];
|
||||||
|
private isProcessingScreenshot = false;
|
||||||
|
private screencastInterval: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a new instances of the {@link Generator} and {@link WorkflowInterpreter} classes and
|
* Initializes a new instances of the {@link Generator} and {@link WorkflowInterpreter} classes and
|
||||||
* assigns the socket instance everywhere.
|
* assigns the socket instance everywhere.
|
||||||
@@ -90,6 +119,46 @@ export class RemoteBrowser {
|
|||||||
this.generator = new WorkflowGenerator(socket);
|
this.generator = new WorkflowGenerator(socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private initializeMemoryManagement(): void {
|
||||||
|
setInterval(() => {
|
||||||
|
const memoryUsage = process.memoryUsage();
|
||||||
|
const heapUsageRatio = memoryUsage.heapUsed / MEMORY_CONFIG.maxHeapSize;
|
||||||
|
|
||||||
|
if (heapUsageRatio > MEMORY_CONFIG.heapUsageThreshold) {
|
||||||
|
logger.warn('High memory usage detected, triggering cleanup');
|
||||||
|
this.performMemoryCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear screenshot queue if it's too large
|
||||||
|
if (this.screenshotQueue.length > SCREENCAST_CONFIG.maxQueueSize) {
|
||||||
|
this.screenshotQueue = this.screenshotQueue.slice(-SCREENCAST_CONFIG.maxQueueSize);
|
||||||
|
}
|
||||||
|
}, MEMORY_CONFIG.gcInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async performMemoryCleanup(): Promise<void> {
|
||||||
|
this.screenshotQueue = [];
|
||||||
|
this.isProcessingScreenshot = false;
|
||||||
|
|
||||||
|
if (global.gc) {
|
||||||
|
global.gc();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset CDP session if needed
|
||||||
|
if (this.client) {
|
||||||
|
try {
|
||||||
|
await this.stopScreencast();
|
||||||
|
this.client = null;
|
||||||
|
if (this.currentPage) {
|
||||||
|
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
|
||||||
|
await this.startScreencast();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error resetting CDP session:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes URLs to prevent navigation loops while maintaining consistent format
|
* Normalizes URLs to prevent navigation loops while maintaining consistent format
|
||||||
*/
|
*/
|
||||||
@@ -157,7 +226,7 @@ export class RemoteBrowser {
|
|||||||
'Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.62 Safari/537.36',
|
'Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.62 Safari/537.36',
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:118.0) Gecko/20100101 Firefox/118.0',
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:118.0) Gecko/20100101 Firefox/118.0',
|
||||||
];
|
];
|
||||||
|
|
||||||
return userAgents[Math.floor(Math.random() * userAgents.length)];
|
return userAgents[Math.floor(Math.random() * userAgents.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +247,7 @@ export class RemoteBrowser {
|
|||||||
"--disable-extensions",
|
"--disable-extensions",
|
||||||
"--no-sandbox",
|
"--no-sandbox",
|
||||||
"--disable-dev-shm-usage",
|
"--disable-dev-shm-usage",
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
const proxyConfig = await getDecryptedProxyConfig(userId);
|
const proxyConfig = await getDecryptedProxyConfig(userId);
|
||||||
let proxyOptions: { server: string, username?: string, password?: string } = { server: '' };
|
let proxyOptions: { server: string, username?: string, password?: string } = { server: '' };
|
||||||
@@ -251,11 +320,11 @@ export class RemoteBrowser {
|
|||||||
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
|
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
|
||||||
await blocker.disableBlockingInPage(this.currentPage);
|
await blocker.disableBlockingInPage(this.currentPage);
|
||||||
console.log('Adblocker initialized');
|
console.log('Adblocker initialized');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.warn('Failed to initialize adblocker, continuing without it:', error.message);
|
console.warn('Failed to initialize adblocker, continuing without it:', error.message);
|
||||||
// Still need to set up the CDP session even if blocker fails
|
// Still need to set up the CDP session even if blocker fails
|
||||||
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
|
this.client = await this.currentPage.context().newCDPSession(this.currentPage);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -319,7 +388,7 @@ export class RemoteBrowser {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.client.on('Page.screencastFrame', ({ data: base64, sessionId }) => {
|
this.client.on('Page.screencastFrame', ({ data: base64, sessionId }) => {
|
||||||
this.emitScreenshot(base64)
|
this.emitScreenshot(Buffer.from(base64, 'base64'))
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
@@ -339,16 +408,49 @@ export class RemoteBrowser {
|
|||||||
* If an interpretation was running it will be stopped.
|
* If an interpretation was running it will be stopped.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public switchOff = async (): Promise<void> => {
|
public async switchOff(): Promise<void> {
|
||||||
await this.interpreter.stopInterpretation();
|
try {
|
||||||
if (this.browser) {
|
await this.interpreter.stopInterpretation();
|
||||||
await this.stopScreencast();
|
|
||||||
await this.browser.close();
|
if (this.screencastInterval) {
|
||||||
} else {
|
clearInterval(this.screencastInterval);
|
||||||
logger.log('error', 'Browser wasn\'t initialized');
|
}
|
||||||
logger.log('error', 'Switching off the browser failed');
|
|
||||||
|
if (this.client) {
|
||||||
|
await this.stopScreencast();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.browser) {
|
||||||
|
await this.browser.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.screenshotQueue = [];
|
||||||
|
//this.performanceMonitor.reset();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error during browser shutdown:', error);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
private async optimizeScreenshot(screenshot: Buffer): Promise<Buffer> {
|
||||||
|
try {
|
||||||
|
return await sharp(screenshot)
|
||||||
|
.jpeg({
|
||||||
|
quality: Math.round(SCREENCAST_CONFIG.compressionQuality * 100),
|
||||||
|
progressive: true
|
||||||
|
})
|
||||||
|
.resize({
|
||||||
|
width: SCREENCAST_CONFIG.maxWidth,
|
||||||
|
height: SCREENCAST_CONFIG.maxHeight,
|
||||||
|
fit: 'inside',
|
||||||
|
withoutEnlargement: true
|
||||||
|
})
|
||||||
|
.toBuffer();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Screenshot optimization failed:', error);
|
||||||
|
return screenshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes and emits a single screenshot to the client side.
|
* Makes and emits a single screenshot to the client side.
|
||||||
@@ -358,7 +460,7 @@ export class RemoteBrowser {
|
|||||||
try {
|
try {
|
||||||
const screenshot = await this.currentPage?.screenshot();
|
const screenshot = await this.currentPage?.screenshot();
|
||||||
if (screenshot) {
|
if (screenshot) {
|
||||||
this.emitScreenshot(screenshot.toString('base64'));
|
this.emitScreenshot(screenshot);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const { message } = e as Error;
|
const { message } = e as Error;
|
||||||
@@ -490,37 +592,85 @@ export class RemoteBrowser {
|
|||||||
* Should be called only once after the browser is fully initialized.
|
* Should be called only once after the browser is fully initialized.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
private startScreencast = async (): Promise<void> => {
|
private async startScreencast(): Promise<void> {
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
logger.log('warn', 'client is not initialized');
|
logger.warn('Client is not initialized');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.client.send('Page.startScreencast', { format: 'jpeg', quality: 75 });
|
|
||||||
logger.log('info', `Browser started with screencasting a page.`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
try {
|
||||||
* Unsubscribes the current page from the screencast session.
|
await this.client.send('Page.startScreencast', {
|
||||||
* @returns {Promise<void>}
|
format: SCREENCAST_CONFIG.format,
|
||||||
*/
|
});
|
||||||
private stopScreencast = async (): Promise<void> => {
|
|
||||||
if (!this.client) {
|
// Set up screencast frame handler
|
||||||
logger.log('error', 'client is not initialized');
|
this.client.on('Page.screencastFrame', async ({ data, sessionId }) => {
|
||||||
logger.log('error', 'Screencast stop failed');
|
try {
|
||||||
} else {
|
const buffer = Buffer.from(data, 'base64');
|
||||||
await this.client.send('Page.stopScreencast');
|
await this.emitScreenshot(buffer);
|
||||||
logger.log('info', `Browser stopped with screencasting.`);
|
await this.client?.send('Page.screencastFrameAck', { sessionId });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Screencast frame processing failed:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('Screencast started successfully');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to start screencast:', error);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
private async stopScreencast(): Promise<void> {
|
||||||
|
if (!this.client) {
|
||||||
|
logger.error('Client is not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.client.send('Page.stopScreencast');
|
||||||
|
this.screenshotQueue = [];
|
||||||
|
this.isProcessingScreenshot = false;
|
||||||
|
logger.info('Screencast stopped successfully');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to stop screencast:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for emitting the screenshot of browser's active page through websocket.
|
* Helper for emitting the screenshot of browser's active page through websocket.
|
||||||
* @param payload the screenshot binary data
|
* @param payload the screenshot binary data
|
||||||
* @returns void
|
* @returns void
|
||||||
*/
|
*/
|
||||||
private emitScreenshot = (payload: any): void => {
|
private emitScreenshot = async (payload: Buffer): Promise<void> => {
|
||||||
const dataWithMimeType = ('data:image/jpeg;base64,').concat(payload);
|
if (this.isProcessingScreenshot) {
|
||||||
this.socket.emit('screencast', dataWithMimeType);
|
if (this.screenshotQueue.length < SCREENCAST_CONFIG.maxQueueSize) {
|
||||||
logger.log('debug', `Screenshot emitted`);
|
this.screenshotQueue.push(payload);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isProcessingScreenshot = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const optimizedScreenshot = await this.optimizeScreenshot(payload);
|
||||||
|
const base64Data = optimizedScreenshot.toString('base64');
|
||||||
|
const dataWithMimeType = `data:image/jpeg;base64,${base64Data}`;
|
||||||
|
|
||||||
|
this.socket.emit('screencast', dataWithMimeType);
|
||||||
|
logger.debug('Screenshot emitted');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Screenshot emission failed:', error);
|
||||||
|
} finally {
|
||||||
|
this.isProcessingScreenshot = false;
|
||||||
|
|
||||||
|
if (this.screenshotQueue.length > 0) {
|
||||||
|
const nextScreenshot = this.screenshotQueue.shift();
|
||||||
|
if (nextScreenshot) {
|
||||||
|
setTimeout(() => this.emitScreenshot(nextScreenshot), 1000 / SCREENCAST_CONFIG.targetFPS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,147 @@
|
|||||||
import React, { useCallback, useEffect, useRef } from 'react';
|
import React, { useCallback, useEffect, useRef, useMemo, Suspense } from 'react';
|
||||||
import { useSocketStore } from '../../context/socket';
|
import { useSocketStore } from '../../context/socket';
|
||||||
import { getMappedCoordinates } from "../../helpers/inputHelpers";
|
|
||||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||||
import { useActionContext } from '../../context/browserActions';
|
import { useActionContext } from '../../context/browserActions';
|
||||||
import DatePicker from './DatePicker';
|
const DatePicker = React.lazy(() => import('./DatePicker'));
|
||||||
import Dropdown from './Dropdown';
|
const Dropdown = React.lazy(() => import('./Dropdown'));
|
||||||
import TimePicker from './TimePicker';
|
const TimePicker = React.lazy(() => import('./TimePicker'));
|
||||||
import DateTimeLocalPicker from './DateTimeLocalPicker';
|
const DateTimeLocalPicker = React.lazy(() => import('./DateTimeLocalPicker'));
|
||||||
|
|
||||||
interface CreateRefCallback {
|
class RAFScheduler {
|
||||||
(ref: React.RefObject<HTMLCanvasElement>): void;
|
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();
|
||||||
|
|
||||||
|
callbacks.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('RAF Scheduler error:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.isProcessing = false;
|
||||||
|
this.frameId = null;
|
||||||
|
|
||||||
|
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?.();
|
||||||
|
|
||||||
|
if (this.lowPriorityQueue.length > 0) {
|
||||||
|
this.process();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CanvasProps {
|
interface CanvasProps {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
onCreateRef: CreateRefCallback;
|
onCreateRef: (ref: React.RefObject<HTMLCanvasElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,225 +152,229 @@ export interface Coordinates {
|
|||||||
y: number;
|
y: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
|
const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
|
||||||
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const { socket } = useSocketStore();
|
const { socket } = useSocketStore();
|
||||||
const { setLastAction, lastAction } = useGlobalInfoStore();
|
const { setLastAction, lastAction } = useGlobalInfoStore();
|
||||||
const { getText, getList } = useActionContext();
|
const { getText, getList } = useActionContext();
|
||||||
const getTextRef = useRef(getText);
|
|
||||||
const getListRef = useRef(getList);
|
|
||||||
|
|
||||||
const [datePickerInfo, setDatePickerInfo] = React.useState<{
|
const scheduler = useRef(new RAFScheduler());
|
||||||
coordinates: Coordinates;
|
const debouncer = useRef(new EventDebouncer(scheduler.current));
|
||||||
selector: string;
|
const measurementCache = useRef(new MeasurementCache(50));
|
||||||
} | null>(null);
|
//const performanceMonitor = useRef(new FrontendPerformanceMonitor());
|
||||||
|
|
||||||
const [dropdownInfo, setDropdownInfo] = React.useState<{
|
const refs = useRef({
|
||||||
coordinates: Coordinates;
|
getText,
|
||||||
selector: string;
|
getList,
|
||||||
options: Array<{
|
lastMousePosition: { x: 0, y: 0 },
|
||||||
value: string;
|
lastFrameTime: 0,
|
||||||
text: string;
|
context: null as CanvasRenderingContext2D | null,
|
||||||
disabled: boolean;
|
});
|
||||||
selected: boolean;
|
|
||||||
}>;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const [timePickerInfo, setTimePickerInfo] = React.useState<{
|
const [state, dispatch] = React.useReducer((state: any, action: any) => {
|
||||||
coordinates: Coordinates;
|
switch (action.type) {
|
||||||
selector: string;
|
case 'BATCH_UPDATE':
|
||||||
} | null>(null);
|
return { ...state, ...action.payload };
|
||||||
|
default:
|
||||||
const [dateTimeLocalInfo, setDateTimeLocalInfo] = React.useState<{
|
return state;
|
||||||
coordinates: Coordinates;
|
|
||||||
selector: string;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const notifyLastAction = (action: string) => {
|
|
||||||
if (lastAction !== action) {
|
|
||||||
setLastAction(action);
|
|
||||||
}
|
}
|
||||||
};
|
}, {
|
||||||
|
datePickerInfo: null,
|
||||||
|
dropdownInfo: null,
|
||||||
|
timePickerInfo: null,
|
||||||
|
dateTimeLocalInfo: null
|
||||||
|
});
|
||||||
|
|
||||||
const lastMousePosition = useRef<Coordinates>({ x: 0, y: 0 });
|
const getEventCoordinates = useCallback((event: MouseEvent): { x: number; y: number } => {
|
||||||
|
if (!canvasRef.current) return { x: 0, y: 0 };
|
||||||
|
|
||||||
useEffect(() => {
|
let rect = measurementCache.current.get(canvasRef.current);
|
||||||
getTextRef.current = getText;
|
if (!rect) {
|
||||||
getListRef.current = getList;
|
rect = canvasRef.current.getBoundingClientRect();
|
||||||
}, [getText, getList]);
|
measurementCache.current.set(canvasRef.current, rect);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (socket) {
|
|
||||||
socket.on('showDatePicker', (info: {coordinates: Coordinates, selector: string}) => {
|
|
||||||
setDatePickerInfo(info);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('showDropdown', (info: {
|
|
||||||
coordinates: Coordinates,
|
|
||||||
selector: string,
|
|
||||||
options: Array<{
|
|
||||||
value: string;
|
|
||||||
text: string;
|
|
||||||
disabled: boolean;
|
|
||||||
selected: boolean;
|
|
||||||
}>;
|
|
||||||
}) => {
|
|
||||||
setDropdownInfo(info);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('showTimePicker', (info: {coordinates: Coordinates, selector: string}) => {
|
|
||||||
setTimePickerInfo(info);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('showDateTimePicker', (info: {coordinates: Coordinates, selector: string}) => {
|
|
||||||
setDateTimeLocalInfo(info);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
socket.off('showDatePicker');
|
|
||||||
socket.off('showDropdown');
|
|
||||||
socket.off('showTimePicker');
|
|
||||||
socket.off('showDateTimePicker');
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}, [socket]);
|
|
||||||
|
|
||||||
const onMouseEvent = useCallback((event: MouseEvent) => {
|
return {
|
||||||
if (socket && canvasRef.current) {
|
x: event.clientX - rect.left,
|
||||||
// Get the canvas bounding rectangle
|
y: event.clientY - rect.top
|
||||||
const rect = canvasRef.current.getBoundingClientRect();
|
};
|
||||||
const clickCoordinates = {
|
}, []);
|
||||||
x: event.clientX - rect.left, // Use relative x coordinate
|
|
||||||
y: event.clientY - rect.top, // Use relative y coordinate
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (event.type) {
|
const handleMouseEvent = useCallback((event: MouseEvent) => {
|
||||||
case 'mousedown':
|
if (!socket || !canvasRef.current) return;
|
||||||
if (getTextRef.current === true) {
|
|
||||||
|
//performanceMonitor.current.measureEventLatency(event);
|
||||||
|
const coordinates = getEventCoordinates(event);
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case 'mousedown':
|
||||||
|
debouncer.current.add(() => {
|
||||||
|
if (refs.current.getText) {
|
||||||
console.log('Capturing Text...');
|
console.log('Capturing Text...');
|
||||||
} else if (getListRef.current === true) {
|
} else if (refs.current.getList) {
|
||||||
console.log('Capturing List...');
|
console.log('Capturing List...');
|
||||||
} else {
|
} else {
|
||||||
socket.emit('input:mousedown', clickCoordinates);
|
socket.emit('input:mousedown', coordinates);
|
||||||
}
|
}
|
||||||
notifyLastAction('click');
|
setLastAction('click');
|
||||||
break;
|
}, true); // High priority
|
||||||
case 'mousemove':
|
break;
|
||||||
if (lastMousePosition.current.x !== clickCoordinates.x ||
|
|
||||||
lastMousePosition.current.y !== clickCoordinates.y) {
|
|
||||||
lastMousePosition.current = {
|
|
||||||
x: clickCoordinates.x,
|
|
||||||
y: clickCoordinates.y,
|
|
||||||
};
|
|
||||||
socket.emit('input:mousemove', {
|
|
||||||
x: clickCoordinates.x,
|
|
||||||
y: clickCoordinates.y,
|
|
||||||
});
|
|
||||||
notifyLastAction('move');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'wheel':
|
|
||||||
const wheelEvent = event as WheelEvent;
|
|
||||||
const deltas = {
|
|
||||||
deltaX: Math.round(wheelEvent.deltaX),
|
|
||||||
deltaY: Math.round(wheelEvent.deltaY),
|
|
||||||
};
|
|
||||||
socket.emit('input:wheel', deltas);
|
|
||||||
notifyLastAction('scroll');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log('Default mouseEvent registered');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [socket]);
|
|
||||||
|
|
||||||
const onKeyboardEvent = useCallback((event: KeyboardEvent) => {
|
case 'mousemove':
|
||||||
if (socket) {
|
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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'wheel':
|
||||||
|
const wheelEvent = event as WheelEvent;
|
||||||
|
debouncer.current.add(() => {
|
||||||
|
socket.emit('input:wheel', {
|
||||||
|
deltaX: Math.round(wheelEvent.deltaX),
|
||||||
|
deltaY: Math.round(wheelEvent.deltaY)
|
||||||
|
});
|
||||||
|
setLastAction('scroll');
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [socket, getEventCoordinates]);
|
||||||
|
|
||||||
|
const handleKeyboardEvent = useCallback((event: KeyboardEvent) => {
|
||||||
|
if (!socket) return;
|
||||||
|
|
||||||
|
debouncer.current.add(() => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'keydown':
|
case 'keydown':
|
||||||
socket.emit('input:keydown', { key: event.key, coordinates: lastMousePosition.current });
|
socket.emit('input:keydown', {
|
||||||
notifyLastAction(`${event.key} pressed`);
|
key: event.key,
|
||||||
|
coordinates: refs.current.lastMousePosition
|
||||||
|
});
|
||||||
|
setLastAction(`${event.key} pressed`);
|
||||||
break;
|
break;
|
||||||
case 'keyup':
|
case 'keyup':
|
||||||
socket.emit('input:keyup', event.key);
|
socket.emit('input:keyup', event.key);
|
||||||
break;
|
break;
|
||||||
default:
|
|
||||||
console.log('Default keyEvent registered');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}, event.type === 'keydown'); // High priority for keydown
|
||||||
}, [socket]);
|
}, [socket]);
|
||||||
|
|
||||||
|
// Setup and cleanup
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canvasRef.current) return;
|
||||||
|
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
refs.current.context = canvas.getContext('2d', {
|
||||||
|
alpha: false,
|
||||||
|
desynchronized: true
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
canvas.removeEventListener('mousedown', handleMouseEvent);
|
||||||
|
canvas.removeEventListener('mousemove', handleMouseEvent);
|
||||||
|
canvas.removeEventListener('wheel', handleMouseEvent);
|
||||||
|
canvas.removeEventListener('keydown', handleKeyboardEvent);
|
||||||
|
canvas.removeEventListener('keyup', handleKeyboardEvent);
|
||||||
|
|
||||||
|
scheduler.current.clear();
|
||||||
|
debouncer.current.clear();
|
||||||
|
measurementCache.current.clear();
|
||||||
|
};
|
||||||
|
}, [handleMouseEvent, handleKeyboardEvent, onCreateRef]);
|
||||||
|
|
||||||
|
// Performance monitoring
|
||||||
|
// useEffect(() => {
|
||||||
|
// const intervalId = setInterval(() => {
|
||||||
|
// console.log('Performance Report:', performanceMonitor.current.getPerformanceReport());
|
||||||
|
// }, 20000);
|
||||||
|
|
||||||
|
// return () => clearInterval(intervalId);
|
||||||
|
// }, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (canvasRef.current) {
|
if (!socket) return;
|
||||||
onCreateRef(canvasRef);
|
|
||||||
canvasRef.current.addEventListener('mousedown', onMouseEvent);
|
|
||||||
canvasRef.current.addEventListener('mousemove', onMouseEvent);
|
|
||||||
canvasRef.current.addEventListener('wheel', onMouseEvent, { passive: true });
|
|
||||||
canvasRef.current.addEventListener('keydown', onKeyboardEvent);
|
|
||||||
canvasRef.current.addEventListener('keyup', onKeyboardEvent);
|
|
||||||
|
|
||||||
return () => {
|
const handlers = {
|
||||||
if (canvasRef.current) {
|
showDatePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { datePickerInfo: info } }),
|
||||||
canvasRef.current.removeEventListener('mousedown', onMouseEvent);
|
showDropdown: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { dropdownInfo: info } }),
|
||||||
canvasRef.current.removeEventListener('mousemove', onMouseEvent);
|
showTimePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { timePickerInfo: info } }),
|
||||||
canvasRef.current.removeEventListener('wheel', onMouseEvent);
|
showDateTimePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { dateTimeLocalInfo: info } })
|
||||||
canvasRef.current.removeEventListener('keydown', onKeyboardEvent);
|
};
|
||||||
canvasRef.current.removeEventListener('keyup', onKeyboardEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
Object.entries(handlers).forEach(([event, handler]) => socket.on(event, handler));
|
||||||
} else {
|
return () => {
|
||||||
console.log('Canvas not initialized');
|
Object.keys(handlers).forEach(event => socket.off(event));
|
||||||
}
|
};
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
}, [onMouseEvent]);
|
const memoizedDimensions = useMemo(() => ({
|
||||||
|
width: width || 900,
|
||||||
|
height: height || 400
|
||||||
|
}), [width, height]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ borderRadius: '0px 0px 5px 5px', overflow: 'hidden', backgroundColor: 'white' }}>
|
<div className="relative bg-white rounded-b-md overflow-hidden">
|
||||||
<canvas
|
<canvas
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
height={400}
|
height={memoizedDimensions.height}
|
||||||
width={900}
|
width={memoizedDimensions.width}
|
||||||
style={{ display: 'block' }}
|
className="block"
|
||||||
/>
|
/>
|
||||||
{datePickerInfo && (
|
<Suspense fallback={null}>
|
||||||
<DatePicker
|
{state.datePickerInfo && (
|
||||||
coordinates={datePickerInfo.coordinates}
|
<DatePicker
|
||||||
selector={datePickerInfo.selector}
|
coordinates={state.datePickerInfo.coordinates}
|
||||||
onClose={() => setDatePickerInfo(null)}
|
selector={state.datePickerInfo.selector}
|
||||||
/>
|
onClose={() => dispatch({
|
||||||
)}
|
type: 'BATCH_UPDATE',
|
||||||
{dropdownInfo && (
|
payload: { datePickerInfo: null }
|
||||||
<Dropdown
|
})}
|
||||||
coordinates={dropdownInfo.coordinates}
|
/>
|
||||||
selector={dropdownInfo.selector}
|
)}
|
||||||
options={dropdownInfo.options}
|
{state.dropdownInfo && (
|
||||||
onClose={() => setDropdownInfo(null)}
|
<Dropdown
|
||||||
/>
|
coordinates={state.dropdownInfo.coordinates}
|
||||||
)}
|
selector={state.dropdownInfo.selector}
|
||||||
{timePickerInfo && (
|
options={state.dropdownInfo.options}
|
||||||
<TimePicker
|
onClose={() => dispatch({
|
||||||
coordinates={timePickerInfo.coordinates}
|
type: 'BATCH_UPDATE',
|
||||||
selector={timePickerInfo.selector}
|
payload: { dropdownInfo: null }
|
||||||
onClose={() => setTimePickerInfo(null)}
|
})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{dateTimeLocalInfo && (
|
{state.timePickerInfo && (
|
||||||
<DateTimeLocalPicker
|
<TimePicker
|
||||||
coordinates={dateTimeLocalInfo.coordinates}
|
coordinates={state.timePickerInfo.coordinates}
|
||||||
selector={dateTimeLocalInfo.selector}
|
selector={state.timePickerInfo.selector}
|
||||||
onClose={() => setDateTimeLocalInfo(null)}
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
};
|
Canvas.displayName = 'Canvas';
|
||||||
|
|
||||||
|
|
||||||
export default Canvas;
|
export default Canvas;
|
||||||
@@ -378,7 +378,6 @@ export const BrowserWindow = () => {
|
|||||||
}
|
}
|
||||||
}, [paginationMode, resetPaginationSelector]);
|
}, [paginationMode, resetPaginationSelector]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onClick={handleClick} style={{ width: '900px' }} id="browser-window">
|
<div onClick={handleClick} style={{ width: '900px' }} id="browser-window">
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user