Skip to content

Commit f3ada7d

Browse files
fix(typings): properly type emits with timeout
When emitting with a timeout (added in version 4.4.0), the "err" argument was not properly typed and would require to split the client and server typings. It will now be automatically inferred as an Error object. Workaround for previous versions: ```ts type WithTimeoutAck<isEmitter extends boolean, args extends any[]> = isEmitter extends true ? [Error, ...args] : args; interface ClientToServerEvents<isEmitter extends boolean = false> { withAck: (data: { argName: boolean }, callback: (...args: WithTimeoutAck<isEmitter, [string]>) => void) => void; } interface ServerToClientEvents<isEmitter extends boolean = false> { } const io = new Server<ClientToServerEvents, ServerToClientEvents<true>>(3000); io.on("connection", (socket) => { socket.on("withAck", (val, cb) => { cb("123"); }); }); const socket: Socket<ServerToClientEvents, ClientToServerEvents<true>> = ioc("http://localhost:3000"); socket.timeout(100).emit("withAck", { argName: true }, (err, val) => { // ... }); ``` Related: socketio/socket.io-client#1555
1 parent a21ad88 commit f3ada7d

6 files changed

+147
-16
lines changed

lib/broadcast-operator.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type {
77
EventNames,
88
EventsMap,
99
TypedEventBroadcaster,
10+
DecorateAcknowledgements,
11+
DecorateAcknowledgementsWithTimeoutAndMultipleResponses,
1012
} from "./typed-events";
1113

1214
export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
@@ -169,12 +171,10 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
169171
*/
170172
public timeout(timeout: number) {
171173
const flags = Object.assign({}, this.flags, { timeout });
172-
return new BroadcastOperator<EmitEvents, SocketData>(
173-
this.adapter,
174-
this.rooms,
175-
this.exceptRooms,
176-
flags
177-
);
174+
return new BroadcastOperator<
175+
DecorateAcknowledgementsWithTimeoutAndMultipleResponses<EmitEvents>,
176+
SocketData
177+
>(this.adapter, this.rooms, this.exceptRooms, flags);
178178
}
179179

180180
/**

lib/index.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import {
3434
EventParams,
3535
StrictEventEmitter,
3636
EventNames,
37+
DecorateAcknowledgements,
38+
DecorateAcknowledgementsWithTimeoutAndMultipleResponses,
3739
} from "./typed-events";
3840
import { patchAdapter, restoreAdapter, serveFile } from "./uws";
3941
import type { BaseServer } from "engine.io/build/server";
@@ -857,7 +859,10 @@ export class Server<
857859
*/
858860
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
859861
ev: Ev,
860-
...args: EventParams<ServerSideEvents, Ev>
862+
...args: EventParams<
863+
DecorateAcknowledgementsWithTimeoutAndMultipleResponses<ServerSideEvents>,
864+
Ev
865+
>
861866
): boolean {
862867
return this.sockets.serverSideEmit(ev, ...args);
863868
}

lib/namespace.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import {
66
EventsMap,
77
StrictEventEmitter,
88
DefaultEventsMap,
9+
DecorateAcknowledgementsWithTimeoutAndMultipleResponses,
910
} from "./typed-events";
1011
import type { Client } from "./client";
1112
import debugModule from "debug";
1213
import type { Adapter, Room, SocketId } from "socket.io-adapter";
13-
import { BroadcastOperator, RemoteSocket } from "./broadcast-operator";
14+
import { BroadcastOperator } from "./broadcast-operator";
1415

1516
const debug = debugModule("socket.io:namespace");
1617

