/* eslint-disable no-param-reassign */ import type { WebSocket, WebSocketServer } from 'ws'; import { Server } from 'node:http'; import { ServerAdapter } from './ServerAdapter'; import { WebSocketConnection } from './WebSocketConnection'; import { ServerController } from '../ServerController'; import { setCookieRequestHandler } from '../utils/setCookieRequestHandler'; export class WebSocketServerAdapter extends ServerAdapter { private pingInterval = 30000; private pingTimer!: NodeJS.Timeout; public constructor(public readonly wss: WebSocketServer) { super(); this.init(); } public close(): Promise { return new Promise((resolve, reject) => { const { wss } = this; wss.close((err) => (err ? reject(err) : resolve())); wss.clients.forEach((client) => client.terminate()); }); } public setServerController(controller: ServerController) { super.setServerController(controller); this.addSetHttpCookieHandler(); } private init() { this.addSetHttpCookieHandler(); this.listenOnConnect(); this.initHeartbeat(); } private listenOnConnect() { this.wss.on('connection', (ws, request) => { ws.on('pong', () => this.heartbeat(ws)); const connection = new WebSocketConnection(ws, request); if (this.controller) { this.controller.onClientConnected(connection); } else { ws.close(undefined, 'The service is not ready yet.'); } }); } private initHeartbeat() { this.pingTimer = setInterval(() => this.ping(), this.pingInterval); this.wss.on('close', () => clearInterval(this.pingTimer)); } private ping() { this.wss.clients.forEach((ws: WebSocket) => { if (ws.isAlive === false) { ws.terminate(); } else { ws.isAlive = false; ws.ping(); } }); } private heartbeat(ws: WebSocket) { ws.isAlive = true; } private addSetHttpCookieHandler() { // eslint-disable-next-line no-underscore-dangle const httpServer: Server = (this.wss as any)._server; setCookieRequestHandler(httpServer, this.controller!); } } declare module 'ws' { // eslint-disable-next-line @typescript-eslint/no-shadow interface WebSocket { isAlive?: boolean; } }