From 5d9220b69adf73e086c27bbb63a4976b348f7c4c Mon Sep 17 00:00:00 2001 From: Steve Baum Date: Wed, 18 Jan 2023 17:50:13 -0600 Subject: [PATCH] feat: add the ability to clean up empty child namespaces (#4602) This commit adds a new option, "cleanupEmptyChildNamespaces". With this option enabled (disabled by default), when a socket disconnects from a dynamic namespace and if there are no other sockets connected to it then the namespace will be cleaned up and its adapter will be closed. Note: the namespace can be connected to later (it will be recreated) Related: https://github.com/socketio/socket.io-redis-adapter/issues/480 --- lib/index.ts | 10 +++++ lib/parent-namespace.ts | 19 ++++++++++ test/namespaces.ts | 81 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) diff --git a/lib/index.ts b/lib/index.ts index d9b6a090a7..1b7e54c3c6 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -98,6 +98,11 @@ interface ServerOptions extends EngineOptions, AttachOptions { */ skipMiddlewares?: boolean; }; + /** + * Whether to remove child namespaces that have no sockets connected to them + * @default false + */ + cleanupEmptyChildNamespaces: boolean; } /** @@ -243,6 +248,7 @@ export class Server< } else { this.adapter(opts.adapter || Adapter); } + opts.cleanupEmptyChildNamespaces = !!opts.cleanupEmptyChildNamespaces; this.sockets = this.of("/"); if (srv || typeof srv == "number") this.attach( @@ -250,6 +256,10 @@ export class Server< ); } + get _opts() { + return this.opts; + } + /** * Sets/gets whether client code is being served. * diff --git a/lib/parent-namespace.ts b/lib/parent-namespace.ts index c8bd56becd..1206f6ccc5 100644 --- a/lib/parent-namespace.ts +++ b/lib/parent-namespace.ts @@ -7,6 +7,9 @@ import type { DefaultEventsMap, } from "./typed-events"; import type { BroadcastOptions } from "socket.io-adapter"; +import debugModule from "debug"; + +const debug = debugModule("socket.io:parent-namespace"); export class ParentNamespace< ListenEvents extends EventsMap = DefaultEventsMap, @@ -52,6 +55,7 @@ export class ParentNamespace< createChild( name: string ): Namespace { + debug("creating child namespace %s", name); const namespace = new Namespace(this.server, name); namespace._fns = this._fns.slice(0); this.listeners("connect").forEach((listener) => @@ -61,6 +65,21 @@ export class ParentNamespace< namespace.on("connection", listener) ); this.children.add(namespace); + + if (this.server._opts.cleanupEmptyChildNamespaces) { + const remove = namespace._remove; + + namespace._remove = (socket) => { + remove.call(namespace, socket); + if (namespace.sockets.size === 0) { + debug("closing child namespace %s", name); + namespace.adapter.close(); + this.server._nsps.delete(namespace.name); + this.children.delete(namespace); + } + }; + } + this.server._nsps.set(name, namespace); return namespace; } diff --git a/test/namespaces.ts b/test/namespaces.ts index 7557e4a57b..380a2d263c 100644 --- a/test/namespaces.ts +++ b/test/namespaces.ts @@ -473,6 +473,24 @@ describe("namespaces", () => { 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); @@ -571,5 +589,68 @@ describe("namespaces", () => { 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+$/); + }); }); });