You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
719 lines
20 KiB
719 lines
20 KiB
import { PacketType } from "socket.io-parser"; |
|
import { on } from "./on.js"; |
|
import { Emitter, } from "@socket.io/component-emitter"; |
|
import debugModule from "debug"; // debug() |
|
const debug = debugModule("socket.io-client:socket"); // debug() |
|
/** |
|
* 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, |
|
}); |
|
/** |
|
* 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 extends Emitter { |
|
/** |
|
* `Socket` constructor. |
|
*/ |
|
constructor(io, nsp, opts) { |
|
super(); |
|
/** |
|
* 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 |
|
* }); |
|
*/ |
|
this.connected = false; |
|
/** |
|
* Buffer for packets received before the CONNECT packet |
|
*/ |
|
this.receiveBuffer = []; |
|
/** |
|
* Buffer for packets that will be sent once the socket is connected |
|
*/ |
|
this.sendBuffer = []; |
|
this.ids = 0; |
|
this.acks = {}; |
|
this.flags = {}; |
|
this.io = io; |
|
this.nsp = nsp; |
|
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 |
|
* }); |
|
*/ |
|
get disconnected() { |
|
return !this.connected; |
|
} |
|
/** |
|
* Subscribe to open, close and packet events |
|
* |
|
* @private |
|
*/ |
|
subEvents() { |
|
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. |
|
* |
|
* @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 |
|
* }); |
|
*/ |
|
get active() { |
|
return !!this.subs; |
|
} |
|
/** |
|
* "Opens" the socket. |
|
* |
|
* @example |
|
* const socket = io({ |
|
* autoConnect: false |
|
* }); |
|
* |
|
* socket.connect(); |
|
*/ |
|
connect() { |
|
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 {@link connect()}. |
|
*/ |
|
open() { |
|
return this.connect(); |
|
} |
|
/** |
|
* 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 |
|
*/ |
|
send(...args) { |
|
args.unshift("message"); |
|
this.emit.apply(this, args); |
|
return this; |
|
} |
|
/** |
|
* 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 |
|
*/ |
|
emit(ev, ...args) { |
|
if (RESERVED_EVENTS.hasOwnProperty(ev)) { |
|
throw new Error('"' + ev.toString() + '" is a reserved event name'); |
|
} |
|
args.unshift(ev); |
|
const packet = { |
|
type: PacketType.EVENT, |
|
data: args, |
|
}; |
|
packet.options = {}; |
|
packet.options.compress = this.flags.compress !== false; |
|
// event ack callback |
|
if ("function" === typeof args[args.length - 1]) { |
|
const id = this.ids++; |
|
debug("emitting packet with ack id %d", id); |
|
const ack = args.pop(); |
|
this._registerAckCallback(id, ack); |
|
packet.id = id; |
|
} |
|
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.notifyOutgoingListeners(packet); |
|
this.packet(packet); |
|
} |
|
else { |
|
this.sendBuffer.push(packet); |
|
} |
|
this.flags = {}; |
|
return this; |
|
} |
|
/** |
|
* @private |
|
*/ |
|
_registerAckCallback(id, ack) { |
|
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. |
|
* |
|
* @param packet |
|
* @private |
|
*/ |
|
packet(packet) { |
|
packet.nsp = this.nsp; |
|
this.io._packet(packet); |
|
} |
|
/** |
|
* Called upon engine `open`. |
|
* |
|
* @private |
|
*/ |
|
onopen() { |
|
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 |
|
*/ |
|
onerror(err) { |
|
if (!this.connected) { |
|
this.emitReserved("connect_error", err); |
|
} |
|
} |
|
/** |
|
* Called upon engine `close`. |
|
* |
|
* @param reason |
|
* @param description |
|
* @private |
|
*/ |
|
onclose(reason, description) { |
|
debug("close (%s)", reason); |
|
this.connected = false; |
|
delete this.id; |
|
this.emitReserved("disconnect", reason, description); |
|
} |
|
/** |
|
* Called with socket packet. |
|
* |
|
* @param packet |
|
* @private |
|
*/ |
|
onpacket(packet) { |
|
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: |
|
case PacketType.BINARY_EVENT: |
|
this.onevent(packet); |
|
break; |
|
case PacketType.ACK: |
|
case PacketType.BINARY_ACK: |
|
this.onack(packet); |
|
break; |
|
case PacketType.DISCONNECT: |
|
this.ondisconnect(); |
|
break; |
|
case PacketType.CONNECT_ERROR: |
|
this.destroy(); |
|
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 |
|
*/ |
|
onevent(packet) { |
|
const args = 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)); |
|
} |
|
} |
|
emitEvent(args) { |
|
if (this._anyListeners && this._anyListeners.length) { |
|
const listeners = this._anyListeners.slice(); |
|
for (const listener of listeners) { |
|
listener.apply(this, args); |
|
} |
|
} |
|
super.emit.apply(this, args); |
|
} |
|
/** |
|
* Produces an ack callback to emit with an event. |
|
* |
|
* @private |
|
*/ |
|
ack(id) { |
|
const self = this; |
|
let sent = false; |
|
return function (...args) { |
|
// 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 |
|
*/ |
|
onack(packet) { |
|
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 |
|
*/ |
|
onconnect(id) { |
|
debug("socket connected with id %s", id); |
|
this.id = id; |
|
this.connected = true; |
|
this.emitBuffered(); |
|
this.emitReserved("connect"); |
|
} |
|
/** |
|
* Emit buffered events (received and emitted). |
|
* |
|
* @private |
|
*/ |
|
emitBuffered() { |
|
this.receiveBuffer.forEach((args) => this.emitEvent(args)); |
|
this.receiveBuffer = []; |
|
this.sendBuffer.forEach((packet) => { |
|
this.notifyOutgoingListeners(packet); |
|
this.packet(packet); |
|
}); |
|
this.sendBuffer = []; |
|
} |
|
/** |
|
* Called upon server disconnect. |
|
* |
|
* @private |
|
*/ |
|
ondisconnect() { |
|
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 |
|
*/ |
|
destroy() { |
|
if (this.subs) { |
|
// clean subscriptions to avoid reconnections |
|
this.subs.forEach((subDestroy) => subDestroy()); |
|
this.subs = undefined; |
|
} |
|
this.io["_destroy"](this); |
|
} |
|
/** |
|
* 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 |
|
*/ |
|
disconnect() { |
|
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 {@link disconnect()}. |
|
* |
|
* @return self |
|
*/ |
|
close() { |
|
return this.disconnect(); |
|
} |
|
/** |
|
* Sets the compress flag. |
|
* |
|
* @example |
|
* socket.compress(false).emit("hello"); |
|
* |
|
* @param compress - if `true`, compresses the sending data |
|
* @return self |
|
*/ |
|
compress(compress) { |
|
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. |
|
* |
|
* @example |
|
* socket.volatile.emit("hello"); // the server may or may not receive it |
|
* |
|
* @returns self |
|
*/ |
|
get volatile() { |
|
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 |
|
*/ |
|
timeout(timeout) { |
|
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 |
|
*/ |
|
onAny(listener) { |
|
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. |
|
* |
|
* @example |
|
* socket.prependAny((event, ...args) => { |
|
* console.log(`got event ${event}`); |
|
* }); |
|
* |
|
* @param listener |
|
*/ |
|
prependAny(listener) { |
|
this._anyListeners = this._anyListeners || []; |
|
this._anyListeners.unshift(listener); |
|
return this; |
|
} |
|
/** |
|
* 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 |
|
*/ |
|
offAny(listener) { |
|
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. |
|
*/ |
|
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 |
|
*/ |
|
onAnyOutgoing(listener) { |
|
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 |
|
*/ |
|
prependAnyOutgoing(listener) { |
|
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) |
|
*/ |
|
offAnyOutgoing(listener) { |
|
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. |
|
*/ |
|
listenersAnyOutgoing() { |
|
return this._anyOutgoingListeners || []; |
|
} |
|
/** |
|
* Notify the listeners for each packet sent |
|
* |
|
* @param packet |
|
* |
|
* @private |
|
*/ |
|
notifyOutgoingListeners(packet) { |
|
if (this._anyOutgoingListeners && this._anyOutgoingListeners.length) { |
|
const listeners = this._anyOutgoingListeners.slice(); |
|
for (const listener of listeners) { |
|
listener.apply(this, packet.data); |
|
} |
|
} |
|
} |
|
}
|
|
|