diff --git a/packages/api/src/classes/Client.ts b/packages/api/src/classes/Client.ts index 22193ee..3db2488 100755 --- a/packages/api/src/classes/Client.ts +++ b/packages/api/src/classes/Client.ts @@ -1,30 +1,22 @@ import { ClientOperation, Packet, ServerOperation } from '@revanced/bot-shared' -import ClientGateway, { ClientGatewayEventHandlers } from './ClientGateway' +import { ClientWebSocketManager, ClientWebSocketEvents, ClientWebSocketManagerOptions } from './ClientWebSocket' /** * The client that connects to the API. */ export default class Client { ready = false - gateway: ClientGateway + ws: ClientWebSocketManager #parseId = 0 constructor(options: ClientOptions) { - this.gateway = new ClientGateway({ - url: options.api.gatewayUrl, - }) - - this.gateway.on('ready', () => { + this.ws = new ClientWebSocketManager(options.api.websocket) + this.ws.on('ready', () => { this.ready = true }) - } + this.ws.on('disconnect', () => { - /** - * Connects to the WebSocket API - * @returns A promise that resolves when the client is ready - */ - connect() { - return this.gateway.connect() + }) } /** @@ -45,7 +37,7 @@ export default class Client { const currentId = (this.#parseId++).toString() - this.gateway.send({ + this.ws.send({ op: ClientOperation.ParseText, d: { text, @@ -58,18 +50,18 @@ export default class Client { const promise = new Promise((rs, rj) => { const parsedTextListener = (packet: CorrectPacket) => { if (packet.d.id !== currentId) return - this.gateway.off('parsedText', parsedTextListener) + this.ws.off('parsedText', parsedTextListener) rs(packet.d) } const parseTextFailedListener = (packet: Packet) => { if (packet.d.id !== currentId) return - this.gateway.off('parseTextFailed', parseTextFailedListener) + this.ws.off('parseTextFailed', parseTextFailedListener) rj() } - this.gateway.on('parsedText', parsedTextListener) - this.gateway.on('parseTextFailed', parseTextFailedListener) + this.ws.on('parsedText', parsedTextListener) + this.ws.on('parseTextFailed', parseTextFailedListener) }) return await promise @@ -85,7 +77,7 @@ export default class Client { const currentId = (this.#parseId++).toString() - this.gateway.send({ + this.ws.send({ op: ClientOperation.ParseImage, d: { image_url: url, @@ -98,18 +90,18 @@ export default class Client { const promise = new Promise((rs, rj) => { const parsedImageListener = (packet: CorrectPacket) => { if (packet.d.id !== currentId) return - this.gateway.off('parsedImage', parsedImageListener) + this.ws.off('parsedImage', parsedImageListener) rs(packet.d) } const parseImageFailedListener = (packet: Packet) => { if (packet.d.id !== currentId) return - this.gateway.off('parseImageFailed', parseImageFailedListener) + this.ws.off('parseImageFailed', parseImageFailedListener) rj() } - this.gateway.on('parsedImage', parsedImageListener) - this.gateway.on('parseImageFailed', parseImageFailedListener) + this.ws.on('parsedImage', parsedImageListener) + this.ws.on('parseImageFailed', parseImageFailedListener) }) return await promise @@ -121,8 +113,8 @@ export default class Client { * @param handler The event handler * @returns The event handler function */ - on(name: TOpName, handler: ClientGatewayEventHandlers[TOpName]) { - this.gateway.on(name, handler) + on(name: TOpName, handler: ClientWebSocketEvents[TOpName]) { + this.ws.on(name, handler) return handler } @@ -132,8 +124,8 @@ export default class Client { * @param handler The event handler to remove * @returns The removed event handler function */ - off(name: TOpName, handler: ClientGatewayEventHandlers[TOpName]) { - this.gateway.off(name, handler) + off(name: TOpName, handler: ClientWebSocketEvents[TOpName]) { + this.ws.off(name, handler) return handler } @@ -143,11 +135,11 @@ export default class Client { * @param handler The event handler * @returns The event handler function */ - once( + once( name: TOpName, - handler: ClientGatewayEventHandlers[TOpName], + handler: ClientWebSocketEvents[TOpName], ) { - this.gateway.once(name, handler) + this.ws.once(name, handler) return handler } @@ -163,5 +155,5 @@ export interface ClientOptions { } export interface ClientApiOptions { - gatewayUrl: string + websocket: ClientWebSocketManagerOptions } diff --git a/packages/api/src/classes/ClientGateway.ts b/packages/api/src/classes/ClientWebSocket.ts similarity index 75% rename from packages/api/src/classes/ClientGateway.ts rename to packages/api/src/classes/ClientWebSocket.ts index 1e4a302..2d71c9c 100755 --- a/packages/api/src/classes/ClientGateway.ts +++ b/packages/api/src/classes/ClientWebSocket.ts @@ -16,18 +16,21 @@ import { RawData, WebSocket } from 'ws' * The class that handles the WebSocket connection to the server. * This is the only relevant class for the time being. But in the future, there may be more classes to handle different protocols of the API. */ -export default class ClientGateway { +export class ClientWebSocketManager { readonly url: string + timeout: number + ready = false disconnected: boolean | DisconnectReason = DisconnectReason.NeverConnected - config: Readonly['d']> | null = null! + config: Readonly['d']> | null = null #hbTimeout: NodeJS.Timeout = null! #socket: WebSocket = null! - #emitter = new EventEmitter() as TypedEmitter + #emitter = new EventEmitter() as TypedEmitter - constructor(options: ClientGatewayOptions) { + constructor(options: ClientWebSocketManagerOptions) { this.url = options.url + this.timeout = options.timeout ?? 10000 } /** @@ -39,6 +42,11 @@ export default class ClientGateway { try { this.#socket = new WebSocket(this.url) + setTimeout(() => { + if (!this.ready) throw new Error('WebSocket connection timed out') + this.#socket.close() + }, this.timeout) + this.#socket.on('open', () => { this.disconnected = false this.#listen() @@ -47,10 +55,14 @@ export default class ClientGateway { rs() }) - const errorHandler = () => this.#handleDisconnect(DisconnectReason.Generic) + this.#socket.on('error', (err) => { + throw err + }) - this.#socket.on('close', errorHandler) - this.#socket.on('error', errorHandler) + this.#socket.on('close', (code, reason) => { + if (code === 1006) throw new Error(`Failed to connect to WebSocket server: ${reason}`) + this.#handleDisconnect(DisconnectReason.Generic) + }) } catch (e) { rj(e) } @@ -63,9 +75,9 @@ export default class ClientGateway { * @param handler The event handler * @returns The event handler function */ - on( + on( name: TOpName, - handler: ClientGatewayEventHandlers[typeof name], + handler: ClientWebSocketEvents[typeof name], ) { this.#emitter.on(name, handler) } @@ -76,9 +88,9 @@ export default class ClientGateway { * @param handler The event handler to remove * @returns The removed event handler function */ - off( + off( name: TOpName, - handler: ClientGatewayEventHandlers[typeof name], + handler: ClientWebSocketEvents[typeof name], ) { this.#emitter.off(name, handler) } @@ -89,9 +101,9 @@ export default class ClientGateway { * @param handler The event handler * @returns The event handler function */ - once( + once( name: TOpName, - handler: ClientGatewayEventHandlers[typeof name], + handler: ClientWebSocketEvents[typeof name], ) { this.#emitter.once(name, handler) } @@ -114,14 +126,14 @@ export default class ClientGateway { */ disconnect() { this.#throwIfDisconnected('Cannot disconnect when already disconnected from the server') - this.#handleDisconnect(DisconnectReason.Generic) + this.#handleDisconnect(DisconnectReason.PlannedDisconnect) } /** * Checks whether the client is ready * @returns Whether the client is ready */ - isReady(): this is ReadiedClientGateway { + isReady(): this is ReadiedClientWebSocketManager { return this.ready } @@ -145,7 +157,7 @@ export default class ClientGateway { return this.#handleDisconnect((packet as Packet).d.reason) default: return this.#emitter.emit( - uncapitalize(ServerOperation[packet.op] as ClientGatewayServerEventName), + uncapitalize(ServerOperation[packet.op] as ClientWebSocketEventName), // @ts-expect-error TypeScript doesn't know that the lines above negate the type enough packet, ) @@ -162,6 +174,7 @@ export default class ClientGateway { clearTimeout(this.#hbTimeout) this.disconnected = reason this.#socket.close() + this.#socket = null! this.#emitter.emit('disconnect', reason) } @@ -190,25 +203,30 @@ export default class ClientGateway { } } -export interface ClientGatewayOptions { +export interface ClientWebSocketManagerOptions { /** - * The gateway URL to connect to + * The URL to connect to */ url: string + /** + * The timeout for the connection + * @default 10000 + */ + timeout?: number } -export type ClientGatewayServerEventName = keyof typeof ServerOperation +export type ClientWebSocketEventName = keyof typeof ServerOperation -export type ClientGatewayEventHandlers = { - [K in Uncapitalize]: ( +export type ClientWebSocketEvents = { + [K in Uncapitalize]: ( packet: Packet<(typeof ServerOperation)[Capitalize]>, ) => Promise | void } & { - hello: (config: NonNullable) => Promise | void + hello: (config: NonNullable) => Promise | void ready: () => Promise | void packet: (packet: Packet) => Promise | void invalidPacket: (packet: Packet) => Promise | void disconnect: (reason: DisconnectReason) => Promise | void } -export type ReadiedClientGateway = RequiredProperty> +export type ReadiedClientWebSocketManager = RequiredProperty> diff --git a/packages/api/src/classes/index.ts b/packages/api/src/classes/index.ts index f2d3e72..6762ec9 100755 --- a/packages/api/src/classes/index.ts +++ b/packages/api/src/classes/index.ts @@ -1,4 +1,3 @@ export { default as Client } from './Client' export * from './Client' -export { default as ClientGateway } from './ClientGateway' -export * from './ClientGateway' +export * from './ClientWebSocket' diff --git a/packages/shared/src/constants/DisconnectReason.ts b/packages/shared/src/constants/DisconnectReason.ts index 6935095..37588ba 100755 --- a/packages/shared/src/constants/DisconnectReason.ts +++ b/packages/shared/src/constants/DisconnectReason.ts @@ -22,6 +22,10 @@ enum DisconnectReason { * The client had never connected to the server (**CLIENT-ONLY**) */ NeverConnected = 5, + /** + * The client disconnected on its own (**CLIENT-ONLY**) + */ + PlannedDisconnect = 6, } export default DisconnectReason diff --git a/packages/shared/src/constants/HumanizedDisconnectReason.ts b/packages/shared/src/constants/HumanizedDisconnectReason.ts index 0db27c2..0f3c536 100755 --- a/packages/shared/src/constants/HumanizedDisconnectReason.ts +++ b/packages/shared/src/constants/HumanizedDisconnectReason.ts @@ -9,6 +9,7 @@ const HumanizedDisconnectReason = { [DisconnectReason.TimedOut]: 'has timed out', [DisconnectReason.ServerError]: 'has been disconnected due to an internal server error', [DisconnectReason.NeverConnected]: 'had never connected to the server', + [DisconnectReason.PlannedDisconnect]: 'has disconnected on its own', } as const satisfies Record export default HumanizedDisconnectReason