UNPKG

@not-true/devtools

Version:

Remote debugging and development tools client library for React Native applications

329 lines (280 loc) 7.99 kB
// @ts-ignore import { io } from "socket.io-client"; import { RemoteDevToolsConfig, DevToolsEventHandlers, LogEntry, StateUpdate, REPLCommand, REPLResult, RemoteConfig, QueueItem } from "../types"; import { DeviceInfoUtils } from "../utils/DeviceInfoUtils"; import { QueueManager } from "../utils/QueueManager"; export class RemoteDevTools { private socket: any | null = null; private config: Required<RemoteDevToolsConfig>; private handlers: DevToolsEventHandlers; private queueManager: QueueManager; private reconnectAttempts = 0; private isConnected = false; private originalConsole: Console; constructor(config: RemoteDevToolsConfig, handlers: DevToolsEventHandlers = {}) { this.config = { maxQueueSize: 1000, maxReconnectAttempts: 5, reconnectDelay: 1000, enableConsoleLogging: true, enableStateSync: true, enableREPL: true, enableRemoteConfig: true, stateUpdateThrottle: 500, deviceInfo: {}, clientInfo: {}, ...config, }; this.handlers = handlers; this.queueManager = new QueueManager(this.config.maxQueueSize); this.originalConsole = { ...console }; if (this.config.enableConsoleLogging) { this.interceptConsole(); } } /** * Connect to the remote DevTools server */ async connect(): Promise<void> { if (this.socket && this.socket.connected) { console.warn("RemoteDevTools: Already connected"); return; } try { this.socket = io(this.config.serverUrl, { autoConnect: true, reconnection: false, timeout: 5000, }); this.setupSocketListeners(); // Wait for connection await new Promise<void>((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error("Connection timeout")); }, 10000); this.socket!.on("connect", () => { clearTimeout(timeout); resolve(); }); this.socket!.on("connect_error", (error: Error) => { clearTimeout(timeout); reject(error); }); }); } catch (error) { this.handleError(error as Error); throw error; } } /** * Disconnect from the remote DevTools server */ disconnect(): void { if (this.socket) { this.socket.disconnect(); this.socket = null; } this.isConnected = false; this.reconnectAttempts = 0; } /** * Send a log entry to the server */ log(level: "log" | "info" | "warn" | "error", ...args: any[]): void { const logEntry: LogEntry = { level, args, timestamp: Date.now(), message: args.map((arg) => (typeof arg === "object" ? JSON.stringify(arg) : String(arg))).join(" "), }; this.sendOrQueue("log", logEntry); } /** * Send state update to the server */ updateState(stateUpdate: StateUpdate): void { if (!this.config.enableStateSync) return; const data = { ...stateUpdate, timestamp: Date.now(), }; this.sendOrQueue("state", data); } /** * Send REPL result to the server */ sendREPLResult(result: REPLResult): void { if (!this.config.enableREPL) return; const data = { ...result, timestamp: Date.now(), }; this.sendOrQueue("repl-result", data); } /** * Get current connection status */ isConnectedToServer(): boolean { return this.isConnected; } /** * Get current configuration */ getConfig(): RemoteDevToolsConfig { return { ...this.config }; } /** * Update configuration */ updateConfig(newConfig: Partial<RemoteDevToolsConfig>): void { this.config = { ...this.config, ...newConfig }; } private setupSocketListeners(): void { if (!this.socket) return; this.socket.on("connect", () => { this.isConnected = true; this.reconnectAttempts = 0; this.registerClient(); this.processQueue(); this.handlers.onConnect?.(); }); this.socket.on("disconnect", () => { this.isConnected = false; this.handlers.onDisconnect?.(); this.scheduleReconnect(); }); this.socket.on("connect_error", (error: any) => { this.handleError(error instanceof Error ? error : new Error(String(error))); this.scheduleReconnect(); }); // Remote config handler this.socket.on("remote-config", (data: RemoteConfig) => { if (this.config.enableRemoteConfig) { this.handlers.onRemoteConfig?.(data); } }); // REPL command handler this.socket.on("repl-execute", async (command: REPLCommand) => { if (this.config.enableREPL && this.handlers.onREPLCommand) { try { const startTime = Date.now(); const result = await this.handlers.onREPLCommand(command); const executionTime = Date.now() - startTime; this.sendREPLResult({ commandId: command.commandId, result, executionTime, }); } catch (error) { const executionTime = Date.now() - Date.now(); this.sendREPLResult({ commandId: command.commandId, result: null, error: error instanceof Error ? error.message : String(error), executionTime, }); } } }); // Feature flags handler this.socket.on("feature-flags", (data: { flags: Record<string, boolean> }) => { // Update local feature flags Object.assign(this.config, { enableConsoleLogging: data.flags.enableConsoleLogging ?? this.config.enableConsoleLogging, enableStateSync: data.flags.enableStateSync ?? this.config.enableStateSync, enableREPL: data.flags.enableREPL ?? this.config.enableREPL, enableRemoteConfig: data.flags.enableRemoteConfig ?? this.config.enableRemoteConfig, }); }); } private registerClient(): void { if (!this.socket) return; const deviceInfo = DeviceInfoUtils.getDeviceInfo(); this.socket.emit("register-client", { name: this.config.name, version: this.config.version, platform: deviceInfo.platform, deviceInfo: { ...deviceInfo, ...this.config.deviceInfo }, clientInfo: this.config.clientInfo, }); } private sendOrQueue(type: QueueItem["type"], data: any): void { if (this.isConnected && this.socket) { const eventName = this.getEventName(type); this.socket.emit(eventName, data); } else { this.queueManager.enqueue({ type, data, timestamp: Date.now() }); } } private processQueue(): void { if (!this.socket) return; while (!this.queueManager.isEmpty()) { const item = this.queueManager.dequeue(); if (item) { const eventName = this.getEventName(item.type); this.socket.emit(eventName, item.data); } } } private getEventName(type: QueueItem["type"]): string { switch (type) { case "log": return "console-log"; case "state": return "state-update"; case "repl-result": return "repl-result"; default: return type; } } private scheduleReconnect(): void { if (this.reconnectAttempts >= this.config.maxReconnectAttempts) { this.handlers.onReconnectError?.(new Error("Max reconnection attempts reached")); return; } this.reconnectAttempts++; const delay = this.config.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); // Exponential backoff setTimeout(() => { if (!this.isConnected) { this.handlers.onReconnect?.(this.reconnectAttempts); this.reconnect(); } }, delay); } private reconnect(): void { if (this.socket) { this.socket.disconnect(); } this.connect().catch((error) => { this.handleError(error); }); } private interceptConsole(): void { const logLevels: Array<"log" | "info" | "warn" | "error"> = ["log", "info", "warn", "error"]; logLevels.forEach((level) => { const originalMethod = this.originalConsole[level]; (console as any)[level] = (...args: any[]) => { // Call original console method originalMethod.apply(console, args); // Send to remote this.log(level, ...args); }; }); } private restoreConsole(): void { Object.assign(console, this.originalConsole); } private handleError(error: Error): void { console.error("RemoteDevTools Error:", error); this.handlers.onError?.(error); } /** * Cleanup and restore original console */ destroy(): void { this.disconnect(); this.restoreConsole(); this.queueManager.clear(); } }