feat: 完善client相关功能 重构server部分

Signed-off-by: MiaoWoo <admin@yumc.pw>
This commit is contained in:
2021-08-14 12:43:20 +08:00
parent 774763be13
commit 53502e12cf
34 changed files with 2893 additions and 20 deletions

View File

@ -0,0 +1,105 @@
import { url } from "./url"
import { Manager, ManagerOptions } from "./manager"
import { Socket, SocketOptions } from "./socket"
const debug = require("../debug")("socket.io-client")
/**
* Module exports.
*/
module.exports = exports = lookup
/**
* Managers cache.
*/
const cache: Record<string, Manager> = (exports.managers = {})
/**
* Looks up an existing `Manager` for multiplexing.
* If the user summons:
*
* `io('http://localhost/a');`
* `io('http://localhost/b');`
*
* We reuse the existing instance based on same scheme/port/host,
* and we initialize sockets for each namespace.
*
* @public
*/
function lookup(opts?: Partial<ManagerOptions & SocketOptions>): Socket
function lookup(
uri: string,
opts?: Partial<ManagerOptions & SocketOptions>
): Socket
function lookup(
uri: string | Partial<ManagerOptions & SocketOptions>,
opts?: Partial<ManagerOptions & SocketOptions>
): Socket
function lookup(
uri: string | Partial<ManagerOptions & SocketOptions>,
opts?: Partial<ManagerOptions & SocketOptions>
): Socket {
if (typeof uri === "object") {
opts = uri
uri = undefined
}
opts = opts || {}
const parsed = url(uri as string, opts.path || "/socket.io")
const source = parsed.source
const id = parsed.id
const path = parsed.path
const sameNamespace = cache[id] && path in cache[id]["nsps"]
const newConnection =
opts.forceNew ||
opts["force new connection"] ||
false === opts.multiplex ||
sameNamespace
let io: Manager
if (newConnection) {
debug("ignoring socket cache for %s", source)
io = new Manager(source, opts)
} else {
if (!cache[id]) {
debug("new io instance for %s", source)
cache[id] = new Manager(source, opts)
}
io = cache[id]
}
if (parsed.query && !opts.query) {
opts.query = parsed.queryKey
}
return io.socket(parsed.path, opts)
}
/**
* Protocol version.
*
* @public
*/
export { protocol } from "../socket.io-parser"
/**
* `connect`.
*
* @param {String} uri
* @public
*/
exports.connect = lookup
/**
* Expose constructors for standalone build.
*
* @public
*/
export { Manager, ManagerOptions } from "./manager"
export { Socket } from "./socket"
export { lookup as io, SocketOptions }
export default lookup

View File

