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();