Skip to content

Commit 5d9220b

Browse files
stevebaum23darrachequesne
authored andcommitted
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: socketio/socket.io-redis-adapter#480
1 parent 1298839 commit 5d9220b

File tree

3 files changed

+110
-0
lines changed

3 files changed

+110
-0
lines changed

Diff for: lib/index.ts

+10
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ interface ServerOptions extends EngineOptions, AttachOptions {
9898
*/
9999
skipMiddlewares?: boolean;
100100
};
101+
/**
102+
* Whether to remove child namespaces that have no sockets connected to them
103+
* @default false
104+
*/
105+
cleanupEmptyChildNamespaces: boolean;
101106
}
102107

103108
/**
@@ -243,13 +248,18 @@ export class Server<
243248
} else {
244249
this.adapter(opts.adapter || Adapter);
245250
}
251+
opts.cleanupEmptyChildNamespaces = !!opts.cleanupEmptyChildNamespaces;
246252
this.sockets = this.of("/");
247253
if (srv || typeof srv == "number")
248254
this.attach(
249255
srv as http.Server | HTTPSServer | Http2SecureServer | number
250256
);
251257
}
252258

259+
get _opts() {
260+
return this.opts;
261+
}
262+
253263
/**
254264
* Sets/gets whether client code is being served.
255265
*

Diff for: lib/parent-namespace.ts

+19
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import type {
77
DefaultEventsMap,
88
} from "./typed-events";
99
import type { BroadcastOptions } from "socket.io-adapter";
10+
import debugModule from "debug";
11+
12+
const debug = debugModule("socket.io:parent-namespace");
1013

1114
export class ParentNamespace<
1215
ListenEvents extends EventsMap = DefaultEventsMap,
@@ -52,6 +55,7 @@ export class ParentNamespace<
5255
createChild(
5356
name: string
5457
): Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
58+
debug("creating child namespace %s", name);
5559
const namespace = new Namespace(this.server, name);
5660
namespace._fns = this._fns.slice(0);
5761
this.listeners("connect").forEach((listener) =>
@@ -61,6 +65,21 @@ export class ParentNamespace<
6165
namespace.on("connection", listener)
6266
);
6367
this.children.add(namespace);
68+
69+
if (this.server._opts.cleanupEmptyChildNamespaces) {
70+
const remove = namespace._remove;
71+
72+
namespace._remove = (socket) => {
73+
remove.call(namespace, socket);
74+
if (namespace.sockets.size === 0) {
75+
debug("closing child namespace %s", name);
76+
namespace.adapter.close();
77+
this.server._nsps.delete(namespace.name);
78+
this.children.delete(namespace);
79+
}
80+
};
81+
}
82+
6483
this.server._nsps.set(name, namespace);
6584
return namespace;
6685
}

Diff for: test/namespaces.ts

+81
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,24 @@ describe("namespaces", () => {
473473
io.of("/nsp");
474474
});
475475

476+
it("should not clean up a non-dynamic namespace", (done) => {
477+
const io = new Server(0, { cleanupEmptyChildNamespaces: true });
478+
const c1 = createClient(io, "/chat");
479+
480+
c1.on("connect", () => {
481+
c1.disconnect();
482+
483+
// Give it some time to disconnect the client
484+
setTimeout(() => {
485+
expect(io._nsps.has("/chat")).to.be(true);
486+
expect(io._nsps.get("/chat")!.sockets.size).to.be(0);
487+
success(done, io);
488+
}, 100);
489+
});
490+
491+
io.of("/chat");
492+
});
493+
476494
describe("dynamic namespaces", () => {
477495
it("should allow connections to dynamic namespaces with a regex", (done) => {
478496
const io = new Server(0);
@@ -571,5 +589,68 @@ describe("namespaces", () => {
571589
one.on("message", handler);
572590
two.on("message", handler);
573591
});
592+
593+
it("should clean up namespace when cleanupEmptyChildNamespaces is on and there are no more sockets in a namespace", (done) => {
594+
const io = new Server(0, { cleanupEmptyChildNamespaces: true });
595+
const c1 = createClient(io, "/dynamic-101");
596+
597+
c1.on("connect", () => {
598+
c1.disconnect();
599+
600+
// Give it some time to disconnect and clean up the namespace
601+
setTimeout(() => {
602+
expect(io._nsps.has("/dynamic-101")).to.be(false);
603+
success(done, io);
604+
}, 100);
605+
});
606+
607+
io.of(/^\/dynamic-\d+$/);
608+
});
609+
610+
it("should allow a client to connect to a cleaned up namespace", (done) => {
611+
const io = new Server(0, { cleanupEmptyChildNamespaces: true });
612+
const c1 = createClient(io, "/dynamic-101");
613+
614+
c1.on("connect", () => {
615+
c1.disconnect();
616+
617+
// Give it some time to disconnect and clean up the namespace
618+
setTimeout(() => {
619+
expect(io._nsps.has("/dynamic-101")).to.be(false);
620+
621+
const c2 = createClient(io, "/dynamic-101");
622+
623+
c2.on("connect", () => {
624+
success(done, io, c2);
625+
});
626+
627+
c2.on("connect_error", () => {
628+
done(
629+
new Error("Client got error when connecting to dynamic namespace")
630+
);
631+
});
632+
}, 100);
633+
});
634+
635+
io.of(/^\/dynamic-\d+$/);
636+
});
637+
638+
it("should not clean up namespace when cleanupEmptyChildNamespaces is off and there are no more sockets in a namespace", (done) => {
639+
const io = new Server(0);
640+
const c1 = createClient(io, "/dynamic-101");
641+
642+
c1.on("connect", () => {
643+
c1.disconnect();
644+
645+
// Give it some time to disconnect and clean up the namespace
646+
setTimeout(() => {
647+
expect(io._nsps.has("/dynamic-101")).to.be(true);
648+
expect(io._nsps.get("/dynamic-101")!.sockets.size).to.be(0);
649+
success(done, io);
650+
}, 100);
651+
});
652+
653+
io.of(/^\/dynamic-\d+$/);
654+
});
574655
});
575656
});

0 commit comments

Comments
 (0)