@ -0,0 +1,816 @@
import eio from "../engine.io-client"
import { Socket, SocketOptions } from "./socket"
import * as parser from "../socket.io-parser"
import { Decoder, Encoder, Packet } from "../socket.io-parser"
import { on } from "./on"
import * as Backoff from "backo2"
import {
DefaultEventsMap,
EventsMap,
StrictEventEmitter,
} from "./typed-events"
const debug = require("../debug")("socket.io-client")
interface EngineOptions {
/**
* The host that we're connecting to. Set from the URI passed when connecting
*/
host: string
/**
* The hostname for our connection. Set from the URI passed when connecting
*/
hostname: string
/**
* If this is a secure connection. Set from the URI passed when connecting
*/
secure: boolean
/**
* The port for our connection. Set from the URI passed when connecting
*/
port: string
/**
* Any query parameters in our uri. Set from the URI passed when connecting
*/
query: { [key: string]: string }
/**
* `http.Agent` to use, defaults to `false` (NodeJS only)
*/
agent: string | boolean
/**
* Whether the client should try to upgrade the transport from
* long-polling to something better.
* @default true
*/
upgrade: boolean
/**
* Forces JSONP for polling transport.
*/
forceJSONP: boolean
/**
* Determines whether to use JSONP when necessary for polling. If
* disabled (by settings to false) an error will be emitted (saying
* "No transports available") if no other transports are available.
* If another transport is available for opening a connection (e.g.
* WebSocket) that transport will be used instead.
* @default true
*/
jsonp: boolean
/**
* Forces base 64 encoding for polling transport even when XHR2
* responseType is available and WebSocket even if the used standard
* supports binary.
*/
forceBase64: boolean
/**
* Enables XDomainRequest for IE8 to avoid loading bar flashing with
* click sound. default to `false` because XDomainRequest has a flaw
* of not sending cookie.
* @default false
*/
enablesXDR: boolean
/**
* The param name to use as our timestamp key
* @default 't'
*/
timestampParam: string
/**
* Whether to add the timestamp with each transport request. Note: this
* is ignored if the browser is IE or Android, in which case requests
* are always stamped
* @default false
*/
timestampRequests: boolean
/**
* A list of transports to try (in order). Engine.io always attempts to
* connect directly with the first one, provided the feature detection test
* for it passes.
* @default ['polling','websocket']
*/
transports: string[]
/**
* The port the policy server listens on
* @default 843
*/
policyPost: number
/**
* If true and if the previous websocket connection to the server succeeded,
* the connection attempt will bypass the normal upgrade process and will
* initially try websocket. A connection attempt following a transport error
* will use the normal upgrade process. It is recommended you turn this on
* only when using SSL/TLS connections, or if you know that your network does
* not block websockets.
* @default false
*/
rememberUpgrade: boolean
/**
* Are we only interested in transports that support binary?
*/
onlyBinaryUpgrades: boolean
/**
* Timeout for xhr-polling requests in milliseconds (0) (only for polling transport)
*/
requestTimeout: number
/**
* Transport options for Node.js client (headers etc)
*/
transportOptions: Object
/**
* (SSL) Certificate, Private key and CA certificates to use for SSL.
* Can be used in Node.js client environment to manually specify
* certificate information.
*/
pfx: string
/**
* (SSL) Private key to use for SSL. Can be used in Node.js client
* environment to manually specify certificate information.
*/
key: string
/**
* (SSL) A string or passphrase for the private key or pfx. Can be
* used in Node.js client environment to manually specify certificate
* information.
*/
passphrase: string
/**
* (SSL) Public x509 certificate to use. Can be used in Node.js client
* environment to manually specify certificate information.
*/
cert: string
/**
* (SSL) An authority certificate or array of authority certificates to
* check the remote host against.. Can be used in Node.js client
* environment to manually specify certificate information.
*/
ca: string | string[]
/**
* (SSL) A string describing the ciphers to use or exclude. Consult the
* [cipher format list]
* (http://www.openssl.org/docs/apps/ciphers.html#CIPHER_LIST_FORMAT) for
* details on the format.. Can be used in Node.js client environment to
* manually specify certificate information.
*/
ciphers: string
/**
* (SSL) If true, the server certificate is verified against the list of
* supplied CAs. An 'error' event is emitted if verification fails.
* Verification happens at the connection level, before the HTTP request
* is sent. Can be used in Node.js client environment to manually specify
* certificate information.
*/
rejectUnauthorized: boolean
/**
* Headers that will be passed for each request to the server (via xhr-polling and via websockets).
* These values then can be used during handshake or for special proxies.
*/
extraHeaders?: { [header: string]: string }
/**
* Whether to include credentials (cookies, authorization headers, TLS
* client certificates, etc.) with cross-origin XHR polling requests
* @default false
*/
withCredentials: boolean
/**
* Whether to automatically close the connection whenever the beforeunload event is received.
* @default true
*/
closeOnBeforeunload: boolean
}
export interface ManagerOptions extends EngineOptions {
/**
* Should we force a new Manager for this connection?
* @default false
*/
forceNew: boolean
/**
* Should we multiplex our connection (reuse existing Manager) ?
* @default true
*/
multiplex: boolean
/**
* The path to get our client file from, in the case of the server
* serving it
* @default '/socket.io'
*/
path: string
/**
* Should we allow reconnections?
* @default true
*/
reconnection: boolean
/**
* How many reconnection attempts should we try?
* @default Infinity
*/
reconnectionAttempts: number
/**
* The time delay in milliseconds between reconnection attempts
* @default 1000
*/
reconnectionDelay: number
/**
* The max time delay in milliseconds between reconnection attempts
* @default 5000
*/
reconnectionDelayMax: number
/**
* Used in the exponential backoff jitter when reconnecting
* @default 0.5
*/
randomizationFactor: number
/**
* The timeout in milliseconds for our connection attempt
* @default 20000
*/
timeout: number
/**
* Should we automatically connect?
* @default true
*/
autoConnect: boolean
/**
* weather we should unref the reconnect timer when it is
* create automatically
* @default false
*/
autoUnref: boolean
/**
* the parser to use. Defaults to an instance of the Parser that ships with socket.io.
*/
parser: any
}
interface ManagerReservedEvents {
open: () => void
error: (err: Error) => void
ping: () => void
packet: (packet: Packet) => void
close: (reason: string) => void
reconnect_failed: () => void
reconnect_attempt: (attempt: number) => void
reconnect_error: (err: Error) => void
reconnect: (attempt: number) => void
}
export class Manager<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents
> extends StrictEventEmitter<{}, {}, ManagerReservedEvents> {
/**
* The Engine.IO client instance
*
* @public
*/
public engine: any
/**
* @private
*/
_autoConnect: boolean
/**
* @private
*/
_readyState: "opening" | "open" | "closed"
/**
* @private
*/
_reconnecting: boolean
private readonly uri: string
public opts: Partial<ManagerOptions>
private nsps: Record<string, Socket> = {};
private subs: Array<ReturnType<typeof on>> = [];
private backoff: Backoff
private _reconnection: boolean
private _reconnectionAttempts: number
private _reconnectionDelay: number
private _randomizationFactor: number
private _reconnectionDelayMax: number
private _timeout: any
private encoder: Encoder
private decoder: Decoder
private skipReconnect: boolean
/**
* `Manager` constructor.
*
* @param uri - engine instance or engine uri/opts
* @param opts - options
* @public
*/
constructor(opts: Partial<ManagerOptions>)
constructor(uri?: string, opts?: Partial<ManagerOptions>)
constructor(
uri?: string | Partial<ManagerOptions>,
opts?: Partial<ManagerOptions>
)
constructor(
uri?: string | Partial<ManagerOptions>,
opts?: Partial<ManagerOptions>
) {
super()
if (uri && "object" === typeof uri) {
opts = uri
uri = undefined
}
opts = opts || {}
opts.path = opts.path || "/socket.io"
this.opts = opts
this.reconnection(opts.reconnection !== false)
this.reconnectionAttempts(opts.reconnectionAttempts || Infinity)
this.reconnectionDelay(opts.reconnectionDelay || 1000)
this.reconnectionDelayMax(opts.reconnectionDelayMax || 5000)
this.randomizationFactor(opts.randomizationFactor ?? 0.5)
this.backoff = new Backoff({
min: this.reconnectionDelay(),
max: this.reconnectionDelayMax(),
jitter: this.randomizationFactor(),
})
this.timeout(null == opts.timeout ? 20000 : opts.timeout)
this._readyState = "closed"
this.uri = uri as string
const _parser = opts.parser || parser
this.encoder = new _parser.Encoder()
this.decoder = new _parser.Decoder()
this._autoConnect = opts.autoConnect !== false
if (this._autoConnect) this.open()
}
/**
* Sets the `reconnection` config.
*
* @param {Boolean} v - true/false if it should automatically reconnect
* @return {Manager} self or value
* @public
*/
public reconnection(v: boolean): this
public reconnection(): boolean
public reconnection(v?: boolean): this | boolean
public reconnection(v?: boolean): this | boolean {
if (!arguments.length) return this._reconnection
this._reconnection = !!v
return this
}
/**
* Sets the reconnection attempts config.
*
* @param {Number} v - max reconnection attempts before giving up
* @return {Manager} self or value
* @public
*/
public reconnectionAttempts(v: number): this
public reconnectionAttempts(): number
public reconnectionAttempts(v?: number): this | number
public reconnectionAttempts(v?: number): this | number {
if (v === undefined) return this._reconnectionAttempts
this._reconnectionAttempts = v
return this
}
/**
* Sets the delay between reconnections.
*
* @param {Number} v - delay
* @return {Manager} self or value
* @public
*/
public reconnectionDelay(v: number): this
public reconnectionDelay(): number
public reconnectionDelay(v?: number): this | number
public reconnectionDelay(v?: number): this | number {
if (v === undefined) return this._reconnectionDelay
this._reconnectionDelay = v
this.backoff?.setMin(v)
return this
}
/**
* Sets the randomization factor
*
* @param v - the randomization factor
* @return self or value
* @public
*/
public randomizationFactor(v: number): this
public randomizationFactor(): number
public randomizationFactor(v?: number): this | number
public randomizationFactor(v?: number): this | number {
if (v === undefined) return this._randomizationFactor
this._randomizationFactor = v
this.backoff?.setJitter(v)
return this
}
/**
* Sets the maximum delay between reconnections.
*
* @param v - delay
* @return self or value
* @public
*/
public reconnectionDelayMax(v: number): this
public reconnectionDelayMax(): number
public reconnectionDelayMax(v?: number): this | number
public reconnectionDelayMax(v?: number): this | number {
if (v === undefined) return this._reconnectionDelayMax
this._reconnectionDelayMax = v
this.backoff?.setMax(v)
return this
}
/**
* Sets the connection timeout. `false` to disable
*
* @param v - connection timeout
* @return self or value
* @public
*/
public timeout(v: number | boolean): this
public timeout(): number | boolean
public timeout(v?: number | boolean): this | number | boolean
public timeout(v?: number | boolean): this | number | boolean {
if (!arguments.length) return this._timeout
this._timeout = v
return this
}
/**
* Starts trying to reconnect if reconnection is enabled and we have not
* started reconnecting yet
*
* @private
*/
private maybeReconnectOnOpen() {
// Only try to reconnect if it's the first time we're connecting
if (
!this._reconnecting &&
this._reconnection &&
this.backoff.attempts === 0
) {
// keeps reconnection from firing twice for the same reconnection loop
this.reconnect()
}
}
/**
* Sets the current transport `socket`.
*
* @param {Function} fn - optional, callback
* @return self
* @public
*/
public open(fn?: (err?: Error) => void): this {
debug("readyState %s", this._readyState)
if (~this._readyState.indexOf("open")) return this
debug("opening %s", this.uri)
// @ts-ignore
this.engine = eio(this.uri, this.opts)
const socket = this.engine
const self = this
this._readyState = "opening"
this.skipReconnect = false
// emit `open`
const openSubDestroy = on(socket, "open", function () {
self.onopen()
fn && fn()
})
// emit `error`
const errorSub = on(socket, "error", (err) => {
debug("error")
self.cleanup()
self._readyState = "closed"
this.emitReserved("error", err)
if (fn) {
fn(err)
} else {
// Only do this if there is no fn to handle the error
self.maybeReconnectOnOpen()
}
})
if (false !== this._timeout) {
const timeout = this._timeout
debug("connect attempt will timeout after %d", timeout)
if (timeout === 0) {
openSubDestroy() // prevents a race condition with the 'open' event
}
// set timer
const timer = setTimeout(() => {
debug("connect attempt timed out after %d", timeout)
openSubDestroy()
socket.close()
socket.emit("error", new Error("timeout"))
}, timeout)
if (this.opts.autoUnref) {
timer.unref()
}
this.subs.push(function subDestroy(): void {
clearTimeout(timer)
})
}
this.subs.push(openSubDestroy)
this.subs.push(errorSub)
return this
}
/**
* Alias for open()
*
* @return self
* @public
*/
public connect(fn?: (err?: Error) => void): this {
return this.open(fn)
}
/**
* Called upon transport open.
*
* @private
*/
private onopen(): void {
debug("open")
// clear old subs
this.cleanup()
// mark as open
this._readyState = "open"
this.emitReserved("open")
// add new subs
const socket = this.engine
this.subs.push(
on(socket, "ping", this.onping.bind(this)),
on(socket, "data", this.ondata.bind(this)),
on(socket, "error", this.onerror.bind(this)),
on(socket, "close", this.onclose.bind(this)),
on(this.decoder, "decoded", this.ondecoded.bind(this))
)
}
/**
* Called upon a ping.
*
* @private
*/
private onping(): void {
this.emitReserved("ping")
}
/**
* Called with data.
*
* @private
*/
private ondata(data): void {
this.decoder.add(data)
}
/**
* Called when parser fully decodes a packet.
*
* @private
*/
private ondecoded(packet): void {
this.emitReserved("packet", packet)
}
/**
* Called upon socket error.
*
* @private
*/
private onerror(err): void {
debug("error", err)
this.emitReserved("error", err)
}
/**
* Creates a new socket for the given `nsp`.
*
* @return {Socket}
* @public
*/
public socket(nsp: string, opts?: Partial<SocketOptions>): Socket {
let socket = this.nsps[nsp]
if (!socket) {
socket = new Socket(this, nsp, opts)
this.nsps[nsp] = socket
}
return socket
}
/**
* Called upon a socket close.
*
* @param socket
* @private
*/
_destroy(socket: Socket): void {
const nsps = Object.keys(this.nsps)
for (const nsp of nsps) {
const socket = this.nsps[nsp]
if (socket.active) {
debug("socket %s is still active, skipping close", nsp)
return
}
}
this._close()
}
/**
* Writes a packet.
*
* @param packet
* @private
*/
_packet(packet: Partial<Packet & { query: string; options: any }>): void {
debug("writing packet %j", packet)
const encodedPackets = this.encoder.encode(packet as Packet)
for (let i = 0; i < encodedPackets.length; i++) {
this.engine.write(encodedPackets[i], packet.options)
}
}
/**
* Clean up transport subscriptions and packet buffer.
*
* @private
*/
private cleanup(): void {
debug("cleanup")
this.subs.forEach((subDestroy) => subDestroy())
this.subs.length = 0
this.decoder.destroy()
}
/**
* Close the current socket.
*
* @private
*/
_close(): void {
debug("disconnect")
this.skipReconnect = true
this._reconnecting = false
if ("opening" === this._readyState) {
// `onclose` will not fire because
// an open event never happened
this.cleanup()
}
this.backoff.reset()
this._readyState = "closed"
if (this.engine) this.engine.close()
}
/**
* Alias for close()
*
* @private
*/
private disconnect(): void {
return this._close()
}
/**
* Called upon engine close.
*
* @private
*/
private onclose(reason: string): void {
debug("onclose")
this.cleanup()
this.backoff.reset()
this._readyState = "closed"
this.emitReserved("close", reason)
if (this._reconnection && !this.skipReconnect) {
this.reconnect()
}
}
/**
* Attempt a reconnection.
*
* @private
*/
private reconnect(): this | void {
if (this._reconnecting || this.skipReconnect) return this
const self = this
if (this.backoff.attempts >= this._reconnectionAttempts) {
debug("reconnect failed")
this.backoff.reset()
this.emitReserved("reconnect_failed")
this._reconnecting = false
} else {
const delay = this.backoff.duration()
debug("will wait %dms before reconnect attempt", delay)
this._reconnecting = true
const timer = setTimeout(() => {
if (self.skipReconnect) return
debug("attempting reconnect")
this.emitReserved("reconnect_attempt", self.backoff.attempts)
// check again for the case socket closed in above events
if (self.skipReconnect) return
self.open((err) => {
if (err) {
debug("reconnect attempt error")
self._reconnecting = false
self.reconnect()
this.emitReserved("reconnect_error", err)
} else {
debug("reconnect success")
self.onreconnect()
}
})
}, delay)
if (this.opts.autoUnref) {
timer.unref()
}
this.subs.push(function subDestroy() {
clearTimeout(timer)
})
}
}
/**
* Called upon successful reconnect.
*
* @private
*/
private onreconnect(): void {
const attempt = this.backoff.attempts
this._reconnecting = false
this.backoff.reset()
this.emitReserved("reconnect", attempt)
}
}

