@@ -1,14 +1,17 @@
|
||||
// import { Packet, PacketType } from "socket.io-parser"
|
||||
import { Packet, PacketType } from "../socket.io-parser"
|
||||
import { on } from "./on"
|
||||
import { Manager } from "./manager"
|
||||
import { on } from "./on.js"
|
||||
import { Manager } from "./manager.js"
|
||||
import {
|
||||
DefaultEventsMap,
|
||||
EventNames,
|
||||
EventParams,
|
||||
EventsMap,
|
||||
StrictEventEmitter,
|
||||
} from "./typed-events"
|
||||
Emitter,
|
||||
} from "@socket.io/component-emitter"
|
||||
// import debugModule from "debug" // debug()
|
||||
|
||||
// const debug = debugModule("socket.io-client:socket") // debug()
|
||||
const debug = require("../debug")("socket.io-client")
|
||||
|
||||
export interface SocketOptions {
|
||||
@@ -35,26 +38,110 @@ const RESERVED_EVENTS = Object.freeze({
|
||||
interface Flags {
|
||||
compress?: boolean
|
||||
volatile?: boolean
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export type DisconnectDescription =
|
||||
| Error
|
||||
| {
|
||||
description: string
|
||||
context?: CloseEvent | XMLHttpRequest
|
||||
}
|
||||
|
||||
interface SocketReservedEvents {
|
||||
connect: () => void
|
||||
connect_error: (err: Error) => void
|
||||
disconnect: (reason: Socket.DisconnectReason) => void
|
||||
disconnect: (
|
||||
reason: Socket.DisconnectReason,
|
||||
description?: DisconnectDescription
|
||||
) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* A Socket is the fundamental class for interacting with the server.
|
||||
*
|
||||
* A Socket belongs to a certain Namespace (by default /) and uses an underlying {@link Manager} to communicate.
|
||||
*
|
||||
* @example
|
||||
* const socket = io();
|
||||
*
|
||||
* socket.on("connect", () => {
|
||||
* console.log("connected");
|
||||
* });
|
||||
*
|
||||
* // send an event to the server
|
||||
* socket.emit("foo", "bar");
|
||||
*
|
||||
* socket.on("foobar", () => {
|
||||
* // an event was received from the server
|
||||
* });
|
||||
*
|
||||
* // upon disconnection
|
||||
* socket.on("disconnect", (reason) => {
|
||||
* console.log(`disconnected due to ${reason}`);
|
||||
* });
|
||||
*/
|
||||
export class Socket<
|
||||
ListenEvents extends EventsMap = DefaultEventsMap,
|
||||
EmitEvents extends EventsMap = ListenEvents
|
||||
> extends StrictEventEmitter<ListenEvents, EmitEvents, SocketReservedEvents> {
|
||||
> extends Emitter<ListenEvents, EmitEvents, SocketReservedEvents> {
|
||||
public readonly io: Manager<ListenEvents, EmitEvents>
|
||||
|
||||
/**
|
||||
* A unique identifier for the session.
|
||||
*
|
||||
* @example
|
||||
* const socket = io();
|
||||
*
|
||||
* console.log(socket.id); // undefined
|
||||
*
|
||||
* socket.on("connect", () => {
|
||||
* console.log(socket.id); // "G5p5..."
|
||||
* });
|
||||
*/
|
||||
public id: string
|
||||
public connected: boolean
|
||||
public disconnected: boolean
|
||||
|
||||
/**
|
||||
* Whether the socket is currently connected to the server.
|
||||
*
|
||||
* @example
|
||||
* const socket = io();
|
||||
*
|
||||
* socket.on("connect", () => {
|
||||
* console.log(socket.connected); // true
|
||||
* });
|
||||
*
|
||||
* socket.on("disconnect", () => {
|
||||
* console.log(socket.connected); // false
|
||||
* });
|
||||
*/
|
||||
public connected: boolean = false;
|
||||
|
||||
/**
|
||||
* Credentials that are sent when accessing a namespace.
|
||||
*
|
||||
* @example
|
||||
* const socket = io({
|
||||
* auth: {
|
||||
* token: "abcd"
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // or with a function
|
||||
* const socket = io({
|
||||
* auth: (cb) => {
|
||||
* cb({ token: localStorage.token })
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
public auth: { [key: string]: any } | ((cb: (data: object) => void) => void)
|
||||
/**
|
||||
* Buffer for packets received before the CONNECT packet
|
||||
*/
|
||||
public receiveBuffer: Array<ReadonlyArray<any>> = [];
|
||||
/**
|
||||
* Buffer for packets that will be sent once the socket is connected
|
||||
*/
|
||||
public sendBuffer: Array<Packet> = [];
|
||||
|
||||
private readonly nsp: string
|
||||
@@ -64,29 +151,39 @@ export class Socket<
|
||||
private flags: Flags = {};
|
||||
private subs?: Array<VoidFunction>
|
||||
private _anyListeners: Array<(...args: any[]) => void>
|
||||
private _anyOutgoingListeners: Array<(...args: any[]) => void>
|
||||
|
||||
/**
|
||||
* `Socket` constructor.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
constructor(io: Manager, nsp: string, opts?: Partial<SocketOptions>) {
|
||||
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()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the socket is currently disconnected
|
||||
*
|
||||
* @example
|
||||
* const socket = io();
|
||||
*
|
||||
* socket.on("connect", () => {
|
||||
* console.log(socket.disconnected); // false
|
||||
* });
|
||||
*
|
||||
* socket.on("disconnect", () => {
|
||||
* console.log(socket.disconnected); // true
|
||||
* });
|
||||
*/
|
||||
public get disconnected(): boolean {
|
||||
return !this.connected
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to open, close and packet events
|
||||
*
|
||||
@@ -105,7 +202,21 @@ export class Socket<
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the Socket will try to reconnect when its Manager connects or reconnects
|
||||
* Whether the Socket will try to reconnect when its Manager connects or reconnects.
|
||||
*
|
||||
* @example
|
||||
* const socket = io();
|
||||
*
|
||||
* console.log(socket.active); // true
|
||||
*
|
||||
* socket.on("disconnect", (reason) => {
|
||||
* if (reason === "io server disconnect") {
|
||||
* // the disconnection was initiated by the server, you need to manually reconnect
|
||||
* console.log(socket.active); // false
|
||||
* }
|
||||
* // else the socket will automatically try to reconnect
|
||||
* console.log(socket.active); // true
|
||||
* });
|
||||
*/
|
||||
public get active(): boolean {
|
||||
return !!this.subs
|
||||
@@ -114,7 +225,12 @@ export class Socket<
|
||||
/**
|
||||
* "Opens" the socket.
|
||||
*
|
||||
* @public
|
||||
* @example
|
||||
* const socket = io({
|
||||
* autoConnect: false
|
||||
* });
|
||||
*
|
||||
* socket.connect();
|
||||
*/
|
||||
public connect(): this {
|
||||
if (this.connected) return this
|
||||
@@ -126,7 +242,7 @@ export class Socket<
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for connect()
|
||||
* Alias for {@link connect()}.
|
||||
*/
|
||||
public open(): this {
|
||||
return this.connect()
|
||||
@@ -135,8 +251,17 @@ export class Socket<
|
||||
/**
|
||||
* Sends a `message` event.
|
||||
*
|
||||
* This method mimics the WebSocket.send() method.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
|
||||
*
|
||||
* @example
|
||||
* socket.send("hello");
|
||||
*
|
||||
* // this is equivalent to
|
||||
* socket.emit("message", "hello");
|
||||
*
|
||||
* @return self
|
||||
* @public
|
||||
*/
|
||||
public send(...args: any[]): this {
|
||||
args.unshift("message")
|
||||
@@ -149,15 +274,25 @@ export class Socket<
|
||||
* Override `emit`.
|
||||
* If the event is in `events`, it's emitted normally.
|
||||
*
|
||||
* @example
|
||||
* socket.emit("hello", "world");
|
||||
*
|
||||
* // all serializable datastructures are supported (no need to call JSON.stringify)
|
||||
* socket.emit("hello", 1, "2", { 3: ["4"], 5: Uint8Array.from([6]) });
|
||||
*
|
||||
* // with an acknowledgement from the server
|
||||
* socket.emit("hello", "world", (val) => {
|
||||
* // ...
|
||||
* });
|
||||
*
|
||||
* @return self
|
||||
* @public
|
||||
*/
|
||||
public emit<Ev extends EventNames<EmitEvents>>(
|
||||
ev: Ev,
|
||||
...args: EventParams<EmitEvents, Ev>
|
||||
): this {
|
||||
if (RESERVED_EVENTS.hasOwnProperty(ev)) {
|
||||
throw new Error('"' + ev + '" is a reserved event name')
|
||||
throw new Error('"' + ev.toString() + '" is a reserved event name')
|
||||
}
|
||||
|
||||
args.unshift(ev)
|
||||
@@ -171,9 +306,12 @@ export class Socket<
|
||||
|
||||
// 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 id = this.ids++
|
||||
debug("emitting packet with ack id %d", id)
|
||||
|
||||
const ack = args.pop() as Function
|
||||
this._registerAckCallback(id, ack)
|
||||
packet.id = id
|
||||
}
|
||||
|
||||
const isTransportWritable =
|
||||
@@ -186,6 +324,7 @@ export class Socket<
|
||||
if (discardPacket) {
|
||||
debug("discard packet as the transport is not currently writable")
|
||||
} else if (this.connected) {
|
||||
this.notifyOutgoingListeners(packet)
|
||||
this.packet(packet)
|
||||
} else {
|
||||
this.sendBuffer.push(packet)
|
||||
@@ -196,6 +335,36 @@ export class Socket<
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
private _registerAckCallback(id: number, ack: Function) {
|
||||
const timeout = this.flags.timeout
|
||||
if (timeout === undefined) {
|
||||
this.acks[id] = ack
|
||||
return
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const timer = this.io.setTimeoutFn(() => {
|
||||
delete this.acks[id]
|
||||
for (let i = 0; i < this.sendBuffer.length; i++) {
|
||||
if (this.sendBuffer[i].id === id) {
|
||||
debug("removing packet with ack id %d from the buffer", id)
|
||||
this.sendBuffer.splice(i, 1)
|
||||
}
|
||||
}
|
||||
debug("event with ack id %d has timed out after %d ms", id, timeout)
|
||||
ack.call(this, new Error("operation has timed out"))
|
||||
}, timeout)
|
||||
|
||||
this.acks[id] = (...args) => {
|
||||
// @ts-ignore
|
||||
this.io.clearTimeoutFn(timer)
|
||||
ack.apply(this, [null, ...args])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a packet.
|
||||
*
|
||||
@@ -239,14 +408,17 @@ export class Socket<
|
||||
* Called upon engine `close`.
|
||||
*
|
||||
* @param reason
|
||||
* @param description
|
||||
* @private
|
||||
*/
|
||||
private onclose(reason: Socket.DisconnectReason): void {
|
||||
private onclose(
|
||||
reason: Socket.DisconnectReason,
|
||||
description?: DisconnectDescription
|
||||
): void {
|
||||
debug("close (%s)", reason)
|
||||
this.connected = false
|
||||
this.disconnected = true
|
||||
delete this.id
|
||||
this.emitReserved("disconnect", reason)
|
||||
this.emitReserved("disconnect", reason, description)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -276,17 +448,11 @@ export class Socket<
|
||||
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
|
||||
@@ -296,6 +462,7 @@ export class Socket<
|
||||
break
|
||||
|
||||
case PacketType.CONNECT_ERROR:
|
||||
this.destroy()
|
||||
const err = new Error(packet.data.message)
|
||||
// @ts-ignore
|
||||
err.data = packet.data.data
|
||||
@@ -386,7 +553,6 @@ export class Socket<
|
||||
debug("socket connected with id %s", id)
|
||||
this.id = id
|
||||
this.connected = true
|
||||
this.disconnected = false
|
||||
this.emitBuffered()
|
||||
this.emitReserved("connect")
|
||||
}
|
||||
@@ -400,7 +566,10 @@ export class Socket<
|
||||
this.receiveBuffer.forEach((args) => this.emitEvent(args))
|
||||
this.receiveBuffer = []
|
||||
|
||||
this.sendBuffer.forEach((packet) => this.packet(packet))
|
||||
this.sendBuffer.forEach((packet) => {
|
||||
this.notifyOutgoingListeners(packet)
|
||||
this.packet(packet)
|
||||
})
|
||||
this.sendBuffer = []
|
||||
}
|
||||
|
||||
@@ -432,10 +601,20 @@ export class Socket<
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the socket manually.
|
||||
* Disconnects the socket manually. In that case, the socket will not try to reconnect.
|
||||
*
|
||||
* If this is the last active Socket instance of the {@link Manager}, the low-level connection will be closed.
|
||||
*
|
||||
* @example
|
||||
* const socket = io();
|
||||
*
|
||||
* socket.on("disconnect", (reason) => {
|
||||
* // console.log(reason); prints "io client disconnect"
|
||||
* });
|
||||
*
|
||||
* socket.disconnect();
|
||||
*
|
||||
* @return self
|
||||
* @public
|
||||
*/
|
||||
public disconnect(): this {
|
||||
if (this.connected) {
|
||||
@@ -454,10 +633,9 @@ export class Socket<
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for disconnect()
|
||||
* Alias for {@link disconnect()}.
|
||||
*
|
||||
* @return self
|
||||
* @public
|
||||
*/
|
||||
public close(): this {
|
||||
return this.disconnect()
|
||||
@@ -466,9 +644,11 @@ export class Socket<
|
||||
/**
|
||||
* Sets the compress flag.
|
||||
*
|
||||
* @example
|
||||
* socket.compress(false).emit("hello");
|
||||
*
|
||||
* @param compress - if `true`, compresses the sending data
|
||||
* @return self
|
||||
* @public
|
||||
*/
|
||||
public compress(compress: boolean): this {
|
||||
this.flags.compress = compress
|
||||
@@ -479,20 +659,44 @@ export class Socket<
|
||||
* Sets a modifier for a subsequent event emission that the event message will be dropped when this socket is not
|
||||
* ready to send messages.
|
||||
*
|
||||
* @example
|
||||
* socket.volatile.emit("hello"); // the server may or may not receive it
|
||||
*
|
||||
* @returns self
|
||||
* @public
|
||||
*/
|
||||
public get volatile(): this {
|
||||
this.flags.volatile = true
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a modifier for a subsequent event emission that the callback will be called with an error when the
|
||||
* given number of milliseconds have elapsed without an acknowledgement from the server:
|
||||
*
|
||||
* @example
|
||||
* socket.timeout(5000).emit("my-event", (err) => {
|
||||
* if (err) {
|
||||
* // the server did not acknowledge the event in the given delay
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* @returns self
|
||||
*/
|
||||
public timeout(timeout: number): this {
|
||||
this.flags.timeout = timeout
|
||||
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.
|
||||
*
|
||||
* @example
|
||||
* socket.onAny((event, ...args) => {
|
||||
* console.log(`got ${event}`);
|
||||
* });
|
||||
*
|
||||
* @param listener
|
||||
* @public
|
||||
*/
|
||||
public onAny(listener: (...args: any[]) => void): this {
|
||||
this._anyListeners = this._anyListeners || []
|
||||
@@ -504,8 +708,12 @@ export class Socket<
|
||||
* 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.
|
||||
*
|
||||
* @example
|
||||
* socket.prependAny((event, ...args) => {
|
||||
* console.log(`got event ${event}`);
|
||||
* });
|
||||
*
|
||||
* @param listener
|
||||
* @public
|
||||
*/
|
||||
public prependAny(listener: (...args: any[]) => void): this {
|
||||
this._anyListeners = this._anyListeners || []
|
||||
@@ -516,8 +724,20 @@ export class Socket<
|
||||
/**
|
||||
* Removes the listener that will be fired when any event is emitted.
|
||||
*
|
||||
* @example
|
||||
* const catchAllListener = (event, ...args) => {
|
||||
* console.log(`got event ${event}`);
|
||||
* }
|
||||
*
|
||||
* socket.onAny(catchAllListener);
|
||||
*
|
||||
* // remove a specific listener
|
||||
* socket.offAny(catchAllListener);
|
||||
*
|
||||
* // or remove all listeners
|
||||
* socket.offAny();
|
||||
*
|
||||
* @param listener
|
||||
* @public
|
||||
*/
|
||||
public offAny(listener?: (...args: any[]) => void): this {
|
||||
if (!this._anyListeners) {
|
||||
@@ -540,12 +760,108 @@ export class Socket<
|
||||
/**
|
||||
* 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 || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
|
||||
* callback.
|
||||
*
|
||||
* Note: acknowledgements sent to the server are not included.
|
||||
*
|
||||
* @example
|
||||
* socket.onAnyOutgoing((event, ...args) => {
|
||||
* console.log(`sent event ${event}`);
|
||||
* });
|
||||
*
|
||||
* @param listener
|
||||
*/
|
||||
public onAnyOutgoing(listener: (...args: any[]) => void): this {
|
||||
this._anyOutgoingListeners = this._anyOutgoingListeners || []
|
||||
this._anyOutgoingListeners.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.
|
||||
*
|
||||
* Note: acknowledgements sent to the server are not included.
|
||||
*
|
||||
* @example
|
||||
* socket.prependAnyOutgoing((event, ...args) => {
|
||||
* console.log(`sent event ${event}`);
|
||||
* });
|
||||
*
|
||||
* @param listener
|
||||
*/
|
||||
public prependAnyOutgoing(listener: (...args: any[]) => void): this {
|
||||
this._anyOutgoingListeners = this._anyOutgoingListeners || []
|
||||
this._anyOutgoingListeners.unshift(listener)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener that will be fired when any event is emitted.
|
||||
*
|
||||
* @example
|
||||
* const catchAllListener = (event, ...args) => {
|
||||
* console.log(`sent event ${event}`);
|
||||
* }
|
||||
*
|
||||
* socket.onAnyOutgoing(catchAllListener);
|
||||
*
|
||||
* // remove a specific listener
|
||||
* socket.offAnyOutgoing(catchAllListener);
|
||||
*
|
||||
* // or remove all listeners
|
||||
* socket.offAnyOutgoing();
|
||||
*
|
||||
* @param [listener] - the catch-all listener (optional)
|
||||
*/
|
||||
public offAnyOutgoing(listener?: (...args: any[]) => void): this {
|
||||
if (!this._anyOutgoingListeners) {
|
||||
return this
|
||||
}
|
||||
if (listener) {
|
||||
const listeners = this._anyOutgoingListeners
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
if (listener === listeners[i]) {
|
||||
listeners.splice(i, 1)
|
||||
return this
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._anyOutgoingListeners = []
|
||||
}
|
||||
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 listenersAnyOutgoing() {
|
||||
return this._anyOutgoingListeners || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the listeners for each packet sent
|
||||
*
|
||||
* @param packet
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private notifyOutgoingListeners(packet: Packet) {
|
||||
if (this._anyOutgoingListeners && this._anyOutgoingListeners.length) {
|
||||
const listeners = this._anyOutgoingListeners.slice()
|
||||
for (const listener of listeners) {
|
||||
listener.apply(this, packet.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Socket {
|
||||
@@ -555,4 +871,5 @@ export namespace Socket {
|
||||
| "ping timeout"
|
||||
| "transport close"
|
||||
| "transport error"
|
||||
| "parse error"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user