import type { SocketId } from "socket.io-adapter"; import { Server, Namespace, Socket } from ".."; import expect from "expect.js"; import { success, createClient, successFn, createPartialDone, } from "./support/util"; describe("namespaces", () => { it("should be accessible through .sockets", () => { const io = new Server(); expect(io.sockets).to.be.a(Namespace); }); it("should be aliased", () => { const io = new Server(); expect(io.use).to.be.a("function"); expect(io.to).to.be.a("function"); expect(io["in"]).to.be.a("function"); expect(io.emit).to.be.a("function"); expect(io.send).to.be.a("function"); expect(io.write).to.be.a("function"); expect(io.allSockets).to.be.a("function"); expect(io.compress).to.be.a("function"); }); it("should return an immutable broadcast operator", () => { const io = new Server(); const operator = io.local.to(["room1", "room2"]).except("room3"); operator.compress(true).emit("hello"); operator.volatile.emit("hello"); operator.to("room4").emit("hello"); operator.except("room5").emit("hello"); io.to("room6").emit("hello"); // @ts-ignore expect(operator.rooms).to.contain("room1", "room2"); // @ts-ignore expect(operator.exceptRooms).to.contain("room3"); // @ts-ignore expect(operator.flags).to.eql({ local: true }); }); it("should automatically connect", (done) => { const io = new Server(0); const socket = createClient(io); socket.on("connect", successFn(done, io, socket)); }); it("should fire a `connection` event", (done) => { const io = new Server(0); const clientSocket = createClient(io); io.on("connection", (socket) => { expect(socket).to.be.a(Socket); success(done, io, clientSocket); }); }); it("should fire a `connect` event", (done) => { const io = new Server(0); const clientSocket = createClient(io); io.on("connect", (socket) => { expect(socket).to.be.a(Socket); success(done, io, clientSocket); }); }); it("should work with many sockets", (done) => { const io = new Server(0); io.of("/chat"); io.of("/news"); const chat = createClient(io, "/chat"); const news = createClient(io, "/news"); let total = 2; chat.on("connect", () => { --total || success(done, io, chat, news); }); news.on("connect", () => { --total || success(done, io, chat, news); }); }); it('should be able to equivalently start with "" or "/" on server', (done) => { const io = new Server(0); const c1 = createClient(io, "/"); const c2 = createClient(io, "/abc"); let total = 2; io.of("").on("connection", () => { --total || success(done, io, c1, c2); }); io.of("abc").on("connection", () => { --total || success(done, io, c1, c2); }); }); it('should be equivalent for "" and "/" on client', (done) => { const io = new Server(0); const c1 = createClient(io, ""); io.of("/").on("connection", successFn(done, io, c1)); }); it("should work with `of` and many sockets", (done) => { const io = new Server(0); const chat = createClient(io, "/chat"); const news = createClient(io, "/news"); let total = 2; io.of("/news").on("connection", (socket) => { expect(socket).to.be.a(Socket); --total || success(done, io, chat, news); }); io.of("/news").on("connection", (socket) => { expect(socket).to.be.a(Socket); --total || success(done, io, chat, news); }); }); it("should work with `of` second param", (done) => { const io = new Server(0); const chat = createClient(io, "/chat"); const news = createClient(io, "/news"); let total = 2; io.of("/news", (socket) => { expect(socket).to.be.a(Socket); --total || success(done, io, chat, news); }); io.of("/news", (socket) => { expect(socket).to.be.a(Socket); --total || success(done, io, chat, news); }); }); it("should disconnect upon transport disconnection", (done) => { const io = new Server(0); const chat = createClient(io, "/chat"); const news = createClient(io, "/news"); let total = 2; let totald = 2; let s; io.of("/news", (socket) => { socket.on("disconnect", (reason) => { --totald || success(done, io, chat, news); }); --total || close(); }); io.of("/chat", (socket) => { s = socket; socket.on("disconnect", (reason) => { --totald || success(done, io, chat, news); }); --total || close(); }); function close() { s.disconnect(true); } }); it("should fire a `disconnecting` event just before leaving all rooms", (done) => { const io = new Server(0); const socket = createClient(io); io.on("connection", (s) => { s.join("a"); // FIXME not sure why process.nextTick() is needed here process.nextTick(() => s.disconnect()); let total = 2; s.on("disconnecting", (reason) => { expect(s.rooms).to.contain(s.id, "a"); total--; }); s.on("disconnect", (reason) => { expect(s.rooms.size).to.eql(0); --total || success(done, io, socket); }); }); }); it("should return error connecting to non-existent namespace", (done) => { const io = new Server(0); const socket = createClient(io, "/doesnotexist"); socket.on("connect_error", (err) => { expect(err.message).to.be("Invalid namespace"); success(done, io); }); }); it("should not reuse same-namespace connections", (done) => { const io = new Server(0); const clientSocket1 = createClient(io); const clientSocket2 = createClient(io); let connections = 0; io.on("connection", () => { connections++; if (connections === 2) { success(done, io, clientSocket1, clientSocket2); } }); }); it("should find all clients in a namespace", (done) => { const io = new Server(0); const chatSids: string[] = []; let otherSid: SocketId | null = null; const c1 = createClient(io, "/chat"); const c2 = createClient(io, "/chat", { forceNew: true }); const c3 = createClient(io, "/other", { forceNew: true }); let total = 3; io.of("/chat").on("connection", (socket) => { chatSids.push(socket.id); --total || getSockets(); }); io.of("/other").on("connection", (socket) => { otherSid = socket.id; --total || getSockets(); }); async function getSockets() { const sids = await io.of("/chat").allSockets(); expect(sids).to.contain(chatSids[0], chatSids[1]); expect(sids).to.not.contain(otherSid); success(done, io, c1, c2, c3); } }); it("should find all clients in a namespace room", (done) => { const io = new Server(0); let chatFooSid: SocketId | null = null; let chatBarSid: SocketId | null = null; let otherSid: SocketId | null = null; const c1 = createClient(io, "/chat"); const c2 = createClient(io, "/chat", { forceNew: true }); const c3 = createClient(io, "/other", { forceNew: true }); let chatIndex = 0; let total = 3; io.of("/chat").on("connection", (socket) => { if (chatIndex++) { socket.join("foo"); chatFooSid = socket.id; --total || getSockets(); } else { socket.join("bar"); chatBarSid = socket.id; --total || getSockets(); } }); io.of("/other").on("connection", (socket) => { socket.join("foo"); otherSid = socket.id; --total || getSockets(); }); async function getSockets() { const sids = await io.of("/chat").in("foo").allSockets(); expect(sids).to.contain(chatFooSid); expect(sids).to.not.contain(chatBarSid); expect(sids).to.not.contain(otherSid); success(done, io, c1, c2, c3); } }); it("should find all clients across namespace rooms", (done) => { const io = new Server(0); let chatFooSid: SocketId | null = null; let chatBarSid: SocketId | null = null; let otherSid: SocketId | null = null; const c1 = createClient(io, "/chat"); const c2 = createClient(io, "/chat", { forceNew: true }); const c3 = createClient(io, "/other", { forceNew: true }); let chatIndex = 0; let total = 3; io.of("/chat").on("connection", (socket) => { if (chatIndex++) { socket.join("foo"); chatFooSid = socket.id; --total || getSockets(); } else { socket.join("bar"); chatBarSid = socket.id; --total || getSockets(); } }); io.of("/other").on("connection", (socket) => { socket.join("foo"); otherSid = socket.id; --total || getSockets(); }); async function getSockets() { const sids = await io.of("/chat").allSockets(); expect(sids).to.contain(chatFooSid, chatBarSid); expect(sids).to.not.contain(otherSid); success(done, io, c1, c2, c3); } }); it("should not emit volatile event after regular event", (done) => { const io = new Server(0); let counter = 0; io.of("/chat").on("connection", (s) => { // Wait to make sure there are no packets being sent for opening the connection setTimeout(() => { io.of("/chat").emit("ev", "data"); io.of("/chat").volatile.emit("ev", "data"); }, 50); }); const socket = createClient(io, "/chat"); socket.on("ev", () => { counter++; }); setTimeout(() => { expect(counter).to.be(1); success(done, io, socket); }, 500); }); it("should emit volatile event", (done) => { const io = new Server(0); let counter = 0; io.of("/chat").on("connection", (s) => { // Wait to make sure there are no packets being sent for opening the connection setTimeout(() => { io.of("/chat").volatile.emit("ev", "data"); }, 100); }); const socket = createClient(io, "/chat"); socket.on("ev", () => { counter++; }); setTimeout(() => { expect(counter).to.be(1); success(done, io, socket); }, 500); }); it("should enable compression by default", (done) => { const io = new Server(0); const socket = createClient(io, "/chat"); io.of("/chat").on("connection", (s) => { s.conn.once("packetCreate", (packet) => { expect(packet.options.compress).to.be(true); success(done, io, socket); }); io.of("/chat").emit("woot", "hi"); }); }); it("should disable compression", (done) => { const io = new Server(0); const socket = createClient(io, "/chat"); io.of("/chat").on("connection", (s) => { s.conn.once("packetCreate", (packet) => { expect(packet.options.compress).to.be(false); success(done, io, socket); }); io.of("/chat").compress(false).emit("woot", "hi"); }); }); it("should throw on reserved event", () => { const io = new Server(); expect(() => io.emit("connect")).to.throwException( /"connect" is a reserved event name/ ); }); it("should close a client without namespace", (done) => { const io = new Server(0, { connectTimeout: 10, }); const socket = createClient(io); // @ts-ignore socket.io.engine.write = () => {}; // prevent the client from sending a CONNECT packet socket.on("disconnect", successFn(done, io, socket)); }); it("should exclude a specific socket when emitting", (done) => { const io = new Server(0); const socket1 = createClient(io, "/"); const socket2 = createClient(io, "/"); socket2.on("a", () => { done(new Error("should not happen")); }); socket1.on("a", successFn(done, io, socket1, socket2)); socket2.on("connect", () => { io.except(socket2.id).emit("a"); }); }); it("should exclude a specific socket when emitting (in a namespace)", (done) => { const io = new Server(0); const nsp = io.of("/nsp"); const socket1 = createClient(io, "/nsp"); const socket2 = createClient(io, "/nsp"); socket2.on("a", () => { done(new Error("not")); }); socket1.on("a", successFn(done, io, socket1, socket2)); socket2.on("connect", () => { nsp.except(socket2.id).emit("a"); }); }); it("should exclude a specific room when emitting", (done) => { const io = new Server(0); const nsp = io.of("/nsp"); const socket1 = createClient(io, "/nsp"); const socket2 = createClient(io, "/nsp"); socket1.on("a", successFn(done, io, socket1, socket2)); socket2.on("a", () => { done(new Error("not")); }); nsp.on("connection", (socket) => { socket.on("broadcast", () => { socket.join("room1"); nsp.except("room1").emit("a"); }); }); socket2.emit("broadcast"); }); it("should emit an 'new_namespace' event", (done) => { const io = new Server(); io.on("new_namespace", (namespace) => { expect(namespace.name).to.eql("/nsp"); done(); }); io.of("/nsp"); }); it("should not clean up a non-dynamic namespace", (done) => { const io = new Server(0, { cleanupEmptyChildNamespaces: true }); const c1 = createClient(io, "/chat"); c1.on("connect", () => { c1.disconnect(); // Give it some time to disconnect the client setTimeout(() => { expect(io._nsps.has("/chat")).to.be(true); expect(io._nsps.get("/chat")!.sockets.size).to.be(0); success(done, io); }, 100); }); io.of("/chat"); }); describe("dynamic namespaces", () => { it("should allow connections to dynamic namespaces with a regex", (done) => { const io = new Server(0); const socket = createClient(io, "/dynamic-101"); const partialDone = createPartialDone(4, successFn(done, io, socket)); let dynamicNsp = io .of(/^\/dynamic-\d+$/) .on("connect", (socket) => { expect(socket.nsp.name).to.be("/dynamic-101"); dynamicNsp.emit("hello", 1, "2", { 3: "4" }); partialDone(); }) .use((socket, next) => { next(); partialDone(); }); socket.on("connect_error", (err) => { expect().fail(); }); socket.on("connect", () => { partialDone(); }); socket.on("hello", (a, b, c) => { expect(a).to.eql(1); expect(b).to.eql("2"); expect(c).to.eql({ 3: "4" }); partialDone(); }); }); it("should allow connections to dynamic namespaces with a function", (done) => { const io = new Server(0); const socket = createClient(io, "/dynamic-101"); io.of((name, query, next) => next(null, "/dynamic-101" === name)); socket.on("connect", successFn(done, io, socket)); }); it("should disallow connections when no dynamic namespace matches", (done) => { const io = new Server(0); const socket = createClient(io, "/abc"); io.of(/^\/dynamic-\d+$/); io.of((name, query, next) => next(null, "/dynamic-101" === name)); socket.on("connect_error", (err) => { expect(err.message).to.be("Invalid namespace"); success(done, io, socket); }); }); it("should emit an 'new_namespace' event for a dynamic namespace", (done) => { const io = new Server(0); io.of(/^\/dynamic-\d+$/); const socket = createClient(io, "/dynamic-101"); io.on("new_namespace", (namespace) => { expect(namespace.name).to.be("/dynamic-101"); success(done, io, socket); }); }); it("should handle race conditions with dynamic namespaces (#4136)", (done) => { const io = new Server(0); const counters = { connected: 0, created: 0, events: 0, }; const buffer: Function[] = []; io.on("new_namespace", (namespace) => { counters.created++; }); const handler = () => { if (++counters.events === 2) { expect(counters.created).to.equal(1); success(done, io, one, two); } }; io.of((name, query, next) => { buffer.push(next); if (buffer.length === 2) { buffer.forEach((next) => next(null, true)); } }).on("connection", (socket) => { if (++counters.connected === 2) { io.of("/dynamic-101").emit("message"); } }); let one = createClient(io, "/dynamic-101"); let two = createClient(io, "/dynamic-101"); one.on("message", handler); two.on("message", handler); }); it("should clean up namespace when cleanupEmptyChildNamespaces is on and there are no more sockets in a namespace", (done) => { const io = new Server(0, { cleanupEmptyChildNamespaces: true }); const c1 = createClient(io, "/dynamic-101"); c1.on("connect", () => { c1.disconnect(); // Give it some time to disconnect and clean up the namespace setTimeout(() => { expect(io._nsps.has("/dynamic-101")).to.be(false); success(done, io); }, 100); }); io.of(/^\/dynamic-\d+$/); }); it("should allow a client to connect to a cleaned up namespace", (done) => { const io = new Server(0, { cleanupEmptyChildNamespaces: true }); const c1 = createClient(io, "/dynamic-101"); c1.on("connect", () => { c1.disconnect(); // Give it some time to disconnect and clean up the namespace setTimeout(() => { expect(io._nsps.has("/dynamic-101")).to.be(false); const c2 = createClient(io, "/dynamic-101"); c2.on("connect", () => { success(done, io, c2); }); c2.on("connect_error", () => { done( new Error("Client got error when connecting to dynamic namespace") ); }); }, 100); }); io.of(/^\/dynamic-\d+$/); }); it("should not clean up namespace when cleanupEmptyChildNamespaces is off and there are no more sockets in a namespace", (done) => { const io = new Server(0); const c1 = createClient(io, "/dynamic-101"); c1.on("connect", () => { c1.disconnect(); // Give it some time to disconnect and clean up the namespace setTimeout(() => { expect(io._nsps.has("/dynamic-101")).to.be(true); expect(io._nsps.get("/dynamic-101")!.sockets.size).to.be(0); success(done, io); }, 100); }); io.of(/^\/dynamic-\d+$/); }); it("should NOT clean up namespace when cleanupEmptyChildNamespaces is OFF and client is rejected in middleware", (done) => { const io = new Server(0, { cleanupEmptyChildNamespaces: false }); io.of(/^\/dynamic-\d+$/).use((socket, next) => { next(new Error("You shall not pass!")); }); const c1 = createClient(io, "/dynamic-101"); c1.on("connect", () => { done(new Error("Should not connect")); }); c1.on("connect_error", () => { setTimeout(() => { expect(io._nsps.has("/dynamic-101")).to.be(true); expect(io._nsps.get("/dynamic-101")!.sockets.size).to.be(0); success(done, io, c1); }, 100); }); }); it("should clean up namespace when cleanupEmptyChildNamespaces is ON and client is rejected in middleware", (done) => { const io = new Server(0, { cleanupEmptyChildNamespaces: true }); io.of(/^\/dynamic-\d+$/).use((socket, next) => { next(new Error("You shall not pass!")); }); const c1 = createClient(io, "/dynamic-101"); c1.on("connect", () => { done(new Error("Should not connect")); }); c1.on("connect_error", () => { setTimeout(() => { expect(io._nsps.has("/dynamic-101")).to.be(false); success(done, io, c1); }, 100); }); }); it("should NOT clean up namespace when cleanupEmptyChildNamespaces is ON and client is rejected in middleware but there are other clients connected", (done) => { const io = new Server(0, { cleanupEmptyChildNamespaces: true }); let clientIdxToReject = 0; io.of(/^\/dynamic-\d+$/).use((socket, next) => { if (clientIdxToReject) { next(new Error("You shall not pass!")); } else { next(); } clientIdxToReject++; }); const c1 = createClient(io, "/dynamic-101"); c1.on("connect", () => { const c2 = createClient(io, "/dynamic-101"); c2.on("connect", () => { done(new Error("Client 2 should not connect")); }); c2.on("connect_error", () => { setTimeout(() => { expect(io._nsps.has("/dynamic-101")).to.be(true); expect(io._nsps.get("/dynamic-101")!.sockets.size).to.be(1); success(done, io, c1, c2); }, 100); }); }); c1.on("connect_error", () => { done(new Error("Client 1 should not get an error")); }); }); it("should attach a child namespace to its parent upon manual creation", () => { const io = new Server(0); const parentNamespace = io.of(/^\/dynamic-\d+$/); const childNamespace = io.of("/dynamic-101"); // @ts-ignore expect(parentNamespace.children.has(childNamespace)).to.be(true); io.close(); }); }); });