Skip to content

Commit

Permalink
feat(socketio): allow injecting the disconnection reason (#2373)
Browse files Browse the repository at this point in the history
closes #2372
  • Loading branch information
derevnjuk committed Jul 11, 2023
1 parent 8390c28 commit e5227e2
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 39 deletions.
4 changes: 2 additions & 2 deletions docs/tutorials/snippets/socketio/socket-service.ts
@@ -1,4 +1,4 @@
import {IO, Nsp, Socket, SocketService, SocketSession} from "@tsed/socketio";
import {IO, Nsp, Socket, SocketService, SocketSession, Reason} from "@tsed/socketio";
import * as SocketIO from "socket.io";

@SocketService("/my-namespace")
Expand All @@ -23,5 +23,5 @@ export class MySocketService {
/**
* Triggered when a client disconnects from the Namespace.
*/
$onDisconnect(@Socket socket: SocketIO.Socket) {}
$onDisconnect(@Socket socket: SocketIO.Socket, @Reason reason: string) {}
}
6 changes: 3 additions & 3 deletions packages/third-parties/socketio/jest.config.js
Expand Up @@ -8,10 +8,10 @@ module.exports = {
},
coverageThreshold: {
global: {
statements: 99.59,
branches: 94.5,
statements: 99.6,
branches: 94.59,
functions: 100,
lines: 99.59
lines: 99.6
}
}
};
4 changes: 2 additions & 2 deletions packages/third-parties/socketio/readme.md
Expand Up @@ -67,7 +67,7 @@ Example:

```typescript
import * as SocketIO from "socket.io";
import {SocketService, IO, Nsp, Socket, SocketSession} from "@tsed/socketio";
import {SocketService, IO, Nsp, Socket, SocketSession, Reason} from "@tsed/socketio";

@SocketService("/my-namespace")
export class MySocketService {
Expand All @@ -88,7 +88,7 @@ export class MySocketService {
/**
* Triggered when a client disconnects from the Namespace.
*/
$onDisconnect(@Socket socket: SocketIO.Socket) {}
$onDisconnect(@Socket socket: SocketIO.Socket, @Reason reason: string) {}
}
```

Expand Down
Expand Up @@ -197,6 +197,50 @@ describe("SocketHandlersBuilder", () => {
}
);
});

