diff --git a/lib/broadcast-operator.ts b/lib/broadcast-operator.ts index 2bc7271ab6..fb78ad6847 100644 --- a/lib/broadcast-operator.ts +++ b/lib/broadcast-operator.ts @@ -7,6 +7,8 @@ import type { EventNames, EventsMap, TypedEventBroadcaster, + DecorateAcknowledgements, + DecorateAcknowledgementsWithTimeoutAndMultipleResponses, } from "./typed-events"; export class BroadcastOperator @@ -169,12 +171,10 @@ export class BroadcastOperator */ public timeout(timeout: number) { const flags = Object.assign({}, this.flags, { timeout }); - return new BroadcastOperator( - this.adapter, - this.rooms, - this.exceptRooms, - flags - ); + return new BroadcastOperator< + DecorateAcknowledgementsWithTimeoutAndMultipleResponses, + SocketData + >(this.adapter, this.rooms, this.exceptRooms, flags); } /** diff --git a/lib/index.ts b/lib/index.ts index f6ace29308..d9b6a090a7 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -34,6 +34,8 @@ import { EventParams, StrictEventEmitter, EventNames, + DecorateAcknowledgements, + DecorateAcknowledgementsWithTimeoutAndMultipleResponses, } from "./typed-events"; import { patchAdapter, restoreAdapter, serveFile } from "./uws"; import type { BaseServer } from "engine.io/build/server"; @@ -857,7 +859,10 @@ export class Server< */ public serverSideEmit>( ev: Ev, - ...args: EventParams + ...args: EventParams< + DecorateAcknowledgementsWithTimeoutAndMultipleResponses, + Ev + > ): boolean { return this.sockets.serverSideEmit(ev, ...args); } diff --git a/lib/namespace.ts b/lib/namespace.ts index ff88e2ffe3..fd298e6eb4 100644 --- a/lib/namespace.ts +++ b/lib/namespace.ts @@ -6,11 +6,12 @@ import { EventsMap, StrictEventEmitter, DefaultEventsMap, + DecorateAcknowledgementsWithTimeoutAndMultipleResponses, } from "./typed-events"; import type { Client } from "./client"; import debugModule from "debug"; import type { Adapter, Room, SocketId } from "socket.io-adapter"; -import { BroadcastOperator, RemoteSocket } from "./broadcast-operator"; +import { BroadcastOperator } from "./broadcast-operator"; const debug = debugModule("socket.io:namespace"); @@ -494,7 +495,10 @@ export class Namespace< */ public serverSideEmit>( ev: Ev, - ...args: EventParams + ...args: EventParams< + DecorateAcknowledgementsWithTimeoutAndMultipleResponses, + Ev + > ): boolean { if (RESERVED_EVENTS.has(ev)) { throw new Error(`"${String(ev)}" is a reserved event name`); diff --git a/lib/socket.ts b/lib/socket.ts index 3314398351..8dd5d4ff4e 100644 --- a/lib/socket.ts +++ b/lib/socket.ts @@ -2,6 +2,8 @@ import { Packet, PacketType } from "socket.io-parser"; import debugModule from "debug"; import type { Server } from "./index"; import { + DecorateAcknowledgements, + DecorateAcknowledgementsWithMultipleResponses, DefaultEventsMap, EventNames, EventParams, @@ -842,7 +844,14 @@ export class Socket< * * @returns self */ - public timeout(timeout: number): this { + public timeout( + timeout: number + ): Socket< + ListenEvents, + DecorateAcknowledgements, + ServerSideEvents, + SocketData + > { this.flags.timeout = timeout; return this; } @@ -1152,11 +1161,9 @@ export class Socket< private newBroadcastOperator() { const flags = Object.assign({}, this.flags); this.flags = {}; - return new BroadcastOperator( - this.adapter, - new Set(), - new Set([this.id]), - flags - ); + return new BroadcastOperator< + DecorateAcknowledgementsWithMultipleResponses, + SocketData + >(this.adapter, new Set(), new Set([this.id]), flags); } } diff --git a/lib/typed-events.ts b/lib/typed-events.ts index 324365531c..20b7122636 100644 --- a/lib/typed-events.ts +++ b/lib/typed-events.ts @@ -178,3 +178,52 @@ export abstract class StrictEventEmitter< >[]; } } + +type PrependTimeoutError = { + [K in keyof T]: T[K] extends (...args: infer Params) => infer Result + ? (err: Error, ...args: Params) => Result + : T[K]; +}; + +type ExpectMultipleResponses = { + [K in keyof T]: T[K] extends (err: Error, arg: infer Param) => infer Result + ? (err: Error, arg: Param[]) => Result + : T[K]; +}; + +/** + * Utility type to decorate the acknowledgement callbacks with a timeout error. + * + * This is needed because the timeout() flag breaks the symmetry between the sender and the receiver: + * + * @example + * interface Events { + * "my-event": (val: string) => void; + * } + * + * socket.on("my-event", (cb) => { + * cb("123"); // one single argument here + * }); + * + * socket.timeout(1000).emit("my-event", (err, val) => { + * // two arguments there (the "err" argument is not properly typed) + * }); + * + */ +export type DecorateAcknowledgements = { + [K in keyof E]: E[K] extends (...args: infer Params) => infer Result + ? (...args: PrependTimeoutError) => Result + : E[K]; +}; + +export type DecorateAcknowledgementsWithTimeoutAndMultipleResponses = { + [K in keyof E]: E[K] extends (...args: infer Params) => infer Result + ? (...args: ExpectMultipleResponses>) => Result + : E[K]; +}; + +export type DecorateAcknowledgementsWithMultipleResponses = { + [K in keyof E]: E[K] extends (...args: infer Params) => infer Result + ? (...args: ExpectMultipleResponses) => Result + : E[K]; +}; diff --git a/test/socket.io.test-d.ts b/test/socket.io.test-d.ts index bb68400587..1785da348f 100644 --- a/test/socket.io.test-d.ts +++ b/test/socket.io.test-d.ts @@ -167,10 +167,25 @@ describe("server", () => { describe("listen and emit event maps", () => { interface ClientToServerEvents { helloFromClient: (message: string) => void; + ackFromClient: ( + a: string, + b: number, + ack: (c: string, d: number) => void + ) => void; } interface ServerToClientEvents { helloFromServer: (message: string, x: number) => void; + ackFromServer: ( + a: boolean, + b: string, + ack: (c: boolean, d: string) => void + ) => void; + multipleAckFromServer: ( + a: boolean, + b: string, + ack: (c: string) => void + ) => void; } describe("on", () => { @@ -185,6 +200,13 @@ describe("server", () => { expectType(message); done(); }); + + s.on("ackFromClient", (a, b, cb) => { + expectType(a); + expectType(b); + expectType<(c: string, d: number) => void>(cb); + cb("123", 456); + }); }); }); }); @@ -213,8 +235,41 @@ describe("server", () => { sio.to("room").emit("helloFromServer", "hi", 1); sio.timeout(1000).emit("helloFromServer", "hi", 1); + sio + .timeout(1000) + .emit("multipleAckFromServer", true, "123", (err, c) => { + expectType(err); + expectType(c); + }); + sio.on("connection", (s) => { s.emit("helloFromServer", "hi", 10); + + s.emit("ackFromServer", true, "123", (c, d) => { + expectType(c); + expectType(d); + }); + + s.timeout(1000).emit("ackFromServer", true, "123", (err, c, d) => { + expectType(err); + expectType(c); + expectType(d); + }); + + s.timeout(1000) + .to("room") + .emit("multipleAckFromServer", true, "123", (err, c) => { + expectType(err); + expectType(c); + }); + + s.to("room") + .timeout(1000) + .emit("multipleAckFromServer", true, "123", (err, c) => { + expectType(err); + expectType(c); + }); + done(); }); }); @@ -253,6 +308,7 @@ describe("server", () => { interface InterServerEvents { helloFromServerToServer: (message: string, x: number) => void; + ackFromServerToServer: (foo: string, cb: (bar: number) => void) => void; } describe("on", () => { @@ -281,6 +337,16 @@ describe("server", () => { expectType(message); expectType(x); }); + + sio.serverSideEmit("ackFromServerToServer", "foo", (err, bar) => { + expectType(err); + expectType(bar); + }); + + sio.on("ackFromServerToServer", (foo, cb) => { + expectType(foo); + expectType<(bar: number) => void>(cb); + }); }); }); });