Skip to content

Commit 8e32537

Browse files
authored
feat: add ack support (#620)
1 parent 52b280a commit 8e32537

File tree

6 files changed

+174
-7
lines changed

6 files changed

+174
-7
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,25 @@ export class MessageController {
169169
}
170170
```
171171

172+
#### `@MessageAck()` decorator
173+
174+
To get received message ack use `@MessageAck()` decorator:
175+
176+
```typescript
177+
import { SocketController, OnMessage, MessageAck, MessageBody } from 'socket-controllers';
178+
179+
@SocketController()
180+
export class MessageController {
181+
@OnMessage('save')
182+
save(@MessageBody() message: any, @MessageAck() ack: Function) {
183+
console.log('received message: ', message);
184+
ack('callback message');
185+
}
186+
}
187+
```
188+
189+
> note: ack must be the last parameter in `emit`, otherwise it will be `null`
190+
172191
#### `@SocketQueryParam()` decorator
173192

174193
To get received query parameter use `@SocketQueryParam()` decorator.

src/SocketControllers.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -181,13 +181,14 @@ export class SocketControllers {
181181
for (const messageAction of messageActions) {
182182
socket.on(messageAction.options.name, (...args: any[]) => {
183183
const messages: any[] = args.slice(0, -1);
184-
const ack: any = args[args.length - 1];
184+
let ack: Function | null = args[args.length - 1];
185185

186186
if (!(ack instanceof Function)) {
187187
messages.push(ack);
188+
ack = null;
188189
}
189190

190-
void this.executeAction(socket, controller, messageAction, messageAction.options.name as string, messages);
191+
void this.executeAction(socket, controller, messageAction, messageAction.options.name as string, messages, ack);
191192
});
192193
}
193194
}
@@ -197,9 +198,10 @@ export class SocketControllers {
197198
controller: HandlerMetadata<ControllerMetadata>,
198199
action: ActionMetadata,
199200
eventName?: string,
200-
data?: any[]
201+
data?: any[],
202+
ack?: Function | null
201203
) {
202-
const parameters = this.resolveParameters(socket, controller.metadata, action.parameters || [], data);
204+
const parameters = this.resolveParameters(socket, controller.metadata, action.parameters || [], data, ack);
203205

204206
let container = this.container;
205207
if (this.options.scopedContainerGetter) {
@@ -263,12 +265,13 @@ export class SocketControllers {
263265
socket: Socket,
264266
controllerMetadata: ControllerMetadata,
265267
parameterMetadatas: ParameterMetadata[],
266-
data?: any[]
268+
data?: any[],
269+
ack?: Function | null
267270
) {
268271
const parameters = [];
269272

270273
for (const metadata of parameterMetadatas) {
271-
const parameterValue = this.resolveParameter(socket, controllerMetadata, metadata, data) as never;
274+
const parameterValue = this.resolveParameter(socket, controllerMetadata, metadata, data, ack) as never;
272275
parameters[metadata.index] = this.transformActionValue(
273276
parameterValue,
274277
metadata.reflectedType as never,
@@ -280,7 +283,13 @@ export class SocketControllers {
280283
return parameters;
281284
}
282285

283-
private resolveParameter(socket: Socket, controller: ControllerMetadata, parameter: ParameterMetadata, data?: any[]) {
286+
private resolveParameter(
287+
socket: Socket,
288+
controller: ControllerMetadata,
289+
parameter: ParameterMetadata,
290+
data?: any[],
291+
ack?: Function | null
292+
) {
284293
switch (parameter.type) {
285294
case ParameterType.CONNECTED_SOCKET:
286295
return socket;
@@ -292,6 +301,8 @@ export class SocketControllers {
292301
return socket.rooms;
293302
case ParameterType.MESSAGE_BODY:
294303
return data?.[(parameter.options.index as number) || 0];
304+
case ParameterType.MESSAGE_ACK:
305+
return ack;
295306
case ParameterType.SOCKET_QUERY_PARAM:
296307
return socket.handshake.query[parameter.options.name as string];
297308
case ParameterType.SOCKET_REQUEST:

src/decorators/MessageAck.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata';
2+
import { ParameterType } from '../types/enums/ParameterType';
3+
4+
export function MessageAck() {
5+
return function (object: Object, methodName: string, index: number) {
6+
const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index];
7+
addParameterToActionMetadata(object.constructor, methodName, {
8+
index,
9+
reflectedType: format,
10+
type: ParameterType.MESSAGE_ACK,
11+
options: {},
12+
});
13+
};
14+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './decorators/ConnectedSocket';
22
export * from './decorators/EmitOnFail';
33
export * from './decorators/EmitOnSuccess';
44
export * from './decorators/MessageBody';
5+
export * from './decorators/MessageAck';
56
export * from './decorators/Middleware';
67
export * from './decorators/NspParam';
78
export * from './decorators/NspParams';

src/types/enums/ParameterType.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export enum ParameterType {
22
CUSTOM,
33
CONNECTED_SOCKET,
44
MESSAGE_BODY,
5+
MESSAGE_ACK,
56
SOCKET_QUERY_PARAM,
67
SOCKET_IO,
78
SOCKET_ID,
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { createServer, Server as HttpServer } from 'http';
2+
import { Server } from 'socket.io';
3+
import { io, Socket } from 'socket.io-client';
4+
import { SocketControllers } from '../../src/SocketControllers';
5+
import { Container, Service } from 'typedi';
6+
import { SocketController } from '../../src/decorators/SocketController';
7+
import { OnConnect } from '../../src/decorators/OnConnect';
8+
import { ConnectedSocket } from '../../src/decorators/ConnectedSocket';
9+
import { waitForEvent } from '../utilities/waitForEvent';
10+
import { MessageAck, MessageBody, OnMessage, SocketId } from '../../src';
11+
12+
describe('MessageAck', () => {
13+
const PORT = 8080;
14+
const PATH_FOR_CLIENT = `ws://localhost:${PORT}`;
15+
16+
let httpServer: HttpServer;
17+
let wsApp: Server;
18+
let wsClient: Socket;
19+
let testResult;
20+
let testAckResult;
21+
let socketControllers: SocketControllers;
22+
23+
beforeEach(done => {
24+
httpServer = createServer();
25+
wsApp = new Server(httpServer, {
26+
cors: {
27+
origin: '*',
28+
},
29+
});
30+
httpServer.listen(PORT, () => {
31+
done();
32+
});
33+
});
34+
35+
afterEach(() => {
36+
testResult = undefined;
37+
testAckResult = undefined;
38+
39+
Container.reset();
40+
wsClient.close();
41+
wsClient = null;
42+
socketControllers = null;
43+
return new Promise(resolve => {
44+
if (wsApp)
45+
return wsApp.close(() => {
46+
resolve(null);
47+
});
48+
resolve(null);
49+
});
50+
});
51+
52+
it('Event ack is retrieved correctly', async () => {
53+
@SocketController('/string')
54+
@Service()
55+
class TestController {
56+
@OnConnect()
57+
connected(@ConnectedSocket() socket: Socket, @SocketId() socketId: string) {
58+
testResult = socketId;
59+
socket.emit('connected');
60+
}
61+
62+
@OnMessage('test')
63+
test(@MessageBody() data: any, @ConnectedSocket() socket: Socket, @MessageAck() ack: Function) {
64+
testResult = data;
65+
testAckResult = ack;
66+
socket.emit('return');
67+
}
68+
69+
@OnMessage('test2')
70+
test2(
71+
@MessageAck() ack: Function,
72+
@MessageBody({ index: 1 }) data1: any,
73+
@MessageBody({ index: 0 }) data0: any,
74+
@ConnectedSocket() socket: Socket
75+
) {
76+
testResult = { data1, data0 };
77+
ack?.('test ack2');
78+
socket.emit('return2');
79+
}
80+
81+
@OnMessage('test3')
82+
test3(
83+
@ConnectedSocket() socket: Socket,
84+
@MessageAck() ack: Function,
85+
@MessageBody({ index: 1 }) data1: any,
86+
@MessageBody({ index: 0 }) data0: any
87+
) {
88+
testResult = { data1, data0 };
89+
testAckResult = ack;
90+
socket.emit('return3');
91+
}
92+
}
93+
94+
socketControllers = new SocketControllers({
95+
io: wsApp,
96+
container: Container,
97+
controllers: [TestController],
98+
});
99+
wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true });
100+
101+
await waitForEvent(wsClient, 'connected');
102+
103+
const ack = (ack: any) => (testAckResult = ack);
104+
105+
wsClient.emit('test', 'test data');
106+
await waitForEvent(wsClient, 'return');
107+
expect(testResult).toEqual('test data');
108+
expect(testAckResult).toBeNull();
109+
110+
wsClient.emit('test2', 'test data 0', 'test data 1', 'test data 2', ack);
111+
await waitForEvent(wsClient, 'return2');
112+
expect(testResult).toEqual({ data0: 'test data 0', data1: 'test data 1' });
113+
expect(testAckResult).toEqual('test ack2');
114+
115+
// ack should be the last parameter
116+
wsClient.emit('test3', 'test data 0', 'test data 1', ack, 'test data 2');
117+
await waitForEvent(wsClient, 'return3');
118+
expect(testResult).toEqual({ data0: 'test data 0', data1: 'test data 1' });
119+
expect(testAckResult).toBeNull();
120+
});
121+
});

0 commit comments

Comments
 (0)