View File

@ -0,0 +1,14 @@
// import type * as Emitter from "component-emitter";
import { EventEmitter } from "events"
import { StrictEventEmitter } from "./typed-events"
export function on(
obj: EventEmitter | StrictEventEmitter<any, any>,
ev: string,
fn: (err?: any) => any
): VoidFunction {
obj.on(ev, fn)
return function subDestroy(): void {
obj.off(ev, fn)
}
}

View File

@ -0,0 +1,558 @@
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<ListenEvents, EmitEvents, SocketReservedEvents> {
public readonly io: Manager<ListenEvents, EmitEvents>
public id: string
public connected: boolean
public disconnected: boolean
public auth: { [key: string]: any } | ((cb: (data: object) => void) => void)
public receiveBuffer: Array<ReadonlyArray<any>> = [];
public sendBuffer: Array<Packet> = [];
private readonly nsp: string
private ids: number = 0;
private acks: object = {};
private flags: Flags = {};
private subs?: Array<VoidFunction>
private _anyListeners: 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()
}
/**
* 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 extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): 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<Packet>): 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<any> = 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<any>): 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"
}

View File

@ -0,0 +1,157 @@
import { EventEmitter } from "events"
/**
* An events map is an interface that maps event names to their value, which
* represents the type 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 EventNames<Map>
> = Parameters<Map[Ev]>
/**
* The event names that are either in ReservedEvents or in UserEvents
*/
export type ReservedOrUserEventNames<
ReservedEventsMap extends EventsMap,
UserEvents extends EventsMap
> = EventNames<ReservedEventsMap> | EventNames<UserEvents>
/**
* Type of a listener of a user event or a reserved event. If `Ev` is in
* `ReservedEvents`, the reserved event listener is returned.
*/
export type ReservedOrUserListener<
ReservedEvents extends EventsMap,
UserEvents extends EventsMap,
Ev extends ReservedOrUserEventNames<ReservedEvents, UserEvents>
> = FallbackToUntypedListener<
Ev extends EventNames<ReservedEvents>
? ReservedEvents[Ev]
: Ev extends EventNames<UserEvents>
? UserEvents[Ev]
: never
>
/**
* Returns an untyped listener type if `T` is `never`; otherwise, returns `T`.
*
* This is a hack to mitigate https://github.com/socketio/socket.io/issues/3833.
* Needed because of https://github.com/microsoft/TypeScript/issues/41778
*/
type FallbackToUntypedListener<T> = [T] extends [never]
? (...args: any[]) => void
: T
/**
* Strictly typed version of an `EventEmitter`. A `TypedEventEmitter` takes type
* parameters for mappings of event names to event data types, and strictly
* types method calls to the `EventEmitter` according to these event maps.
*
* @typeParam ListenEvents - `EventsMap` of user-defined events that can be
* listened to with `on` or `once`
* @typeParam EmitEvents - `EventsMap` of user-defined events that can be
* emitted with `emit`
* @typeParam ReservedEvents - `EventsMap` of reserved events, that can be
* emitted by socket.io with `emitReserved`, and can be listened to with
* `listen`.
*/
export abstract class StrictEventEmitter<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap,
ReservedEvents extends EventsMap = {}
> extends EventEmitter {
/**
* Adds the `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
on<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this {
super.on(ev as string, listener)
return this
}
/**
* Adds a one-time `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
once<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this {
super.once(ev as string, listener)
return this
}
/**
* Emits an event.
*
* @param ev Name of the event
* @param args Values to send to listeners of this event
*/
// @ts-ignore
emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): this {
super.emit(ev as string, ...args)
return this
}
/**
* Emits a reserved event.
*
* This method is `protected`, so that only a class extending
* `StrictEventEmitter` can emit its own reserved events.
*
* @param ev Reserved event name
* @param args Arguments to emit along with the event
*/
protected emitReserved<Ev extends EventNames<ReservedEvents>>(
ev: Ev,
...args: EventParams<ReservedEvents, Ev>
): this {
super.emit(ev as string, ...args)
return this
}
/**
* Returns the listeners listening to an event.
*
* @param event Event name
* @returns Array of listeners subscribed to `event`
*/
listeners<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
event: Ev
): ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>[] {
return super.listeners(event as string) as ReservedOrUserListener<
ReservedEvents,
ListenEvents,
Ev
>[]
}
}

