Skip to content

Commit

Permalink
fix(typings): properly type emits with timeout
Browse files Browse the repository at this point in the history
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
  • Loading branch information
darrachequesne committed Jan 19, 2023
1 parent a21ad88 commit f3ada7d
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 16 deletions.
12 changes: 6 additions & 6 deletions lib/broadcast-operator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type {
EventNames,
EventsMap,
TypedEventBroadcaster,
DecorateAcknowledgements,
DecorateAcknowledgementsWithTimeoutAndMultipleResponses,
} from "./typed-events";

export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
Expand Down Expand Up @@ -169,12 +171,10 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
*/
public timeout(timeout: number) {
const flags = Object.assign({}, this.flags, { timeout });
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
this.exceptRooms,
flags
);
return new BroadcastOperator<
DecorateAcknowledgementsWithTimeoutAndMultipleResponses<EmitEvents>,
SocketData
>(this.adapter, this.rooms, this.exceptRooms, flags);
}

/**
Expand Down
7 changes: 6 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -857,7 +859,10 @@ export class Server<
*/
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
...args: EventParams<ServerSideEvents, Ev>
...args: EventParams<
DecorateAcknowledgementsWithTimeoutAndMultipleResponses<ServerSideEvents>,
Ev
>
): boolean {
return this.sockets.serverSideEmit(ev, ...args);
}
Expand Down
8 changes: 6 additions & 2 deletions lib/namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -494,7 +495,10 @@ export class Namespace<
*/
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
...args: EventParams<ServerSideEvents, Ev>
...args: EventParams<
DecorateAcknowledgementsWithTimeoutAndMultipleResponses<ServerSideEvents>,
Ev
>
): boolean {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${String(ev)}" is a reserved event name`);
Expand Down
21 changes: 14 additions & 7 deletions lib/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -842,7 +844,14 @@ export class Socket<
*
* @returns self
*/
public timeout(timeout: number): this {
public timeout(
timeout: number
): Socket<
ListenEvents,
DecorateAcknowledgements<EmitEvents>,
ServerSideEvents,
SocketData
> {
this.flags.timeout = timeout;
return this;
}
Expand Down Expand Up @@ -1152,11 +1161,9 @@ export class Socket<
private newBroadcastOperator() {
const flags = Object.assign({}, this.flags);
this.flags = {};
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
new Set<Room>(),
new Set<Room>([this.id]),
flags
);
return new BroadcastOperator<
DecorateAcknowledgementsWithMultipleResponses<EmitEvents>,
SocketData
>(this.adapter, new Set<Room>(), new Set<Room>([this.id]), flags);
}
}
49 changes: 49 additions & 0 deletions lib/typed-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,52 @@ export abstract class StrictEventEmitter<
>[];
}
}

type PrependTimeoutError<T extends any[]> = {
[K in keyof T]: T[K] extends (...args: infer Params) => infer Result
? (err: Error, ...args: Params) => Result
: T[K];
};

type ExpectMultipleResponses<T extends any[]> = {
[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<E> = {
[K in keyof E]: E[K] extends (...args: infer Params) => infer Result
? (...args: PrependTimeoutError<Params>) => Result
: E[K];
};

export type DecorateAcknowledgementsWithTimeoutAndMultipleResponses<E> = {
[K in keyof E]: E[K] extends (...args: infer Params) => infer Result
? (...args: ExpectMultipleResponses<PrependTimeoutError<Params>>) => Result
: E[K];
};

export type DecorateAcknowledgementsWithMultipleResponses<E> = {
[K in keyof E]: E[K] extends (...args: infer Params) => infer Result
? (...args: ExpectMultipleResponses<Params>) => Result
: E[K];
};
66 changes: 66 additions & 0 deletions test/socket.io.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -185,6 +200,13 @@ describe("server", () => {
expectType<string>(message);
done();
});

s.on("ackFromClient", (a, b, cb) => {
expectType<string>(a);
expectType<number>(b);
expectType<(c: string, d: number) => void>(cb);
cb("123", 456);
});
});
});
});
Expand Down Expand Up @@ -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<Error>(err);
expectType<string[]>(c);
});

sio.on("connection", (s) => {
s.emit("helloFromServer", "hi", 10);

s.emit("ackFromServer", true, "123", (c, d) => {
expectType<boolean>(c);
expectType<string>(d);
});

s.timeout(1000).emit("ackFromServer", true, "123", (err, c, d) => {
expectType<Error>(err);
expectType<boolean>(c);
expectType<string>(d);
});

s.timeout(1000)
.to("room")
.emit("multipleAckFromServer", true, "123", (err, c) => {
expectType<Error>(err);
expectType<string[]>(c);
});

s.to("room")
.timeout(1000)
.emit("multipleAckFromServer", true, "123", (err, c) => {
expectType<Error>(err);
expectType<string[]>(c);
});

done();
});
});
Expand Down Expand Up @@ -253,6 +308,7 @@ describe("server", () => {

interface InterServerEvents {
helloFromServerToServer: (message: string, x: number) => void;
ackFromServerToServer: (foo: string, cb: (bar: number) => void) => void;
}

describe("on", () => {
Expand Down Expand Up @@ -281,6 +337,16 @@ describe("server", () => {
expectType<string>(message);
expectType<number>(x);
});

sio.serverSideEmit("ackFromServerToServer", "foo", (err, bar) => {
expectType<Error>(err);
expectType<number[]>(bar);
});

sio.on("ackFromServerToServer", (foo, cb) => {
expectType<string>(foo);
expectType<(bar: number) => void>(cb);
});
});
});
});
Expand Down

0 comments on commit f3ada7d

Please sign in to comment.