/* * Copyright © 2024 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). * 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited. */ import { io, Socket, ManagerOptions, SocketOptions } from "socket.io-client"; import { IOIncomingMessage, IOOutgoingMessage } from "./types/io-message"; type SocketIoClientConfig = Partial; export class SocketIoClient { /** * Default configuration for the socket client * @static */ static defaultConfig: SocketIoClientConfig = { // Socket options ackTimeout: 1000, // auth: undefined, retries: 3, // Manager options autoConnect: true, // parser: undefined, // randomizationFactor:0.5, reconnection: true, reconnectionAttempts: 100, reconnectionDelay: 1000, reconnectionDelayMax: 5000, timeout: 20000, // Low Level Options addTrailingSlash: true, // eg: https://domain.path/ => https://domain.path/ // autoUnref:false, // firefox only option // path: "/socket.io", // This is the socket path in the server, leave it as default unless changed manually in server transports: ["websocket", "polling"], // ["websocket","polling", "websocket"] upgrade: true, withCredentials: true, }; private socket: Socket; private config: SocketIoClientConfig; private initialized: boolean = false; constructor(apiUrl: string, socketConfig?: SocketIoClientConfig) { this.config = { ...SocketIoClient.defaultConfig, ...socketConfig, autoConnect: false, }; const url = new URL(apiUrl); this.socket = io(url.origin, this.config); } /** * Initializes the socket client and sets up event handlers. * @param handlers Event handlers for connection, disconnection, and connection errors */ public init({ onConnect, onDisconnect, onConnectError, }: { onConnect?: () => void; onDisconnect?: (reason: string, details: any) => void; onConnectError?: (error: Error) => void; }) { if (!this.initialized) this.socket.connect(); onConnect && this.uniqueOn("connect", onConnect); onDisconnect && this.uniqueOn("disconnect", onDisconnect); onConnectError && this.uniqueOn("connect_error", onConnectError); this.initialized = true; } /** * Registers an event handler for the specified event and removes any existing handlers. * @param event The event name * @param callback The callback function to handle the event */ private uniqueOn(event: string, callback: (...args: any) => void) { this.socket.off(event); this.socket.on(event, callback); } /** * Disconnects the socket client. */ public disconnect() { this.socket.disconnect(); } /** * Registers an event handler for the specified event. * @param event The event name * @param callback The callback function to handle the event */ public on(event: string, callback: (data: T) => void) { this.socket.on(event, callback); } /** * Removes an event handler for the specified event. * @param event The event name * @param callback The callback function to remove */ public off(event: string, callback: (...args: any) => void) { this.socket.off(event, callback); } /** * Sends a request to the server and waits for an acknowledgment. * @param options The request options including URL and method * @returns The response from the server * @throws Error if the request fails */ public async request( options: Pick & Partial, ): Promise> { const response: IOIncomingMessage = await this.socket.emitWithAck( options.method, options, ); if (response.statusCode >= 200 && response.statusCode < 300) { return response; } throw new Error( `Request failed with status code ${response.statusCode}: ${JSON.stringify( response.body, )}`, ); } /** * Sends a GET request to the server. * @param url The URL to send the request to * @param options Optional request options * @returns The response from the server */ public async get( url: string, options?: Partial>, ): Promise> { return this.request({ method: "get", url, ...options, }); } }