Skip to content

Commit 5902365

Browse files
feat: add support for typed events
Syntax: ```ts interface ServerToClientEvents { "my-event": (a: number, b: string, c: number[]) => void; } interface ClientToServerEvents { hello: (message: string) => void; } const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io(); socket.emit("hello", "world"); socket.on("my-event", (a, b, c) => { // ... }); ``` The events are not typed by default (inferred as any), so this change is backward compatible. Related: socketio/socket.io#3742
1 parent 78ec5a6 commit 5902365

File tree

7 files changed

+1581
-29
lines changed

7 files changed

+1581
-29
lines changed

lib/manager.ts

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import * as eio from "engine.io-client";
22
import { Socket, SocketOptions } from "./socket";
3-
import Emitter = require("component-emitter");
43
import * as parser from "socket.io-parser";
54
import { Decoder, Encoder, Packet } from "socket.io-parser";
65
import { on } from "./on";
76
import * as Backoff from "backo2";
7+
import {
8+
DefaultEventsMap,
9+
EventsMap,
10+
StrictEventEmitter,
11+
} from "./typed-events";
812

913
const debug = require("debug")("socket.io-client:manager");
1014

@@ -258,7 +262,22 @@ export interface ManagerOptions extends EngineOptions {
258262
parser: any;
259263
}
260264

