tiny-server-essentials
Version:
A good utility toolkit to unify Express v5 and Socket.IO v4 into a seamless development experience with modular helpers, server wrappers, and WebSocket tools.
336 lines (291 loc) • 9.62 kB
JavaScript
'use strict';
var EventEmitter = require('events');
var socket_ioClient = require('socket.io-client');
var proxyUser = require('./proxyUser.cjs');
var proxyArgs = require('./proxyArgs.cjs');
/** @typedef {import('../server/index.mjs').ProxyUserDisconnect} ProxyUserDisconnect */
/** @typedef {import('../server/index.mjs').ProxyUserConnection} ProxyUserConnection */
/** @typedef {import('../server/index.mjs').ProxyUserConnectionUpdated} ProxyUserConnectionUpdated */
/** @typedef {import('../server/index.mjs').ProxyRequest} ProxyRequest */
/**
* SocketIoProxyClient
* -------------------
* This class acts as a high-level client wrapper for a remote Proxy Server.
*
* The proxy server forwards real Socket.IO client events (from real users) to this instance.
* Each connected user becomes a `SocketIoProxyUser`, allowing you to:
* - Listen to the user's custom events
* - Emit events back to the user
* - Detect connection, disconnection and data updates
*
* The class controls reconnection, user mapping, event routing and authentication logic.
*
* Lifecycle Summary:
* 1. `connect()` → authenticates with the proxy and starts receiving events.
* 2. Proxy notifies about users:
* - `PROXY_USER_CONNECTION`
* - `PROXY_USER_UPDATE`
* - `PROXY_USER_DISCONNECT`
* 3. Incoming user events arrive as `PROXY_REQUEST`.
* 4. You can broadcast events through `.to(room).emit()`.
*
* When destroyed, all listeners and user references are cleaned up.
*
* @beta
*/
class SocketIoProxyClient extends EventEmitter {
#debugMode = false;
/**
* Whether debug logging is enabled.
* @returns {boolean}
*/
get debugMode() {
return this.#debugMode;
}
/**
* Enables or disables debug logging. When enabled,
* every proxy event is logged to the console.
* @param {boolean} value
*/
set debugMode(value) {
if (typeof value !== 'boolean') throw new Error('Invalid debug mode value!');
this.#debugMode = value;
}
#isDestroyed = false;
/**
* Returns true if the client has been destroyed and cannot be reused.
* @returns {boolean}
*/
get isDestroyed() {
return this.#isDestroyed;
}
#enabled = false;
#firstTime = true;
/** @type {import('socket.io-client').Socket} */
#client;
/**
* The underlying raw Socket.IO client instance.
* This can be used for low-level Socket.IO operations if needed.
* @returns {import('socket.io-client').Socket}
*/
get client() {
return this.#client;
}
/** @type {null|string|number} */
#auth = null;
/**
* Sets the authentication value that will be sent when connecting
* to the proxy server through the `AUTH_PROXY` event.
* @param {null|string|number} value
*/
set auth(value) {
if (typeof value !== 'string' && typeof value !== 'number' && value !== null)
throw new Error('Invalid Server Auth!');
this.#auth = value;
}
#connected = false;
/**
* True if the proxy is authenticated AND the socket is alive.
* @returns {boolean}
*/
get connected() {
return this.#connected && this.#client.connected && this.#client.id ? true : false;
}
/** @type {null|number} */
#connTimeout = 500;
/**
* Sets the reconnection attempt interval in milliseconds.
* Set to `null` to disable auto retry.
* @param {null|number} value
*/
set connTimeout(value) {
if (typeof value !== 'number' && value !== null) throw new Error('Invalid connection timeout!');
this.#connTimeout = value;
}
/**
* Gets the reconnection attempt interval.
* @returns {null|number}
*/
get connTimeout() {
return this.#connTimeout;
}
/**
* Map of all current proxied users.
* Key = user socket ID, Value = SocketIoProxyUser instance.
* @type {Map<string, SocketIoProxyUser>}
*/
#sockets = new Map();
/**
* Creates a new Proxy Client connected to a Proxy server.
*
* @param {string} proxyAddress - The URL of the proxy server.
* @param {import('socket.io-client').ManagerOptions} cfg - Socket.IO manager configuration.
*
* Configuration notes:
* - reconnection is always disabled (handled manually)
* - autoConnect is disabled; connection occurs on `.connect()`
*
* Events emitted by this class:
* - `connect` when authentication succeeds
* - `disconnect` when connection to the proxy is lost
* - `connection` when a new proxied user appears
*/
constructor(proxyAddress, cfg) {
super();
/**
* Client Config
* @type {import('socket.io-client').ManagerOptions} cfg
*/
const clientCfg = { ...cfg };
clientCfg.reconnection = false;
clientCfg.autoConnect = false;
this.#client = socket_ioClient.io(proxyAddress, clientCfg);
/** Handle disconnection and trigger retry logic */
this.#client.on('disconnect', () => {
this.emit('disconnect');
this.#connected = false;
});
/** Periodic reconnection attempts */
const retryConn = () => {
if (!this.#firstTime && !this.#connected) this.connect();
if (this.#enabled) setTimeout(retryConn, this.#connTimeout ?? 0);
};
setTimeout(retryConn, this.#connTimeout ?? 0);
/**
* PROXY_REQUEST
* ----------------
* Forwarded events emitted *by a specific remote user*.
* Structure:
* socketId: string
* eventName: string
* args...
*/
this.#client.on(
'PROXY_REQUEST',
/** @type {(...args: ProxyRequest) => void} */
(socketId, eventName, ...args) => {
if (this.#debugMode) console.log('PROXY_REQUEST', socketId, eventName, ...args);
if (typeof socketId !== 'string' || typeof eventName !== 'string') return;
const socket = this.#sockets.get(socketId);
if (!socket) return;
socket._emit(eventName, ...args);
},
);
/**
* PROXY_USER_CONNECTION
* -----------------------
* Fired when a new remote user connects to the proxy.
*/
this.#client.on('PROXY_USER_CONNECTION', (/** @type {ProxyUserConnection} */ socketInfo) => {
if (this.#debugMode) console.log('PROXY_USER_CONNECTION', socketInfo);
const socket = new proxyUser(socketInfo, this.#client);
this.#sockets.set(socketInfo.id, socket);
this.emit('connection', socket);
});
/**
* PROXY_USER_DISCONNECT
* ----------------------
* Fired when a remote user disconnects.
*/
this.#client.on('PROXY_USER_DISCONNECT', (/** @type {ProxyUserDisconnect} */ socketInfo) => {
if (this.#debugMode) console.log('PROXY_USER_DISCONNECT', socketInfo);
const socket = this.#sockets.get(socketInfo.id);
if (!socket) return;
socket._emit('disconnect', socketInfo.reason, socketInfo.desc);
socket._disconnect();
this.#sockets.delete(socketInfo.id);
});
/**
* PROXY_USER_UPDATE
* -------------------
* Fired when a user's data is modified.
* Includes room changes or custom metadata modifications.
*/
this.#client.on(
'PROXY_USER_UPDATE',
(
/** @type {ProxyUserConnectionUpdated} */ socketInfo,
/** @type {string} */ type,
/** @type {string|null|undefined} */ room,
) => {
if (this.#debugMode) console.log('PROXY_USER_UPDATE', socketInfo);
const socket = this.#sockets.get(socketInfo.id);
if (!socket) return;
socket._updateData(socketInfo.changes, type, room);
},
);
}
/**
* Prepares a room-targeted broadcast mechanism.
* Works similarly to Socket.IO `.to(room).emit()`.
*
* @param {string|string[]} room
* @returns {{ emit(eventName: string, ...args: any): import('socket.io-client').Socket }}
*/
to(room) {
return {
emit: (eventName, ...args) =>
this.#client.emit('PROXY_BROADCAST_OPERATOR', ...proxyArgs([room, eventName, ...args])),
};
}
/**
* Establishes a connection with the proxy server and completes authentication.
*
* @returns {Promise<boolean>} Resolves `true` if authentication occurs,
* `false` if already connected.
*
* Flow:
* 1. Connect to raw Socket.IO server (if not already)
* 2. Emit `AUTH_PROXY`
* 3. Proxy responds via callback and confirms the session
*/
connect() {
return new Promise((resolve, reject) => {
if (this.#isDestroyed) reject(new Error('Destroyed instance!'));
this.#firstTime = false;
this.#enabled = true;
if (this.connected) {
resolve(false);
return;
}
const sendConnect = () =>
this.#client.emit('AUTH_PROXY', this.#auth, () => {
this.emit('connect');
this.#connected = true;
resolve(true);
});
if (this.#client.connected) sendConnect();
else {
this.client.once('connect', () => sendConnect());
this.client.connect();
}
});
}
/**
* Fully disconnects from the proxy and resets internal state.
* All user objects are destroyed.
*/
disconnect() {
this.client.disconnect();
this.#enabled = false;
this.#firstTime = true;
this.#sockets.forEach((socket) => {
socket.removeAllListeners();
try {
socket.disconnect();
} catch {}
});
this.#sockets.clear();
}
/**
* Completely destroys the instance.
* Used when the client will never be reused.
*/
destroy() {
this.disconnect();
this.#client.removeAllListeners();
this.removeAllListeners();
this.#isDestroyed = true;
}
}
module.exports = SocketIoProxyClient;