@underpostnet/underpost
Version:
Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc
164 lines (150 loc) • 5.32 kB
JavaScript
/**
* Client-side WebSocket provider using Socket.IO.
* Manages a singleton socket connection, channel registration, and event dispatching.
*
* @module client/core/SocketIo
* @namespace SocketIoProvider
*/
import { io } from 'socket.io/client-dist/socket.io.esm.min.js';
import { SocketEventType, socketEvents } from './ClientEvents.js';
import { loggerFactory } from './Logger.js';
import { getWsBasePath, getWsBaseUrl } from '../../services/core/core.service.js';
const logger = loggerFactory(import.meta);
/**
* @class SocketIoProvider
* @classdesc Provides static methods for managing a singleton Socket.IO client connection,
* channel event dispatching, and message emission.
* @memberof SocketIoProvider
*/
class SocketIoProvider {
/**
* Event callback registry, keyed by channel name → unique callback ID → handler function.
* Built-in channels: `connect`, `connect_error`, `disconnect`.
*
* @static
* @type {Object.<string, Object.<string, function>>}
*/
static Event = {
connect: {},
connect_error: {},
disconnect: {},
};
/**
* The active Socket.IO client socket instance.
*
* @static
* @type {import('socket.io-client').Socket|null}
*/
static socket = null;
/**
* The current WebSocket host URL.
*
* @static
* @type {string|undefined}
*/
static host;
static onConnect(listener, options = {}) {
return socketEvents.on(SocketEventType.connect, listener, options);
}
static offConnect(key) {
return socketEvents.off(key);
}
static onConnectError(listener, options = {}) {
return socketEvents.on(SocketEventType.connectError, listener, options);
}
static offConnectError(key) {
return socketEvents.off(key);
}
static onDisconnect(listener, options = {}) {
return socketEvents.on(SocketEventType.disconnect, listener, options);
}
static offDisconnect(key) {
return socketEvents.off(key);
}
static onChannel(type, listener, options = {}) {
return socketEvents.on(SocketEventType.channel(type), listener, options);
}
static offChannel(key) {
return socketEvents.off(key);
}
/**
* Emits a JSON-serialized payload to the server on the specified channel.
*
* @static
* @param {string} [channel=''] - The channel/event name to emit on.
* @param {Object} [payload={}] - The data to send.
* @returns {void}
*/
static Emit(channel = '', payload = {}) {
try {
this.socket.emit(channel, JSON.stringify(payload));
} catch (error) {
logger.error(error);
}
}
/**
* Initializes (or re-initializes) the Socket.IO connection.
* Disconnects any existing socket, creates a new connection, and registers
* built-in event listeners and custom channels.
*
* @static
* @async
* @param {Object} options - Connection options.
* @param {string} [options.host] - Override WebSocket host URL.
* @param {Object.<string, Object>} [options.channels] - Channel definitions to register listeners for.
* @returns {Promise<void>}
*/
static async instance(options) {
if (this.socket) this.socket.disconnect();
const path = getWsBasePath();
this.host = options.host ? options.host : getWsBaseUrl({ wsBasePath: '' });
logger.info(`ws host:`, {
host: this.host,
path,
});
const connectOptions = {
path: path === '/' ? undefined : path,
withCredentials: true,
extraHeaders: {},
transports: ['websocket', 'polling', 'flashsocket'],
};
this.socket = io(this.host, connectOptions);
this.socket.on('connect', () => {
logger.info(`event: connect | session id: ${this.socket.id}`);
socketEvents.emit(SocketEventType.connect, { id: this.socket.id });
Object.keys(this.Event.connect).map((keyEvent) => this.Event.connect[keyEvent]());
});
this.socket.on('connect_error', (err) => {
logger.info(`event: connect_error | reason: ${err.message}`);
socketEvents.emit(SocketEventType.connectError, { error: err });
Object.keys(this.Event.connect_error).map((keyEvent) => this.Event.connect_error[keyEvent](err));
});
this.socket.on('disconnect', (reason) => {
logger.info(`event: disconnect | reason: ${reason}`);
socketEvents.emit(SocketEventType.disconnect, { reason });
Object.keys(this.Event.disconnect).map((keyEvent) => this.Event.disconnect[keyEvent](reason));
});
if (options && 'channels' in options) this.setChannels(options.channels);
}
/**
* Registers socket listeners for each channel key, routing incoming messages
* to all registered callbacks in `Event[channel]`.
*
* @static
* @param {Object.<string, Object>} channels - Channel definitions keyed by channel name.
* @returns {void}
*/
static setChannels(channels) {
Object.keys(channels).map((type) => {
logger.info(`load chanel`, type);
if (!this.Event[type]) this.Event[type] = {};
this.socket.on(type, (...args) => {
socketEvents.emit(SocketEventType.channel(type), { type, args });
Object.keys(this.Event[type]).map((keyEvent) => this.Event[type][keyEvent](args));
});
});
}
}
/** @type {SocketIoProvider} Backward compatibility alias. */
const SocketIo = SocketIoProvider;
export { SocketIoProvider, SocketIo };