Skip to content

Commit

Permalink
Add typed events
Browse files Browse the repository at this point in the history
  • Loading branch information
MaximeKjaer committed Mar 2, 2021
1 parent b25495c commit 1e2f9cf
Show file tree
Hide file tree
Showing 11 changed files with 1,832 additions and 83 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ jobs:
- run: npm test
env:
CI: true
- run: npm run test:types
54 changes: 40 additions & 14 deletions lib/broadcast-operator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@ import type { BroadcastFlags, Room, SocketId } from "socket.io-adapter";
import { Handshake, RESERVED_EVENTS, Socket } from "./socket";
import { PacketType } from "socket.io-parser";
import type { Adapter } from "socket.io-adapter";
import type {
EventParams,
EventNames,
EventsMap,
DefaultEventsMap,
} from "./index";

export class BroadcastOperator {
export class BroadcastOperator<
UserEvents extends EventsMap,
UserEmitEvents extends EventsMap = UserEvents
> {
constructor(
private readonly adapter: Adapter,
private readonly rooms: Set<Room> = new Set<Room>(),
Expand All @@ -18,7 +27,9 @@ export class BroadcastOperator {
* @return a new BroadcastOperator instance
* @public
*/
public to(room: Room | Room[]): BroadcastOperator {
public to(
room: Room | Room[]
): BroadcastOperator<UserEvents, UserEmitEvents> {
const rooms = new Set(this.rooms);
if (Array.isArray(room)) {
room.forEach((r) => rooms.add(r));
Expand All @@ -40,7 +51,9 @@ export class BroadcastOperator {
* @return a new BroadcastOperator instance
* @public
*/
public in(room: Room | Room[]): BroadcastOperator {
public in(
room: Room | Room[]
): BroadcastOperator<UserEvents, UserEmitEvents> {
return this.to(room);
}

Expand All @@ -51,7 +64,9 @@ export class BroadcastOperator {
* @return a new BroadcastOperator instance
* @public
*/
public except(room: Room | Room[]): BroadcastOperator {
public except(
room: Room | Room[]
): BroadcastOperator<UserEvents, UserEmitEvents> {
const exceptRooms = new Set(this.exceptRooms);
if (Array.isArray(room)) {
room.forEach((r) => exceptRooms.add(r));
Expand All @@ -73,7 +88,9 @@ export class BroadcastOperator {
* @return a new BroadcastOperator instance
* @public
*/
public compress(compress: boolean): BroadcastOperator {
public compress(
compress: boolean
): BroadcastOperator<UserEvents, UserEmitEvents> {
const flags = Object.assign({}, this.flags, { compress });
return new BroadcastOperator(
this.adapter,
Expand All @@ -91,7 +108,7 @@ export class BroadcastOperator {
* @return a new BroadcastOperator instance
* @public
*/
public get volatile(): BroadcastOperator {
public get volatile(): BroadcastOperator<UserEvents, UserEmitEvents> {
const flags = Object.assign({}, this.flags, { volatile: true });
return new BroadcastOperator(
this.adapter,
Expand All @@ -107,7 +124,7 @@ export class BroadcastOperator {
* @return a new BroadcastOperator instance
* @public
*/
public get local(): BroadcastOperator {
public get local(): BroadcastOperator<UserEvents, UserEmitEvents> {
const flags = Object.assign({}, this.flags, { local: true });
return new BroadcastOperator(
this.adapter,
Expand All @@ -123,18 +140,21 @@ export class BroadcastOperator {
* @return Always true
* @public
*/
public emit(ev: string | Symbol, ...args: any[]): true {
public emit<Ev extends EventNames<UserEmitEvents>>(
ev: Ev,
...args: EventParams<UserEmitEvents, Ev>
): true {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${ev}" is a reserved event name`);
}
// set up packet object
args.unshift(ev);
const data = [ev, ...args];
const packet = {
type: PacketType.EVENT,
data: args,
data: data,
};

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

Expand Down Expand Up @@ -246,13 +266,16 @@ interface SocketDetails {
/**
* Expose of subset of the attributes and methods of the Socket class
*/
export class RemoteSocket {
export class RemoteSocket<
UserEvents extends EventsMap = DefaultEventsMap,
UserEmitEvents extends EventsMap = UserEvents
> {
public readonly id: SocketId;
public readonly handshake: Handshake;
public readonly rooms: Set<Room>;
public readonly data: any;

private readonly operator: BroadcastOperator;
private readonly operator: BroadcastOperator<UserEvents, UserEmitEvents>;

constructor(adapter: Adapter, details: SocketDetails) {
this.id = details.id;
Expand All @@ -262,7 +285,10 @@ export class RemoteSocket {
this.operator = new BroadcastOperator(adapter, new Set([this.id]));
}

public emit(ev: string, ...args: any[]): boolean {
public emit<Ev extends EventNames<UserEmitEvents>>(
ev: Ev,
...args: EventParams<UserEmitEvents, Ev>
): true {
return this.operator.emit(ev, ...args);
}

Expand Down
13 changes: 8 additions & 5 deletions lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@ import { Decoder, Encoder, Packet, PacketType } from "socket.io-parser";
import debugModule = require("debug");
import url = require("url");
import type { IncomingMessage } from "http";
import type { Namespace, Server } from "./index";
import type { DefaultEventsMap, EventsMap, Namespace, Server } from "./index";
import type { Socket } from "./socket";
import type { SocketId } from "socket.io-adapter";

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

export class Client {
export class Client<
UserEvents extends EventsMap = DefaultEventsMap,
UserEmitEvents extends EventsMap = UserEvents
> {
public readonly conn;

private readonly id: string;
private readonly server: Server;
private readonly server: Server<UserEvents, UserEmitEvents>;
private readonly encoder: Encoder;
private readonly decoder: Decoder;
private sockets: Map<SocketId, Socket> = new Map();
Expand All @@ -26,7 +29,7 @@ export class Client {
* @param conn
* @package
*/
constructor(server: Server, conn: Socket) {
constructor(server: Server<UserEvents, UserEmitEvents>, conn: Socket) {
this.server = server;
this.conn = conn;
this.encoder = server.encoder;
Expand Down Expand Up @@ -87,7 +90,7 @@ export class Client {
this.server._checkNamespace(
name,
auth,
(dynamicNspName: Namespace | false) => {
(dynamicNspName: Namespace<UserEvents, UserEmitEvents> | false) => {
if (dynamicNspName) {
debug("dynamic namespace %s was created", dynamicNspName);
this.doConnect(name, auth);
Expand Down
103 changes: 88 additions & 15 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Adapter, Room, SocketId } from "socket.io-adapter";
import * as parser from "socket.io-parser";
import type { Encoder } from "socket.io-parser";
import debugModule from "debug";
import { Socket } from "./socket";
import { ServerReservedEventsMap, Socket } from "./socket";
import type { CookieSerializeOptions } from "cookie";
import type { CorsOptions } from "cors";
import type { BroadcastOperator, RemoteSocket } from "./broadcast-operator";
Expand Down Expand Up @@ -156,8 +156,58 @@ interface ServerOptions extends EngineAttachOptions {
connectTimeout: number;
}

export class Server extends EventEmitter {
public readonly sockets: Namespace;
/**
* An events map is an interface that maps event names to their values. Values
* can be either:
*
* - `void`, indicating that the `on` listener takes no parameters
* - a function type, representing the the type of the `on` listener
* - any other type, representing the type of the first parameter of the `on` listener
*/
export interface EventsMap {
[event: string]: any;
}

/**
* The default events map, used if no EventsMap is given. Using this EventsMap
* is equivalent to accepting all event names, and any data.
*/
export interface DefaultEventsMap {
[event: string]: (...args: any[]) => void;
}

/**
* Returns a union type containing all the keys of an event map.
*/
export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol);

/** The tuple type representing the parameters of an event listener */
export type EventParams<
Map extends EventsMap,
Ev extends keyof Map
> = Map[Ev] extends (...args: any[]) => void
? Parameters<Map[Ev]>
: Map[Ev] extends void
? []
: [Map[Ev]];

/**
* The type of listener callback that listens to the `Ev` event, as defined in `Map`
*/
export type Listener<
Map extends EventsMap,
Ev extends keyof Map
> = Map[Ev] extends (...args: any[]) => void
? Map[Ev]
: Map[Ev] extends void
? (...args: []) => void
: (...args: [Map[Ev]]) => void;

export class Server<
UserEvents extends EventsMap = DefaultEventsMap,
UserEmitEvents extends EventsMap = UserEvents
> extends EventEmitter {
public readonly sockets: Namespace<UserEvents, UserEmitEvents>;

/** @private */
readonly _parser: typeof parser;
Expand All @@ -167,8 +217,11 @@ export class Server extends EventEmitter {
/**
* @private
*/
_nsps: Map<string, Namespace> = new Map();
private parentNsps: Map<ParentNspNameMatchFn, ParentNamespace> = new Map();
_nsps: Map<string, Namespace<UserEvents, UserEmitEvents>> = new Map();
private parentNsps: Map<
ParentNspNameMatchFn,
ParentNamespace<UserEvents, UserEmitEvents>
> = new Map();
private _adapter?: typeof Adapter;
private _serveClient: boolean;
private opts: Partial<EngineOptions>;
Expand Down Expand Up @@ -248,7 +301,7 @@ export class Server extends EventEmitter {
_checkNamespace(
name: string,
auth: { [key: string]: any },
fn: (nsp: Namespace | false) => void
fn: (nsp: Namespace<UserEvents, UserEmitEvents> | false) => void
): void {
if (this.parentNsps.size === 0) return fn(false);

Expand Down Expand Up @@ -558,7 +611,7 @@ export class Server extends EventEmitter {
public of(
name: string | RegExp | ParentNspNameMatchFn,
fn?: (socket: Socket) => void
): Namespace {
): Namespace<UserEvents, UserEmitEvents> {
if (typeof name === "function" || name instanceof RegExp) {
const parentNsp = new ParentNamespace(this);
debug("initializing parent namespace %s", parentNsp.name);
Expand Down Expand Up @@ -629,7 +682,9 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public to(room: Room | Room[]): BroadcastOperator {
public to(
room: Room | Room[]
): BroadcastOperator<UserEvents, UserEmitEvents> {
return this.sockets.to(room);
}

Expand All @@ -640,18 +695,34 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public in(room: Room | Room[]): BroadcastOperator {
public in(
room: Room | Room[]
): BroadcastOperator<UserEvents, UserEmitEvents> {
return this.sockets.in(room);
}

public on<
Ev extends EventNames<
UserEvents & ServerReservedEventsMap<UserEvents, UserEmitEvents>
>
>(
ev: Ev,
listener: Listener<
UserEvents & ServerReservedEventsMap<UserEvents, UserEmitEvents>,
Ev
>
): this {
return super.on(ev, listener);
}

/**
* Excludes a room when emitting.
*
* @param name
* @return self
* @public
*/
public except(name: Room | Room[]): Server {
public except(name: Room | Room[]): Server<UserEvents, UserEmitEvents> {
this.sockets.except(name);
return this;
}
Expand All @@ -662,7 +733,7 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public send(...args: readonly any[]): this {
public send(...args: EventParams<UserEmitEvents, "message">): this {
this.sockets.emit("message", ...args);
return this;
}
Expand All @@ -673,7 +744,7 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public write(...args: readonly any[]): this {
public write(...args: EventParams<UserEmitEvents, "message">): this {
this.sockets.emit("message", ...args);
return this;
}
Expand All @@ -694,7 +765,9 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public compress(compress: boolean): BroadcastOperator {
public compress(
compress: boolean
): BroadcastOperator<UserEvents, UserEmitEvents> {
return this.sockets.compress(compress);
}

Expand All @@ -706,7 +779,7 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public get volatile(): BroadcastOperator {
public get volatile(): BroadcastOperator<UserEvents, UserEmitEvents> {
return this.sockets.volatile;
}

Expand All @@ -716,7 +789,7 @@ export class Server extends EventEmitter {
* @return self
* @public
*/
public get local(): BroadcastOperator {
public get local(): BroadcastOperator<UserEvents, UserEmitEvents> {
return this.sockets.local;
}

Expand Down
Loading

0 comments on commit 1e2f9cf

Please sign in to comment.