Spaces:
Paused
Paused
import { WebSocketState, type ImageModificationParams, type OnServerResponse } from "@/types"; | |
/** | |
* FacePoke class manages the WebSocket connection | |
*/ | |
export class FacePoke { | |
private ws: WebSocket | null = null; | |
private isUnloading: boolean = false; | |
private onServerResponse: OnServerResponse = async () => {}; | |
private reconnectAttempts: number = 0; | |
private readonly maxReconnectAttempts: number = 5; | |
private readonly reconnectDelay: number = 5000; | |
private readonly eventListeners: Map<string, Set<Function>> = new Map(); | |
/** | |
* Creates an instance of FacePoke. | |
* Initializes the WebSocket connection. | |
*/ | |
constructor() { | |
console.log(`[FacePoke] Initializing FacePoke instance`); | |
this.initializeWebSocket(); | |
this.setupUnloadHandler(); | |
} | |
/** | |
* Sets the callback function for handling modified images. | |
* @param handler - The function to be called when a modified image is received. | |
*/ | |
public setOnServerResponse(handler: OnServerResponse): void { | |
this.onServerResponse = handler; | |
console.log(`[FacePoke] onServerResponse handler set`); | |
} | |
/** | |
* Starts or restarts the WebSocket connection. | |
*/ | |
public async startWebSocket(): Promise<void> { | |
console.log(`[FacePoke] Starting WebSocket connection.`); | |
if (!this.ws || this.ws.readyState !== WebSocketState.OPEN) { | |
await this.initializeWebSocket(); | |
} | |
} | |
/** | |
* Initializes the WebSocket connection. | |
* Implements exponential backoff for reconnection attempts. | |
*/ | |
private async initializeWebSocket(): Promise<void> { | |
console.log(`[FacePoke] Initializing WebSocket connection`); | |
const connect = () => { | |
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
this.ws = new WebSocket(`${protocol}//${window.location.host}/ws`); | |
this.ws.onopen = this.handleWebSocketOpen.bind(this); | |
this.ws.onclose = this.handleWebSocketClose.bind(this); | |
this.ws.onerror = this.handleWebSocketError.bind(this); | |
this.ws.onmessage = (this.handleWebSocketMessage.bind(this) as any) | |
}; | |
connect(); // Initial connection attempt | |
} | |
private handleWebSocketMessage(msg: MessageEvent) { | |
if (typeof msg.data === "string") { | |
this.onServerResponse({ loaded: JSON.parse(msg.data) as any }); | |
} else if (typeof msg.data !== "undefined" ) { | |
this.onServerResponse({ image: msg.data as unknown as Blob }); | |
} | |
} | |
/** | |
* Handles the WebSocket open event. | |
*/ | |
private handleWebSocketOpen(): void { | |
console.log(`[FacePoke] WebSocket connection opened`); | |
this.reconnectAttempts = 0; // Reset reconnect attempts on successful connection | |
this.emitEvent('websocketOpen'); | |
} | |
/** | |
* Handles WebSocket close events. | |
* Implements reconnection logic with exponential backoff. | |
* @param event - The CloseEvent containing close information. | |
*/ | |
private handleWebSocketClose(event: CloseEvent): void { | |
if (event.wasClean) { | |
console.log(`[FacePoke] WebSocket connection closed cleanly, code=${event.code}, reason=${event.reason}`); | |
} else { | |
console.warn(`[FacePoke] WebSocket connection abruptly closed`); | |
} | |
this.emitEvent('websocketClose', event); | |
// Attempt to reconnect after a delay, unless the page is unloading or max attempts reached | |
if (!this.isUnloading && this.reconnectAttempts < this.maxReconnectAttempts) { | |
this.reconnectAttempts++; | |
const delay = Math.min(1000 * (2 ** this.reconnectAttempts), 30000); // Exponential backoff, max 30 seconds | |
console.log(`[FacePoke] Attempting to reconnect in ${delay}ms (Attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})...`); | |
setTimeout(() => this.initializeWebSocket(), delay); | |
} else if (this.reconnectAttempts >= this.maxReconnectAttempts) { | |
console.error(`[FacePoke] Max reconnect attempts reached. Please refresh the page.`); | |
this.emitEvent('maxReconnectAttemptsReached'); | |
} | |
} | |
/** | |
* Handles WebSocket errors. | |
* @param error - The error event. | |
*/ | |
private handleWebSocketError(error: Event): void { | |
console.error(`[FacePoke] WebSocket error:`, error); | |
this.emitEvent('websocketError', error); | |
} | |
/** | |
* Cleans up resources and closes connections. | |
*/ | |
public cleanup(): void { | |
console.log('[FacePoke] Starting cleanup process'); | |
if (this.ws) { | |
this.ws.close(); | |
this.ws = null; | |
} | |
this.eventListeners.clear(); | |
console.log('[FacePoke] Cleanup completed'); | |
this.emitEvent('cleanup'); | |
} | |
public async loadImage(image: string): Promise<void> { | |
// Extract the base64 part if it's a data URL | |
const base64Data = image.split(',')[1] || image; | |
const buffer = new Uint8Array(atob(base64Data).split('').map(char => char.charCodeAt(0))); | |
const blob = new Blob([buffer], { type: 'application/octet-binary' }); | |
this.sendBlobMessage(await blob.arrayBuffer()); | |
} | |
public transformImage(uuid: string, params: Partial<ImageModificationParams>): void { | |
this.sendJsonMessage({ uuid, params }); | |
} | |
private sendBlobMessage(buffer: ArrayBuffer): void { | |
if (!this.ws || this.ws.readyState !== WebSocketState.OPEN) { | |
const error = new Error('WebSocket connection is not open'); | |
console.error('[FacePoke] Error sending JSON message:', error); | |
this.emitEvent('sendJsonMessageError', error); | |
throw error; | |
} | |
try { | |
this.ws.send(buffer); | |
} catch (err) { | |
console.error(`failed to send the WebSocket message: ${err}`) | |
} | |
} | |
/** | |
* Sends a JSON message through the WebSocket connection with request tracking. | |
* @param message - The message to send. | |
* @throws Error if the WebSocket is not open. | |
*/ | |
private sendJsonMessage<T>(message: T): void { | |
if (!this.ws || this.ws.readyState !== WebSocketState.OPEN) { | |
const error = new Error('WebSocket connection is not open'); | |
console.error('[FacePoke] Error sending JSON message:', error); | |
this.emitEvent('sendJsonMessageError', error); | |
throw error; | |
} | |
try { | |
this.ws.send(JSON.stringify(message)); | |
} catch (err) { | |
console.error(`failed to send the WebSocket message: ${err}`) | |
} | |
} | |
/** | |
* Sets up the unload handler to clean up resources when the page is unloading. | |
*/ | |
private setupUnloadHandler(): void { | |
window.addEventListener('beforeunload', () => { | |
console.log('[FacePoke] Page is unloading, cleaning up resources'); | |
this.isUnloading = true; | |
if (this.ws) { | |
this.ws.close(1000, 'Page is unloading'); | |
} | |
this.cleanup(); | |
}); | |
} | |
/** | |
* Adds an event listener for a specific event type. | |
* @param eventType - The type of event to listen for. | |
* @param listener - The function to be called when the event is emitted. | |
*/ | |
public addEventListener(eventType: string, listener: Function): void { | |
if (!this.eventListeners.has(eventType)) { | |
this.eventListeners.set(eventType, new Set()); | |
} | |
this.eventListeners.get(eventType)!.add(listener); | |
console.log(`[FacePoke] Added event listener for '${eventType}'`); | |
} | |
/** | |
* Removes an event listener for a specific event type. | |
* @param eventType - The type of event to remove the listener from. | |
* @param listener - The function to be removed from the listeners. | |
*/ | |
public removeEventListener(eventType: string, listener: Function): void { | |
const listeners = this.eventListeners.get(eventType); | |
if (listeners) { | |
listeners.delete(listener); | |
console.log(`[FacePoke] Removed event listener for '${eventType}'`); | |
} | |
} | |
/** | |
* Emits an event to all registered listeners for that event type. | |
* @param eventType - The type of event to emit. | |
* @param data - Optional data to pass to the event listeners. | |
*/ | |
private emitEvent(eventType: string, data?: any): void { | |
const listeners = this.eventListeners.get(eventType); | |
if (listeners) { | |
console.log(`[FacePoke] Emitting event '${eventType}' with data:`, data); | |
listeners.forEach(listener => listener(data)); | |
} | |
} | |
} | |
/** | |
* Singleton instance of the FacePoke class. | |
*/ | |
export const facePoke = new FacePoke(); | |