@@ -494,7 +495,10 @@ export class Namespace<
494495
*/
495496
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
496497
ev: Ev,
497-
...args: EventParams<ServerSideEvents, Ev>
498+
...args: EventParams<
499+
DecorateAcknowledgementsWithTimeoutAndMultipleResponses<ServerSideEvents>,
500+
Ev
501+
>
498502
): boolean {
499503
if (RESERVED_EVENTS.has(ev)) {
500504
throw new Error(`"${String(ev)}" is a reserved event name`);

lib/socket.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Packet, PacketType } from "socket.io-parser";
22
import debugModule from "debug";
33
import type { Server } from "./index";
44
import {
5+
DecorateAcknowledgements,
6+
DecorateAcknowledgementsWithMultipleResponses,
57
DefaultEventsMap,
68
EventNames,
79
EventParams,
@@ -842,7 +844,14 @@ export class Socket<
842844
*
843845
* @returns self
844846
*/
845-
public timeout(timeout: number): this {
847+
public timeout(
848+
timeout: number
849+
): Socket<
850+
ListenEvents,
851+
DecorateAcknowledgements<EmitEvents>,
852+
ServerSideEvents,
853+
SocketData
854+
> {
846855
this.flags.timeout = timeout;
847856
return this;
848857
}
@@ -1152,11 +1161,9 @@ export class Socket<
11521161
private newBroadcastOperator() {
11531162
const flags = Object.assign({}, this.flags);
11541163
this.flags = {};
1155-
return new BroadcastOperator<EmitEvents, SocketData>(
1156-
this.adapter,
1157-
new Set<Room>(),
1158-
new Set<Room>([this.id]),
1159-
flags
1160-
);
1164+
return new BroadcastOperator<
1165+
DecorateAcknowledgementsWithMultipleResponses<EmitEvents>,
1166+
SocketData
1167+
>(this.adapter, new Set<Room>(), new Set<Room>([this.id]), flags);
11611168
}
11621169
}

lib/typed-events.ts

+49
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,52 @@ export abstract class StrictEventEmitter<
178178
>[];
179179
}
180180
}
181+
182+
type PrependTimeoutError<T extends any[]> = {
183+
[K in keyof T]: T[K] extends (...args: infer Params) => infer Result
184+
? (err: Error, ...args: Params) => Result
185+
: T[K];
186+
};
187+
188+
type ExpectMultipleResponses<T extends any[]> = {
189+
[K in keyof T]: T[K] extends (err: Error, arg: infer Param) => infer Result
190+
? (err: Error, arg: Param[]) => Result
191+
: T[K];
192+
};
193+
194+
/**
195+
* Utility type to decorate the acknowledgement callbacks with a timeout error.
196+
*
197+
* This is needed because the timeout() flag breaks the symmetry between the sender and the receiver:
198+
*
199+
* @example
200+
* interface Events {
201+
* "my-event": (val: string) => void;
202+
* }
203+
*
204+
* socket.on("my-event", (cb) => {
205+
* cb("123"); // one single argument here
206+
* });
207+
*
208+
* socket.timeout(1000).emit("my-event", (err, val) => {
209+
* // two arguments there (the "err" argument is not properly typed)
210+
* });
211+
*
212+
*/
213+
export type DecorateAcknowledgements<E> = {
214+
[K in keyof E]: E[K] extends (...args: infer Params) => infer Result
215+
? (...args: PrependTimeoutError<Params>) => Result
216+
: E[K];
217+
};
218+
219+
export type DecorateAcknowledgementsWithTimeoutAndMultipleResponses<E> = {
220+
[K in keyof E]: E[K] extends (...args: infer Params) => infer Result
221+
? (...args: ExpectMultipleResponses<PrependTimeoutError<Params>>) => Result
222+
: E[K];
223+
};
224+
225+
export type DecorateAcknowledgementsWithMultipleResponses<E> = {
226+
[K in keyof E]: E[K] extends (...args: infer Params) => infer Result
227+
? (...args: ExpectMultipleResponses<Params>) => Result
228+
: E[K];
229+
};

test/socket.io.test-d.ts

+66
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,25 @@ describe("server", () => {
167167
describe("listen and emit event maps", () => {
168168
interface ClientToServerEvents {
169169
helloFromClient: (message: string) => void;
170+
ackFromClient: (
171+
a: string,
172+
b: number,
173+
ack: (c: string, d: number) => void
174+
) => void;
170175
}
171176

172177
interface ServerToClientEvents {
173178
helloFromServer: (message: string, x: number) => void;
179+
ackFromServer: (
180+
a: boolean,
181+
b: string,
182+
ack: (c: boolean, d: string) => void
183+
) => void;
184+
multipleAckFromServer: (
185+
a: boolean,
186+
b: string,
187+
ack: (c: string) => void
188+
) => void;
174189
}
175190

176191
describe("on", () => {
@@ -185,6 +200,13 @@ describe("server", () => {
185200
expectType<string>(message);
186201
done();
187202
});
203+
204+
s.on("ackFromClient", (a, b, cb) => {
205+
expectType<string>(a);
206+
expectType<number>(b);
207+
expectType<(c: string, d: number) => void>(cb);
208+
cb("123", 456);
209+
});
188210
});
189211
});
190212
});
@@ -213,8 +235,41 @@ describe("server", () => {
213235
sio.to("room").emit("helloFromServer", "hi", 1);
214236
sio.timeout(1000).emit("helloFromServer", "hi", 1);
215237

238+
sio
239+
.timeout(1000)
240+
.emit("multipleAckFromServer", true, "123", (err, c) => {
241+
expectType<Error>(err);
242+
expectType<string[]>(c);
243+
});
244+
216245
sio.on("connection", (s) => {
217246
s.emit("helloFromServer", "hi", 10);
247+
248+
s.emit("ackFromServer", true, "123", (c, d) => {
249+
expectType<boolean>(c);
250+
expectType<string>(d);
251+
});
252+
253+
s.timeout(1000).emit("ackFromServer", true, "123", (err, c, d) => {
254+
expectType<Error>(err);
255+
expectType<boolean>(c);
256+
expectType<string>(d);
257+
});
258+
259+
s.timeout(1000)
260+
.to("room")
261+
.emit("multipleAckFromServer", true, "123", (err, c) => {
262+
expectType<Error>(err);
263+
expectType<string[]>(c);
264+
});
265+
266+
s.to("room")
267+
.timeout(1000)
268+
.emit("multipleAckFromServer", true, "123", (err, c) => {
269+
expectType<Error>(err);
270+
expectType<string[]>(c);
271+
});
272+
218273
done();
219274
});
220275
});
@@ -253,6 +308,7 @@ describe("server", () => {
253308

254309
interface InterServerEvents {
255310
helloFromServerToServer: (message: string, x: number) => void;
311+
ackFromServerToServer: (foo: string, cb: (bar: number) => void) => void;
256312
}
257313

258314
describe("on", () => {
@@ -281,6 +337,16 @@ describe("server", () => {
281337
expectType<string>(message);
282338
expectType<number>(x);
283339
});
340+
341+
sio.serverSideEmit("ackFromServerToServer", "foo", (err, bar) => {
342+
expectType<Error>(err);
343+
expectType<number[]>(bar);
344+
});
345+
346+
sio.on("ackFromServerToServer", (foo, cb) => {
347+
expectType<string>(foo);
348+
expectType<(bar: number) => void>(cb);
349+
});
284350
});
285351
});
286352
});

0 commit comments

Comments
 (0)