Spaces:
Paused
Paused
File size: 8,078 Bytes
e123fec d69879c e123fec d69879c e123fec d69879c e123fec d69879c e123fec d69879c 38348ec d69879c e123fec d69879c e123fec d69879c e123fec d69879c e123fec d69879c e123fec d69879c e123fec d69879c e123fec d69879c e123fec d69879c ea331ab d69879c ea331ab d69879c ea331ab d69879c e123fec d69879c e123fec d69879c 8378233 e123fec d69879c e123fec d69879c e123fec d69879c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
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();
|