UNPKG

node-express-socket

Version:

A structure to add socket manager in express routes

228 lines (182 loc) 6.38 kB
const express = require("express"); const { Server } = require("socket.io"); const { match } = require("path-to-regexp"); const { URL } = require("url"); /* -------------------------------------------------------------------------- */ /* Patch Express Router Globally */ /* -------------------------------------------------------------------------- */ const originalRouter = express.Router; express.Router = function (...args) { const router = originalRouter(...args); router.socketHandlers = {}; router.socket = function (path, ...handlers) { if (!this.socketHandlers[path]) { this.socketHandlers[path] = []; } this.socketHandlers[path].push(...handlers); }; return router; }; /* -------------------------------------------------------------------------- */ /* Recursively Collect Socket Routes */ /* -------------------------------------------------------------------------- */ function collectSocketRoutes(stack, prefix = "") { const routes = []; stack.forEach((layer) => { // Direct socket handlers if (layer.handle?.socketHandlers) { for (const [path, handlers] of Object.entries( layer.handle.socketHandlers )) { const fullPath = (prefix + path) .replace(/\/+/g, "/") .replace(/\/$/, "") || "/"; routes.push({ path: fullPath, handlers, }); } } // Nested router if (layer.name === "router" && layer.handle?.stack) { let mountPath = layer.path || ""; const newPrefix = (prefix + "/" + mountPath) .replace(/\/+/g, "/") .replace(/\/$/, ""); routes.push( ...collectSocketRoutes(layer.handle.stack, newPrefix) ); } }); return routes; } /* -------------------------------------------------------------------------- */ /* Convert Params to Wildcard Event Name */ /* -------------------------------------------------------------------------- */ function wildcardEventName(path) { return path.replace(/:[^/]+/g, "*"); } /* -------------------------------------------------------------------------- */ /* Middleware Execution Engine */ /* -------------------------------------------------------------------------- */ function runHandlers(handlers, req, res) { let index = 0; function dispatch(err) { if (index >= handlers.length) return; const handler = handlers[index++]; const isErrorMiddleware = handler.length === 4; try { if (err) { if (isErrorMiddleware) { const result = handler(err, req, res, dispatch); if (result?.then) result.catch(dispatch); } else { dispatch(err); } } else { if (!isErrorMiddleware) { const result = handler(req, res, dispatch); if (result?.then) result.catch(dispatch); } else { dispatch(); } } } catch (error) { dispatch(error); } } dispatch(); } /* -------------------------------------------------------------------------- */ /* Main Setup */ /* -------------------------------------------------------------------------- */ function setupExpressSocket(app, server) { const io = new Server(server); /* ------------------------ HTTP → Socket Helpers ------------------------- */ app.use((req, res, next) => { res.sendSocket = (event, data) => { if (req.socketId) { const target = io.sockets.sockets.get(req.socketId); if (target) target.emit(event, data); } }; res.sendSocketTo = (data, ...socketIds) => { socketIds.forEach((id) => { const target = io.sockets.sockets.get(id); if (target) target.emit(data.event, data.payload); }); }; res.broadcastSocket = ( event, data, options = { excludeSender: true } ) => { if (options.excludeSender && req.socketId) { const sender = io.sockets.sockets.get(req.socketId); if (sender) sender.broadcast.emit(event, data); } else { io.emit(event, data); } }; next(); }); /* --------------------------- Socket Handling ---------------------------- */ io.on("connection", (socket) => { console.log("Socket connected:", socket.id); socket.onAny((eventName, ...args) => { let pathOnly = eventName; let query = {}; try { const url = new URL(eventName, "http://localhost"); pathOnly = url.pathname; url.searchParams.forEach((v, k) => { query[k] = v !== "" ? v : true; }); } catch { // ignore non-url event names } const allRoutes = collectSocketRoutes(app._router.stack); allRoutes.forEach((route) => { const matcher = match(route.path.replace(/\/+$/, ""), { decode: decodeURIComponent, }); const matched = matcher(pathOnly.replace(/\/+$/, "")); if (!matched) return; const req = { path: pathOnly, socket, socketId: socket.id, params: matched.params, query, body: args.reduce((acc, cur, index) => { if (typeof cur === "object" && cur !== null) { return { ...acc, ...cur }; } return { ...acc, [`arg${index + 1}`]: cur }; }, {}), }; const res = { send: (data) => { socket.emit(wildcardEventName(route.path), data); }, sendTo: (data, ...socketIds) => { const event = wildcardEventName(route.path); socketIds.forEach((id) => { const target = io.sockets.sockets.get(id); if (target) target.emit(event, data); }); }, broadcast: (data, options = { excludeSender: true }) => { const event = wildcardEventName(route.path); if (options.excludeSender) socket.broadcast.emit(event, data); else io.emit(event, data); }, }; runHandlers(route.handlers, req, res); }); }); }); return io; } module.exports = setupExpressSocket;