Skip to content

Commit 0107510

Browse files
MaximeKjaerdarrachequesne
authored andcommitted
feat: add support for typed events (#3822)
Syntax: ```ts interface ClientToServerEvents { "my-event": (a: number, b: string, c: number[]) => void; } interface ServerToClientEvents { hello: (message: string) => void; } const io = new Server<ClientToServerEvents, ServerToClientEvents>(httpServer); io.emit("hello", "world"); io.on("connection", (socket) => { socket.on("my-event", (a, b, c) => { // ... }); socket.emit("hello", "again"); }); ``` The events are not typed by default (inferred as any), so this change is backward compatible. Note: we could also have reused the method here ([1]) to add types to the EventEmitter, instead of creating a StrictEventEmitter class. Related: #3742 [1]: https://github.com/binier/tiny-typed-emitter
1 parent b25495c commit 0107510

11 files changed

+1956
-117
lines changed

lib/broadcast-operator.ts

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,15 @@ import type { BroadcastFlags, Room, SocketId } from "socket.io-adapter";
22
import { Handshake, RESERVED_EVENTS, Socket } from "./socket";
33
import { PacketType } from "socket.io-parser";
44
import type { Adapter } from "socket.io-adapter";
5+
import type {
6+
EventParams,
7+
EventNames,
8+
EventsMap,
9+
TypedEventBroadcaster,
10+
} from "./typed-events";
511

6-
export class BroadcastOperator {
12+
export class BroadcastOperator<EmitEvents extends EventsMap>
13+
implements TypedEventBroadcaster<EmitEvents> {
714
constructor(
815
private readonly adapter: Adapter,
916
private readonly rooms: Set<Room> = new Set<Room>(),
@@ -18,7 +25,7 @@ export class BroadcastOperator {
1825
* @return a new BroadcastOperator instance
1926
* @public
2027
*/
21-
public to(room: Room | Room[]): BroadcastOperator {
28+
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
2229
const rooms = new Set(this.rooms);
2330
if (Array.isArray(room)) {
2431
room.forEach((r) => rooms.add(r));
@@ -40,7 +47,7 @@ export class BroadcastOperator {
4047
* @return a new BroadcastOperator instance
4148
* @public
4249
*/
43-
public in(room: Room | Room[]): BroadcastOperator {
50+
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
4451
return this.to(room);
4552
}
4653

@@ -51,7 +58,7 @@ export class BroadcastOperator {
5158
* @return a new BroadcastOperator instance
5259
* @public
5360
*/
54-
public except(room: Room | Room[]): BroadcastOperator {
61+
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> {
5562
const exceptRooms = new Set(this.exceptRooms);
5663
if (Array.isArray(room)) {
5764
room.forEach((r) => exceptRooms.add(r));
@@ -73,7 +80,7 @@ export class BroadcastOperator {
7380
* @return a new BroadcastOperator instance
7481
* @public
7582
*/
76-
public compress(compress: boolean): BroadcastOperator {
83+
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
7784
const flags = Object.assign({}, this.flags, { compress });
7885
return new BroadcastOperator(
7986
this.adapter,
@@ -91,7 +98,7 @@ export class BroadcastOperator {
9198
* @return a new BroadcastOperator instance
9299
* @public
93100
*/
94-
public get volatile(): BroadcastOperator {
101+
public get volatile(): BroadcastOperator<EmitEvents> {
95102
const flags = Object.assign({}, this.flags, { volatile: true });
96103
return new BroadcastOperator(
97104
this.adapter,
@@ -107,7 +114,7 @@ export class BroadcastOperator {
107114
* @return a new BroadcastOperator instance
108115
* @public
109116
*/
110-
public get local(): BroadcastOperator {
117+
public get local(): BroadcastOperator<EmitEvents> {
111118
const flags = Object.assign({}, this.flags, { local: true });
112119
return new BroadcastOperator(
113120
this.adapter,
@@ -123,18 +130,21 @@ export class BroadcastOperator {
123130
* @return Always true
124131
* @public
125132
*/
126-
public emit(ev: string | Symbol, ...args: any[]): true {
133+
public emit<Ev extends EventNames<EmitEvents>>(
134+
ev: Ev,
135+
...args: EventParams<EmitEvents, Ev>
136+
): true {
127137
if (RESERVED_EVENTS.has(ev)) {
128138
throw new Error(`"${ev}" is a reserved event name`);
129139
}
130140
// set up packet object
131-
args.unshift(ev);
141+
const data = [ev, ...args];
132142
const packet = {
133143
type: PacketType.EVENT,
134-
data: args,
144+
data: data,
135145
};
136146

137-
if ("function" == typeof args[args.length - 1]) {
147+
if ("function" == typeof data[data.length - 1]) {
138148
throw new Error("Callbacks are not supported when broadcasting");
139149
}
140150

@@ -166,7 +176,7 @@ export class BroadcastOperator {
166176
*
167177
* @public
168178
*/
169-
public fetchSockets(): Promise<RemoteSocket[]> {
179+
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
170180
return this.adapter
171181
.fetchSockets({
172182
rooms: this.rooms,
@@ -176,7 +186,7 @@ export class BroadcastOperator {
176186
return sockets.map((socket) => {
177187
if (socket instanceof Socket) {
178188
// FIXME the TypeScript compiler complains about missing private properties
179-
return (socket as unknown) as RemoteSocket;
189+
return (socket as unknown) as RemoteSocket<EmitEvents>;
180190
} else {
181191
return new RemoteSocket(this.adapter, socket as SocketDetails);
182192
}
@@ -246,13 +256,14 @@ interface SocketDetails {
246256
/**
247257
* Expose of subset of the attributes and methods of the Socket class
248258
*/
249-
export class RemoteSocket {
259+
export class RemoteSocket<EmitEvents extends EventsMap>
260+
implements TypedEventBroadcaster<EmitEvents> {
250261
public readonly id: SocketId;
251262
public readonly handshake: Handshake;
252263
public readonly rooms: Set<Room>;
253264
public readonly data: any;
254265

255-
private readonly operator: BroadcastOperator;
266+
private readonly operator: BroadcastOperator<EmitEvents>;
256267

257268
constructor(adapter: Adapter, details: SocketDetails) {
258269
this.id = details.id;
@@ -262,7 +273,10 @@ export class RemoteSocket {
262273
this.operator = new BroadcastOperator(adapter, new Set([this.id]));
263274
}
264275

265-
public emit(ev: string, ...args: any[]): boolean {
276+
public emit<Ev extends EventNames<EmitEvents>>(
277+
ev: Ev,
278+
...args: EventParams<EmitEvents, Ev>
279+
): true {
266280
return this.operator.emit(ev, ...args);
267281
}
268282

lib/client.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,24 @@ import debugModule = require("debug");
33
import url = require("url");
44
import type { IncomingMessage } from "http";
55
import type { Namespace, Server } from "./index";
6+
import type { EventsMap } from "./typed-events";
67
import type { Socket } from "./socket";
78
import type { SocketId } from "socket.io-adapter";
89

910
const debug = debugModule("socket.io:client");
1011

11-
export class Client {
12+
export class Client<
13+
ListenEvents extends EventsMap,
14+
EmitEvents extends EventsMap
15+
> {
1216
public readonly conn;
1317

1418
private readonly id: string;
15-
private readonly server: Server;
19+
private readonly server: Server<ListenEvents, EmitEvents>;
1620
private readonly encoder: Encoder;
1721
private readonly decoder: Decoder;
18-
private sockets: Map<SocketId, Socket> = new Map();
19-
private nsps: Map<string, Socket> = new Map();
22+
private sockets: Map<SocketId, Socket<ListenEvents, EmitEvents>> = new Map();
23+
private nsps: Map<string, Socket<ListenEvents, EmitEvents>> = new Map();
2024
private connectTimeout?: NodeJS.Timeout;
2125

2226
/**
@@ -26,7 +30,7 @@ export class Client {
2630
* @param conn
2731
* @package
2832
*/
29-
constructor(server: Server, conn: Socket) {
33+
constructor(server: Server<ListenEvents, EmitEvents>, conn: any) {
3034
this.server = server;
3135
this.conn = conn;
3236
this.encoder = server.encoder;
@@ -87,7 +91,7 @@ export class Client {
8791
this.server._checkNamespace(
8892
name,
8993
auth,
90-
(dynamicNspName: Namespace | false) => {
94+
(dynamicNspName: Namespace<ListenEvents, EmitEvents> | false) => {
9195
if (dynamicNspName) {
9296
debug("dynamic namespace %s was created", dynamicNspName);
9397
this.doConnect(name, auth);
@@ -145,7 +149,7 @@ export class Client {
145149
*
146150
* @private
147151
*/
148-
_remove(socket: Socket): void {
152+
_remove(socket: Socket<ListenEvents, EmitEvents>): void {
149153
if (this.sockets.has(socket.id)) {
150154
const nsp = this.sockets.get(socket.id)!.nsp.name;
151155
this.sockets.delete(socket.id);

lib/index.ts

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import path = require("path");
77
import engine = require("engine.io");
88
import { Client } from "./client";
99
import { EventEmitter } from "events";
10-
import { ExtendedError, Namespace } from "./namespace";
10+
import {
11+
ExtendedError,
12+
Namespace,
13+
NamespaceReservedEventsMap,
14+
} from "./namespace";
1115
import { ParentNamespace } from "./parent-namespace";
1216
import { Adapter, Room, SocketId } from "socket.io-adapter";
1317
import * as parser from "socket.io-parser";
@@ -17,6 +21,12 @@ import { Socket } from "./socket";
1721
import type { CookieSerializeOptions } from "cookie";
1822
import type { CorsOptions } from "cors";
1923
import type { BroadcastOperator, RemoteSocket } from "./broadcast-operator";
24+
import {
25+
EventsMap,
26+
DefaultEventsMap,
27+
EventParams,
28+
StrictEventEmitter,
29+
} from "./typed-events";
2030

2131
const debug = debugModule("socket.io:server");
2232

@@ -156,8 +166,15 @@ interface ServerOptions extends EngineAttachOptions {
156166
connectTimeout: number;
157167
}
158168

159-
export class Server extends EventEmitter {
160-
public readonly sockets: Namespace;
169+
export class Server<
170+
ListenEvents extends EventsMap = DefaultEventsMap,
171+
EmitEvents extends EventsMap = ListenEvents
172+
> extends StrictEventEmitter<
173+
{},
174+
EmitEvents,
175+
NamespaceReservedEventsMap<ListenEvents, EmitEvents>
176+
> {
177+
public readonly sockets: Namespace<ListenEvents, EmitEvents>;
161178

162179
/** @private */
163180
readonly _parser: typeof parser;
@@ -167,8 +184,11 @@ export class Server extends EventEmitter {
167184
/**
168185
* @private
169186
*/
170-
_nsps: Map<string, Namespace> = new Map();
171-
private parentNsps: Map<ParentNspNameMatchFn, ParentNamespace> = new Map();
187+
_nsps: Map<string, Namespace<ListenEvents, EmitEvents>> = new Map();
188+
private parentNsps: Map<
189+
ParentNspNameMatchFn,
190+
ParentNamespace<ListenEvents, EmitEvents>
191+
> = new Map();
172192
private _adapter?: typeof Adapter;
173193
private _serveClient: boolean;
174194
private opts: Partial<EngineOptions>;
@@ -248,7 +268,7 @@ export class Server extends EventEmitter {
248268
_checkNamespace(
249269
name: string,
250270
auth: { [key: string]: any },
251-
fn: (nsp: Namespace | false) => void
271+
fn: (nsp: Namespace<ListenEvents, EmitEvents> | false) => void
252272
): void {
253273
if (this.parentNsps.size === 0) return fn(false);
254274

@@ -557,8 +577,8 @@ export class Server extends EventEmitter {
557577
*/
558578
public of(
559579
name: string | RegExp | ParentNspNameMatchFn,
560-
fn?: (socket: Socket) => void
561-
): Namespace {
580+
fn?: (socket: Socket<ListenEvents, EmitEvents>) => void
581+
): Namespace<ListenEvents, EmitEvents> {
562582
if (typeof name === "function" || name instanceof RegExp) {
563583
const parentNsp = new ParentNamespace(this);
564584
debug("initializing parent namespace %s", parentNsp.name);
@@ -616,7 +636,10 @@ export class Server extends EventEmitter {
616636
* @public
617637
*/
618638
public use(
619-
fn: (socket: Socket, next: (err?: ExtendedError) => void) => void
639+
fn: (
640+
socket: Socket<ListenEvents, EmitEvents>,
641+
next: (err?: ExtendedError) => void
642+
) => void
620643
): this {
621644
this.sockets.use(fn);
622645
return this;
@@ -629,7 +652,7 @@ export class Server extends EventEmitter {
629652
* @return self
630653
* @public
631654
*/
632-
public to(room: Room | Room[]): BroadcastOperator {
655+
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
633656
return this.sockets.to(room);
634657
}
635658

@@ -640,7 +663,7 @@ export class Server extends EventEmitter {
640663
* @return self
641664
* @public
642665
*/
643-
public in(room: Room | Room[]): BroadcastOperator {
666+
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
644667
return this.sockets.in(room);
645668
}
646669

@@ -651,7 +674,7 @@ export class Server extends EventEmitter {
651674
* @return self
652675
* @public
653676
*/
654-
public except(name: Room | Room[]): Server {
677+
public except(name: Room | Room[]): Server<ListenEvents, EmitEvents> {
655678
this.sockets.except(name);
656679
return this;
657680
}
@@ -662,7 +685,7 @@ export class Server extends EventEmitter {
662685
* @return self
663686
* @public
664687
*/
665-
public send(...args: readonly any[]): this {
688+
public send(...args: EventParams<EmitEvents, "message">): this {
666689
this.sockets.emit("message", ...args);
667690
return this;
668691
}
@@ -673,7 +696,7 @@ export class Server extends EventEmitter {
673696
* @return self
674697
* @public
675698
*/
676-
public write(...args: readonly any[]): this {
699+
public write(...args: EventParams<EmitEvents, "message">): this {
677700
this.sockets.emit("message", ...args);
678701
return this;
679702
}
@@ -694,7 +717,7 @@ export class Server extends EventEmitter {
694717
* @return self
695718
* @public
696719
*/
697-
public compress(compress: boolean): BroadcastOperator {
720+
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
698721
return this.sockets.compress(compress);
699722
}
700723

@@ -706,7 +729,7 @@ export class Server extends EventEmitter {
706729
* @return self
707730
* @public
708731
*/
709-
public get volatile(): BroadcastOperator {
732+
public get volatile(): BroadcastOperator<EmitEvents> {
710733
return this.sockets.volatile;
711734
}
712735

@@ -716,7 +739,7 @@ export class Server extends EventEmitter {
716739
* @return self
717740
* @public
718741
*/
719-
public get local(): BroadcastOperator {
742+
public get local(): BroadcastOperator<EmitEvents> {
720743
return this.sockets.local;
721744
}
722745

@@ -725,7 +748,7 @@ export class Server extends EventEmitter {
725748
*
726749
* @public
727750
*/
728-
public fetchSockets(): Promise<RemoteSocket[]> {
751+
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
729752
return this.sockets.fetchSockets();
730753
}
731754

0 commit comments

Comments
 (0)