node-express-socket
Version:
A structure to add socket manager in express routes
228 lines (182 loc) • 6.38 kB
JavaScript
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;