View File

@ -0,0 +1,97 @@
import * as parseuri from "parseuri"
const debug = require("../debug")("socket.io-client")
type ParsedUrl = {
source: string
protocol: string
authority: string
userInfo: string
user: string
password: string
host: string
port: string
relative: string
path: string
directory: string
file: string
query: string
anchor: string
pathNames: Array<string>
queryKey: { [key: string]: string }
// Custom properties (not native to parseuri):
id: string
href: string
}
/**
* URL parser.
*
* @param uri - url
* @param path - the request path of the connection
* @param loc - An object meant to mimic window.location.
* Defaults to window.location.
* @public
*/
export function url(
uri: string | ParsedUrl,
path: string = "",
loc?: Location
): ParsedUrl {
let obj = uri as ParsedUrl
// default to window.location
loc = loc || (typeof location !== "undefined" && location)
if (null == uri) uri = loc.protocol + "//" + loc.host
// relative path support
if (typeof uri === "string") {
if ("/" === uri.charAt(0)) {
if ("/" === uri.charAt(1)) {
uri = loc.protocol + uri
} else {
uri = loc.host + uri
}
}
if (!/^(https?|wss?):\/\//.test(uri)) {
debug("protocol-less url %s", uri)
if ("undefined" !== typeof loc) {
uri = loc.protocol + "//" + uri
} else {
uri = "https://" + uri
}
}
// parse
debug("parse %s", uri)
obj = parseuri(uri) as ParsedUrl
}
// make sure we treat `localhost:80` and `localhost` equally
if (!obj.port) {
if (/^(http|ws)$/.test(obj.protocol)) {
obj.port = "80"
} else if (/^(http|ws)s$/.test(obj.protocol)) {
obj.port = "443"
}
}
obj.path = obj.path || "/"
const ipv6 = obj.host.indexOf(":") !== -1
const host = ipv6 ? "[" + obj.host + "]" : obj.host
// define unique id
obj.id = obj.protocol + "://" + host + ":" + obj.port + path
// define href
obj.href =
obj.protocol +
"://" +
host +
(loc && loc.port === obj.port ? "" : ":" + obj.port)
return obj
}