UNPKG

zeroant-common

Version:
162 lines (149 loc) 5.3 kB
import { AddonPlugin } from 'zeroant-factory/addon.plugin' import { v4 } from 'uuid' import { WebSocket } from 'ws' import { ErrorCode, ErrorDescription, WebSocketCloseCode } from '../constants.js' import { PubSocketConfig } from '../config/pubSocket.config.js' import { TtlUtils } from 'zeroant-util/ttl.util' import { BadRequest } from 'zeroant-response/clientErrors/badRequest.clientError' import { PubSocketPayload } from '../dto/socket.payload.js' declare module 'ws' { interface WebSocket { retries: number } } export class PubSocket<PubSocketTopic extends string> extends AddonPlugin { connections: Record<string, WebSocket> = {} pingInterval = TtlUtils.oneMinute retryInterval = TtlUtils.oneSecond * 3 indefiniteRetryInterval = TtlUtils.oneMinute * 3 maxRetry = 5 reconnectTimer: Record<string, number> = {} pingingTimer: Record<string, NodeJS.Timeout> = {} shutingDown = false _options!: { url: string[] key: string[] } get enabled() { return this.context.config.addons.lazyGet(PubSocketConfig).usePub } async initialize() { if (!this.enabled) { return } this.debug('info', 'Enabled') const options = this.context.config.addons.lazyGet(PubSocketConfig).options this._options = options const uuid = v4() for (const url of this._options.url) { this.createSub(uuid, url, this._options.key[0]) } } to<T extends Record<string, any>>(sub?: string) { const service = { send: (topic: PubSocketTopic, data: T) => { if (sub === null || sub === undefined) { throw new BadRequest( ErrorCode.INVALID_PAYLOAD, ErrorDescription.INVALID_PAYLOAD, 'Invalid payload for pub socket send event please provide a valid subscriber' ) } const payload = new PubSocketPayload<PubSocketTopic, T>(topic, data, sub) for (const socket of Object.values(this.connections)) { this.debug('debug', 'socket sending message', socket.uuid, socket.readyState) if (socket.readyState === socket.OPEN) { socket.send(payload.toBuffer()) } } }, broadcast: (topic: PubSocketTopic, data: T) => { if (topic === null || topic === undefined) { throw new BadRequest( ErrorCode.INVALID_PAYLOAD, ErrorDescription.INVALID_PAYLOAD, 'Invalid payload for pub socket broadcast event please provide a valid topic' ) } const payload = new PubSocketPayload<PubSocketTopic, T>(topic, data) for (const socket of Object.values(this.connections)) { if (socket.readyState === socket.OPEN) { socket.send(payload.toBuffer()) } } } } return service } createSub(uuid: string, url: string, key: string, retries = 0) { const websocket = new WebSocket(url, { auth: `pub:${key}` }) as WebSocket websocket.uuid = uuid websocket.sub = key websocket.retries = retries websocket.onclose = (event) => { if (this.shutingDown) { // this.debug('info', 'ShutingDown', 'No Action initiated') return } this.debug('info', 'Closed', event.code.toString(), event.reason) this.tryClearPingingInterval(uuid) this.tryClearReconnectInterval(uuid) if (event.code !== WebSocketCloseCode.SERVICE_RESTART) { websocket.retries += 1 } if (websocket.retries < this.maxRetry) { this.debug('info', 'Retrying next', websocket.retries, 'in', this.retryInterval, 'ms') setTimeout(() => { this.createSub(uuid, url, key, websocket.retries) }, this.retryInterval) } else if (websocket.retries >= this.maxRetry) { this.debug('info', 'Retrying exhausted waiting for', this.indefiniteRetryInterval, 'ms', 'to next retries') setTimeout(() => { this.createSub(uuid, url, key, 0) }, this.indefiniteRetryInterval) } } websocket.onerror = (event) => { if (this.shutingDown) { // this.debug('info', 'ShutingDown', 'No Action initiated') return } this.debug('error', 'OnError', event.message) } websocket.onopen = (event) => { if (this.shutingDown) { this.debug('info', 'ShutingDown', 'No Action initiated') return } this.tryClearReconnectInterval(uuid) this.pingingTimer[uuid] = setInterval(() => { this.debug('info', 'Pinging', `websocket client ${uuid}`) websocket.ping() }, this.pingInterval) } this.connections[uuid] = websocket } tryClearReconnectInterval(uuid: string) { if (this.reconnectTimer[uuid] !== undefined) { clearTimeout(this.reconnectTimer[uuid]) } } tryClearPingingInterval(uuid: string) { if (this.pingingTimer[uuid] !== undefined) { clearInterval(this.pingingTimer[uuid]) } } close() { this.shutingDown = true for (const [uuid, socket] of Object.entries(this.connections)) { this.tryClearPingingInterval(uuid) this.tryClearReconnectInterval(uuid) if (socket.readyState === socket.OPEN || socket.readyState === socket.CONNECTING) { socket.close() } } this.debug('info', 'Stopped') } }