import { Packet, PacketType } from "../socket.io-parser" import { on } from "./on" import { Manager } from "./manager" import { DefaultEventsMap, EventNames, EventParams, EventsMap, StrictEventEmitter, } from "./typed-events" const debug = require("../debug")("socket.io-client") export interface SocketOptions { /** * the authentication payload sent when connecting to the Namespace */ auth: { [key: string]: any } | ((cb: (data: object) => void) => void) } /** * Internal events. * These events can't be emitted by the user. */ const RESERVED_EVENTS = Object.freeze({ connect: 1, connect_error: 1, disconnect: 1, disconnecting: 1, // EventEmitter reserved events: https://nodejs.org/api/events.html#events_event_newlistener newListener: 1, removeListener: 1, }) interface Flags { compress?: boolean volatile?: boolean } interface SocketReservedEvents { connect: () => void connect_error: (err: Error) => void disconnect: (reason: Socket.DisconnectReason) => void } export class Socket< ListenEvents extends EventsMap = DefaultEventsMap, EmitEvents extends EventsMap = ListenEvents > extends StrictEventEmitter { public readonly io: Manager public id: string public connected: boolean public disconnected: boolean public auth: { [key: string]: any } | ((cb: (data: object) => void) => void) public receiveBuffer: Array> = []; public sendBuffer: Array = []; private readonly nsp: string private ids: number = 0; private acks: object = {}; private flags: Flags = {}; private subs?: Array private _anyListeners: Array<(...args: any[]) => void> /** * `Socket` constructor. * * @public */ constructor(io: Manager, nsp: string, opts?: Partial) { super() this.io = io this.nsp = nsp this.ids = 0 this.acks = {} this.receiveBuffer = [] this.sendBuffer = [] this.connected = false this.disconnected = true this.flags = {} if (opts && opts.auth) { this.auth = opts.auth } if (this.io._autoConnect) this.open() } /** * Subscribe to open, close and packet events * * @private */ private subEvents(): void { if (this.subs) return const io = this.io this.subs = [ on(io, "open", this.onopen.bind(this)), on(io, "packet", this.onpacket.bind(this)), on(io, "error", this.onerror.bind(this)), on(io, "close", this.onclose.bind(this)), ] } /** * Whether the Socket will try to reconnect when its Manager connects or reconnects */ public get active(): boolean { return !!this.subs } /** * "Opens" the socket. * * @public */ public connect(): this { if (this.connected) return this this.subEvents() if (!this.io["_reconnecting"]) this.io.open() // ensure open if ("open" === this.io._readyState) this.onopen() return this } /** * Alias for connect() */ public open(): this { return this.connect() } /** * Sends a `message` event. * * @return self * @public */ public send(...args: any[]): this { args.unshift("message") // @ts-ignore this.emit.apply(this, args) return this } /** * Override `emit`. * If the event is in `events`, it's emitted normally. * * @return self * @public */ public emit>( ev: Ev, ...args: EventParams ): this { if (RESERVED_EVENTS.hasOwnProperty(ev)) { throw new Error('"' + ev + '" is a reserved event name') } args.unshift(ev) const packet: any = { type: PacketType.EVENT, data: args, } packet.options = {} packet.options.compress = this.flags.compress !== false // event ack callback if ("function" === typeof args[args.length - 1]) { debug("emitting packet with ack id %d", this.ids) this.acks[this.ids] = args.pop() packet.id = this.ids++ } const isTransportWritable = this.io.engine && this.io.engine.transport && this.io.engine.transport.writable const discardPacket = this.flags.volatile && (!isTransportWritable || !this.connected) if (discardPacket) { debug("discard packet as the transport is not currently writable") } else if (this.connected) { this.packet(packet) } else { this.sendBuffer.push(packet) } this.flags = {} return this } /** * Sends a packet. * * @param packet * @private */ private packet(packet: Partial): void { packet.nsp = this.nsp this.io._packet(packet) } /** * Called upon engine `open`. * * @private */ private onopen(): void { debug("transport is open - connecting") if (typeof this.auth == "function") { this.auth((data) => { this.packet({ type: PacketType.CONNECT, data }) }) } else { this.packet({ type: PacketType.CONNECT, data: this.auth }) } } /** * Called upon engine or manager `error`. * * @param err * @private */ private onerror(err: Error): void { if (!this.connected) { this.emitReserved("connect_error", err) } } /** * Called upon engine `close`. * * @param reason * @private */ private onclose(reason: Socket.DisconnectReason): void { debug("close (%s)", reason) this.connected = false this.disconnected = true delete this.id this.emitReserved("disconnect", reason) } /** * Called with socket packet. * * @param packet * @private */ private onpacket(packet: Packet): void { const sameNamespace = packet.nsp === this.nsp if (!sameNamespace) return switch (packet.type) { case PacketType.CONNECT: if (packet.data && packet.data.sid) { const id = packet.data.sid this.onconnect(id) } else { this.emitReserved( "connect_error", new Error( "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/)" ) ) } break case PacketType.EVENT: this.onevent(packet) break case PacketType.BINARY_EVENT: this.onevent(packet) break case PacketType.ACK: this.onack(packet) break case PacketType.BINARY_ACK: this.onack(packet) break case PacketType.DISCONNECT: this.ondisconnect() break case PacketType.CONNECT_ERROR: const err = new Error(packet.data.message) // @ts-ignore err.data = packet.data.data this.emitReserved("connect_error", err) break } } /** * Called upon a server event. * * @param packet * @private */ private onevent(packet: Packet): void { const args: Array = packet.data || [] debug("emitting event %j", args) if (null != packet.id) { debug("attaching ack callback to event") args.push(this.ack(packet.id)) } if (this.connected) { this.emitEvent(args) } else { this.receiveBuffer.push(Object.freeze(args)) } } private emitEvent(args: ReadonlyArray): void { if (this._anyListeners && this._anyListeners.length) { const listeners = this._anyListeners.slice() for (const listener of listeners) { // @ts-ignore listener.apply(this, args) } } // @ts-ignore super.emit.apply(this, args) } /** * Produces an ack callback to emit with an event. * * @private */ private ack(id: number): (...args: any[]) => void { const self = this let sent = false return function (...args: any[]) { // prevent double callbacks if (sent) return sent = true debug("sending ack %j", args) self.packet({ type: PacketType.ACK, id: id, data: args, }) } } /** * Called upon a server acknowlegement. * * @param packet * @private */ private onack(packet: Packet): void { const ack = this.acks[packet.id] if ("function" === typeof ack) { debug("calling ack %s with %j", packet.id, packet.data) ack.apply(this, packet.data) delete this.acks[packet.id] } else { debug("bad ack %s", packet.id) } } /** * Called upon server connect. * * @private */ private onconnect(id: string): void { debug("socket connected with id %s", id) this.id = id this.connected = true this.disconnected = false this.emitBuffered() this.emitReserved("connect") } /** * Emit buffered events (received and emitted). * * @private */ private emitBuffered(): void { this.receiveBuffer.forEach((args) => this.emitEvent(args)) this.receiveBuffer = [] this.sendBuffer.forEach((packet) => this.packet(packet)) this.sendBuffer = [] } /** * Called upon server disconnect. * * @private */ private ondisconnect(): void { debug("server disconnect (%s)", this.nsp) this.destroy() this.onclose("io server disconnect") } /** * Called upon forced client/server side disconnections, * this method ensures the manager stops tracking us and * that reconnections don't get triggered for this. * * @private */ private destroy(): void { if (this.subs) { // clean subscriptions to avoid reconnections this.subs.forEach((subDestroy) => subDestroy()) this.subs = undefined } this.io["_destroy"](this) } /** * Disconnects the socket manually. * * @return self * @public */ public disconnect(): this { if (this.connected) { debug("performing disconnect (%s)", this.nsp) this.packet({ type: PacketType.DISCONNECT }) } // remove socket from pool this.destroy() if (this.connected) { // fire events this.onclose("io client disconnect") } return this } /** * Alias for disconnect() * * @return self * @public */ public close(): this { return this.disconnect() } /** * Sets the compress flag. * * @param compress - if `true`, compresses the sending data * @return self * @public */ public compress(compress: boolean): this { this.flags.compress = compress return this } /** * Sets a modifier for a subsequent event emission that the event message will be dropped when this socket is not * ready to send messages. * * @returns self * @public */ public get volatile(): this { this.flags.volatile = true return this } /** * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the * callback. * * @param listener * @public */ public onAny(listener: (...args: any[]) => void): this { this._anyListeners = this._anyListeners || [] this._anyListeners.push(listener) return this } /** * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the * callback. The listener is added to the beginning of the listeners array. * * @param listener * @public */ public prependAny(listener: (...args: any[]) => void): this { this._anyListeners = this._anyListeners || [] this._anyListeners.unshift(listener) return this } /** * Removes the listener that will be fired when any event is emitted. * * @param listener * @public */ public offAny(listener?: (...args: any[]) => void): this { if (!this._anyListeners) { return this } if (listener) { const listeners = this._anyListeners for (let i = 0; i < listeners.length; i++) { if (listener === listeners[i]) { listeners.splice(i, 1) return this } } } else { this._anyListeners = [] } return this } /** * Returns an array of listeners that are listening for any event that is specified. This array can be manipulated, * e.g. to remove listeners. * * @public */ public listenersAny() { return this._anyListeners || [] } } export namespace Socket { export type DisconnectReason = | "io server disconnect" | "io client disconnect" | "ping timeout" | "transport close" | "transport error" }