it("should pass the disconnection reason", () => {
const instance = {
$onDisconnect: jest.fn()
};

const provider: any = {
store: {
get: jest.fn().mockReturnValue({
injectNamespace: "nsp",
handlers: {
$onDisconnect: {
eventName: "onDisconnect"
}
}
})
}
};
const nspStub: any = {nsp: "nsp"};
const reason = "transport error";
const socketStub: any = {
on: jest.fn()
};

const builder: any = new SocketHandlersBuilder(provider, {
get() {
return instance;
}
} as any);
const invokeStub = jest.spyOn(builder, "invoke").mockReturnValue(undefined);
jest.spyOn(builder, "destroySession").mockReturnValue(undefined);

builder.onDisconnect(socketStub, nspStub, reason);

expect(invokeStub).toBeCalledWith(
instance,
{eventName: "onDisconnect"},
{
reason,
socket: socketStub,
nsp: nspStub
}
);
});
});
describe("createSession()", () => {
it("should create session for the socket", () => {
Expand Down Expand Up @@ -390,6 +434,23 @@ describe("SocketHandlersBuilder", () => {
});
});

describe("when REASON", () => {
it("should return a disconnect reason", () => {
const {builder} = createFixture();

const result = builder.buildParameters(
{
0: {
filter: SocketFilters.REASON
}
},
{reason: "transport error"}
);

expect(result).toEqual(["transport error"]);
});
});

describe("when ERROR", () => {
it("should return a list of parameters", () => {
const {builder} = createFixture();
Expand Down
Expand Up @@ -92,12 +92,12 @@ export class SocketHandlersBuilder {
}
}

public onDisconnect(socket: Socket, nsp: Namespace) {
public onDisconnect(socket: Socket, nsp: Namespace, reason?: string) {
const instance = this.injector.get(this.provider.token);
const {socketProviderMetadata} = this;

if (instance.$onDisconnect) {
this.invoke(instance, socketProviderMetadata.$onDisconnect, {socket, nsp});
this.invoke(instance, socketProviderMetadata.$onDisconnect, {socket, nsp, reason});
}

this.destroySession(socket);
Expand Down Expand Up @@ -253,6 +253,9 @@ export class SocketHandlersBuilder {

case SocketFilters.SOCKET_NSP:
return scope.socket.nsp;

case SocketFilters.REASON:
return scope.reason;
}
});
}
Expand Down
25 changes: 25 additions & 0 deletions packages/third-parties/socketio/src/decorators/reason.spec.ts
@@ -0,0 +1,25 @@
import {Store} from "@tsed/core";
import {Nsp, SocketErr} from "../index";
import {Reason} from "./reason";

describe("Reason", () => {
it("should set metadata", () => {
class Test {}

Reason(Test, "test", 0);
const store = Store.from(Test);

expect(store.get("socketIO")).toEqual({
handlers: {
test: {
parameters: {
"0": {
filter: "reason",
mapIndex: undefined
}
}
}
}
});
});
});
30 changes: 30 additions & 0 deletions packages/third-parties/socketio/src/decorators/reason.ts
@@ -0,0 +1,30 @@
import {SocketFilter} from "./socketFilter";
import {SocketFilters} from "../interfaces/SocketFilters";

/**
* Inject the disconnection reason into the decorated parameter.
*
* This decorator is used in conjunction with the `$onDisconnect` event handler to handle disconnection reasons in SocketIO services.
* It allows you to access the reason for the disconnection in your method implementation. For details please refer to the [Socket.io documentation](https://socket.io/docs/v4/server-api/#event-disconnect).
*
* @example
* ```typescript
* @SocketService("/nsp")
* export class MyWS {
* public async $onDisconnect(
* @Reason reason: string = ''
* ) {
* // your implementation
* }
* }
* ```
*
* @experimental This decorator is experimental and may change or be removed in future versions.
* @param target
* @param {string} propertyKey
* @param {number} index
* @decorator
*/
export function Reason(target: unknown, propertyKey: string, index: number) {
return SocketFilter(SocketFilters.REASON)(target, propertyKey, index);
}
1 change: 1 addition & 0 deletions packages/third-parties/socketio/src/index.ts
Expand Up @@ -16,6 +16,7 @@ export * from "./decorators/inputAndBroadcastOthers";
export * from "./decorators/inputAndEmit";
export * from "./decorators/io";
export * from "./decorators/nsp";
export * from "./decorators/reason";
export * from "./decorators/socket";
export * from "./decorators/socketErr";
export * from "./decorators/socketEventName";
Expand Down
Expand Up @@ -8,5 +8,6 @@ export enum SocketFilters {
NSP = "nsp",
SESSION = "session",
ERR = "error",
SOCKET_NSP = "socket_nsp"
SOCKET_NSP = "socket_nsp",
REASON = "reason"
}
Expand Up @@ -24,48 +24,30 @@ async function createServiceFixture() {
use: ioStub
}
]);
const reason = "transport error";

return {namespace, ioStub, service, instance, socket};
return {namespace, ioStub, service, instance, socket, reason};
}

describe("SocketIOService", () => {
beforeEach(() => PlatformTest.create());
afterEach(() => PlatformTest.reset());

describe("getNsp(string)", () => {
it("should call io.of and create namespace", async () => {
const {service, namespace, ioStub, socket, instance} = await createServiceFixture();
describe("getNsp", () => {
it.each([{input: "/"}, {input: /test/}])("should call io.of with $input and create namespace", async ({input}) => {
const {service, namespace, ioStub, socket, instance, reason} = await createServiceFixture();

const nspConf = service.getNsp("/");
const nspConf = service.getNsp(input);
nspConf.instances.push(instance);

namespace.on.mock.calls[0][1](socket);
socket.on.mock.calls[0][1]();
socket.on.mock.calls[0][1](reason);

expect(ioStub.of).toBeCalledWith("/");
expect(ioStub.of).toBeCalledWith(input);
expect(namespace.on).toBeCalledWith("connection", expect.any(Function));
expect(instance.onConnection).toBeCalledWith(socket, namespace);
expect(socket.on).toBeCalledWith("disconnect", expect.any(Function));
expect(instance.onDisconnect).toBeCalledWith(socket, namespace);
});
});

describe("getNsp(RegExp)", () => {
it("should call io.of and create namespace", async () => {
const {service, namespace, ioStub, socket, instance} = await createServiceFixture();
const regexp = new RegExp(/test/);

const nspConf = service.getNsp(regexp);
nspConf.instances.push(instance);

namespace.on.mock.calls[0][1](socket);
socket.on.mock.calls[0][1]();

expect(ioStub.of).toBeCalledWith(regexp);
expect(namespace.on).toBeCalledWith("connection", expect.any(Function));
expect(instance.onConnection).toBeCalledWith(socket, namespace);
expect(socket.on).toBeCalledWith("disconnect", expect.any(Function));
expect(instance.onDisconnect).toBeCalledWith(socket, namespace);
expect(instance.onDisconnect).toBeCalledWith(socket, namespace, reason);
});
});
});
Expand Up @@ -33,9 +33,9 @@ export class SocketIOService {
builder.onConnection(socket, conf.nsp);
});

socket.on("disconnect", () => {
socket.on("disconnect", (reason: string) => {
conf.instances.forEach((builder: SocketHandlersBuilder) => {
builder.onDisconnect(socket, conf.nsp);
builder.onDisconnect(socket, conf.nsp, reason);
});
});
});
Expand Down

0 comments on commit e5227e2

Please sign in to comment.