Files
BetterDesk/web-nodejs/services/wsUpgradeRouter.js
T
UNITRONIX ce988724c1 fix(console): share one WebSocket upgrade dispatcher (#295)
Collapse 11 per-service upgrade listeners into wsUpgradeRouter so Node no longer emits a false MaxListenersExceededWarning at panel startup.
2026-07-22 18:21:50 +02:00

86 lines
2.9 KiB
JavaScript

/**
* Shared WebSocket upgrade router for the BetterDesk console HTTP server.
*
* Previously each WS service called server.on('upgrade'), which stacked 11
* listeners and triggered Node's MaxListenersExceededWarning (default max=10).
* Reconnects never added listeners — the warning was a false-positive leak
* signal (GitHub #295). This module attaches exactly one upgrade listener per
* server and dispatches by pathname.
*/
'use strict';
/** @type {WeakMap<object, { routes: Array<{ match: Function, handle: Function }> }>} */
const routers = new WeakMap();
/**
* Ensure a single upgrade dispatcher is attached to the given server.
* @param {import('http').Server|import('events').EventEmitter} server
*/
function ensureRouter(server) {
let state = routers.get(server);
if (state) return state;
state = { routes: [] };
routers.set(server, state);
server.on('upgrade', (req, socket, head) => {
let pathname;
try {
pathname = new URL(req.url, `http://${req.headers.host || 'localhost'}`).pathname;
} catch {
try {
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
} catch (_) { /* closed */ }
try { socket.destroy(); } catch (_) { /* closed */ }
return;
}
for (const route of state.routes) {
if (route.match(pathname, req)) {
route.handle(req, socket, head);
return;
}
}
// No registered route — leave the socket alone (same as the old
// multi-listener chain when every handler early-returned).
});
return state;
}
/**
* Register a WebSocket upgrade handler on the shared per-server router.
* Idempotent with respect to the HTTP listener: only one server.on('upgrade')
* is ever attached per server instance.
*
* @param {import('http').Server|import('events').EventEmitter} server
* @param {(pathname: string, req: import('http').IncomingMessage) => boolean} match
* @param {(req: import('http').IncomingMessage, socket: import('net').Socket, head: Buffer) => void} handle
*/
function registerUpgradeHandler(server, match, handle) {
if (!server || typeof server.on !== 'function') {
throw new TypeError('registerUpgradeHandler: server must be an EventEmitter');
}
if (typeof match !== 'function' || typeof handle !== 'function') {
throw new TypeError('registerUpgradeHandler: match and handle must be functions');
}
const state = ensureRouter(server);
state.routes.push({ match, handle });
}
/**
* Number of routes registered for this server (for tests / diagnostics).
* @param {object} server
* @returns {number}
*/
function registeredRouteCount(server) {
const state = routers.get(server);
return state ? state.routes.length : 0;
}
module.exports = {
registerUpgradeHandler,
registeredRouteCount,
};