261-
export class Manager extends Emitter {
265+
interface ManagerReservedEvents {
266+
open: () => void;
267+
error: (err: Error) => void;
268+
ping: () => void;
269+
packet: (packet: Packet) => void;
270+
close: (reason: string) => void;
271+
reconnect_failed: () => void;
272+
reconnect_attempt: (attempt: number) => void;
273+
reconnect_error: (err: Error) => void;
274+
reconnect: (attempt: number) => void;
275+
}
276+
277+
export class Manager<
278+
ListenEvents extends EventsMap = DefaultEventsMap,
279+
EmitEvents extends EventsMap = ListenEvents
280+
> extends StrictEventEmitter<{}, {}, ManagerReservedEvents> {
262281
/**
263282
* The Engine.IO client instance
264283
*
@@ -487,7 +506,7 @@ export class Manager extends Emitter {
487506
debug("error");
488507
self.cleanup();
489508
self._readyState = "closed";
490-
super.emit("error", err);
509+
this.emitReserved("error", err);
491510
if (fn) {
492511
fn(err);
493512
} else {
@@ -546,7 +565,7 @@ export class Manager extends Emitter {
546565

547566
// mark as open
548567
this._readyState = "open";
549-
super.emit("open");
568+
this.emitReserved("open");
550569

551570
// add new subs
552571
const socket = this.engine;
@@ -565,7 +584,7 @@ export class Manager extends Emitter {
565584
* @private
566585
*/
567586
private onping(): void {
568-
super.emit("ping");
587+
this.emitReserved("ping");
569588
}
570589

571590
/**
@@ -583,7 +602,7 @@ export class Manager extends Emitter {
583602
* @private
584603
*/
585604
private ondecoded(packet): void {
586-
super.emit("packet", packet);
605+
this.emitReserved("packet", packet);
587606
}
588607

589608
/**
@@ -593,7 +612,7 @@ export class Manager extends Emitter {
593612
*/
594613
private onerror(err): void {
595614
debug("error", err);
596-
super.emit("error", err);
615+
this.emitReserved("error", err);
597616
}
598617

599618
/**
@@ -701,7 +720,7 @@ export class Manager extends Emitter {
701720
this.cleanup();
702721
this.backoff.reset();
703722
this._readyState = "closed";
704-
super.emit("close", reason);
723+
this.emitReserved("close", reason);
705724

706725
if (this._reconnection && !this.skipReconnect) {
707726
this.reconnect();
@@ -721,7 +740,7 @@ export class Manager extends Emitter {
721740
if (this.backoff.attempts >= this._reconnectionAttempts) {
722741
debug("reconnect failed");
723742
this.backoff.reset();
724-
super.emit("reconnect_failed");
743+
this.emitReserved("reconnect_failed");
725744
this._reconnecting = false;
726745
} else {
727746
const delay = this.backoff.duration();
@@ -732,7 +751,7 @@ export class Manager extends Emitter {
732751
if (self.skipReconnect) return;
733752

734753
debug("attempting reconnect");
735-
super.emit("reconnect_attempt", self.backoff.attempts);
754+
this.emitReserved("reconnect_attempt", self.backoff.attempts);
736755

737756
// check again for the case socket closed in above events
738757
if (self.skipReconnect) return;
@@ -742,7 +761,7 @@ export class Manager extends Emitter {
742761
debug("reconnect attempt error");
743762
self._reconnecting = false;
744763
self.reconnect();
745-
super.emit("reconnect_error", err);
764+
this.emitReserved("reconnect_error", err);
746765
} else {
747766
debug("reconnect success");
748767
self.onreconnect();
@@ -765,6 +784,6 @@ export class Manager extends Emitter {
765784
const attempt = this.backoff.attempts;
766785
this._reconnecting = false;
767786
this.backoff.reset();
768-
super.emit("reconnect", attempt);
787+
this.emitReserved("reconnect", attempt);
769788
}
770789
}

lib/on.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type * as Emitter from "component-emitter";
2+
import { StrictEventEmitter } from "./typed-events";
23

34
export function on(
4-
obj: Emitter,
5+
obj: Emitter | StrictEventEmitter<any, any>,
56
ev: string,
67
fn: (err?: any) => any
78
): VoidFunction {

lib/socket.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { Packet, PacketType } from "socket.io-parser";
2-
import Emitter = require("component-emitter");
32
import { on } from "./on";
43
import { Manager } from "./manager";
4+
import {
5+
DefaultEventsMap,
6+
EventNames,
7+
EventParams,
8+
EventsMap,
9+
StrictEventEmitter,
10+
} from "./typed-events";
511

612
const debug = require("debug")("socket.io-client:socket");
713

@@ -31,8 +37,17 @@ interface Flags {
3137
volatile?: boolean;
3238
}
3339

34-
export class Socket extends Emitter {
35-
public readonly io: Manager;
40+
interface SocketReservedEvents {
41+
connect: () => void;
42+
connect_error: (err: Error) => void;
43+
disconnect: (reason: Socket.DisconnectReason) => void;
44+
}
45+
46+
export class Socket<
47+
ListenEvents extends EventsMap = DefaultEventsMap,
48+
EmitEvents extends EventsMap = ListenEvents
49+
> extends StrictEventEmitter<ListenEvents, EmitEvents, SocketReservedEvents> {
50+
public readonly io: Manager<ListenEvents, EmitEvents>;
3651

3752
public id: string;
3853
public connected: boolean;
@@ -133,11 +148,13 @@ export class Socket extends Emitter {
133148
* Override `emit`.
134149
* If the event is in `events`, it's emitted normally.
135150
*
136-
* @param ev - event name
137151
* @return self
138152
* @public
139153
*/
140-
public emit(ev: string, ...args: any[]): this {
154+
public emit<Ev extends EventNames<EmitEvents>>(
155+
ev: Ev,
156+
...args: EventParams<EmitEvents, Ev>
157+
): this {
141158
if (RESERVED_EVENTS.hasOwnProperty(ev)) {
142159
throw new Error('"' + ev + '" is a reserved event name');
143160
}
@@ -213,7 +230,7 @@ export class Socket extends Emitter {
213230
*/
214231
private onerror(err: Error): void {
215232
if (!this.connected) {
216-
super.emit("connect_error", err);
233+
this.emitReserved("connect_error", err);
217234
}
218235
}
219236

@@ -228,7 +245,7 @@ export class Socket extends Emitter {
228245
this.connected = false;
229246
this.disconnected = true;
230247
delete this.id;
231-
super.emit("disconnect", reason);
248+
this.emitReserved("disconnect", reason);
232249
}
233250

234251
/**
@@ -248,7 +265,7 @@ export class Socket extends Emitter {
248265
const id = packet.data.sid;
249266
this.onconnect(id);
250267
} else {
251-
super.emit(
268+
this.emitReserved(
252269
"connect_error",
253270
new Error(
254271
"It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"
@@ -281,7 +298,7 @@ export class Socket extends Emitter {
281298
const err = new Error(packet.data.message);
282299
// @ts-ignore
283300
err.data = packet.data.data;
284-
super.emit("connect_error", err);
301+
this.emitReserved("connect_error", err);
285302
break;
286303
}
287304
}
@@ -367,7 +384,7 @@ export class Socket extends Emitter {
367384
this.id = id;
368385
this.connected = true;
369386
this.disconnected = false;
370-
super.emit("connect");
387+
this.emitReserved("connect");
371388
this.emitBuffered();
372389
}
373390

lib/typed-events.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import Emitter = require("component-emitter");
2+
3+
/**
4+
* An events map is an interface that maps event names to their value, which
5+
* represents the type of the `on` listener.
6+
*/
7+
export interface EventsMap {
8+
[event: string]: any;
9+
}
10+
11+
/**
12+
* The default events map, used if no EventsMap is given. Using this EventsMap
13+
* is equivalent to accepting all event names, and any data.
14+
*/
15+
export interface DefaultEventsMap {
16+
[event: string]: (...args: any[]) => void;
17+
}
18+
19+
/**
20+
* Returns a union type containing all the keys of an event map.
21+
*/
22+
export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol);
23+
24+
/** The tuple type representing the parameters of an event listener */
25+
export type EventParams<
26+
Map extends EventsMap,
27+
Ev extends EventNames<Map>
28+
> = Parameters<Map[Ev]>;
29+
30+
/**
31+
* The event names that are either in ReservedEvents or in UserEvents
32+
*/
33+
export type ReservedOrUserEventNames<
34+
ReservedEventsMap extends EventsMap,
35+
UserEvents extends EventsMap
36+
> = EventNames<ReservedEventsMap> | EventNames<UserEvents>;
37+
38+
/**
39+
* Type of a listener of a user event or a reserved event. If `Ev` is in
40+
* `ReservedEvents`, the reserved event listener is returned.
41+
*/
42+
export type ReservedOrUserListener<
43+
ReservedEvents extends EventsMap,
44+
UserEvents extends EventsMap,
45+
Ev extends ReservedOrUserEventNames<ReservedEvents, UserEvents>
46+
> = Ev extends EventNames<ReservedEvents>
47+
? ReservedEvents[Ev]
48+
: Ev extends EventNames<UserEvents>
49+
? UserEvents[Ev]
50+
: never;
51+
52+
/**
53+
* Strictly typed version of an `EventEmitter`. A `TypedEventEmitter` takes type
54+
* parameters for mappings of event names to event data types, and strictly
55+
* types method calls to the `EventEmitter` according to these event maps.
56+
*
57+
* @typeParam ListenEvents - `EventsMap` of user-defined events that can be
58+
* listened to with `on` or `once`
59+
* @typeParam EmitEvents - `EventsMap` of user-defined events that can be
60+
* emitted with `emit`
61+
* @typeParam ReservedEvents - `EventsMap` of reserved events, that can be
62+
* emitted by socket.io with `emitReserved`, and can be listened to with
63+
* `listen`.
64+
*/
65+
export abstract class StrictEventEmitter<
66+
ListenEvents extends EventsMap,
67+
EmitEvents extends EventsMap,
68+
ReservedEvents extends EventsMap = {}
69+
> extends Emitter {
70+
/**
71+
* Adds the `listener` function as an event listener for `ev`.
72+
*
73+
* @param ev Name of the event
74+
* @param listener Callback function
75+
*/
76+
on<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
77+
ev: Ev,
78+
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
79+
): this {
80+
super.on(ev as string, listener);
81+
return this;
82+
}
83+
84+
/**
85+
* Adds a one-time `listener` function as an event listener for `ev`.
86+
*
87+
* @param ev Name of the event
88+
* @param listener Callback function
89+
*/
90+
once<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
91+
ev: Ev,
92+
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
93+
): this {
94+
super.once(ev as string, listener);
95+
return this;
96+
}
97+
98+
/**
99+
* Emits an event.
100+
*
101+
* @param ev Name of the event
102+
* @param args Values to send to listeners of this event
103+
*/
104+
emit<Ev extends EventNames<EmitEvents>>(
105+
ev: Ev,
106+
...args: EventParams<EmitEvents, Ev>
107+
): this {
108+
super.emit(ev as string, ...args);
109+
return this;
110+
}
111+
112+
/**
113+
* Emits a reserved event.
114+
*
115+
* This method is `protected`, so that only a class extending
116+
* `StrictEventEmitter` can emit its own reserved events.
117+
*
118+
* @param ev Reserved event name
119+
* @param args Arguments to emit along with the event
120+
*/
121+
protected emitReserved<Ev extends EventNames<ReservedEvents>>(
122+
ev: Ev,
123+
...args: EventParams<ReservedEvents, Ev>
124+
): this {
125+
super.emit(ev as string, ...args);
126+
return this;
127+
}
128+
129+
/**
130+
* Returns the listeners listening to an event.
131+
*
132+
* @param event Event name
133+
* @returns Array of listeners subscribed to `event`
134+
*/
135+
listeners<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
136+
event: Ev
137+
): ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>[] {
138+
return super.listeners(event as string) as ReservedOrUserListener<
139+
ReservedEvents,
140+
ListenEvents,
141+
Ev
142+
>[];
143+
}
144+
}

0 commit comments

Comments
 (0)