FacePoke / client /src /lib /facePoke.ts
wongfei2009's picture
Update facePoke.ts
38348ec unverified
raw
history blame
8.08 kB
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();