diff --git a/lib/index.ts b/lib/index.ts index f6ace29308..3ae2427fff 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -96,6 +96,11 @@ interface ServerOptions extends EngineOptions, AttachOptions { */ skipMiddlewares?: boolean; }; + /** + * whether or not to remove child namespaces that have no sockets connected to them + * @default false + */ + cleanupEmptyChildNamespaces: boolean; } /** @@ -153,6 +158,7 @@ export class Server< * */ public engine: BaseServer; + public readonly cleanupEmptyChildNamespaces: boolean; /** @private */ readonly _parser: typeof parser; @@ -226,6 +232,7 @@ export class Server< this.path(opts.path || "/socket.io"); this.connectTimeout(opts.connectTimeout || 45000); this.serveClient(false !== opts.serveClient); + this.cleanupEmptyChildNamespaces = !!opts.cleanupEmptyChildNamespaces; this._parser = opts.parser || parser; this.encoder = new this._parser.Encoder(); this.opts = opts; diff --git a/lib/namespace.ts b/lib/namespace.ts index ff88e2ffe3..0dbc12296c 100644 --- a/lib/namespace.ts +++ b/lib/namespace.ts @@ -147,19 +147,24 @@ export class Namespace< /** @private */ _ids: number = 0; + private cleanupEmptyNamespaceFunc?: (nsp: Namespace) => void; + /** * Namespace constructor. * * @param server instance * @param name + * @param cleanupEmptyNamespaceFunc (optional) function to run if the namespace is empty */ constructor( server: Server, - name: string + name: string, + cleanupEmptyNamespaceFunc?: (nsp: Namespace) => void ) { super(); this.server = server; this.name = name; + this.cleanupEmptyNamespaceFunc = cleanupEmptyNamespaceFunc; this._initAdapter(); } @@ -694,4 +699,20 @@ export class Namespace< this.adapter ).disconnectSockets(close); } + + /** + * Cleans up the namespace if necessary (if the server option cleanupEmptyChildNamespaces is true and there are no sockets connected to the namespace). + * + */ + public cleanupEmptyNamespace() { + if ( + !this.server.cleanupEmptyChildNamespaces || + this.sockets.size !== 0 || + typeof this.cleanupEmptyNamespaceFunc !== "function" + ) { + return; + } + + this.cleanupEmptyNamespaceFunc(this); + } } diff --git a/lib/parent-namespace.ts b/lib/parent-namespace.ts index c8bd56becd..0371c18443 100644 --- a/lib/parent-namespace.ts +++ b/lib/parent-namespace.ts @@ -52,7 +52,11 @@ export class ParentNamespace< createChild( name: string ): Namespace { - const namespace = new Namespace(this.server, name); + const namespace = new Namespace(this.server, name, (nsp: Namespace) => { + nsp.adapter.close(); + this.server._nsps.delete(nsp.name); + this.children.delete(nsp); + }); namespace._fns = this._fns.slice(0); this.listeners("connect").forEach((listener) => namespace.on("connect", listener) diff --git a/lib/socket.ts b/lib/socket.ts index 3314398351..4dd7389099 100644 --- a/lib/socket.ts +++ b/lib/socket.ts @@ -713,6 +713,7 @@ export class Socket< this.client._remove(this); this.connected = false; this.emitReserved("disconnect", reason); + this.nsp.cleanupEmptyNamespace(); return; } diff --git a/test/namespaces.ts b/test/namespaces.ts index 7557e4a57b..4930933ab0 100644 --- a/test/namespaces.ts +++ b/test/namespaces.ts @@ -473,6 +473,34 @@ describe("namespaces", () => { io.of("/nsp"); }); + it("should not clean up parent 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, "/chat"); + + c1.on("connect", () => { + c1.disconnect(); + + // Give it some time to disconnect the client + setTimeout(() => { + const socket = createClient(io, "/chat"); + + socket.on("connect_error", () => { + done( + new Error( + "Client got a connect error when connecting to a parent namespace" + ) + ); + }); + + socket.on("connect", () => { + success(done, io, socket); + }); + }, 500); + }); + + const nsp = io.of("/chat"); + }); + describe("dynamic namespaces", () => { it("should allow connections to dynamic namespaces with a regex", (done) => { const io = new Server(0); @@ -571,5 +599,65 @@ 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(() => { + 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); + success(done, io); + }, 300); + }); + + io.of(/^\/dynamic-\d+$/); + }); }); });