import type { WebSocket , Event as OpenEvent , MessageEvent , ErrorEvent , CloseEvent } from 'ws' import { TypedArray, assert } from '@blackglory/prelude' import { Queue, Emitter } from '@blackglory/structures' import { assertNever } from 'assert-never' export enum BinaryType { NodeBuffer , ArrayBuffer , Fragments } export enum State { Closed , Connecting , Connected , Closing } enum ReadyState { CONNECTING = 0 , OPEN = 1 , CLOSING = 2 , CLOSED = 3 } type Data = | string | ArrayBufferLike | Buffer | TypedArray | DataView export class ExtraWebSocket extends Emitter<{ open: [event: OpenEvent] message: [event: MessageEvent] error: [event: ErrorEvent] close: [event: CloseEvent] }> { private instance?: WebSocket private binaryType = BinaryType.NodeBuffer protected unsentMessages = new Queue() constructor(private createWebSocket: () => WebSocket) { super() } getState(): State { if (this.instance) { switch (this.instance.readyState) { case ReadyState.CONNECTING: case 0: return State.Connecting case ReadyState.OPEN: case 1: return State.Connected case ReadyState.CLOSING: case 2: return State.Closing case ReadyState.CLOSED: case 3: return State.Closed default: assertNever(this.instance.readyState, 'Unknown state') } } else { return State.Closed } } getBinaryType(): BinaryType { return this.binaryType } setBinaryType(val: BinaryType): void { this.binaryType = val if (this.instance) { switch (val) { case BinaryType.NodeBuffer: { this.instance.binaryType = 'nodebuffer' break } case BinaryType.ArrayBuffer: { this.instance.binaryType = 'arraybuffer' break } case BinaryType.Fragments: { this.instance.binaryType = 'fragments' break } default: assertNever(val, 'Unknown binary type') } } } connect(signal?: AbortSignal): Promise { return new Promise((resolve, reject) => { signal?.throwIfAborted() assert(this.getState() === State.Closed, 'WebSocket is not closed') const self = this const ws = this.createWebSocket() this.instance = ws signal?.addEventListener('abort', abortListener, { once: true }) ws.addEventListener('error', errorListener, { once: true }) ws.addEventListener('open', event => this.emit('open', event)) ws.addEventListener('message', event => this.emit('message', event)) ws.addEventListener('error', event => this.emit('error', event)) ws.addEventListener('close', event => this.emit('close', event)) this.setBinaryType(this.binaryType) ws.addEventListener('open', openListener, { once: true }) function abortListener(): void { assert(signal) ws.removeAllListeners('open') ws.removeAllListeners('message') ws.removeAllListeners('error') ws.removeAllListeners('close') ws.close() if (self.instance === ws) self.instance = undefined reject(signal.reason) } function errorListener(err: ErrorEvent): void { ws.removeAllListeners('open') ws.removeAllListeners('message') ws.removeAllListeners('error') ws.removeAllListeners('close') signal?.removeEventListener('abort', abortListener) if (self.instance === ws) self.instance = undefined reject(err.error) } function openListener(event: OpenEvent): void { ws.removeEventListener('error', errorListener) signal?.removeEventListener('abort', abortListener) while (self.unsentMessages.size) { const message = self.unsentMessages.dequeue()! self.send(message) } resolve() } }) } close(code?: number, reason?: string): Promise { return new Promise(resolve => { const self = this const ws = this.instance if (!ws) return resolve() switch (this.getState()) { case State.Closed: { resolve() break } case State.Closing: { ws.addEventListener('close', closeListener, { once: true }) break } default: { ws.addEventListener('close', closeListener, { once: true }) ws.close(code, reason) } } function closeListener(): void { if (self.instance === ws) self.instance = undefined resolve() } }) } send(data: Data): void { if (this.getState() === State.Connected) { assert(this.instance, 'WebSocket is not created') this.instance.send(data, err => { if (err) this.unsentMessages.enqueue(data) }) } else { this.unsentMessages.enqueue(data) } } ping(): void { assert(this.instance, 'WebSocket is not created') this.instance.ping() } }