From f3ada7d8ccc02eeced2b9b9ac8e4bc921eb630d2 Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Wed, 18 Jan 2023 08:39:29 +0100 Subject: [PATCH] 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 true ? [Error, ...args] : args; interface ClientToServerEvents { withAck: (data: { argName: boolean }, callback: (...args: WithTimeoutAck) => void) => void; } interface ServerToClientEvents { } const io = new Server>(3000); io.on("connection", (socket) => { socket.on("withAck", (val, cb) => { cb("123"); }); }); const socket: Socket> = ioc("http://localhost:3000"); socket.timeout(100).emit("withAck", { argName: true }, (err, val) => { // ... }); ``` Related: https://github.com/socketio/socket.io-client/issues/1555 --- lib/broadcast-operator.ts | 12 +++---- lib/index.ts | 7 ++++- lib/namespace.ts | 8 +++-- lib/socket.ts | 21 ++++++++----- lib/typed-events.ts | 49 +++++++++++++++++++++++++++++ test/socket.io.test-d.ts | 66 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 147 insertions(+), 16 deletions(-) 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); + }); }); }); });