feat: upgrade socket.io to v4

Signed-off-by: MiaoWoo <admin@yumc.pw>
This commit is contained in:
2022-11-21 23:18:39 +08:00
parent e563e1b507
commit df0d246136
45 changed files with 6585 additions and 1734 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@ import 'core-js'
process.on('exit', () => require.disable()) process.on('exit', () => require.disable())
global.setGlobal('Proxy', require('./proxy').Proxy) global.setGlobal('Proxy', require('./proxy').Proxy)
global.setGlobal('XMLHttpRequest', require('./xml-http-request').XMLHttpRequest) global.setGlobal('XMLHttpRequest', require('./xml-http-request').XMLHttpRequest)
global.setGlobal('Buffer', require('./buffer').Buffer)
global.setGlobal('Blob', require('blob-polyfill').Blob) global.setGlobal('Blob', require('blob-polyfill').Blob)
console.i18n("ms.polyfill.completed", { time: (new Date().getTime() - polyfillStartTime) / 1000 }) console.i18n("ms.polyfill.completed", { time: (new Date().getTime() - polyfillStartTime) / 1000 })
export default true export default true

View File

@@ -19,6 +19,7 @@
"test": "echo \"Error: run tests from root\" && exit 1" "test": "echo \"Error: run tests from root\" && exit 1"
}, },
"dependencies": { "dependencies": {
"@socket.io/component-emitter": "^3.1.0",
"backo2": "^1.0.2", "backo2": "^1.0.2",
"parseuri": "^0.0.6" "parseuri": "^0.0.6"
}, },

View File

@@ -38,7 +38,7 @@ export class WebSocket extends EventEmitter {
private client: Transport private client: Transport
constructor(url: string, subProtocol: string = '', headers: WebSocketHeader = {}) { constructor(url: string, subProtocol: string | string[] = '', headers: WebSocketHeader = {}) {
super() super()
this.manager = manager this.manager = manager
this._url = url this._url = url

View File

@@ -1 +1,54 @@
export = (namepsace) => (...args) => { }//console.debug(namepsace, ...args) export = (namepsace) => (...args) => { console.trace(`[${namepsace}] ` + format(...args)) }//console.debug(namepsace, ...args)
let formatters: any = {}
formatters.s = function (v) {
return v
}
formatters.j = function (v) {
try {
return JSON.stringify(v)
} catch (error: any) {
return '[UnexpectedJSONParseError]: ' + error.message
}
}
/**
* Coerce `val`.
*
* @param {Mixed} val
* @return {Mixed}
* @api private
*/
function coerce(val) {
if (val instanceof Error) {
return val.stack || val.message
}
return val
}
function format(...args) {
// Apply any `formatters` transformations
args[0] = coerce(args[0])
if (typeof args[0] !== 'string') {
// Anything else let's inspect with %O
args.unshift('%O')
}
let index = 0
args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => {
// If we encounter an escaped % then don't increase the array index
if (match === '%%') {
return '%'
}
index++
const formatter = formatters[format]
if (typeof formatter === 'function') {
const val = args[index]
match = formatter.call(format, val)
// Now we need to remove `args[index]` since it's inlined in the `format`
args.splice(index, 1)
index--
}
return match
})
return args[0]
}

View File

@@ -0,0 +1,12 @@
// imported from https://github.com/component/has-cors
let value = false;
try {
value = typeof XMLHttpRequest !== 'undefined' &&
'withCredentials' in new XMLHttpRequest();
} catch (err) {
// if XMLHttp support is disabled in IE then it will throw
// when trying to create
}
export const hasCORS = value;

View File

@@ -0,0 +1,38 @@
// imported from https://github.com/galkn/querystring
/**
* Compiles a querystring
* Returns string representation of the object
*
* @param {Object}
* @api private
*/
export function encode(obj) {
let str = ''
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
if (str.length) str += '&'
str += encodeURIComponent(i) + '=' + encodeURIComponent(obj[i])
}
}
return str
}
/**
* Parses a simple querystring into an object
*
* @param {String} qs
* @api private
*/
export function decode(qs) {
let qry = {}
let pairs = qs.split('&')
for (let i = 0, l = pairs.length; i < l; i++) {
let pair = pairs[i].split('=')
qry[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1])
}
return qry
}

View File

@@ -0,0 +1,68 @@
// imported from https://github.com/galkn/parseuri
/**
* Parses an URI
*
* @author Steven Levithan <stevenlevithan.com> (MIT license)
* @api private
*/
const re = /^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
const parts = [
'source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor'
]
export function parse(str) {
const src = str,
b = str.indexOf('['),
e = str.indexOf(']')
if (b != -1 && e != -1) {
str = str.substring(0, b) + str.substring(b, e).replace(/:/g, ';') + str.substring(e, str.length)
}
let m = re.exec(str || ''),
uri = {} as any,
i = 14
while (i--) {
uri[parts[i]] = m[i] || ''
}
if (b != -1 && e != -1) {
uri.source = src
uri.host = uri.host.substring(1, uri.host.length - 1).replace(/;/g, ':')
uri.authority = uri.authority.replace('[', '').replace(']', '').replace(/;/g, ':')
uri.ipv6uri = true
}
uri.pathNames = pathNames(uri, uri['path'])
uri.queryKey = queryKey(uri, uri['query'])
return uri
}
function pathNames(obj, path) {
const regx = /\/{2,9}/g,
names = path.replace(regx, "/").split("/")
if (path.slice(0, 1) == '/' || path.length === 0) {
names.splice(0, 1)
}
if (path.slice(-1) == '/') {
names.splice(names.length - 1, 1)
}
return names
}
function queryKey(uri, query) {
const data = {}
query.replace(/(?:^|&)([^&=]*)=?([^&]*)/g, function ($0, $1, $2) {
if ($1) {
data[$1] = $2
}
})
return data
}

View File

@@ -0,0 +1,62 @@
// imported from https://github.com/unshiftio/yeast
'use strict'
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_'.split('')
, length = 64
, map = {}
let seed = 0
, i = 0
, prev
/**
* Return a string representing the specified number.
*
* @param {Number} num The number to convert.
* @returns {String} The string representation of the number.
* @api public
*/
export function encode(num) {
let encoded = ''
do {
encoded = alphabet[num % length] + encoded
num = Math.floor(num / length)
} while (num > 0)
return encoded
}
/**
* Return the integer value specified by the given string.
*
* @param {String} str The string to convert.
* @returns {Number} The integer value represented by the string.
* @api public
*/
export function decode(str) {
let decoded = 0
for (i = 0; i < str.length; i++) {
decoded = decoded * length + map[str.charAt(i)]
}
return decoded
}
/**
* Yeast: A tiny growing id generator.
*
* @returns {String} A unique id.
* @api public
*/
export function yeast() {
const now = encode(+new Date())
if (now !== prev) return seed = 0, prev = now
return now + '.' + encode(seed++)
}
//
// Map each character to its index.
//
for (; i < length; i++) map[alphabet[i]] = i

View File

@@ -1,16 +1,10 @@
import { Socket } from './socket' import { Socket } from "./socket"
export default (uri, opts) => new Socket(uri, opts) export { Socket }
export { SocketOptions } from "./socket"
/** export const protocol = Socket.protocol
* Expose deps for legacy compatibility export { Transport } from "./transport"
* and standalone browser access. export { transports } from "./transports/index"
*/ export { installTimerFunctions } from "./util"
const protocol = Socket.protocol // this is an int export { parse } from "./contrib/parseuri"
export { Socket, protocol } export { nextTick } from "./transports/websocket-constructor.js"
// module.exports.Transport = require("./transport")
// module.exports.transports = require("./transports/index")
// module.exports.parser = require("../engine.io-parser")
export * from './transport'
export * from './transports/index'
export * from '../engine.io-parser'

View File

@@ -1,21 +1,278 @@
import transports from "./transports" // import { transports } from "./transports/index.js";
// const transports = require("./transports/index") import { transports } from "./transports"
const Emitter = require("component-emitter") import { installTimerFunctions, byteLength } from "./util"
const debug = (...args: any) => console.debug('engine.io-client:socket', ...args)//require("debug")("engine.io-client:socket") import { decode } from "./contrib/parseqs"
import parser from "../engine.io-parser" import { parse } from "./contrib/parseuri"
const parseuri = require("parseuri") // import debugModule from "debug"; // debug()
const parseqs = require("parseqs") import { Emitter } from "@socket.io/component-emitter"
import { installTimerFunctions } from "./util" // import { protocol } from "engine.io-parser";
import { protocol } from "../engine.io-parser"
import { CloseDetails } from "./transport"
// const debug = debugModule("engine.io-client:socket"); // debug()
const debug = require('../debug')('engine.io-client:socket')
export interface SocketOptions {
/**
* 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 | number
/**
* Any query parameters in our uri. Set from the URI passed when connecting
*/
query: { [key: string]: any }
/**
* `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 base 64 encoding for polling transport even when XHR2
* responseType is available and WebSocket even if the used standard
* supports binary.
*/
forceBase64: 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
/**
* Whether to always use the native timeouts. This allows the client to
* reconnect when the native timeout functions are overridden, such as when
* mock clocks are installed.
* @default false
*/
useNativeTimers: boolean
/**
* weather we should unref the reconnect timer when it is
* create automatically
* @default false
*/
autoUnref: boolean
/**
* parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable.
* @default false
*/
perMessageDeflate: { threshold: number }
/**
* The path to get our client file from, in the case of the server
* serving it
* @default '/engine.io'
*/
path: string
/**
* Either a single protocol string or an array of protocol strings. These strings are used to indicate sub-protocols,
* so that a single server can implement multiple WebSocket sub-protocols (for example, you might want one server to
* be able to handle different types of interactions depending on the specified protocol)
* @default []
*/
protocols: string | string[]
}
interface SocketReservedEvents {
open: () => void
handshake: (data) => void
packet: (packet) => void
packetCreate: (packet) => void
data: (data) => void
message: (data) => void
drain: () => void
flush: () => void
heartbeat: () => void
ping: () => void
pong: () => void
error: (err: string | Error) => void
upgrading: (transport) => void
upgrade: (transport) => void
upgradeError: (err: Error) => void
close: (reason: string, description?: CloseDetails | Error) => void
}
export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
public id: string
public transport: any
public binaryType: string
private readyState: string
private writeBuffer
private prevBufferLen: number
private upgrades
private pingInterval: number
private pingTimeout: number
private pingTimeoutTimer: NodeJS.Timer
private setTimeoutFn: typeof setTimeout
private clearTimeoutFn: typeof clearTimeout
private readonly beforeunloadEventListener: () => void
private readonly offlineEventListener: () => void
private upgrading: boolean
private maxPayload?: number
private readonly opts: Partial<SocketOptions>
private readonly secure: boolean
private readonly hostname: string
private readonly port: string | number
private readonly transports: string[]
static priorWebsocketSuccess: boolean
static protocol = protocol;
export class Socket extends Emitter {
/** /**
* Socket constructor. * Socket constructor.
* *
* @param {String|Object} uri or options * @param {String|Object} uri or options
* @param {Object} options * @param {Object} opts - options
* @api public * @api public
*/ */
constructor(uri, opts: any = {}) { constructor(uri, opts: Partial<SocketOptions> = {}) {
super() super()
if (uri && "object" === typeof uri) { if (uri && "object" === typeof uri) {
@@ -24,13 +281,13 @@ export class Socket extends Emitter {
} }
if (uri) { if (uri) {
uri = parseuri(uri) uri = parse(uri)
opts.hostname = uri.host opts.hostname = uri.host
opts.secure = uri.protocol === "https" || uri.protocol === "wss" opts.secure = uri.protocol === "https" || uri.protocol === "wss"
opts.port = uri.port opts.port = uri.port
if (uri.query) opts.query = uri.query if (uri.query) opts.query = uri.query
} else if (opts.host) { } else if (opts.host) {
opts.hostname = parseuri(opts.host).host opts.hostname = parse(opts.host).host
} }
installTimerFunctions(this, opts) installTimerFunctions(this, opts)
@@ -53,10 +310,10 @@ export class Socket extends Emitter {
(typeof location !== "undefined" && location.port (typeof location !== "undefined" && location.port
? location.port ? location.port
: this.secure : this.secure
? 443 ? "443"
: 80) : "80")
this.transports = ["websocket"] this.transports = opts.transports || ["polling", "websocket"]
this.readyState = "" this.readyState = ""
this.writeBuffer = [] this.writeBuffer = []
this.prevBufferLen = 0 this.prevBufferLen = 0
@@ -67,7 +324,6 @@ export class Socket extends Emitter {
agent: false, agent: false,
withCredentials: false, withCredentials: false,
upgrade: true, upgrade: true,
jsonp: true,
timestampParam: "t", timestampParam: "t",
rememberUpgrade: false, rememberUpgrade: false,
rejectUnauthorized: true, rejectUnauthorized: true,
@@ -83,7 +339,7 @@ export class Socket extends Emitter {
this.opts.path = this.opts.path.replace(/\/$/, "") + "/" this.opts.path = this.opts.path.replace(/\/$/, "") + "/"
if (typeof this.opts.query === "string") { if (typeof this.opts.query === "string") {
this.opts.query = parseqs.decode(this.opts.query) this.opts.query = decode(this.opts.query)
} }
// set on handshake // set on handshake
@@ -100,21 +356,20 @@ export class Socket extends Emitter {
// Firefox closes the connection when the "beforeunload" event is emitted but not Chrome. This event listener // Firefox closes the connection when the "beforeunload" event is emitted but not Chrome. This event listener
// ensures every browser behaves the same (no "disconnect" event at the Socket.IO level when the page is // ensures every browser behaves the same (no "disconnect" event at the Socket.IO level when the page is
// closed/reloaded) // closed/reloaded)
addEventListener( this.beforeunloadEventListener = () => {
"beforeunload", if (this.transport) {
() => { // silently close the transport
if (this.transport) { this.transport.removeAllListeners()
// silently close the transport this.transport.close()
this.transport.removeAllListeners() }
this.transport.close() }
} addEventListener("beforeunload", this.beforeunloadEventListener, false)
},
false
)
} }
if (this.hostname !== "localhost") { if (this.hostname !== "localhost") {
this.offlineEventListener = () => { this.offlineEventListener = () => {
this.onClose("transport close") this.onClose("transport close", {
description: "network connection lost"
})
} }
addEventListener("offline", this.offlineEventListener, false) addEventListener("offline", this.offlineEventListener, false)
} }
@@ -130,15 +385,12 @@ export class Socket extends Emitter {
* @return {Transport} * @return {Transport}
* @api private * @api private
*/ */
createTransport(name, opt?) { private createTransport(name) {
if (name != 'websocket') {
throw new Error('Only Support WebSocket in MiaoScript!')
}
debug('creating transport "%s"', name) debug('creating transport "%s"', name)
const query: any = clone(this.opts.query) const query: any = Object.assign({}, this.opts.query)
// append engine.io protocol identifier // append engine.io protocol identifier
query.EIO = parser.protocol query.EIO = protocol
// transport name // transport name
query.transport = name query.transport = name
@@ -159,8 +411,8 @@ export class Socket extends Emitter {
} }
) )
debug("options: %j", JSON.stringify(opts)) debug("options: %j", opts)
debug("new func", transports[name])
return new transports[name](opts) return new transports[name](opts)
} }
@@ -169,7 +421,7 @@ export class Socket extends Emitter {
* *
* @api private * @api private
*/ */
open() { private open() {
let transport let transport
if ( if (
this.opts.rememberUpgrade && this.opts.rememberUpgrade &&
@@ -180,7 +432,7 @@ export class Socket extends Emitter {
} else if (0 === this.transports.length) { } else if (0 === this.transports.length) {
// Emit error on next tick so it can be listened to // Emit error on next tick so it can be listened to
this.setTimeoutFn(() => { this.setTimeoutFn(() => {
this.emit("error", "No transports available") this.emitReserved("error", "No transports available")
}, 0) }, 0)
return return
} else { } else {
@@ -191,8 +443,8 @@ export class Socket extends Emitter {
// Retry with the next transport if the transport is disabled (jsonp: false) // Retry with the next transport if the transport is disabled (jsonp: false)
try { try {
transport = this.createTransport(transport) transport = this.createTransport(transport)
} catch (error: any) { } catch (e) {
debug("error while creating transport: %s", error) debug("error while creating transport: %s", e)
this.transports.shift() this.transports.shift()
this.open() this.open()
return return
@@ -207,7 +459,7 @@ export class Socket extends Emitter {
* *
* @api private * @api private
*/ */
setTransport(transport) { private setTransport(transport) {
debug("setting transport %s", transport.name) debug("setting transport %s", transport.name)
if (this.transport) { if (this.transport) {
@@ -223,9 +475,7 @@ export class Socket extends Emitter {
.on("drain", this.onDrain.bind(this)) .on("drain", this.onDrain.bind(this))
.on("packet", this.onPacket.bind(this)) .on("packet", this.onPacket.bind(this))
.on("error", this.onError.bind(this)) .on("error", this.onError.bind(this))
.on("close", () => { .on("close", reason => this.onClose("transport close", reason))
this.onClose("transport close")
})
} }
/** /**
@@ -234,9 +484,9 @@ export class Socket extends Emitter {
* @param {String} transport name * @param {String} transport name
* @api private * @api private
*/ */
probe(name) { private probe(name) {
debug('probing transport "%s"', name) debug('probing transport "%s"', name)
let transport = this.createTransport(name, { probe: 1 }) let transport = this.createTransport(name)
let failed = false let failed = false
Socket.priorWebsocketSuccess = false Socket.priorWebsocketSuccess = false
@@ -251,7 +501,7 @@ export class Socket extends Emitter {
if ("pong" === msg.type && "probe" === msg.data) { if ("pong" === msg.type && "probe" === msg.data) {
debug('probe transport "%s" pong', name) debug('probe transport "%s" pong', name)
this.upgrading = true this.upgrading = true
this.emit("upgrading", transport) this.emitReserved("upgrading", transport)
if (!transport) return if (!transport) return
Socket.priorWebsocketSuccess = "websocket" === transport.name Socket.priorWebsocketSuccess = "websocket" === transport.name
@@ -265,16 +515,17 @@ export class Socket extends Emitter {
this.setTransport(transport) this.setTransport(transport)
transport.send([{ type: "upgrade" }]) transport.send([{ type: "upgrade" }])
this.emit("upgrade", transport) this.emitReserved("upgrade", transport)
transport = null transport = null
this.upgrading = false this.upgrading = false
this.flush() this.flush()
}) })
} else { } else {
debug('probe transport "%s" failed', name) debug('probe transport "%s" failed', name)
const err: any = new Error("probe error") const err = new Error("probe error")
// @ts-ignore
err.transport = transport.name err.transport = transport.name
this.emit("upgradeError", err) this.emitReserved("upgradeError", err)
} }
}) })
} }
@@ -293,14 +544,15 @@ export class Socket extends Emitter {
// Handle any error that happens while probing // Handle any error that happens while probing
const onerror = err => { const onerror = err => {
const error: any = new Error("probe error: " + err) const error = new Error("probe error: " + err)
// @ts-ignore
error.transport = transport.name error.transport = transport.name
freezeTransport() freezeTransport()
debug('probe transport "%s" failed because of error: %s', name, err) debug('probe transport "%s" failed because of error: %s', name, err)
this.emit("upgradeError", error) this.emitReserved("upgradeError", error)
} }
function onTransportClose() { function onTransportClose() {
@@ -325,8 +577,8 @@ export class Socket extends Emitter {
transport.removeListener("open", onTransportOpen) transport.removeListener("open", onTransportOpen)
transport.removeListener("error", onerror) transport.removeListener("error", onerror)
transport.removeListener("close", onTransportClose) transport.removeListener("close", onTransportClose)
this.removeListener("close", onclose) this.off("close", onclose)
this.removeListener("upgrading", onupgrade) this.off("upgrading", onupgrade)
} }
transport.once("open", onTransportOpen) transport.once("open", onTransportOpen)
@@ -342,13 +594,13 @@ export class Socket extends Emitter {
/** /**
* Called when connection is deemed open. * Called when connection is deemed open.
* *
* @api public * @api private
*/ */
onOpen() { private onOpen() {
debug("socket open") debug("socket open")
this.readyState = "open" this.readyState = "open"
Socket.priorWebsocketSuccess = "websocket" === this.transport.name Socket.priorWebsocketSuccess = "websocket" === this.transport.name
this.emit("open") this.emitReserved("open")
this.flush() this.flush()
// we check for `readyState` in case an `open` // we check for `readyState` in case an `open`
@@ -372,7 +624,7 @@ export class Socket extends Emitter {
* *
* @api private * @api private
*/ */
onPacket(packet) { private onPacket(packet) {
if ( if (
"opening" === this.readyState || "opening" === this.readyState ||
"open" === this.readyState || "open" === this.readyState ||
@@ -380,10 +632,10 @@ export class Socket extends Emitter {
) { ) {
debug('socket receive: type "%s", data "%s"', packet.type, packet.data) debug('socket receive: type "%s", data "%s"', packet.type, packet.data)
this.emit("packet", packet) this.emitReserved("packet", packet)
// Socket is live - any packet counts // Socket is live - any packet counts
this.emit("heartbeat") this.emitReserved("heartbeat")
switch (packet.type) { switch (packet.type) {
case "open": case "open":
@@ -393,19 +645,20 @@ export class Socket extends Emitter {
case "ping": case "ping":
this.resetPingTimeout() this.resetPingTimeout()
this.sendPacket("pong") this.sendPacket("pong")
this.emit("ping") this.emitReserved("ping")
this.emit("pong") this.emitReserved("pong")
break break
case "error": case "error":
const err: any = new Error("server error") const err = new Error("server error")
// @ts-ignore
err.code = packet.data err.code = packet.data
this.onError(err) this.onError(err)
break break
case "message": case "message":
this.emit("data", packet.data) this.emitReserved("data", packet.data)
this.emit("message", packet.data) this.emitReserved("message", packet.data)
break break
} }
} else { } else {
@@ -416,16 +669,17 @@ export class Socket extends Emitter {
/** /**
* Called upon handshake completion. * Called upon handshake completion.
* *
* @param {Object} handshake obj * @param {Object} data - handshake obj
* @api private * @api private
*/ */
onHandshake(data) { private onHandshake(data) {
this.emit("handshake", data) this.emitReserved("handshake", data)
this.id = data.sid this.id = data.sid
this.transport.query.sid = data.sid this.transport.query.sid = data.sid
this.upgrades = this.filterUpgrades(data.upgrades) this.upgrades = this.filterUpgrades(data.upgrades)
this.pingInterval = data.pingInterval this.pingInterval = data.pingInterval
this.pingTimeout = data.pingTimeout this.pingTimeout = data.pingTimeout
this.maxPayload = data.maxPayload
this.onOpen() this.onOpen()
// In case open handler closes socket // In case open handler closes socket
if ("closed" === this.readyState) return if ("closed" === this.readyState) return
@@ -437,7 +691,7 @@ export class Socket extends Emitter {
* *
* @api private * @api private
*/ */
resetPingTimeout() { private resetPingTimeout() {
this.clearTimeoutFn(this.pingTimeoutTimer) this.clearTimeoutFn(this.pingTimeoutTimer)
this.pingTimeoutTimer = this.setTimeoutFn(() => { this.pingTimeoutTimer = this.setTimeoutFn(() => {
this.onClose("ping timeout") this.onClose("ping timeout")
@@ -452,7 +706,7 @@ export class Socket extends Emitter {
* *
* @api private * @api private
*/ */
onDrain() { private onDrain() {
this.writeBuffer.splice(0, this.prevBufferLen) this.writeBuffer.splice(0, this.prevBufferLen)
// setting prevBufferLen = 0 is very important // setting prevBufferLen = 0 is very important
@@ -461,7 +715,7 @@ export class Socket extends Emitter {
this.prevBufferLen = 0 this.prevBufferLen = 0
if (0 === this.writeBuffer.length) { if (0 === this.writeBuffer.length) {
this.emit("drain") this.emitReserved("drain")
} else { } else {
this.flush() this.flush()
} }
@@ -472,22 +726,53 @@ export class Socket extends Emitter {
* *
* @api private * @api private
*/ */
flush() { private flush() {
if ( if (
"closed" !== this.readyState && "closed" !== this.readyState &&
this.transport.writable && this.transport.writable &&
!this.upgrading && !this.upgrading &&
this.writeBuffer.length this.writeBuffer.length
) { ) {
debug("flushing %d packets in socket", this.writeBuffer.length) const packets = this.getWritablePackets()
this.transport.send(this.writeBuffer) debug("flushing %d packets in socket", packets.length)
this.transport.send(packets)
// keep track of current length of writeBuffer // keep track of current length of writeBuffer
// splice writeBuffer and callbackBuffer on `drain` // splice writeBuffer and callbackBuffer on `drain`
this.prevBufferLen = this.writeBuffer.length this.prevBufferLen = packets.length
this.emit("flush") this.emitReserved("flush")
} }
} }
/**
* Ensure the encoded size of the writeBuffer is below the maxPayload value sent by the server (only for HTTP
* long-polling)
*
* @private
*/
private getWritablePackets() {
const shouldCheckPayloadSize =
this.maxPayload &&
this.transport.name === "polling" &&
this.writeBuffer.length > 1
if (!shouldCheckPayloadSize) {
return this.writeBuffer
}
let payloadSize = 1 // first packet type
for (let i = 0; i < this.writeBuffer.length; i++) {
const data = this.writeBuffer[i].data
if (data) {
payloadSize += byteLength(data)
}
if (i > 0 && payloadSize > this.maxPayload) {
debug("only send %d out of %d packets", i, this.writeBuffer.length)
return this.writeBuffer.slice(0, i)
}
payloadSize += 2 // separator + packet type
}
debug("payload size is %d (max: %d)", payloadSize, this.maxPayload)
return this.writeBuffer
}
/** /**
* Sends a message. * Sends a message.
* *
@@ -497,12 +782,12 @@ export class Socket extends Emitter {
* @return {Socket} for chaining. * @return {Socket} for chaining.
* @api public * @api public
*/ */
write(msg, options, fn) { public write(msg, options, fn?) {
this.sendPacket("message", msg, options, fn) this.sendPacket("message", msg, options, fn)
return this return this
} }
send(msg, options, fn) { public send(msg, options, fn?) {
this.sendPacket("message", msg, options, fn) this.sendPacket("message", msg, options, fn)
return this return this
} }
@@ -516,7 +801,7 @@ export class Socket extends Emitter {
* @param {Function} callback function. * @param {Function} callback function.
* @api private * @api private
*/ */
sendPacket(type, data?, options?, fn?) { private sendPacket(type, data?, options?, fn?) {
if ("function" === typeof data) { if ("function" === typeof data) {
fn = data fn = data
data = undefined data = undefined
@@ -539,7 +824,7 @@ export class Socket extends Emitter {
data: data, data: data,
options: options options: options
} }
this.emit("packetCreate", packet) this.emitReserved("packetCreate", packet)
this.writeBuffer.push(packet) this.writeBuffer.push(packet)
if (fn) this.once("flush", fn) if (fn) this.once("flush", fn)
this.flush() this.flush()
@@ -548,9 +833,9 @@ export class Socket extends Emitter {
/** /**
* Closes the connection. * Closes the connection.
* *
* @api private * @api public
*/ */
close() { public close() {
const close = () => { const close = () => {
this.onClose("forced close") this.onClose("forced close")
debug("socket closing - telling transport to close") debug("socket closing - telling transport to close")
@@ -558,8 +843,8 @@ export class Socket extends Emitter {
} }
const cleanupAndClose = () => { const cleanupAndClose = () => {
this.removeListener("upgrade", cleanupAndClose) this.off("upgrade", cleanupAndClose)
this.removeListener("upgradeError", cleanupAndClose) this.off("upgradeError", cleanupAndClose)
close() close()
} }
@@ -595,10 +880,10 @@ export class Socket extends Emitter {
* *
* @api private * @api private
*/ */
onError(err) { private onError(err) {
debug("socket error %j", err) debug("socket error %j", err)
Socket.priorWebsocketSuccess = false Socket.priorWebsocketSuccess = false
this.emit("error", err) this.emitReserved("error", err)
this.onClose("transport error", err) this.onClose("transport error", err)
} }
@@ -607,7 +892,7 @@ export class Socket extends Emitter {
* *
* @api private * @api private
*/ */
onClose(reason, desc?) { private onClose(reason: string, description?: CloseDetails | Error) {
if ( if (
"opening" === this.readyState || "opening" === this.readyState ||
"open" === this.readyState || "open" === this.readyState ||
@@ -616,7 +901,6 @@ export class Socket extends Emitter {
debug('socket close with reason: "%s"', reason) debug('socket close with reason: "%s"', reason)
// clear timers // clear timers
this.clearTimeoutFn(this.pingIntervalTimer)
this.clearTimeoutFn(this.pingTimeoutTimer) this.clearTimeoutFn(this.pingTimeoutTimer)
// stop event from firing again for transport // stop event from firing again for transport
@@ -629,6 +913,11 @@ export class Socket extends Emitter {
this.transport.removeAllListeners() this.transport.removeAllListeners()
if (typeof removeEventListener === "function") { if (typeof removeEventListener === "function") {
removeEventListener(
"beforeunload",
this.beforeunloadEventListener,
false
)
removeEventListener("offline", this.offlineEventListener, false) removeEventListener("offline", this.offlineEventListener, false)
} }
@@ -639,7 +928,7 @@ export class Socket extends Emitter {
this.id = null this.id = null
// emit close event // emit close event
this.emit("close", reason, desc) this.emitReserved("close", reason, description)
// clean buffers after, so users can still // clean buffers after, so users can still
// grab the buffers on `close` event // grab the buffers on `close` event
@@ -655,7 +944,7 @@ export class Socket extends Emitter {
* @api private * @api private
* *
*/ */
filterUpgrades(upgrades) { private filterUpgrades(upgrades) {
const filteredUpgrades = [] const filteredUpgrades = []
let i = 0 let i = 0
const j = upgrades.length const j = upgrades.length
@@ -666,23 +955,3 @@ export class Socket extends Emitter {
return filteredUpgrades return filteredUpgrades
} }
} }
Socket.priorWebsocketSuccess = false
/**
* Protocol version.
*
* @api public
*/
Socket.protocol = parser.protocol // this is an int
function clone(obj) {
const o = {}
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
o[i] = obj[i]
}
}
return o
}

View File

@@ -1,15 +1,59 @@
import parser from "../engine.io-parser" // import { decodePacket, Packet, RawData } from "engine.io-parser"
const Emitter = require("component-emitter") import { decodePacket, Packet, RawData } from "../engine.io-parser"
import { Emitter } from "@socket.io/component-emitter"
import { installTimerFunctions } from "./util" import { installTimerFunctions } from "./util"
const debug = (...args: any) => console.debug('engine.io-client:transport', ...args)//require("debug")("engine.io-client:transport") // import debugModule from "debug"; // debug()
import { SocketOptions } from "./socket"
// const debug = debugModule("engine.io-client:transport"); // debug()
const debug = require('../debug')("engine.io-client:transport") // debug()
class TransportError extends Error {
public readonly type = "TransportError";
constructor(
reason: string,
readonly description: any,
readonly context: any
) {
super(reason)
}
}
export interface CloseDetails {
description: string
context?: CloseEvent | XMLHttpRequest
}
interface TransportReservedEvents {
open: () => void
error: (err: TransportError) => void
packet: (packet: Packet) => void
close: (details?: CloseDetails) => void
poll: () => void
pollComplete: () => void
drain: () => void
}
export abstract class Transport extends Emitter<
{},
{},
TransportReservedEvents
> {
protected opts: SocketOptions
protected supportsBinary: boolean
protected query: object
protected readyState: string
protected writable: boolean = false;
protected socket: any
protected setTimeoutFn: typeof setTimeout
export class Transport extends Emitter {
/** /**
* Transport abstract constructor. * Transport abstract constructor.
* *
* @param {Object} options. * @param {Object} options.
* @api private * @api private
*/ */
constructor(opts) { constructor(opts) {
super() super()
installTimerFunctions(this, opts) installTimerFunctions(this, opts)
@@ -23,15 +67,17 @@ export class Transport extends Emitter {
/** /**
* Emits an error. * Emits an error.
* *
* @param {String} str * @param {String} reason
* @param description
* @param context - the error context
* @return {Transport} for chaining * @return {Transport} for chaining
* @api public * @api protected
*/ */
onError(msg, desc) { protected onError(reason: string, description: any, context?: any) {
const err: any = new Error(msg) super.emitReserved(
err.type = "TransportError" "error",
err.description = desc new TransportError(reason, description, context)
this.emit("error", err) )
return this return this
} }
@@ -40,7 +86,7 @@ export class Transport extends Emitter {
* *
* @api public * @api public
*/ */
open() { private open() {
if ("closed" === this.readyState || "" === this.readyState) { if ("closed" === this.readyState || "" === this.readyState) {
this.readyState = "opening" this.readyState = "opening"
this.doOpen() this.doOpen()
@@ -52,9 +98,9 @@ export class Transport extends Emitter {
/** /**
* Closes the transport. * Closes the transport.
* *
* @api private * @api public
*/ */
close() { public close() {
if ("opening" === this.readyState || "open" === this.readyState) { if ("opening" === this.readyState || "open" === this.readyState) {
this.doClose() this.doClose()
this.onClose() this.onClose()
@@ -64,12 +110,12 @@ export class Transport extends Emitter {
} }
/** /**
* Sends multiple packets. * Sends multiple packets.
* *
* @param {Array} packets * @param {Array} packets
* @api private * @api public
*/ */
send(packets) { public send(packets) {
if ("open" === this.readyState) { if ("open" === this.readyState) {
this.write(packets) this.write(packets)
} else { } else {
@@ -81,39 +127,45 @@ export class Transport extends Emitter {
/** /**
* Called upon open * Called upon open
* *
* @api private * @api protected
*/ */
onOpen() { protected onOpen() {
this.readyState = "open" this.readyState = "open"
this.writable = true this.writable = true
this.emit("open") super.emitReserved("open")
} }
/** /**
* Called with data. * Called with data.
* *
* @param {String} data * @param {String} data
* @api private * @api protected
*/ */
onData(data) { protected onData(data: RawData) {
const packet = parser.decodePacket(data, this.socket.binaryType) const packet = decodePacket(data, this.socket.binaryType)
this.onPacket(packet) this.onPacket(packet)
} }
/** /**
* Called with a decoded packet. * Called with a decoded packet.
*
* @api protected
*/ */
onPacket(packet) { protected onPacket(packet: Packet) {
this.emit("packet", packet) super.emitReserved("packet", packet)
} }
/** /**
* Called upon close. * Called upon close.
* *
* @api private * @api protected
*/ */
onClose() { protected onClose(details?: CloseDetails) {
this.readyState = "closed" this.readyState = "closed"
this.emit("close") super.emitReserved("close", details)
} }
protected abstract doOpen()
protected abstract doClose()
protected abstract write(packets)
} }

View File

@@ -1,4 +1,5 @@
import { WS } from "./websocket" import { WS } from "./websocket.js"
export default {
'websocket': WS export const transports = {
websocket: WS,
} }

View File

@@ -0,0 +1,6 @@
import { WebSocket as ws } from "../../client"
export const WebSocket = ws
export const usingBrowserWebSocket = false
export const defaultBinaryType = "nodebuffer"
export const nextTick = process.nextTick

View File

@@ -1,21 +1,18 @@
import { Transport } from '../transport' import { Transport } from "../transport"
// const Transport = require("../transport") import { encode } from "../contrib/parseqs"
import parser from '../../engine.io-parser' import { yeast } from "../contrib/yeast"
// const parser = require("../engine.io-parser") import { pick } from "../util"
const parseqs = require("parseqs") import {
const yeast = require("yeast") defaultBinaryType,
import { pick } from '../util' nextTick,
// const { pick } = require("../util") usingBrowserWebSocket,
import { WebSocket } from '../../client' WebSocket
const usingBrowserWebSocket = true } from "./websocket-constructor"
// const { // import debugModule from "debug" // debug()
// WebSocket, import { encodePacket } from "../../engine.io-parser"
// usingBrowserWebSocket,
// defaultBinaryType,
// nextTick
// } = require("./websocket-constructor")
const debug = (...args: any) => console.debug('engine.io-client:websocket', ...args)//require("debug")("engine.io-client:websocket") // const debug = debugModule("engine.io-client:websocket") // debug()
const debug = (...args: any) => console.debug('engine.io-client:websocket', ...args)
// detect ReactNative environment // detect ReactNative environment
const isReactNative = const isReactNative =
@@ -24,6 +21,8 @@ const isReactNative =
navigator.product.toLowerCase() === "reactnative" navigator.product.toLowerCase() === "reactnative"
export class WS extends Transport { export class WS extends Transport {
private ws: any
/** /**
* WebSocket transport constructor. * WebSocket transport constructor.
* *
@@ -86,17 +85,17 @@ export class WS extends Transport {
} }
try { try {
this.ws = new WebSocket(uri, protocols) this.ws =
// usingBrowserWebSocket && !isReactNative usingBrowserWebSocket && !isReactNative
// ? protocols ? protocols
// ? new WebSocket(uri, protocols) ? new WebSocket(uri, protocols)
// : new WebSocket(uri) : new WebSocket(uri)
// : new WebSocket(uri, protocols, opts) : new WebSocket(uri, protocols, opts)
} catch (err) { } catch (err: any) {
return this.emit("error", err) return this.emitReserved("error", err)
} }
this.ws.binaryType = this.socket.binaryType || 'arraybuffer' this.ws.binaryType = this.socket.binaryType || defaultBinaryType
this.addEventListeners() this.addEventListeners()
} }
@@ -113,7 +112,11 @@ export class WS extends Transport {
} }
this.onOpen() this.onOpen()
} }
this.ws.onclose = this.onClose.bind(this) this.ws.onclose = closeEvent =>
this.onClose({
description: "websocket connection closed",
context: closeEvent
})
this.ws.onmessage = ev => this.onData(ev.data) this.ws.onmessage = ev => this.onData(ev.data)
this.ws.onerror = e => this.onError("websocket error", e) this.ws.onerror = e => this.onError("websocket error", e)
} }
@@ -133,9 +136,9 @@ export class WS extends Transport {
const packet = packets[i] const packet = packets[i]
const lastPacket = i === packets.length - 1 const lastPacket = i === packets.length - 1
parser.encodePacket(packet, this.supportsBinary, data => { encodePacket(packet, this.supportsBinary, data => {
// always create a new object (GH-437) // always create a new object (GH-437)
const opts: any = {} const opts: { compress?: boolean } = {}
if (!usingBrowserWebSocket) { if (!usingBrowserWebSocket) {
if (packet.options) { if (packet.options) {
opts.compress = packet.options.compress opts.compress = packet.options.compress
@@ -143,6 +146,7 @@ export class WS extends Transport {
if (this.opts.perMessageDeflate) { if (this.opts.perMessageDeflate) {
const len = const len =
// @ts-ignore
"string" === typeof data ? Buffer.byteLength(data) : data.length "string" === typeof data ? Buffer.byteLength(data) : data.length
if (len < this.opts.perMessageDeflate.threshold) { if (len < this.opts.perMessageDeflate.threshold) {
opts.compress = false opts.compress = false
@@ -160,31 +164,22 @@ export class WS extends Transport {
} else { } else {
this.ws.send(data, opts) this.ws.send(data, opts)
} }
} catch (error: any) { } catch (e) {
debug("websocket closed before onclose event") debug("websocket closed before onclose event")
} }
if (lastPacket) { if (lastPacket) {
// fake drain // fake drain
// defer to next tick to allow Socket to clear writeBuffer // defer to next tick to allow Socket to clear writeBuffer
process.nextTick(() => { nextTick(() => {
this.writable = true this.writable = true
this.emit("drain") this.emitReserved("drain")
}, this.setTimeoutFn) }, this.setTimeoutFn)
} }
}) })
} }
} }
/**
* Called upon close
*
* @api private
*/
onClose() {
Transport.prototype.onClose.call(this)
}
/** /**
* Closes socket. * Closes socket.
* *
@@ -203,7 +198,7 @@ export class WS extends Transport {
* @api private * @api private
*/ */
uri() { uri() {
let query = this.query || {} let query: { b64?: number } = this.query || {}
const schema = this.opts.secure ? "wss" : "ws" const schema = this.opts.secure ? "wss" : "ws"
let port = "" let port = ""
@@ -226,21 +221,16 @@ export class WS extends Transport {
query.b64 = 1 query.b64 = 1
} }
query = parseqs.encode(query) const encodedQuery = encode(query)
// prepend ? to query
if (query.length) {
query = "?" + query
}
const ipv6 = this.opts.hostname.indexOf(":") !== -1 const ipv6 = this.opts.hostname.indexOf(":") !== -1
return ( return (
schema + schema +
"://" + "://" +
(ipv6 ? "[" + this.opts.hostname + "]" : this.opts.hostname) + (ipv6 ? "[" + this.opts.hostname + "]" : this.opts.hostname) +
port + port +
this.opts.path + this.opts.path +
query (encodedQuery.length ? "?" + encodedQuery : "")
) )
} }
@@ -251,9 +241,6 @@ export class WS extends Transport {
* @api public * @api public
*/ */
check() { check() {
return ( return !!WebSocket
!!WebSocket &&
!("__initialize" in WebSocket && this.name === WS.prototype.name)
)
} }
} }

View File

@@ -1,4 +1,6 @@
const pick = (obj, ...attr) => { // import { globalThisShim as globalThis } from "./globalThis.js"
export function pick(obj, ...attr) {
return attr.reduce((acc, k) => { return attr.reduce((acc, k) => {
if (obj.hasOwnProperty(k)) { if (obj.hasOwnProperty(k)) {
acc[k] = obj[k] acc[k] = obj[k]
@@ -11,7 +13,7 @@ const pick = (obj, ...attr) => {
const NATIVE_SET_TIMEOUT = setTimeout const NATIVE_SET_TIMEOUT = setTimeout
const NATIVE_CLEAR_TIMEOUT = clearTimeout const NATIVE_CLEAR_TIMEOUT = clearTimeout
const installTimerFunctions = (obj, opts) => { export function installTimerFunctions(obj, opts) {
if (opts.useNativeTimers) { if (opts.useNativeTimers) {
obj.setTimeoutFn = NATIVE_SET_TIMEOUT.bind(globalThis) obj.setTimeoutFn = NATIVE_SET_TIMEOUT.bind(globalThis)
obj.clearTimeoutFn = NATIVE_CLEAR_TIMEOUT.bind(globalThis) obj.clearTimeoutFn = NATIVE_CLEAR_TIMEOUT.bind(globalThis)
@@ -20,4 +22,34 @@ const installTimerFunctions = (obj, opts) => {
obj.clearTimeoutFn = clearTimeout.bind(globalThis) obj.clearTimeoutFn = clearTimeout.bind(globalThis)
} }
} }
export { pick, installTimerFunctions }
// base64 encoded buffers are about 33% bigger (https://en.wikipedia.org/wiki/Base64)
const BASE64_OVERHEAD = 1.33
// we could also have used `new Blob([obj]).size`, but it isn't supported in IE9
export function byteLength(obj) {
if (typeof obj === "string") {
return utf8Length(obj)
}
// arraybuffer or blob
return Math.ceil((obj.byteLength || obj.size) * BASE64_OVERHEAD)
}
function utf8Length(str) {
let c = 0,
length = 0
for (let i = 0, l = str.length; i < l; i++) {
c = str.charCodeAt(i)
if (c < 0x80) {
length += 1
} else if (c < 0x800) {
length += 2
} else if (c < 0xd800 || c >= 0xe000) {
length += 3
} else {
i++
length += 4
}
}
return length
}

View File

@@ -12,10 +12,28 @@ Object.keys(PACKET_TYPES).forEach(key => {
PACKET_TYPES_REVERSE[PACKET_TYPES[key]] = key PACKET_TYPES_REVERSE[PACKET_TYPES[key]] = key
}) })
const ERROR_PACKET = { type: "error", data: "parser error" } const ERROR_PACKET: Packet = { type: "error", data: "parser error" }
export = { export { PACKET_TYPES, PACKET_TYPES_REVERSE, ERROR_PACKET }
PACKET_TYPES,
PACKET_TYPES_REVERSE, export type PacketType =
ERROR_PACKET | "open"
| "close"
| "ping"
| "pong"
| "message"
| "upgrade"
| "noop"
| "error"
// RawData should be "string | Buffer | ArrayBuffer | ArrayBufferView | Blob", but Blob does not exist in Node.js and
// requires to add the dom lib in tsconfig.json
export type RawData = any
export interface Packet {
type: PacketType
options?: { compress: boolean }
data?: RawData
} }
export type BinaryType = "nodebuffer" | "arraybuffer" | "blob"

View File

@@ -1,6 +1,15 @@
const { PACKET_TYPES_REVERSE, ERROR_PACKET } = require("./commons") import {
ERROR_PACKET,
PACKET_TYPES_REVERSE,
Packet,
BinaryType,
RawData
} from "./commons.js"
export const decodePacket = (encodedPacket, binaryType) => { const decodePacket = (
encodedPacket: RawData,
binaryType?: BinaryType
): Packet => {
if (typeof encodedPacket !== "string") { if (typeof encodedPacket !== "string") {
return { return {
type: "message", type: "message",
@@ -28,17 +37,18 @@ export const decodePacket = (encodedPacket, binaryType) => {
} }
} }
const mapBinary = (data, binaryType) => { const mapBinary = (data: RawData, binaryType?: BinaryType) => {
const isBuffer = Buffer.isBuffer(data)
switch (binaryType) { switch (binaryType) {
case "arraybuffer": case "arraybuffer":
return Buffer.isBuffer(data) ? toArrayBuffer(data) : data return isBuffer ? toArrayBuffer(data) : data
case "nodebuffer": case "nodebuffer":
default: default:
return data // assuming the data is already a Buffer return data // assuming the data is already a Buffer
} }
} }
const toArrayBuffer = buffer => { const toArrayBuffer = (buffer: Buffer): ArrayBuffer => {
const arrayBuffer = new ArrayBuffer(buffer.length) const arrayBuffer = new ArrayBuffer(buffer.length)
const view = new Uint8Array(arrayBuffer) const view = new Uint8Array(arrayBuffer)
for (let i = 0; i < buffer.length; i++) { for (let i = 0; i < buffer.length; i++) {
@@ -46,3 +56,5 @@ const toArrayBuffer = buffer => {
} }
return arrayBuffer return arrayBuffer
} }
export default decodePacket

View File

@@ -1,7 +1,10 @@
const { PACKET_TYPES } = require("./commons") import { PACKET_TYPES, Packet, RawData } from "./commons.js"
export const encodePacket = ({ type, data }, supportsBinary, callback) => { const encodePacket = (
console.trace('encodePacket', type, JSON.stringify(data)) { type, data }: Packet,
supportsBinary: boolean,
callback: (encodedPacket: RawData) => void
) => {
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) { if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
const buffer = toBuffer(data) const buffer = toBuffer(data)
return callback(encodeBuffer(buffer, supportsBinary)) return callback(encodeBuffer(buffer, supportsBinary))
@@ -21,6 +24,8 @@ const toBuffer = data => {
} }
// only 'message' packets can contain binary, so the type prefix is not needed // only 'message' packets can contain binary, so the type prefix is not needed
const encodeBuffer = (data, supportsBinary) => { const encodeBuffer = (data: Buffer, supportsBinary: boolean): RawData => {
return supportsBinary ? data : "b" + data.toString("base64") return supportsBinary ? data : "b" + data.toString("base64")
} }
export default encodePacket

View File

@@ -1,9 +1,13 @@
import { encodePacket } from "./encodePacket" import encodePacket from "./encodePacket.js"
import { decodePacket } from "./decodePacket" import decodePacket from "./decodePacket.js"
import { Packet, PacketType, RawData, BinaryType } from "./commons.js"
const SEPARATOR = String.fromCharCode(30) // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text const SEPARATOR = String.fromCharCode(30) // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
const encodePayload = (packets, callback) => { const encodePayload = (
packets: Packet[],
callback: (encodedPayload: string) => void
) => {
// some packets may be added to the array while encoding, so the initial length must be saved // some packets may be added to the array while encoding, so the initial length must be saved
const length = packets.length const length = packets.length
const encodedPackets = new Array(length) const encodedPackets = new Array(length)
@@ -20,7 +24,10 @@ const encodePayload = (packets, callback) => {
}) })
} }
const decodePayload = (encodedPayload, binaryType) => { const decodePayload = (
encodedPayload: string,
binaryType?: BinaryType
): Packet[] => {
const encodedPackets = encodedPayload.split(SEPARATOR) const encodedPackets = encodedPayload.split(SEPARATOR)
const packets = [] const packets = []
for (let i = 0; i < encodedPackets.length; i++) { for (let i = 0; i < encodedPackets.length; i++) {
@@ -33,10 +40,14 @@ const decodePayload = (encodedPayload, binaryType) => {
return packets return packets
} }
export default { export const protocol = 4
protocol: 4, export {
encodePacket, encodePacket,
encodePayload, encodePayload,
decodePacket, decodePacket,
decodePayload decodePayload,
Packet,
PacketType,
RawData,
BinaryType
} }

View File

@@ -1,10 +1,45 @@
// import { createServer } from "http"
import { Server, AttachOptions, ServerOptions } from "./server"
import transports from "./transports/index"
import * as parser from "../engine.io-parser"
// export { Server, transports, listen, attach, parser }
export { Server, transports, attach, parser }
export { AttachOptions, ServerOptions } from "./server"
// export { uServer } from "./userver";
export { Socket } from "./socket"
export { Transport } from "./transport"
export const protocol = parser.protocol
/** /**
* Module dependencies. * Creates an http.Server exclusively used for WS upgrades.
*
* @param {Number} port
* @param {Function} callback
* @param {Object} options
* @return {Server} websocket.io server
* @api public
*/ */
// const http = require("http") // function listen(port, options: AttachOptions & ServerOptions, fn) {
// const Server = require("./server") // if ("function" === typeof options) {
import { Server } from './server' // fn = options;
// options = {};
// }
// const server = createServer(function(req, res) {
// res.writeHead(501);
// res.end("Not Implemented");
// });
// // create engine server
// const engine = attach(server, options);
// engine.httpServer = server;
// server.listen(port, fn);
// return engine;
// }
/** /**
* Captures upgrade requests for a http.Server. * Captures upgrade requests for a http.Server.
@@ -15,12 +50,8 @@ import { Server } from './server'
* @api public * @api public
*/ */
function attach(srv, options) { function attach(server, options: AttachOptions & ServerOptions) {
const engine = new Server(options) const engine = new Server(options)
engine.attach(srv, options) engine.attach(server, options)
return engine return engine
} }
export = {
attach
}

View File

@@ -0,0 +1,484 @@
// imported from https://github.com/socketio/engine.io-parser/tree/2.2.x
/**
* Module dependencies.
*/
var utf8 = require('./utf8')
/**
* Current protocol version.
*/
export const protocol = 3
const hasBinary = (packets) => {
for (const packet of packets) {
if (packet.data instanceof ArrayBuffer || ArrayBuffer.isView(packet.data)) {
return true
}
}
return false
}
/**
* Packet types.
*/
export const packets = {
open: 0 // non-ws
, close: 1 // non-ws
, ping: 2
, pong: 3
, message: 4
, upgrade: 5
, noop: 6
}
var packetslist = Object.keys(packets)
/**
* Premade error packet.
*/
var err = { type: 'error', data: 'parser error' }
const EMPTY_BUFFER = Buffer.concat([])
/**
* Encodes a packet.
*
* <packet type id> [ <data> ]
*
* Example:
*
* 5hello world
* 3
* 4
*
* Binary is encoded in an identical principle
*
* @api private
*/
export function encodePacket(packet, supportsBinary, utf8encode, callback) {
if (typeof supportsBinary === 'function') {
callback = supportsBinary
supportsBinary = null
}
if (typeof utf8encode === 'function') {
callback = utf8encode
utf8encode = null
}
if (Buffer.isBuffer(packet.data)) {
return encodeBuffer(packet, supportsBinary, callback)
} else if (packet.data && (packet.data.buffer || packet.data) instanceof ArrayBuffer) {
return encodeBuffer({ type: packet.type, data: arrayBufferToBuffer(packet.data) }, supportsBinary, callback)
}
// Sending data as a utf-8 string
var encoded = packets[packet.type]
// data fragment is optional
if (undefined !== packet.data) {
encoded += utf8encode ? utf8.encode(String(packet.data), { strict: false }) : String(packet.data)
}
return callback('' + encoded)
};
/**
* Encode Buffer data
*/
function encodeBuffer(packet, supportsBinary, callback) {
if (!supportsBinary) {
return encodeBase64Packet(packet, callback)
}
var data = packet.data
var typeBuffer = Buffer.allocUnsafe(1)
typeBuffer[0] = packets[packet.type]
return callback(Buffer.concat([typeBuffer, data]))
}
/**
* Encodes a packet with binary data in a base64 string
*
* @param {Object} packet, has `type` and `data`
* @return {String} base64 encoded message
*/
export function encodeBase64Packet(packet, callback) {
var data = Buffer.isBuffer(packet.data) ? packet.data : arrayBufferToBuffer(packet.data)
var message = 'b' + packets[packet.type]
message += data.toString('base64')
return callback(message)
};
/**
* Decodes a packet. Data also available as an ArrayBuffer if requested.
*
* @return {Object} with `type` and `data` (if any)
* @api private
*/
export function decodePacket(data, binaryType, utf8decode) {
if (data === undefined) {
return err
}
var type
// String data
if (typeof data === 'string') {
type = data.charAt(0)
if (type === 'b') {
return decodeBase64Packet(data.slice(1), binaryType)
}
if (utf8decode) {
data = tryDecode(data)
if (data === false) {
return err
}
}
if (Number(type) != type || !packetslist[type]) {
return err
}
if (data.length > 1) {
return { type: packetslist[type], data: data.slice(1) }
} else {
return { type: packetslist[type] }
}
}
// Binary data
if (binaryType === 'arraybuffer') {
// wrap Buffer/ArrayBuffer data into an Uint8Array
var intArray = new Uint8Array(data)
type = intArray[0]
return { type: packetslist[type], data: intArray.buffer.slice(1) }
}
if (data instanceof ArrayBuffer) {
data = arrayBufferToBuffer(data)
}
type = data[0]
return { type: packetslist[type], data: data.slice(1) }
};
function tryDecode(data) {
try {
data = utf8.decode(data, { strict: false })
} catch (e) {
return false
}
return data
}
/**
* Decodes a packet encoded in a base64 string.
*
* @param {String} base64 encoded message
* @return {Object} with `type` and `data` (if any)
*/
export function decodeBase64Packet(msg, binaryType) {
var type = packetslist[msg.charAt(0)]
var data = Buffer.from(msg.slice(1), 'base64')
if (binaryType === 'arraybuffer') {
var abv = new Uint8Array(data.length)
for (var i = 0; i < abv.length; i++) {
abv[i] = data[i]
}
// @ts-ignore
data = abv.buffer
}
return { type: type, data: data }
};
/**
* Encodes multiple messages (payload).
*
* <length>:data
*
* Example:
*
* 11:hello world2:hi
*
* If any contents are binary, they will be encoded as base64 strings. Base64
* encoded strings are marked with a b before the length specifier
*
* @param {Array} packets
* @api private
*/
export function encodePayload(packets, supportsBinary, callback) {
if (typeof supportsBinary === 'function') {
callback = supportsBinary
supportsBinary = null
}
if (supportsBinary && hasBinary(packets)) {
return encodePayloadAsBinary(packets, callback)
}
if (!packets.length) {
return callback('0:')
}
function encodeOne(packet, doneCallback) {
encodePacket(packet, supportsBinary, false, function (message) {
doneCallback(null, setLengthHeader(message))
})
}
map(packets, encodeOne, function (err, results) {
return callback(results.join(''))
})
};
function setLengthHeader(message) {
return message.length + ':' + message
}
/**
* Async array map using after
*/
function map(ary, each, done) {
const results = new Array(ary.length)
let count = 0
for (let i = 0; i < ary.length; i++) {
each(ary[i], (error, msg) => {
results[i] = msg
if (++count === ary.length) {
done(null, results)
}
})
}
}
/*
* Decodes data when a payload is maybe expected. Possible binary contents are
* decoded from their base64 representation
*
* @param {String} data, callback method
* @api public
*/
export function decodePayload(data, binaryType, callback) {
if (typeof data !== 'string') {
return decodePayloadAsBinary(data, binaryType, callback)
}
if (typeof binaryType === 'function') {
callback = binaryType
binaryType = null
}
if (data === '') {
// parser error - ignoring payload
return callback(err, 0, 1)
}
var length = '', n, msg, packet
for (var i = 0, l = data.length; i < l; i++) {
var chr = data.charAt(i)
if (chr !== ':') {
length += chr
continue
}
// @ts-ignore
if (length === '' || (length != (n = Number(length)))) {
// parser error - ignoring payload
return callback(err, 0, 1)
}
msg = data.slice(i + 1, i + 1 + n)
if (length != msg.length) {
// parser error - ignoring payload
return callback(err, 0, 1)
}
if (msg.length) {
packet = decodePacket(msg, binaryType, false)
if (err.type === packet.type && err.data === packet.data) {
// parser error in individual packet - ignoring payload
return callback(err, 0, 1)
}
var more = callback(packet, i + n, l)
if (false === more) return
}
// advance cursor
i += n
length = ''
}
if (length !== '') {
// parser error - ignoring payload
return callback(err, 0, 1)
}
};
/**
*
* Converts a buffer to a utf8.js encoded string
*
* @api private
*/
function bufferToString(buffer) {
var str = ''
for (var i = 0, l = buffer.length; i < l; i++) {
str += String.fromCharCode(buffer[i])
}
return str
}
/**
*
* Converts a utf8.js encoded string to a buffer
*
* @api private
*/
function stringToBuffer(string) {
var buf = Buffer.allocUnsafe(string.length)
for (var i = 0, l = string.length; i < l; i++) {
buf.writeUInt8(string.charCodeAt(i), i)
}
return buf
}
/**
*
* Converts an ArrayBuffer to a Buffer
*
* @api private
*/
function arrayBufferToBuffer(data) {
// data is either an ArrayBuffer or ArrayBufferView.
var length = data.byteLength || data.length
var offset = data.byteOffset || 0
return Buffer.from(data.buffer || data, offset, length)
}
/**
* Encodes multiple messages (payload) as binary.
*
* <1 = binary, 0 = string><number from 0-9><number from 0-9>[...]<number
* 255><data>
*
* Example:
* 1 3 255 1 2 3, if the binary contents are interpreted as 8 bit integers
*
* @param {Array} packets
* @return {Buffer} encoded payload
* @api private
*/
export function encodePayloadAsBinary(packets, callback) {
if (!packets.length) {
return callback(EMPTY_BUFFER)
}
map(packets, encodeOneBinaryPacket, function (err, results) {
return callback(Buffer.concat(results))
})
};
function encodeOneBinaryPacket(p, doneCallback) {
function onBinaryPacketEncode(packet) {
var encodingLength = '' + packet.length
var sizeBuffer
if (typeof packet === 'string') {
sizeBuffer = Buffer.allocUnsafe(encodingLength.length + 2)
sizeBuffer[0] = 0 // is a string (not true binary = 0)
for (var i = 0; i < encodingLength.length; i++) {
sizeBuffer[i + 1] = parseInt(encodingLength[i], 10)
}
sizeBuffer[sizeBuffer.length - 1] = 255
return doneCallback(null, Buffer.concat([sizeBuffer, stringToBuffer(packet)]))
}
sizeBuffer = Buffer.allocUnsafe(encodingLength.length + 2)
sizeBuffer[0] = 1 // is binary (true binary = 1)
for (var i = 0; i < encodingLength.length; i++) {
sizeBuffer[i + 1] = parseInt(encodingLength[i], 10)
}
sizeBuffer[sizeBuffer.length - 1] = 255
doneCallback(null, Buffer.concat([sizeBuffer, packet]))
}
encodePacket(p, true, true, onBinaryPacketEncode)
}
/*
* Decodes data when a payload is maybe expected. Strings are decoded by
* interpreting each byte as a key code for entries marked to start with 0. See
* description of encodePayloadAsBinary
* @param {Buffer} data, callback method
* @api public
*/
export function decodePayloadAsBinary(data, binaryType, callback) {
if (typeof binaryType === 'function') {
callback = binaryType
binaryType = null
}
var bufferTail = data
var buffers = []
var i
while (bufferTail.length > 0) {
var strLen = ''
var isString = bufferTail[0] === 0
for (i = 1; ; i++) {
if (bufferTail[i] === 255) break
// 310 = char length of Number.MAX_VALUE
if (strLen.length > 310) {
return callback(err, 0, 1)
}
strLen += '' + bufferTail[i]
}
bufferTail = bufferTail.slice(strLen.length + 1)
var msgLength = parseInt(strLen, 10)
var msg = bufferTail.slice(1, msgLength + 1)
if (isString) msg = bufferToString(msg)
buffers.push(msg)
bufferTail = bufferTail.slice(msgLength + 1)
}
var total = buffers.length
for (i = 0; i < total; i++) {
var buffer = buffers[i]
callback(decodePacket(buffer, binaryType, true), i, total)
}
}

View File

@@ -0,0 +1,210 @@
/*! https://mths.be/utf8js v2.1.2 by @mathias */
var stringFromCharCode = String.fromCharCode
// Taken from https://mths.be/punycode
function ucs2decode(string) {
var output = []
var counter = 0
var length = string.length
var value
var extra
while (counter < length) {
value = string.charCodeAt(counter++)
if (value >= 0xD800 && value <= 0xDBFF && counter < length) {
// high surrogate, and there is a next character
extra = string.charCodeAt(counter++)
if ((extra & 0xFC00) == 0xDC00) { // low surrogate
output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000)
} else {
// unmatched surrogate; only append this code unit, in case the next
// code unit is the high surrogate of a surrogate pair
output.push(value)
counter--
}
} else {
output.push(value)
}
}
return output
}
// Taken from https://mths.be/punycode
function ucs2encode(array) {
var length = array.length
var index = -1
var value
var output = ''
while (++index < length) {
value = array[index]
if (value > 0xFFFF) {
value -= 0x10000
output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800)
value = 0xDC00 | value & 0x3FF
}
output += stringFromCharCode(value)
}
return output
}
function checkScalarValue(codePoint, strict) {
if (codePoint >= 0xD800 && codePoint <= 0xDFFF) {
if (strict) {
throw Error(
'Lone surrogate U+' + codePoint.toString(16).toUpperCase() +
' is not a scalar value'
)
}
return false
}
return true
}
/*--------------------------------------------------------------------------*/
function createByte(codePoint, shift) {
return stringFromCharCode(((codePoint >> shift) & 0x3F) | 0x80)
}
function encodeCodePoint(codePoint, strict) {
if ((codePoint & 0xFFFFFF80) == 0) { // 1-byte sequence
return stringFromCharCode(codePoint)
}
var symbol = ''
if ((codePoint & 0xFFFFF800) == 0) { // 2-byte sequence
symbol = stringFromCharCode(((codePoint >> 6) & 0x1F) | 0xC0)
}
else if ((codePoint & 0xFFFF0000) == 0) { // 3-byte sequence
if (!checkScalarValue(codePoint, strict)) {
codePoint = 0xFFFD
}
symbol = stringFromCharCode(((codePoint >> 12) & 0x0F) | 0xE0)
symbol += createByte(codePoint, 6)
}
else if ((codePoint & 0xFFE00000) == 0) { // 4-byte sequence
symbol = stringFromCharCode(((codePoint >> 18) & 0x07) | 0xF0)
symbol += createByte(codePoint, 12)
symbol += createByte(codePoint, 6)
}
symbol += stringFromCharCode((codePoint & 0x3F) | 0x80)
return symbol
}
function utf8encode(string, opts) {
opts = opts || {}
var strict = false !== opts.strict
var codePoints = ucs2decode(string)
var length = codePoints.length
var index = -1
var codePoint
var byteString = ''
while (++index < length) {
codePoint = codePoints[index]
byteString += encodeCodePoint(codePoint, strict)
}
return byteString
}
/*--------------------------------------------------------------------------*/
function readContinuationByte() {
if (byteIndex >= byteCount) {
throw Error('Invalid byte index')
}
var continuationByte = byteArray[byteIndex] & 0xFF
byteIndex++
if ((continuationByte & 0xC0) == 0x80) {
return continuationByte & 0x3F
}
// If we end up here, its not a continuation byte
throw Error('Invalid continuation byte')
}
function decodeSymbol(strict) {
var byte1
var byte2
var byte3
var byte4
var codePoint
if (byteIndex > byteCount) {
throw Error('Invalid byte index')
}
if (byteIndex == byteCount) {
return false
}
// Read first byte
byte1 = byteArray[byteIndex] & 0xFF
byteIndex++
// 1-byte sequence (no continuation bytes)
if ((byte1 & 0x80) == 0) {
return byte1
}
// 2-byte sequence
if ((byte1 & 0xE0) == 0xC0) {
byte2 = readContinuationByte()
codePoint = ((byte1 & 0x1F) << 6) | byte2
if (codePoint >= 0x80) {
return codePoint
} else {
throw Error('Invalid continuation byte')
}
}
// 3-byte sequence (may include unpaired surrogates)
if ((byte1 & 0xF0) == 0xE0) {
byte2 = readContinuationByte()
byte3 = readContinuationByte()
codePoint = ((byte1 & 0x0F) << 12) | (byte2 << 6) | byte3
if (codePoint >= 0x0800) {
return checkScalarValue(codePoint, strict) ? codePoint : 0xFFFD
} else {
throw Error('Invalid continuation byte')
}
}
// 4-byte sequence
if ((byte1 & 0xF8) == 0xF0) {
byte2 = readContinuationByte()
byte3 = readContinuationByte()
byte4 = readContinuationByte()
codePoint = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0C) |
(byte3 << 0x06) | byte4
if (codePoint >= 0x010000 && codePoint <= 0x10FFFF) {
return codePoint
}
}
throw Error('Invalid UTF-8 detected')
}
var byteArray
var byteCount
var byteIndex
function utf8decode(byteString, opts) {
opts = opts || {}
var strict = false !== opts.strict
byteArray = ucs2decode(byteString)
byteCount = byteArray.length
byteIndex = 0
var codePoints = []
var tmp
while ((tmp = decodeSymbol(strict)) !== false) {
codePoints.push(tmp)
}
return ucs2encode(codePoints)
}
module.exports = {
version: '2.1.2',
encode: utf8encode,
decode: utf8decode
}

View File

@@ -1,52 +1,141 @@
const qs = require("querystring") import * as qs from "querystring"
const parse = require("url").parse import { parse } from "url"
// const base64id = require("base64id") // const base64id = require("base64id")
import transports from './transports' import transports from "./transports"
import { EventEmitter } from 'events' import { EventEmitter } from "events"
// const EventEmitter = require("events").EventEmitter import { Socket } from "./socket"
import { Socket } from './socket' // import debugModule from "debug"
// const debug = require("debug")("engine") // import { serialize } from "cookie"
const debug = function (...args) { } // import { Server as DEFAULT_WS_ENGINE } from "ws"
// const cookieMod = require("cookie") import { WebSocketServer as DEFAULT_WS_ENGINE } from "../server"
// import { IncomingMessage, Server as HttpServer } from "http"
// const DEFAULT_WS_ENGINE = require("ws").Server; // import { CookieSerializeOptions } from "cookie"
import { WebSocketServer } from '../server' // import { CorsOptions, CorsOptionsDelegate } from "cors"
import { Transport } from './transport'
const DEFAULT_WS_ENGINE = WebSocketServer
// const debug = debugModule("engine");
const debug = require("../debug")("engine")
import { Request } from '../server/request' import { Request } from '../server/request'
import { WebSocketClient } from '../server/client' import { WebSocketClient } from '../server/client'
export class Server extends EventEmitter { type Transport = "polling" | "websocket"
public static errors = {
UNKNOWN_TRANSPORT: 0,
UNKNOWN_SID: 1,
BAD_HANDSHAKE_METHOD: 2,
BAD_REQUEST: 3,
FORBIDDEN: 4,
UNSUPPORTED_PROTOCOL_VERSION: 5
}
public static errorMessages = { export interface AttachOptions {
0: "Transport unknown", /**
1: "Session ID unknown", * name of the path to capture
2: "Bad handshake method", * @default "/engine.io"
3: "Bad request", */
4: "Forbidden", path?: string
5: "Unsupported protocol version" /**
} * destroy unhandled upgrade requests
* @default true
*/
destroyUpgrade?: boolean
/**
* milliseconds after which unhandled requests are ended
* @default 1000
*/
destroyUpgradeTimeout?: number
}
private clients = {} export interface ServerOptions {
private clientsCount = 0 /**
public opts: any * how many ms without a pong packet to consider the connection closed
* @default 20000
*/
pingTimeout?: number
/**
* how many ms before sending a new ping packet
* @default 25000
*/
pingInterval?: number
/**
* how many ms before an uncompleted transport upgrade is cancelled
* @default 10000
*/
upgradeTimeout?: number
/**
* how many bytes or characters a message can be, before closing the session (to avoid DoS).
* @default 1e5 (100 KB)
*/
maxHttpBufferSize?: number
/**
* A function that receives a given handshake or upgrade request as its first parameter,
* and can decide whether to continue or not. The second argument is a function that needs
* to be called with the decided information: fn(err, success), where success is a boolean
* value where false means that the request is rejected, and err is an error code.
*/
// allowRequest?: (
// req: IncomingMessage,
// fn: (err: string | null | undefined, success: boolean) => void
// ) => void
/**
* the low-level transports that are enabled
* @default ["polling", "websocket"]
*/
transports?: Transport[]
/**
* whether to allow transport upgrades
* @default true
*/
allowUpgrades?: boolean
/**
* parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable.
* @default false
*/
perMessageDeflate?: boolean | object
/**
* parameters of the http compression for the polling transports (see zlib api docs). Set to false to disable.
* @default true
*/
httpCompression?: boolean | object
/**
* what WebSocket server implementation to use. Specified module must
* conform to the ws interface (see ws module api docs).
* An alternative c++ addon is also available by installing eiows module.
*
* @default `require("ws").Server`
*/
wsEngine?: any
/**
* an optional packet which will be concatenated to the handshake packet emitted by Engine.IO.
*/
initialPacket?: any
/**
* configuration of the cookie that contains the client sid to send as part of handshake response headers. This cookie
* might be used for sticky-session. Defaults to not sending any cookie.
* @default false
*/
cookie?: (/*CookieSerializeOptions & */{ name: string }) | boolean
/**
* the options that will be forwarded to the cors module
*/
// cors?: CorsOptions | CorsOptionsDelegate
/**
* whether to enable compatibility with Socket.IO v2 clients
* @default false
*/
allowEIO3?: boolean
}
private corsMiddleware: any export abstract class BaseServer extends EventEmitter {
public opts: ServerOptions
private ws: any protected clients: any
private perMessageDeflate: any private clientsCount: number
protected corsMiddleware: Function
constructor(opts: any = {}) { /**
* Server constructor.
*
* @param {Object} opts - options
* @api public
*/
constructor(opts: ServerOptions = {}) {
super() super()
this.clients = {}
this.clientsCount = 0
this.opts = Object.assign( this.opts = Object.assign(
{ {
wsEngine: DEFAULT_WS_ENGINE, wsEngine: DEFAULT_WS_ENGINE,
@@ -70,6 +159,7 @@ export class Server extends EventEmitter {
// { // {
// name: "io", // name: "io",
// path: "/", // path: "/",
// // @ts-ignore
// httpOnly: opts.cookie.path !== false, // httpOnly: opts.cookie.path !== false,
// sameSite: "lax" // sameSite: "lax"
// }, // },
@@ -93,42 +183,7 @@ export class Server extends EventEmitter {
// this.init() // this.init()
} }
// /** protected abstract init()
// * Initialize websocket server
// *
// * @api private
// */
// init() {
// if (!~this.opts.transports.indexOf("websocket")) return
// if (this.ws) this.ws.close()
// this.ws = new this.opts.wsEngine({
// noServer: true,
// clientTracking: false,
// perMessageDeflate: this.opts.perMessageDeflate,
// maxPayload: this.opts.maxHttpBufferSize
// })
// if (typeof this.ws.on === "function") {
// this.ws.on("headers", (headersArray, req) => {
// // note: 'ws' uses an array of headers, while Engine.IO uses an object (response.writeHead() accepts both formats)
// // we could also try to parse the array and then sync the values, but that will be error-prone
// const additionalHeaders = {}
// const isInitialRequest = !req._query.sid
// if (isInitialRequest) {
// this.emit("initial_headers", additionalHeaders, req)
// }
// this.emit("headers", additionalHeaders, req)
// Object.keys(additionalHeaders).forEach(key => {
// headersArray.push(`${key}: ${additionalHeaders[key]}`)
// })
// })
// }
// }
/** /**
* Returns a list of available transports for upgrade given a certain transport. * Returns a list of available transports for upgrade given a certain transport.
@@ -136,7 +191,7 @@ export class Server extends EventEmitter {
* @return {Array} * @return {Array}
* @api public * @api public
*/ */
upgrades(transport): Array<any> { public upgrades(transport) {
if (!this.opts.allowUpgrades) return [] if (!this.opts.allowUpgrades) return []
return transports[transport].upgradesTo || [] return transports[transport].upgradesTo || []
} }
@@ -148,7 +203,7 @@ export class Server extends EventEmitter {
// * @return {Boolean} whether the request is valid // * @return {Boolean} whether the request is valid
// * @api private // * @api private
// */ // */
// verify(req, upgrade, fn) { // protected verify(req, upgrade, fn) {
// // transport check // // transport check
// const transport = req._query.transport // const transport = req._query.transport
// if (!~this.opts.transports.indexOf(transport)) { // if (!~this.opts.transports.indexOf(transport)) {
@@ -194,6 +249,13 @@ export class Server extends EventEmitter {
// }) // })
// } // }
// if (transport === "websocket" && !upgrade) {
// debug("invalid transport upgrade")
// return fn(Server.errors.BAD_REQUEST, {
// name: "TRANSPORT_HANDSHAKE_ERROR"
// })
// }
// if (!this.opts.allowRequest) return fn() // if (!this.opts.allowRequest) return fn()
// return this.opts.allowRequest(req, (message, success) => { // return this.opts.allowRequest(req, (message, success) => {
@@ -209,36 +271,234 @@ export class Server extends EventEmitter {
// fn() // fn()
// } // }
/**
* Prepares a request by processing the query string.
*
* @api private
*/
prepare(req) {
// try to leverage pre-existing `req._query` (e.g: from connect)
if (!req._query) {
req._query = ~req.url.indexOf("?") ? qs.parse(parse(req.url).query) : {}
}
}
/** /**
* Closes all clients. * Closes all clients.
* *
* @api public * @api public
*/ */
close() { public close() {
debug("closing all open clients") debug("closing all open clients")
for (let i in this.clients) { for (let i in this.clients) {
if (this.clients.hasOwnProperty(i)) { if (this.clients.hasOwnProperty(i)) {
this.clients[i].close(true) this.clients[i].close(true)
} }
} }
this.cleanup()
return this
}
protected abstract cleanup()
/**
* generate a socket id.
* Overwrite this method to generate your custom socket id
*
* @param {Object} request object
* @api public
*/
public generateId(req) {
// return base64id.generateId()
return req.id
}
/**
* Handshakes a new client.
*
* @param {String} transport name
* @param {Object} request object
* @param {Function} closeConnection
*
* @api protected
*/
// protected async handshake(transportName, req, closeConnection) {
// @java-patch sync handshake
protected handshake(transportName, req, closeConnection) {
const protocol = req._query.EIO === "4" ? 4 : 3 // 3rd revision by default
if (protocol === 3 && !this.opts.allowEIO3) {
debug("unsupported protocol version")
this.emit("connection_error", {
req,
code: Server.errors.UNSUPPORTED_PROTOCOL_VERSION,
message:
Server.errorMessages[Server.errors.UNSUPPORTED_PROTOCOL_VERSION],
context: {
protocol
}
})
closeConnection(Server.errors.UNSUPPORTED_PROTOCOL_VERSION)
return
}
let id
try {
id = this.generateId(req)
} catch (e) {
debug("error while generating an id")
this.emit("connection_error", {
req,
code: Server.errors.BAD_REQUEST,
message: Server.errorMessages[Server.errors.BAD_REQUEST],
context: {
name: "ID_GENERATION_ERROR",
error: e
}
})
closeConnection(Server.errors.BAD_REQUEST)
return
}
debug('handshaking client "%s"', id)
try {
var transport = this.createTransport(transportName, req)
if ("websocket" !== transportName) {
throw new Error('Unsupport polling at MiaoScript!')
}
// if ("polling" === transportName) {
// transport.maxHttpBufferSize = this.opts.maxHttpBufferSize
// transport.httpCompression = this.opts.httpCompression
// } else if ("websocket" === transportName) {
transport.perMessageDeflate = this.opts.perMessageDeflate
// }
if (req._query && req._query.b64) {
transport.supportsBinary = false
} else {
transport.supportsBinary = true
}
} catch (e) {
debug('error handshaking to transport "%s"', transportName)
this.emit("connection_error", {
req,
code: Server.errors.BAD_REQUEST,
message: Server.errorMessages[Server.errors.BAD_REQUEST],
context: {
name: "TRANSPORT_HANDSHAKE_ERROR",
error: e
}
})
closeConnection(Server.errors.BAD_REQUEST)
return
}
const socket = new Socket(id, this, transport, req, protocol)
transport.on("headers", (headers, req) => {
const isInitialRequest = !req._query.sid
if (isInitialRequest) {
if (this.opts.cookie) {
headers["Set-Cookie"] = [
// serialize(this.opts.cookie.name, id, this.opts.cookie)
]
}
this.emit("initial_headers", headers, req)
}
this.emit("headers", headers, req)
})
transport.onRequest(req)
this.clients[id] = socket
this.clientsCount++
socket.once("close", () => {
delete this.clients[id]
this.clientsCount--
})
this.emit("connection", socket)
return transport
}
protected abstract createTransport(transportName, req)
/**
* Protocol errors mappings.
*/
static errors = {
UNKNOWN_TRANSPORT: 0,
UNKNOWN_SID: 1,
BAD_HANDSHAKE_METHOD: 2,
BAD_REQUEST: 3,
FORBIDDEN: 4,
UNSUPPORTED_PROTOCOL_VERSION: 5
};
static errorMessages = {
0: "Transport unknown",
1: "Session ID unknown",
2: "Bad handshake method",
3: "Bad request",
4: "Forbidden",
5: "Unsupported protocol version"
};
}
export class Server extends BaseServer {
// public httpServer?: HttpServer
private ws: any
/**
* Initialize websocket server
*
* @api protected
*/
protected init() {
if (!~this.opts.transports.indexOf("websocket")) return
if (this.ws) this.ws.close()
this.ws = new this.opts.wsEngine({
noServer: true,
clientTracking: false,
perMessageDeflate: this.opts.perMessageDeflate,
maxPayload: this.opts.maxHttpBufferSize
})
if (typeof this.ws.on === "function") {
this.ws.on("headers", (headersArray, req) => {
// note: 'ws' uses an array of headers, while Engine.IO uses an object (response.writeHead() accepts both formats)
// we could also try to parse the array and then sync the values, but that will be error-prone
const additionalHeaders = {}
const isInitialRequest = !req._query.sid
if (isInitialRequest) {
this.emit("initial_headers", additionalHeaders, req)
}
this.emit("headers", additionalHeaders, req)
Object.keys(additionalHeaders).forEach(key => {
headersArray.push(`${key}: ${additionalHeaders[key]}`)
})
})
}
}
protected cleanup() {
if (this.ws) { if (this.ws) {
debug("closing webSocketServer") debug("closing webSocketServer")
this.ws.close() this.ws.close()
// don't delete this.ws because it can be used again if the http server starts listening again // don't delete this.ws because it can be used again if the http server starts listening again
} }
return this }
/**
* Prepares a request by processing the query string.
*
* @api private
*/
private prepare(req) {
// try to leverage pre-existing `req._query` (e.g: from connect)
if (!req._query) {
req._query = ~req.url.indexOf("?") ? qs.parse(parse(req.url).query) : {}
}
}
protected createTransport(transportName, req) {
return new transports[transportName](req)
} }
// /** // /**
@@ -248,7 +508,7 @@ export class Server extends EventEmitter {
// * @param {http.ServerResponse|http.OutgoingMessage} response // * @param {http.ServerResponse|http.OutgoingMessage} response
// * @api public // * @api public
// */ // */
// handleRequest(req, res) { // public handleRequest(req, res) {
// debug('handling "%s" http request "%s"', req.method, req.url) // debug('handling "%s" http request "%s"', req.method, req.url)
// this.prepare(req) // this.prepare(req)
// req.res = res // req.res = res
@@ -284,131 +544,12 @@ export class Server extends EventEmitter {
// } // }
// } // }
/**
* generate a socket id.
* Overwrite this method to generate your custom socket id
*
* @param {Object} request object
* @api public
*/
generateId(req) {
return req.id
}
/**
* Handshakes a new client.
*
* @param {String} transport name
* @param {Object} request object
* @param {Function} closeConnection
*
* @api private
*/
// @java-patch sync handshake
handshake(transportName, req, closeConnection: (code: number) => void) {
console.debug('engine.io server handshake transport', transportName, 'from', req.url)
const protocol = req._query.EIO === "4" ? 4 : 3 // 3rd revision by default
if (protocol === 3 && !this.opts.allowEIO3) {
debug("unsupported protocol version")
this.emit("connection_error", {
req,
code: Server.errors.UNSUPPORTED_PROTOCOL_VERSION,
message:
Server.errorMessages[Server.errors.UNSUPPORTED_PROTOCOL_VERSION],
context: {
protocol
}
})
closeConnection(Server.errors.UNSUPPORTED_PROTOCOL_VERSION)
return
}
let id
try {
id = this.generateId(req)
} catch (error: any) {
console.debug("error while generating an id")
this.emit("connection_error", {
req,
code: Server.errors.BAD_REQUEST,
message: Server.errorMessages[Server.errors.BAD_REQUEST],
context: {
name: "ID_GENERATION_ERROR",
error
}
})
closeConnection(Server.errors.BAD_REQUEST)
return
}
console.debug('engine.io server handshaking client "' + id + '"')
try {
var transport: Transport = new transports[transportName](req)
if ("websocket" !== transportName) {
throw new Error('Unsupport polling at MiaoScript!')
}
// if ("polling" === transportName) {
// transport.maxHttpBufferSize = this.opts.maxHttpBufferSize
// transport.httpCompression = this.opts.httpCompression
// } else if ("websocket" === transportName) {
transport.perMessageDeflate = this.opts.perMessageDeflate
// }
if (req._query && req._query.b64) {
transport.supportsBinary = false
} else {
transport.supportsBinary = true
}
} catch (e: any) {
console.ex(e)
this.emit("connection_error", {
req,
code: Server.errors.BAD_REQUEST,
message: Server.errorMessages[Server.errors.BAD_REQUEST],
context: {
name: "TRANSPORT_HANDSHAKE_ERROR",
error: e
}
})
closeConnection(Server.errors.BAD_REQUEST)
return
}
console.debug(`engine.io server create socket ${id} from transport ${transport.name} protocol ${protocol}`)
const socket = new Socket(id, this, transport, req, protocol)
transport.on("headers", (headers, req) => {
const isInitialRequest = !req._query.sid
if (isInitialRequest) {
if (this.opts.cookie) {
headers["Set-Cookie"] = [
// cookieMod.serialize(this.opts.cookie.name, id, this.opts.cookie)
]
}
this.emit("initial_headers", headers, req)
}
this.emit("headers", headers, req)
})
transport.onRequest(req)
this.clients[id] = socket
this.clientsCount++
socket.once("close", () => {
delete this.clients[id]
this.clientsCount--
})
this.emit("connection", socket)
}
// /** // /**
// * Handles an Engine.IO HTTP Upgrade. // * Handles an Engine.IO HTTP Upgrade.
// * // *
// * @api public // * @api public
// */ // */
// handleUpgrade(req, socket, upgradeHead) { // public handleUpgrade(req, socket, upgradeHead) {
// this.prepare(req) // this.prepare(req)
// this.verify(req, true, (errorCode, errorContext) => { // this.verify(req, true, (errorCode, errorContext) => {
@@ -423,7 +564,7 @@ export class Server extends EventEmitter {
// return // return
// } // }
// const head = Buffer.from(upgradeHead) // eslint-disable-line node/no-deprecated-api // const head = Buffer.from(upgradeHead)
// upgradeHead = null // upgradeHead = null
// // delegate to ws // // delegate to ws
@@ -439,14 +580,14 @@ export class Server extends EventEmitter {
* @param {ws.Socket} websocket * @param {ws.Socket} websocket
* @api private * @api private
*/ */
onWebSocket(req: Request, socket, websocket: WebSocketClient) { private onWebSocket(req: Request, socket, websocket: WebSocketClient) {
websocket.on("error", onUpgradeError) websocket.on("error", onUpgradeError)
if ( if (
transports[req._query.transport] !== undefined && transports[req._query.transport] !== undefined &&
!transports[req._query.transport].prototype.handlesUpgrades !transports[req._query.transport].prototype.handlesUpgrades
) { ) {
console.debug("transport doesnt handle upgraded requests") debug("transport doesnt handle upgraded requests")
websocket.close() websocket.close()
return return
} }
@@ -460,40 +601,37 @@ export class Server extends EventEmitter {
if (id) { if (id) {
const client = this.clients[id] const client = this.clients[id]
if (!client) { if (!client) {
console.debug("upgrade attempt for closed client") debug("upgrade attempt for closed client")
websocket.close() websocket.close()
} else if (client.upgrading) { } else if (client.upgrading) {
console.debug("transport has already been trying to upgrade") debug("transport has already been trying to upgrade")
websocket.close() websocket.close()
} else if (client.upgraded) { } else if (client.upgraded) {
console.debug("transport had already been upgraded") debug("transport had already been upgraded")
websocket.close() websocket.close()
} else { } else {
console.debug("upgrading existing transport") debug("upgrading existing transport")
// transport error handling takes over // transport error handling takes over
websocket.removeListener("error", onUpgradeError) websocket.removeListener("error", onUpgradeError)
const transport = new transports[req._query.transport](req) const transport = this.createTransport(req._query.transport, req)
if (req._query && req._query.b64) { if (req._query && req._query.b64) {
transport.supportsBinary = false transport.supportsBinary = false
} else { } else {
transport.supportsBinary = true transport.supportsBinary = true
} }
transport.perMessageDeflate = this.perMessageDeflate transport.perMessageDeflate = this.opts.perMessageDeflate
client.maybeUpgrade(transport) client.maybeUpgrade(transport)
} }
} else { } else {
// transport error handling takes over
websocket.removeListener("error", onUpgradeError)
// const closeConnection = (errorCode, errorContext) => // const closeConnection = (errorCode, errorContext) =>
// abortUpgrade(socket, errorCode, errorContext) // abortUpgrade(socket, errorCode, errorContext)
this.handshake(req._query.transport, req, () => { }) this.handshake(req._query.transport, req, () => { })
} }
function onUpgradeError() { function onUpgradeError(...args) {
console.debug("websocket error before upgrade") debug("websocket error before upgrade %s", ...args)
// websocket.close() not needed // websocket.close() not needed
} }
} }
@@ -505,7 +643,9 @@ export class Server extends EventEmitter {
* @param {Object} options * @param {Object} options
* @api public * @api public
*/ */
attach(server, options: any = {}) { // public attach(server: HttpServer, options: AttachOptions = {}) {
// @java-patch
public attach(server, options: AttachOptions = {}) {
// let path = (options.path || "/engine.io").replace(/\/$/, "") // let path = (options.path || "/engine.io").replace(/\/$/, "")
// const destroyUpgradeTimeout = options.destroyUpgradeTimeout || 1000 // const destroyUpgradeTimeout = options.destroyUpgradeTimeout || 1000
@@ -514,7 +654,7 @@ export class Server extends EventEmitter {
// path += "/" // path += "/"
// function check(req) { // function check(req) {
// return path === req.url.substr(0, path.length) // return path === req.url.slice(0, path.length)
// } // }
// cache and clean up listeners // cache and clean up listeners
@@ -555,7 +695,11 @@ export class Server extends EventEmitter {
// // and if no eio thing handles the upgrade // // and if no eio thing handles the upgrade
// // then the socket needs to die! // // then the socket needs to die!
// setTimeout(function () { // setTimeout(function () {
// // @ts-ignore
// if (socket.writable && socket.bytesWritten <= 0) { // if (socket.writable && socket.bytesWritten <= 0) {
// socket.on("error", e => {
// debug("error while destroying upgrade: %s", e.message)
// })
// return socket.end() // return socket.end()
// } // }
// }, destroyUpgradeTimeout) // }, destroyUpgradeTimeout)
@@ -601,7 +745,11 @@ export class Server extends EventEmitter {
// * @api private // * @api private
// */ // */
// function abortUpgrade(socket, errorCode, errorContext: any = {}) { // function abortUpgrade(
// socket,
// errorCode,
// errorContext: { message?: string } = {}
// ) {
// socket.on("error", () => { // socket.on("error", () => {
// debug("ignoring error from closed connection") // debug("ignoring error from closed connection")
// }) // })
@@ -622,8 +770,6 @@ export class Server extends EventEmitter {
// socket.destroy() // socket.destroy()
// } // }
// module.exports = Server
/* eslint-disable */ /* eslint-disable */
// /** // /**

View File

@@ -1,38 +1,63 @@
import { EventEmitter } from "events" import { EventEmitter } from "events"
import { Server } from "./server" // import debugModule from "debug"
// import { IncomingMessage } from "http"
import { Transport } from "./transport" import { Transport } from "./transport"
import type { Request } from "../server/request" import { Server } from "./server"
// const debug = require("debug")("engine:socket") // import { setTimeout, clearTimeout } from "timers"
// import { Packet, PacketType, RawData } from "engine.io-parser"
import { Packet, PacketType, RawData } from "../engine.io-parser"
// const debug = debugModule("engine:socket")
const debug = require('../debug')("engine:socket")
export class Socket extends EventEmitter { export class Socket extends EventEmitter {
public id: string public readonly protocol: number
private server: Server // public readonly request: IncomingMessage
private upgrading = false public readonly request: any
private upgraded = false public readonly remoteAddress: string
public readyState = "opening"
private writeBuffer = [] public _readyState: string
private packetsFn = []
private sentCallbackFn = []
private cleanupFn = []
public request: Request
public protocol: number
public remoteAddress: any
public transport: Transport public transport: Transport
private checkIntervalTimer: NodeJS.Timeout private server: Server
private upgradeTimeoutTimer: NodeJS.Timeout private upgrading: boolean
private pingTimeoutTimer: NodeJS.Timeout private upgraded: boolean
private pingIntervalTimer: NodeJS.Timeout private writeBuffer: Packet[]
private packetsFn: any[]
private sentCallbackFn: any[]
private cleanupFn: any[]
private checkIntervalTimer
private upgradeTimeoutTimer
private pingTimeoutTimer
private pingIntervalTimer
private readonly id: string
get readyState() {
return this._readyState
}
set readyState(state) {
debug("readyState updated from %s to %s", this._readyState, state)
this._readyState = state
}
/** /**
* Client class (abstract). * Client class (abstract).
* *
* @api private * @api private
*/ */
constructor(id: string, server: Server, transport: Transport, req: Request, protocol: number) { constructor(id, server, transport, req, protocol) {
super() super()
this.id = id this.id = id
this.server = server this.server = server
this.upgrading = false
this.upgraded = false
this.readyState = "opening"
this.writeBuffer = []
this.packetsFn = []
this.sentCallbackFn = []
this.cleanupFn = []
this.request = req this.request = req
this.protocol = protocol this.protocol = protocol
@@ -57,7 +82,7 @@ export class Socket extends EventEmitter {
* *
* @api private * @api private
*/ */
onOpen() { private onOpen() {
this.readyState = "open" this.readyState = "open"
// sends an `open` packet // sends an `open` packet
@@ -68,7 +93,8 @@ export class Socket extends EventEmitter {
sid: this.id, sid: this.id,
upgrades: this.getAvailableUpgrades(), upgrades: this.getAvailableUpgrades(),
pingInterval: this.server.opts.pingInterval, pingInterval: this.server.opts.pingInterval,
pingTimeout: this.server.opts.pingTimeout pingTimeout: this.server.opts.pingTimeout,
maxPayload: this.server.opts.maxHttpBufferSize
}) })
) )
@@ -95,13 +121,12 @@ export class Socket extends EventEmitter {
* @param {Object} packet * @param {Object} packet
* @api private * @api private
*/ */
onPacket(packet: { type: any; data: any }) { private onPacket(packet: Packet) {
if ("open" !== this.readyState) { if ("open" !== this.readyState) {
console.debug("packet received with closed socket") return debug("packet received with closed socket")
return
} }
// export packet event // export packet event
// debug(`received packet ${packet.type}`) debug(`received packet ${packet.type}`)
this.emit("packet", packet) this.emit("packet", packet)
// Reset ping timeout on any packet, incoming data is a good sign of // Reset ping timeout on any packet, incoming data is a good sign of
@@ -116,7 +141,7 @@ export class Socket extends EventEmitter {
this.onError("invalid heartbeat direction") this.onError("invalid heartbeat direction")
return return
} }
// debug("got ping") debug("got ping")
this.sendPacket("pong") this.sendPacket("pong")
this.emit("heartbeat") this.emit("heartbeat")
break break
@@ -126,7 +151,8 @@ export class Socket extends EventEmitter {
this.onError("invalid heartbeat direction") this.onError("invalid heartbeat direction")
return return
} }
// debug("got pong") debug("got pong")
// this.pingIntervalTimer.refresh()
this.schedulePing() this.schedulePing()
this.emit("heartbeat") this.emit("heartbeat")
break break
@@ -148,8 +174,8 @@ export class Socket extends EventEmitter {
* @param {Error} error object * @param {Error} error object
* @api private * @api private
*/ */
onError(err: string) { private onError(err) {
// debug("transport error") debug("transport error")
this.onClose("transport error", err) this.onClose("transport error", err)
} }
@@ -159,13 +185,12 @@ export class Socket extends EventEmitter {
* *
* @api private * @api private
*/ */
schedulePing() { private schedulePing() {
clearTimeout(this.pingIntervalTimer)
this.pingIntervalTimer = setTimeout(() => { this.pingIntervalTimer = setTimeout(() => {
// debug( debug(
// "writing ping packet - expecting pong within %sms", "writing ping packet - expecting pong within %sms",
// this.server.opts.pingTimeout this.server.opts.pingTimeout
// ) )
this.sendPacket("ping") this.sendPacket("ping")
this.resetPingTimeout(this.server.opts.pingTimeout) this.resetPingTimeout(this.server.opts.pingTimeout)
}, this.server.opts.pingInterval) }, this.server.opts.pingInterval)
@@ -176,7 +201,7 @@ export class Socket extends EventEmitter {
* *
* @api private * @api private
*/ */
resetPingTimeout(timeout: number) { private resetPingTimeout(timeout) {
clearTimeout(this.pingTimeoutTimer) clearTimeout(this.pingTimeoutTimer)
this.pingTimeoutTimer = setTimeout(() => { this.pingTimeoutTimer = setTimeout(() => {
if (this.readyState === "closed") return if (this.readyState === "closed") return
@@ -190,8 +215,7 @@ export class Socket extends EventEmitter {
* @param {Transport} transport * @param {Transport} transport
* @api private * @api private
*/ */
setTransport(transport: Transport) { private setTransport(transport) {
console.debug(`engine.io socket ${this.id} set transport ${transport.name}`)
const onError = this.onError.bind(this) const onError = this.onError.bind(this)
const onPacket = this.onPacket.bind(this) const onPacket = this.onPacket.bind(this)
const flush = this.flush.bind(this) const flush = this.flush.bind(this)
@@ -219,30 +243,33 @@ export class Socket extends EventEmitter {
* @param {Transport} transport * @param {Transport} transport
* @api private * @api private
*/ */
maybeUpgrade(transport: Transport) { private maybeUpgrade(transport) {
console.debug( debug(
'might upgrade socket transport from "', this.transport.name, '" to "', transport.name, '"' 'might upgrade socket transport from "%s" to "%s"',
this.transport.name,
transport.name
) )
this.upgrading = true this.upgrading = true
// set transport upgrade timer // set transport upgrade timer
this.upgradeTimeoutTimer = setTimeout(() => { this.upgradeTimeoutTimer = setTimeout(() => {
console.debug("client did not complete upgrade - closing transport") debug("client did not complete upgrade - closing transport")
cleanup() cleanup()
if ("open" === transport.readyState) { if ("open" === transport.readyState) {
transport.close() transport.close()
} }
}, this.server.opts.upgradeTimeout) }, this.server.opts.upgradeTimeout)
const onPacket = (packet: { type: string; data: string }) => { const onPacket = packet => {
if ("ping" === packet.type && "probe" === packet.data) { if ("ping" === packet.type && "probe" === packet.data) {
debug("got probe ping packet, sending pong")
transport.send([{ type: "pong", data: "probe" }]) transport.send([{ type: "pong", data: "probe" }])
this.emit("upgrading", transport) this.emit("upgrading", transport)
clearInterval(this.checkIntervalTimer) clearInterval(this.checkIntervalTimer)
this.checkIntervalTimer = setInterval(check, 100) this.checkIntervalTimer = setInterval(check, 100)
} else if ("upgrade" === packet.type && this.readyState !== "closed") { } else if ("upgrade" === packet.type && this.readyState !== "closed") {
// debug("got upgrade packet - upgrading") debug("got upgrade packet - upgrading")
cleanup() cleanup()
this.transport.discard() this.transport.discard()
this.upgraded = true this.upgraded = true
@@ -264,7 +291,7 @@ export class Socket extends EventEmitter {
// we force a polling cycle to ensure a fast upgrade // we force a polling cycle to ensure a fast upgrade
const check = () => { const check = () => {
if ("polling" === this.transport.name && this.transport.writable) { if ("polling" === this.transport.name && this.transport.writable) {
// debug("writing a noop packet to polling for fast upgrade") debug("writing a noop packet to polling for fast upgrade")
this.transport.send([{ type: "noop" }]) this.transport.send([{ type: "noop" }])
} }
} }
@@ -284,8 +311,8 @@ export class Socket extends EventEmitter {
this.removeListener("close", onClose) this.removeListener("close", onClose)
} }
const onError = (err: string) => { const onError = err => {
// debug("client did not complete upgrade - %s", err) debug("client did not complete upgrade - %s", err)
cleanup() cleanup()
transport.close() transport.close()
transport = null transport = null
@@ -311,8 +338,8 @@ export class Socket extends EventEmitter {
* *
* @api private * @api private
*/ */
clearTransport() { private clearTransport() {
let cleanup: () => void let cleanup
const toCleanUp = this.cleanupFn.length const toCleanUp = this.cleanupFn.length
@@ -323,7 +350,7 @@ export class Socket extends EventEmitter {
// silence further transport errors and prevent uncaught exceptions // silence further transport errors and prevent uncaught exceptions
this.transport.on("error", function () { this.transport.on("error", function () {
// debug("error triggered by discarded transport") debug("error triggered by discarded transport")
}) })
// ensure transport won't stay open // ensure transport won't stay open
@@ -337,7 +364,7 @@ export class Socket extends EventEmitter {
* Possible reasons: `ping timeout`, `client error`, `parse error`, * Possible reasons: `ping timeout`, `client error`, `parse error`,
* `transport error`, `server close`, `transport close` * `transport error`, `server close`, `transport close`
*/ */
onClose(reason: string, description?: string) { private onClose(reason: string, description?) {
if ("closed" !== this.readyState) { if ("closed" !== this.readyState) {
this.readyState = "closed" this.readyState = "closed"
@@ -365,16 +392,16 @@ export class Socket extends EventEmitter {
* *
* @api private * @api private
*/ */
setupSendCallback() { private setupSendCallback() {
// the message was sent successfully, execute the callback // the message was sent successfully, execute the callback
const onDrain = () => { const onDrain = () => {
if (this.sentCallbackFn.length > 0) { if (this.sentCallbackFn.length > 0) {
const seqFn = this.sentCallbackFn.splice(0, 1)[0] const seqFn = this.sentCallbackFn.splice(0, 1)[0]
if ("function" === typeof seqFn) { if ("function" === typeof seqFn) {
// debug("executing send callback") debug("executing send callback")
seqFn(this.transport) seqFn(this.transport)
} else if (Array.isArray(seqFn)) { } else if (Array.isArray(seqFn)) {
// debug("executing batch send callback") debug("executing batch send callback")
const l = seqFn.length const l = seqFn.length
let i = 0 let i = 0
for (; i < l; i++) { for (; i < l; i++) {
@@ -396,18 +423,18 @@ export class Socket extends EventEmitter {
/** /**
* Sends a message packet. * Sends a message packet.
* *
* @param {String} message * @param {Object} data
* @param {Object} options * @param {Object} options
* @param {Function} callback * @param {Function} callback
* @return {Socket} for chaining * @return {Socket} for chaining
* @api public * @api public
*/ */
send(data: any, options: any, callback: any) { public send(data, options, callback?) {
this.sendPacket("message", data, options, callback) this.sendPacket("message", data, options, callback)
return this return this
} }
write(data: any, options: any, callback?: any) { public write(data, options, callback?) {
this.sendPacket("message", data, options, callback) this.sendPacket("message", data, options, callback)
return this return this
} }
@@ -415,12 +442,14 @@ export class Socket extends EventEmitter {
/** /**
* Sends a packet. * Sends a packet.
* *
* @param {String} packet type * @param {String} type - packet type
* @param {String} optional, data * @param {String} data
* @param {Object} options * @param {Object} options
* @param {Function} callback
*
* @api private * @api private
*/ */
sendPacket(type: string, data?: string, options?: { compress?: any }, callback?: undefined) { private sendPacket(type: PacketType, data?: RawData, options?, callback?) {
if ("function" === typeof options) { if ("function" === typeof options) {
callback = options callback = options
options = null options = null
@@ -430,12 +459,13 @@ export class Socket extends EventEmitter {
options.compress = false !== options.compress options.compress = false !== options.compress
if ("closing" !== this.readyState && "closed" !== this.readyState) { if ("closing" !== this.readyState && "closed" !== this.readyState) {
// console.debug('sending packet "%s" (%s)', type, data) debug('sending packet "%s" (%s)', type, data)
const packet: any = { const packet: Packet = {
type: type, type,
options: options options
} }
if (data) packet.data = data if (data) packet.data = data
// exports packetCreate event // exports packetCreate event
@@ -455,13 +485,13 @@ export class Socket extends EventEmitter {
* *
* @api private * @api private
*/ */
flush() { private flush() {
if ( if (
"closed" !== this.readyState && "closed" !== this.readyState &&
this.transport.writable && this.transport.writable &&
this.writeBuffer.length this.writeBuffer.length
) { ) {
console.trace("flushing buffer to transport") debug("flushing buffer to transport")
this.emit("flush", this.writeBuffer) this.emit("flush", this.writeBuffer)
this.server.emit("flush", this, this.writeBuffer) this.server.emit("flush", this, this.writeBuffer)
const wbuf = this.writeBuffer const wbuf = this.writeBuffer
@@ -483,7 +513,7 @@ export class Socket extends EventEmitter {
* *
* @api private * @api private
*/ */
getAvailableUpgrades() { private getAvailableUpgrades() {
const availableUpgrades = [] const availableUpgrades = []
const allUpgrades = this.server.upgrades(this.transport.name) const allUpgrades = this.server.upgrades(this.transport.name)
let i = 0 let i = 0
@@ -500,11 +530,11 @@ export class Socket extends EventEmitter {
/** /**
* Closes the socket and underlying transport. * Closes the socket and underlying transport.
* *
* @param {Boolean} optional, discard * @param {Boolean} discard - optional, discard the transport
* @return {Socket} for chaining * @return {Socket} for chaining
* @api public * @api public
*/ */
close(discard?: any) { public close(discard?: boolean) {
if ("open" !== this.readyState) return if ("open" !== this.readyState) return
this.readyState = "closing" this.readyState = "closing"
@@ -523,7 +553,7 @@ export class Socket extends EventEmitter {
* @param {Boolean} discard * @param {Boolean} discard
* @api private * @api private
*/ */
closeTransport(discard: any) { private closeTransport(discard) {
if (discard) this.transport.discard() if (discard) this.transport.discard()
this.transport.close(this.onClose.bind(this, "forced close")) this.transport.close(this.onClose.bind(this, "forced close"))
} }

View File

@@ -1,6 +1,13 @@
import { EventEmitter } from 'events' import { EventEmitter } from "events"
import parser_v4 from "../engine.io-parser" import * as parser_v4 from "../engine.io-parser"
import type { WebSocketClient } from '../server/client' import * as parser_v3 from "./parser-v3"
// import debugModule from "debug"
import { IncomingMessage } from "http"
import { Packet } from "../engine.io-parser"
// const debug = debugModule("engine:transport")
const debug = require('../debug')("engine:transport")
/** /**
* Noop function. * Noop function.
* *
@@ -11,15 +18,28 @@ function noop() { }
export abstract class Transport extends EventEmitter { export abstract class Transport extends EventEmitter {
public sid: string public sid: string
public req /**http.IncomingMessage */
public socket: WebSocketClient
public writable: boolean public writable: boolean
public readyState: string public protocol: number
public discarded: boolean
public protocol: Number protected _readyState: string
public parser: any protected discarded: boolean
public perMessageDeflate: any protected parser: any
public supportsBinary: boolean = false protected req: IncomingMessage & { cleanup: Function }
protected supportsBinary: boolean
get readyState() {
return this._readyState
}
set readyState(state) {
debug(
"readyState updated from %s to %s (%s)",
this._readyState,
state,
this.name
)
this._readyState = state
}
/** /**
* Transport constructor. * Transport constructor.
@@ -32,7 +52,7 @@ export abstract class Transport extends EventEmitter {
this.readyState = "open" this.readyState = "open"
this.discarded = false this.discarded = false
this.protocol = req._query.EIO === "4" ? 4 : 3 // 3rd revision by default this.protocol = req._query.EIO === "4" ? 4 : 3 // 3rd revision by default
this.parser = parser_v4//= this.protocol === 4 ? parser_v4 : parser_v3 this.parser = this.protocol === 4 ? parser_v4 : parser_v3
} }
/** /**
@@ -48,10 +68,10 @@ export abstract class Transport extends EventEmitter {
* Called with an incoming HTTP request. * Called with an incoming HTTP request.
* *
* @param {http.IncomingMessage} request * @param {http.IncomingMessage} request
* @api private * @api protected
*/ */
onRequest(req) { protected onRequest(req) {
console.debug(`engine.io transport ${this.socket.id} setting request`, JSON.stringify(req)) debug("setting request")
this.req = req this.req = req
} }
@@ -72,16 +92,18 @@ export abstract class Transport extends EventEmitter {
* *
* @param {String} message error * @param {String} message error
* @param {Object} error description * @param {Object} error description
* @api private * @api protected
*/ */
onError(msg: string, desc?: string) { protected onError(msg: string, desc?) {
if (this.listeners("error").length) { if (this.listeners("error").length) {
const err: any = new Error(msg) const err = new Error(msg)
// @ts-ignore
err.type = "TransportError" err.type = "TransportError"
// @ts-ignore
err.description = desc err.description = desc
this.emit("error", err) this.emit("error", err)
} else { } else {
console.debug(`ignored transport error ${msg} (${desc})`) debug("ignored transport error %s (%s)", msg, desc)
} }
} }
@@ -89,9 +111,9 @@ export abstract class Transport extends EventEmitter {
* Called with parsed out a packets from the data stream. * Called with parsed out a packets from the data stream.
* *
* @param {Object} packet * @param {Object} packet
* @api private * @api protected
*/ */
onPacket(packet) { protected onPacket(packet: Packet) {
this.emit("packet", packet) this.emit("packet", packet)
} }
@@ -99,23 +121,24 @@ export abstract class Transport extends EventEmitter {
* Called with the encoded packet data. * Called with the encoded packet data.
* *
* @param {String} data * @param {String} data
* @api private * @api protected
*/ */
onData(data) { protected onData(data) {
this.onPacket(this.parser.decodePacket(data)) this.onPacket(this.parser.decodePacket(data))
} }
/** /**
* Called upon transport close. * Called upon transport close.
* *
* @api private * @api protected
*/ */
onClose() { protected onClose() {
this.readyState = "closed" this.readyState = "closed"
this.emit("close") this.emit("close")
} }
abstract get supportsFraming() abstract get supportsFraming()
abstract get name() abstract get name()
abstract send(...args: any[]) abstract send(packets)
abstract doClose(d: Function) abstract doClose(fn?)
} }

View File

@@ -1,3 +1,24 @@
// import { Polling as XHR } from "./polling"
// import { JSONP } from "./polling-jsonp"
import { WebSocket } from "./websocket"
export default { export default {
websocket: require("./websocket").WebSocket // polling: polling,
websocket: WebSocket
} }
// /**
// * Polling polymorphic constructor.
// *
// * @api private
// */
// function polling(req) {
// if ("string" === typeof req._query.j) {
// return new JSONP(req)
// } else {
// return new XHR(req)
// }
// }
// polling.upgradesTo = ["websocket"]

View File

@@ -1,8 +1,11 @@
import { Transport } from '../transport' import { Transport } from "../transport"
// const debug = require("debug")("engine:ws") // import debugModule from "debug";
const debug = require('../../debug')("engine:ws")
export class WebSocket extends Transport { export class WebSocket extends Transport {
public perMessageDeflate: any protected perMessageDeflate: any
private socket: any
/** /**
* WebSocket transport * WebSocket transport
@@ -13,7 +16,11 @@ export class WebSocket extends Transport {
constructor(req) { constructor(req) {
super(req) super(req)
this.socket = req.websocket this.socket = req.websocket
this.socket.on("message", this.onData.bind(this)) this.socket.on("message", (data, isBinary) => {
const message = isBinary ? data : data.toString()
debug('received "%s"', message)
super.onData(message)
})
this.socket.once("close", this.onClose.bind(this)) this.socket.once("close", this.onClose.bind(this))
this.socket.on("error", this.onError.bind(this)) this.socket.on("error", this.onError.bind(this))
this.writable = true this.writable = true
@@ -21,10 +28,10 @@ export class WebSocket extends Transport {
} }
/** /**
* Transport name * Transport name
* *
* @api public * @api public
*/ */
get name() { get name() {
return "websocket" return "websocket"
} }
@@ -47,17 +54,6 @@ export class WebSocket extends Transport {
return true return true
} }
/**
* Processes the incoming data.
*
* @param {String} encoded packet
* @api private
*/
onData(data) {
// debug('received "%s"', data)
super.onData(data)
}
/** /**
* Writes a packet payload. * Writes a packet payload.
* *
@@ -65,7 +61,6 @@ export class WebSocket extends Transport {
* @api private * @api private
*/ */
send(packets) { send(packets) {
// console.log('WebSocket send packets', JSON.stringify(packets))
const packet = packets.shift() const packet = packets.shift()
if (typeof packet === "undefined") { if (typeof packet === "undefined") {
this.writable = true this.writable = true
@@ -74,7 +69,7 @@ export class WebSocket extends Transport {
} }
// always creates a new object since ws modifies it // always creates a new object since ws modifies it
const opts: any = {} const opts: { compress?: boolean } = {}
if (packet.options) { if (packet.options) {
opts.compress = packet.options.compress opts.compress = packet.options.compress
} }
@@ -87,7 +82,7 @@ export class WebSocket extends Transport {
opts.compress = false opts.compress = false
} }
} }
console.trace('writing', data) debug('writing "%s"', data)
this.writable = false this.writable = false
this.socket.send(data, opts, err => { this.socket.send(data, opts, err => {
@@ -109,7 +104,7 @@ export class WebSocket extends Transport {
* @api private * @api private
*/ */
doClose(fn) { doClose(fn) {
// debug("closing") debug("closing")
this.socket.close() this.socket.close()
fn && fn() fn && fn()
} }

View File

@@ -1,6 +1,8 @@
import { EventEmitter } from "events" import { EventEmitter } from "events"
export type SocketId = string export type SocketId = string
// we could extend the Room type to "string | number", but that would be a breaking change
// related: https://github.com/socketio/socket.io-redis-adapter/issues/418
export type Room = string export type Room = string
export interface BroadcastFlags { export interface BroadcastFlags {
@@ -9,11 +11,12 @@ export interface BroadcastFlags {
local?: boolean local?: boolean
broadcast?: boolean broadcast?: boolean
binary?: boolean binary?: boolean
timeout?: number
} }
export interface BroadcastOptions { export interface BroadcastOptions {
rooms: Set<Room> rooms: Set<Room>
except?: Set<SocketId> except?: Set<Room>
flags?: BroadcastFlags flags?: BroadcastFlags
} }
@@ -42,6 +45,15 @@ export class Adapter extends EventEmitter {
*/ */
public close(): Promise<void> | void { } public close(): Promise<void> | void { }
/**
* Returns the number of Socket.IO servers in the cluster
*
* @public
*/
public serverCount(): Promise<number> {
return Promise.resolve(1)
}
/** /**
* Adds a socket to a list of room. * Adds a socket to a list of room.
* *
@@ -82,14 +94,14 @@ export class Adapter extends EventEmitter {
this._del(room, id) this._del(room, id)
} }
private _del(room, id) { private _del(room: Room, id: SocketId) {
if (this.rooms.has(room)) { const _room = this.rooms.get(room)
const deleted = this.rooms.get(room).delete(id) if (_room != null) {
const deleted = _room.delete(id)
if (deleted) { if (deleted) {
this.emit("leave-room", room, id) this.emit("leave-room", room, id)
} }
if (this.rooms.get(room).size === 0) { if (_room.size === 0 && this.rooms.delete(room)) {
this.rooms.delete(room)
this.emit("delete-room", room) this.emit("delete-room", room)
} }
} }
@@ -126,7 +138,7 @@ export class Adapter extends EventEmitter {
*/ */
public broadcast(packet: any, opts: BroadcastOptions): void { public broadcast(packet: any, opts: BroadcastOptions): void {
const flags = opts.flags || {} const flags = opts.flags || {}
const basePacketOpts = { const packetOpts = {
preEncoded: true, preEncoded: true,
volatile: flags.volatile, volatile: flags.volatile,
compress: flags.compress compress: flags.compress
@@ -135,22 +147,65 @@ export class Adapter extends EventEmitter {
packet.nsp = this.nsp.name packet.nsp = this.nsp.name
const encodedPackets = this.encoder.encode(packet) const encodedPackets = this.encoder.encode(packet)
const packetOpts = encodedPackets.map(encodedPacket => { this.apply(opts, socket => {
if (typeof encodedPacket === "string") { if (typeof socket.notifyOutgoingListeners === "function") {
return { socket.notifyOutgoingListeners(packet)
...basePacketOpts,
wsPreEncoded: "4" + encodedPacket // "4" being the "message" packet type in Engine.IO
}
} else {
return basePacketOpts
} }
socket.client.writeToEngine(encodedPackets, packetOpts)
}) })
}
/**
* Broadcasts a packet and expects multiple acknowledgements.
*
* Options:
* - `flags` {Object} flags for this packet
* - `except` {Array} sids that should be excluded
* - `rooms` {Array} list of rooms to broadcast to
*
* @param {Object} packet the packet object
* @param {Object} opts the options
* @param clientCountCallback - the number of clients that received the packet
* @param ack - the callback that will be called for each client response
*
* @public
*/
public broadcastWithAck(
packet: any,
opts: BroadcastOptions,
clientCountCallback: (clientCount: number) => void,
ack: (...args: any[]) => void
) {
const flags = opts.flags || {}
const packetOpts = {
preEncoded: true,
volatile: flags.volatile,
compress: flags.compress
}
packet.nsp = this.nsp.name
// we can use the same id for each packet, since the _ids counter is common (no duplicate)
packet.id = this.nsp._ids++
const encodedPackets = this.encoder.encode(packet)
let clientCount = 0
this.apply(opts, socket => { this.apply(opts, socket => {
for (let i = 0; i < encodedPackets.length; i++) { // track the total number of acknowledgements that are expected
socket.client.writeToEngine(encodedPackets[i], packetOpts[i]) clientCount++
// call the ack callback for each client response
socket.acks.set(packet.id, ack)
if (typeof socket.notifyOutgoingListeners === "function") {
socket.notifyOutgoingListeners(packet)
} }
socket.client.writeToEngine(encodedPackets, packetOpts)
}) })
clientCountCallback(clientCount)
} }
/** /**
@@ -272,7 +327,7 @@ export class Adapter extends EventEmitter {
* @param packet - an array of arguments, which may include an acknowledgement callback at the end * @param packet - an array of arguments, which may include an acknowledgement callback at the end
*/ */
public serverSideEmit(packet: any[]): void { public serverSideEmit(packet: any[]): void {
throw new Error( console.warn(
"this adapter does not support the serverSideEmit() functionality" "this adapter does not support the serverSideEmit() functionality"
) )
} }

View File

@@ -0,0 +1,77 @@
/**
* Initialize backoff timer with `opts`.
*
* - `min` initial timeout in milliseconds [100]
* - `max` max timeout [10000]
* - `jitter` [0]
* - `factor` [2]
*
* @param {Object} opts
* @api public
*/
export function Backoff(this: any, opts) {
opts = opts || {}
this.ms = opts.min || 100
this.max = opts.max || 10000
this.factor = opts.factor || 2
this.jitter = opts.jitter > 0 && opts.jitter <= 1 ? opts.jitter : 0
this.attempts = 0
}
/**
* Return the backoff duration.
*
* @return {Number}
* @api public
*/
Backoff.prototype.duration = function () {
var ms = this.ms * Math.pow(this.factor, this.attempts++)
if (this.jitter) {
var rand = Math.random()
var deviation = Math.floor(rand * this.jitter * ms)
ms = (Math.floor(rand * 10) & 1) == 0 ? ms - deviation : ms + deviation
}
return Math.min(ms, this.max) | 0
}
/**
* Reset the number of attempts.
*
* @api public
*/
Backoff.prototype.reset = function () {
this.attempts = 0
}
/**
* Set the minimum duration
*
* @api public
*/
Backoff.prototype.setMin = function (min) {
this.ms = min
}
/**
* Set the maximum duration
*
* @api public
*/
Backoff.prototype.setMax = function (max) {
this.max = max
}
/**
* Set the jitter
*
* @api public
*/
Backoff.prototype.setJitter = function (jitter) {
this.jitter = jitter
}

View File

@@ -4,16 +4,10 @@ import { Socket, SocketOptions } from "./socket"
const debug = require("../debug")("socket.io-client") const debug = require("../debug")("socket.io-client")
/**
* Module exports.
*/
module.exports = exports = lookup
/** /**
* Managers cache. * Managers cache.
*/ */
const cache: Record<string, Manager> = (exports.managers = {}) const cache: Record<string, Manager> = {}
/** /**
* Looks up an existing `Manager` for multiplexing. * Looks up an existing `Manager` for multiplexing.
@@ -76,6 +70,15 @@ function lookup(
return io.socket(parsed.path, opts) return io.socket(parsed.path, opts)
} }
// so that "lookup" can be used both as a function (e.g. `io(...)`) and as a
// namespace (e.g. `io.connect(...)`), for backward compatibility
Object.assign(lookup, {
Manager,
Socket,
io: lookup,
connect: lookup,
})
/** /**
* Protocol version. * Protocol version.
* *
@@ -84,22 +87,18 @@ function lookup(
export { protocol } from "../socket.io-parser" export { protocol } from "../socket.io-parser"
/**
* `connect`.
*
* @param {String} uri
* @public
*/
exports.connect = lookup
/** /**
* Expose constructors for standalone build. * Expose constructors for standalone build.
* *
* @public * @public
*/ */
export { Manager, ManagerOptions } from "./manager" export {
export { Socket } from "./socket" Manager,
export { lookup as io, SocketOptions } ManagerOptions,
export default lookup Socket,
SocketOptions,
lookup as io,
lookup as connect,
lookup as default,
}

View File

@@ -1,210 +1,26 @@
import eio from "../engine.io-client" import {
import { Socket, SocketOptions } from "./socket" Socket as Engine,
SocketOptions as EngineOptions,
installTimerFunctions,
nextTick,
} from "../engine.io-client"
import { Socket, SocketOptions, DisconnectDescription } from "./socket.js"
// import * as parser from "socket.io-parser"
import * as parser from "../socket.io-parser" import * as parser from "../socket.io-parser"
// import { Decoder, Encoder, Packet } from "socket.io-parser"
import { Decoder, Encoder, Packet } from "../socket.io-parser" import { Decoder, Encoder, Packet } from "../socket.io-parser"
import { on } from "./on" import { on } from "./on.js"
import * as Backoff from "backo2" import { Backoff } from "./contrib/backo2"
import { import {
DefaultEventsMap, DefaultEventsMap,
EventsMap, EventsMap,
StrictEventEmitter, Emitter,
} from "./typed-events" } from "@socket.io/component-emitter"
// import debugModule from "debug" // debug()
// const debug = debugModule("socket.io-client:manager") // debug()
const debug = require("../debug")("socket.io-client") 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 { export interface ManagerOptions extends EngineOptions {
/** /**
* Should we force a new Manager for this connection? * Should we force a new Manager for this connection?
@@ -267,13 +83,6 @@ export interface ManagerOptions extends EngineOptions {
*/ */
autoConnect: boolean 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. * the parser to use. Defaults to an instance of the Parser that ships with socket.io.
*/ */
@@ -285,7 +94,7 @@ interface ManagerReservedEvents {
error: (err: Error) => void error: (err: Error) => void
ping: () => void ping: () => void
packet: (packet: Packet) => void packet: (packet: Packet) => void
close: (reason: string) => void close: (reason: string, description?: DisconnectDescription) => void
reconnect_failed: () => void reconnect_failed: () => void
reconnect_attempt: (attempt: number) => void reconnect_attempt: (attempt: number) => void
reconnect_error: (err: Error) => void reconnect_error: (err: Error) => void
@@ -295,13 +104,13 @@ interface ManagerReservedEvents {
export class Manager< export class Manager<
ListenEvents extends EventsMap = DefaultEventsMap, ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents EmitEvents extends EventsMap = ListenEvents
> extends StrictEventEmitter<{}, {}, ManagerReservedEvents> { > extends Emitter<{}, {}, ManagerReservedEvents> {
/** /**
* The Engine.IO client instance * The Engine.IO client instance
* *
* @public * @public
*/ */
public engine: any public engine: Engine
/** /**
* @private * @private
*/ */
@@ -320,7 +129,9 @@ export class Manager<
private nsps: Record<string, Socket> = {}; private nsps: Record<string, Socket> = {};
private subs: Array<ReturnType<typeof on>> = []; private subs: Array<ReturnType<typeof on>> = [];
// @ts-ignore
private backoff: Backoff private backoff: Backoff
private setTimeoutFn: typeof setTimeout
private _reconnection: boolean private _reconnection: boolean
private _reconnectionAttempts: number private _reconnectionAttempts: number
private _reconnectionDelay: number private _reconnectionDelay: number
@@ -358,6 +169,7 @@ export class Manager<
opts.path = opts.path || "/socket.io" opts.path = opts.path || "/socket.io"
this.opts = opts this.opts = opts
installTimerFunctions(this, opts)
this.reconnection(opts.reconnection !== false) this.reconnection(opts.reconnection !== false)
this.reconnectionAttempts(opts.reconnectionAttempts || Infinity) this.reconnectionAttempts(opts.reconnectionAttempts || Infinity)
this.reconnectionDelay(opts.reconnectionDelay || 1000) this.reconnectionDelay(opts.reconnectionDelay || 1000)
@@ -507,8 +319,7 @@ export class Manager<
if (~this._readyState.indexOf("open")) return this if (~this._readyState.indexOf("open")) return this
debug("opening %s", this.uri) debug("opening %s", this.uri)
// @ts-ignore this.engine = new Engine(this.uri, this.opts)
this.engine = eio(this.uri, this.opts)
const socket = this.engine const socket = this.engine
const self = this const self = this
this._readyState = "opening" this._readyState = "opening"
@@ -543,10 +354,11 @@ export class Manager<
} }
// set timer // set timer
const timer = setTimeout(() => { const timer = this.setTimeoutFn(() => {
debug("connect attempt timed out after %d", timeout) debug("connect attempt timed out after %d", timeout)
openSubDestroy() openSubDestroy()
socket.close() socket.close()
// @ts-ignore
socket.emit("error", new Error("timeout")) socket.emit("error", new Error("timeout"))
}, timeout) }, timeout)
@@ -616,7 +428,11 @@ export class Manager<
* @private * @private
*/ */
private ondata(data): void { private ondata(data): void {
this.decoder.add(data) try {
this.decoder.add(data)
} catch (e) {
this.onclose("parse error", e as Error)
}
} }
/** /**
@@ -625,7 +441,10 @@ export class Manager<
* @private * @private
*/ */
private ondecoded(packet): void { private ondecoded(packet): void {
this.emitReserved("packet", packet) // the nextTick call prevents an exception in a user-provided event listener from triggering a disconnection due to a "parse error"
nextTick(() => {
this.emitReserved("packet", packet)
}, this.setTimeoutFn)
} }
/** /**
@@ -713,13 +532,7 @@ export class Manager<
debug("disconnect") debug("disconnect")
this.skipReconnect = true this.skipReconnect = true
this._reconnecting = false this._reconnecting = false
if ("opening" === this._readyState) { this.onclose("forced close")
// `onclose` will not fire because
// an open event never happened
this.cleanup()
}
this.backoff.reset()
this._readyState = "closed"
if (this.engine) this.engine.close() if (this.engine) this.engine.close()
} }
@@ -737,13 +550,13 @@ export class Manager<
* *
* @private * @private
*/ */
private onclose(reason: string): void { private onclose(reason: string, description?: DisconnectDescription): void {
debug("onclose") debug("closed due to %s", reason)
this.cleanup() this.cleanup()
this.backoff.reset() this.backoff.reset()
this._readyState = "closed" this._readyState = "closed"
this.emitReserved("close", reason) this.emitReserved("close", reason, description)
if (this._reconnection && !this.skipReconnect) { if (this._reconnection && !this.skipReconnect) {
this.reconnect() this.reconnect()
@@ -770,7 +583,7 @@ export class Manager<
debug("will wait %dms before reconnect attempt", delay) debug("will wait %dms before reconnect attempt", delay)
this._reconnecting = true this._reconnecting = true
const timer = setTimeout(() => { const timer = this.setTimeoutFn(() => {
if (self.skipReconnect) return if (self.skipReconnect) return
debug("attempting reconnect") debug("attempting reconnect")

View File

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

View File

@@ -1,14 +1,17 @@
// import { Packet, PacketType } from "socket.io-parser"
import { Packet, PacketType } from "../socket.io-parser" import { Packet, PacketType } from "../socket.io-parser"
import { on } from "./on" import { on } from "./on.js"
import { Manager } from "./manager" import { Manager } from "./manager.js"
import { import {
DefaultEventsMap, DefaultEventsMap,
EventNames, EventNames,
EventParams, EventParams,
EventsMap, EventsMap,
StrictEventEmitter, Emitter,
} from "./typed-events" } 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") const debug = require("../debug")("socket.io-client")
export interface SocketOptions { export interface SocketOptions {
@@ -35,26 +38,110 @@ const RESERVED_EVENTS = Object.freeze({
interface Flags { interface Flags {
compress?: boolean compress?: boolean
volatile?: boolean volatile?: boolean
timeout?: number
} }
export type DisconnectDescription =
| Error
| {
description: string
context?: CloseEvent | XMLHttpRequest
}
interface SocketReservedEvents { interface SocketReservedEvents {
connect: () => void connect: () => void
connect_error: (err: Error) => 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< export class Socket<
ListenEvents extends EventsMap = DefaultEventsMap, ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents EmitEvents extends EventsMap = ListenEvents
> extends StrictEventEmitter<ListenEvents, EmitEvents, SocketReservedEvents> { > extends Emitter<ListenEvents, EmitEvents, SocketReservedEvents> {
public readonly io: Manager<ListenEvents, EmitEvents> 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 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) public auth: { [key: string]: any } | ((cb: (data: object) => void) => void)
/**
* Buffer for packets received before the CONNECT packet
*/
public receiveBuffer: Array<ReadonlyArray<any>> = []; public receiveBuffer: Array<ReadonlyArray<any>> = [];
/**
* Buffer for packets that will be sent once the socket is connected
*/
public sendBuffer: Array<Packet> = []; public sendBuffer: Array<Packet> = [];
private readonly nsp: string private readonly nsp: string
@@ -64,29 +151,39 @@ export class Socket<
private flags: Flags = {}; private flags: Flags = {};
private subs?: Array<VoidFunction> private subs?: Array<VoidFunction>
private _anyListeners: Array<(...args: any[]) => void> private _anyListeners: Array<(...args: any[]) => void>
private _anyOutgoingListeners: Array<(...args: any[]) => void>
/** /**
* `Socket` constructor. * `Socket` constructor.
*
* @public
*/ */
constructor(io: Manager, nsp: string, opts?: Partial<SocketOptions>) { constructor(io: Manager, nsp: string, opts?: Partial<SocketOptions>) {
super() super()
this.io = io this.io = io
this.nsp = nsp this.nsp = nsp
this.ids = 0
this.acks = {}
this.receiveBuffer = []
this.sendBuffer = []
this.connected = false
this.disconnected = true
this.flags = {}
if (opts && opts.auth) { if (opts && opts.auth) {
this.auth = opts.auth this.auth = opts.auth
} }
if (this.io._autoConnect) this.open() 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 * 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 { public get active(): boolean {
return !!this.subs return !!this.subs
@@ -114,7 +225,12 @@ export class Socket<
/** /**
* "Opens" the socket. * "Opens" the socket.
* *
* @public * @example
* const socket = io({
* autoConnect: false
* });
*
* socket.connect();
*/ */
public connect(): this { public connect(): this {
if (this.connected) return this if (this.connected) return this
@@ -126,7 +242,7 @@ export class Socket<
} }
/** /**
* Alias for connect() * Alias for {@link connect()}.
*/ */
public open(): this { public open(): this {
return this.connect() return this.connect()
@@ -135,8 +251,17 @@ export class Socket<
/** /**
* Sends a `message` event. * 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 * @return self
* @public
*/ */
public send(...args: any[]): this { public send(...args: any[]): this {
args.unshift("message") args.unshift("message")
@@ -149,15 +274,25 @@ export class Socket<
* Override `emit`. * Override `emit`.
* If the event is in `events`, it's emitted normally. * 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 * @return self
* @public
*/ */
public emit<Ev extends EventNames<EmitEvents>>( public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev, ev: Ev,
...args: EventParams<EmitEvents, Ev> ...args: EventParams<EmitEvents, Ev>
): this { ): this {
if (RESERVED_EVENTS.hasOwnProperty(ev)) { 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) args.unshift(ev)
@@ -171,9 +306,12 @@ export class Socket<
// event ack callback // event ack callback
if ("function" === typeof args[args.length - 1]) { if ("function" === typeof args[args.length - 1]) {
debug("emitting packet with ack id %d", this.ids) const id = this.ids++
this.acks[this.ids] = args.pop() debug("emitting packet with ack id %d", id)
packet.id = this.ids++
const ack = args.pop() as Function
this._registerAckCallback(id, ack)
packet.id = id
} }
const isTransportWritable = const isTransportWritable =
@@ -186,6 +324,7 @@ export class Socket<
if (discardPacket) { if (discardPacket) {
debug("discard packet as the transport is not currently writable") debug("discard packet as the transport is not currently writable")
} else if (this.connected) { } else if (this.connected) {
this.notifyOutgoingListeners(packet)
this.packet(packet) this.packet(packet)
} else { } else {
this.sendBuffer.push(packet) this.sendBuffer.push(packet)
@@ -196,6 +335,36 @@ export class Socket<
return this 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. * Sends a packet.
* *
@@ -239,14 +408,17 @@ export class Socket<
* Called upon engine `close`. * Called upon engine `close`.
* *
* @param reason * @param reason
* @param description
* @private * @private
*/ */
private onclose(reason: Socket.DisconnectReason): void { private onclose(
reason: Socket.DisconnectReason,
description?: DisconnectDescription
): void {
debug("close (%s)", reason) debug("close (%s)", reason)
this.connected = false this.connected = false
this.disconnected = true
delete this.id delete this.id
this.emitReserved("disconnect", reason) this.emitReserved("disconnect", reason, description)
} }
/** /**
@@ -276,17 +448,11 @@ export class Socket<
break break
case PacketType.EVENT: case PacketType.EVENT:
this.onevent(packet)
break
case PacketType.BINARY_EVENT: case PacketType.BINARY_EVENT:
this.onevent(packet) this.onevent(packet)
break break
case PacketType.ACK: case PacketType.ACK:
this.onack(packet)
break
case PacketType.BINARY_ACK: case PacketType.BINARY_ACK:
this.onack(packet) this.onack(packet)
break break
@@ -296,6 +462,7 @@ export class Socket<
break break
case PacketType.CONNECT_ERROR: case PacketType.CONNECT_ERROR:
this.destroy()
const err = new Error(packet.data.message) const err = new Error(packet.data.message)
// @ts-ignore // @ts-ignore
err.data = packet.data.data err.data = packet.data.data
@@ -386,7 +553,6 @@ export class Socket<
debug("socket connected with id %s", id) debug("socket connected with id %s", id)
this.id = id this.id = id
this.connected = true this.connected = true
this.disconnected = false
this.emitBuffered() this.emitBuffered()
this.emitReserved("connect") this.emitReserved("connect")
} }
@@ -400,7 +566,10 @@ export class Socket<
this.receiveBuffer.forEach((args) => this.emitEvent(args)) this.receiveBuffer.forEach((args) => this.emitEvent(args))
this.receiveBuffer = [] this.receiveBuffer = []
this.sendBuffer.forEach((packet) => this.packet(packet)) this.sendBuffer.forEach((packet) => {
this.notifyOutgoingListeners(packet)
this.packet(packet)
})
this.sendBuffer = [] 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 * @return self
* @public
*/ */
public disconnect(): this { public disconnect(): this {
if (this.connected) { if (this.connected) {
@@ -454,10 +633,9 @@ export class Socket<
} }
/** /**
* Alias for disconnect() * Alias for {@link disconnect()}.
* *
* @return self * @return self
* @public
*/ */
public close(): this { public close(): this {
return this.disconnect() return this.disconnect()
@@ -466,9 +644,11 @@ export class Socket<
/** /**
* Sets the compress flag. * Sets the compress flag.
* *
* @example
* socket.compress(false).emit("hello");
*
* @param compress - if `true`, compresses the sending data * @param compress - if `true`, compresses the sending data
* @return self * @return self
* @public
*/ */
public compress(compress: boolean): this { public compress(compress: boolean): this {
this.flags.compress = compress 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 * Sets a modifier for a subsequent event emission that the event message will be dropped when this socket is not
* ready to send messages. * ready to send messages.
* *
* @example
* socket.volatile.emit("hello"); // the server may or may not receive it
*
* @returns self * @returns self
* @public
*/ */
public get volatile(): this { public get volatile(): this {
this.flags.volatile = true this.flags.volatile = true
return this 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 * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback. * callback.
* *
* @example
* socket.onAny((event, ...args) => {
* console.log(`got ${event}`);
* });
*
* @param listener * @param listener
* @public
*/ */
public onAny(listener: (...args: any[]) => void): this { public onAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || [] 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 * 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. * callback. The listener is added to the beginning of the listeners array.
* *
* @example
* socket.prependAny((event, ...args) => {
* console.log(`got event ${event}`);
* });
*
* @param listener * @param listener
* @public
*/ */
public prependAny(listener: (...args: any[]) => void): this { public prependAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || [] this._anyListeners = this._anyListeners || []
@@ -516,8 +724,20 @@ export class Socket<
/** /**
* Removes the listener that will be fired when any event is emitted. * 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 * @param listener
* @public
*/ */
public offAny(listener?: (...args: any[]) => void): this { public offAny(listener?: (...args: any[]) => void): this {
if (!this._anyListeners) { 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, * Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
* e.g. to remove listeners. * e.g. to remove listeners.
*
* @public
*/ */
public listenersAny() { public listenersAny() {
return this._anyListeners || [] 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 { export namespace Socket {
@@ -555,4 +871,5 @@ export namespace Socket {
| "ping timeout" | "ping timeout"
| "transport close" | "transport close"
| "transport error" | "transport error"
| "parse error"
} }

View File

@@ -1,157 +0,0 @@
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

@@ -1,5 +1,8 @@
import * as parseuri from "parseuri" // import { parse } from "engine.io-client"
import { parse } from "../engine.io-client"
// import debugModule from "debug" // debug()
// const debug = debugModule("socket.io-client:url"); // debug()
const debug = require("../debug")("socket.io-client") const debug = require("../debug")("socket.io-client")
type ParsedUrl = { type ParsedUrl = {
@@ -67,7 +70,7 @@ export function url(
// parse // parse
debug("parse %s", uri) debug("parse %s", uri)
obj = parseuri(uri) as ParsedUrl obj = parse(uri) as ParsedUrl
} }
// make sure we treat `localhost:80` and `localhost` equally // make sure we treat `localhost:80` and `localhost` equally

View File

@@ -1,4 +1,4 @@
import { isBinary } from "./is-binary" import { isBinary } from "./is-binary.js"
/** /**
* Replaces every Buffer | ArrayBuffer | Blob | File in packet with a numbered placeholder. * Replaces every Buffer | ArrayBuffer | Blob | File in packet with a numbered placeholder.
@@ -33,7 +33,7 @@ function _deconstructPacket(data, buffers) {
} else if (typeof data === "object" && !(data instanceof Date)) { } else if (typeof data === "object" && !(data instanceof Date)) {
const newData = {} const newData = {}
for (const key in data) { for (const key in data) {
if (data.hasOwnProperty(key)) { if (Object.prototype.hasOwnProperty.call(data, key)) {
newData[key] = _deconstructPacket(data[key], buffers) newData[key] = _deconstructPacket(data[key], buffers)
} }
} }
@@ -60,15 +60,23 @@ export function reconstructPacket(packet, buffers) {
function _reconstructPacket(data, buffers) { function _reconstructPacket(data, buffers) {
if (!data) return data if (!data) return data
if (data && data._placeholder) { if (data && data._placeholder === true) {
return buffers[data.num] // appropriate buffer (should be natural order anyway) const isIndexValid =
typeof data.num === "number" &&
data.num >= 0 &&
data.num < buffers.length
if (isIndexValid) {
return buffers[data.num] // appropriate buffer (should be natural order anyway)
} else {
throw new Error("illegal attachments")
}
} else if (Array.isArray(data)) { } else if (Array.isArray(data)) {
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
data[i] = _reconstructPacket(data[i], buffers) data[i] = _reconstructPacket(data[i], buffers)
} }
} else if (typeof data === "object") { } else if (typeof data === "object") {
for (const key in data) { for (const key in data) {
if (data.hasOwnProperty(key)) { if (Object.prototype.hasOwnProperty.call(data, key)) {
data[key] = _reconstructPacket(data[key], buffers) data[key] = _reconstructPacket(data[key], buffers)
} }
} }

View File

@@ -1,8 +1,10 @@
import EventEmitter = require("events") import { Emitter } from "@socket.io/component-emitter"
import { deconstructPacket, reconstructPacket } from "./binary" import { deconstructPacket, reconstructPacket } from "./binary.js"
import { isBinary, hasBinary } from "./is-binary" import { isBinary, hasBinary } from "./is-binary.js"
// import debugModule from "debug" // debug()
// const debug = require("debug")("socket.io-parser") // const debug = debugModule("socket.io-parser") // debug()
const debug = require("../debug")("socket.io-client")
/** /**
* Protocol version. * Protocol version.
@@ -35,6 +37,12 @@ export interface Packet {
*/ */
export class Encoder { export class Encoder {
/**
* Encoder constructor
*
* @param {function} replacer - custom replacer to pass down to JSON.parse
*/
constructor(private replacer?: (this: any, key: string, value: any) => any) { }
/** /**
* Encode a packet as a single string if non-binary, or as a * Encode a packet as a single string if non-binary, or as a
* buffer sequence, depending on packet type. * buffer sequence, depending on packet type.
@@ -42,7 +50,7 @@ export class Encoder {
* @param {Object} obj - packet object * @param {Object} obj - packet object
*/ */
public encode(obj: Packet) { public encode(obj: Packet) {
console.trace("encoding packet", JSON.stringify(obj)) debug("encoding packet %j", obj)
if (obj.type === PacketType.EVENT || obj.type === PacketType.ACK) { if (obj.type === PacketType.EVENT || obj.type === PacketType.ACK) {
if (hasBinary(obj)) { if (hasBinary(obj)) {
@@ -85,10 +93,10 @@ export class Encoder {
// json data // json data
if (null != obj.data) { if (null != obj.data) {
str += JSON.stringify(obj.data) str += JSON.stringify(obj.data, this.replacer)
} }
console.trace("encoded", JSON.stringify(obj), "as", str) debug("encoded %j as %s", obj, str)
return str return str
} }
@@ -108,15 +116,24 @@ export class Encoder {
} }
} }
interface DecoderReservedEvents {
decoded: (packet: Packet) => void
}
/** /**
* A socket.io Decoder instance * A socket.io Decoder instance
* *
* @return {Object} decoder * @return {Object} decoder
*/ */
export class Decoder extends EventEmitter { export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
private reconstructor: BinaryReconstructor private reconstructor: BinaryReconstructor
constructor() { /**
* Decoder constructor
*
* @param {function} reviver - custom reviver to pass down to JSON.stringify
*/
constructor(private reviver?: (this: any, key: string, value: any) => any) {
super() super()
} }
@@ -129,6 +146,9 @@ export class Decoder extends EventEmitter {
public add(obj: any) { public add(obj: any) {
let packet let packet
if (typeof obj === "string") { if (typeof obj === "string") {
if (this.reconstructor) {
throw new Error("got plaintext data when reconstructing a packet")
}
packet = this.decodeString(obj) packet = this.decodeString(obj)
if ( if (
packet.type === PacketType.BINARY_EVENT || packet.type === PacketType.BINARY_EVENT ||
@@ -139,11 +159,11 @@ export class Decoder extends EventEmitter {
// no attachments, labeled binary but no binary data to follow // no attachments, labeled binary but no binary data to follow
if (packet.attachments === 0) { if (packet.attachments === 0) {
super.emit("decoded", packet) super.emitReserved("decoded", packet)
} }
} else { } else {
// non-binary full packet // non-binary full packet
super.emit("decoded", packet) super.emitReserved("decoded", packet)
} }
} else if (isBinary(obj) || obj.base64) { } else if (isBinary(obj) || obj.base64) {
// raw binary data // raw binary data
@@ -154,7 +174,7 @@ export class Decoder extends EventEmitter {
if (packet) { if (packet) {
// received final buffer // received final buffer
this.reconstructor = null this.reconstructor = null
super.emit("decoded", packet) super.emitReserved("decoded", packet)
} }
} }
} else { } else {
@@ -223,7 +243,7 @@ export class Decoder extends EventEmitter {
// look up json data // look up json data
if (str.charAt(++i)) { if (str.charAt(++i)) {
const payload = tryParse(str.substr(i)) const payload = this.tryParse(str.substr(i))
if (Decoder.isPayloadValid(p.type, payload)) { if (Decoder.isPayloadValid(p.type, payload)) {
p.data = payload p.data = payload
} else { } else {
@@ -231,10 +251,18 @@ export class Decoder extends EventEmitter {
} }
} }
console.trace("decoded", str, "as", p) debug("decoded %s as %j", str, p)
return p return p
} }
private tryParse(str) {
try {
return JSON.parse(str, this.reviver)
} catch (e) {
return false
}
}
private static isPayloadValid(type: PacketType, payload: any): boolean { private static isPayloadValid(type: PacketType, payload: any): boolean {
switch (type) { switch (type) {
case PacketType.CONNECT: case PacketType.CONNECT:
@@ -262,14 +290,6 @@ export class Decoder extends EventEmitter {
} }
} }
function tryParse(str) {
try {
return JSON.parse(str)
} catch (error: any) {
return false
}
}
/** /**
* A manager of a binary event's 'buffer sequence'. Should * A manager of a binary event's 'buffer sequence'. Should
* be constructed whenever a packet of type BINARY_EVENT is * be constructed whenever a packet of type BINARY_EVENT is

View File

@@ -7,14 +7,14 @@ const isView = (obj: any) => {
} }
const toString = Object.prototype.toString const toString = Object.prototype.toString
const withNativeBlob = false const withNativeBlob =
// typeof Blob === "function" || typeof Blob === "function" ||
// (typeof Blob !== "undefined" && (typeof Blob !== "undefined" &&
// toString.call(Blob) === "[object BlobConstructor]") toString.call(Blob) === "[object BlobConstructor]")
const withNativeFile = false const withNativeFile =
// typeof File === "function" || typeof File === "function" ||
// (typeof File !== "undefined" && (typeof File !== "undefined" &&
// toString.call(File) === "[object FileConstructor]") toString.call(File) === "[object FileConstructor]")
/** /**
* Returns true if obj is a Buffer, an ArrayBuffer, a Blob or a File. * Returns true if obj is a Buffer, an ArrayBuffer, a Blob or a File.
@@ -24,8 +24,9 @@ const withNativeFile = false
export function isBinary(obj: any) { export function isBinary(obj: any) {
return ( return (
(withNativeArrayBuffer && (obj instanceof ArrayBuffer || isView(obj))) (withNativeArrayBuffer && (obj instanceof ArrayBuffer || isView(obj))) ||
// || (withNativeBlob && obj instanceof Blob) || (withNativeFile && obj instanceof File) (withNativeBlob && obj instanceof Blob) ||
(withNativeFile && obj instanceof File)
) )
} }

View File

@@ -12,7 +12,7 @@ import type {
TypedEventBroadcaster, TypedEventBroadcaster,
} from "./typed-events" } from "./typed-events"
export class BroadcastOperator<EmitEvents extends EventsMap> export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
implements TypedEventBroadcaster<EmitEvents> implements TypedEventBroadcaster<EmitEvents>
{ {
constructor( constructor(
@@ -25,18 +25,27 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/** /**
* Targets a room when emitting. * Targets a room when emitting.
* *
* @param room * @example
* @return a new BroadcastOperator instance * // the “foo” event will be broadcast to all connected clients in the “room-101” room
* @public * io.to("room-101").emit("foo", "bar");
*
* // with an array of rooms (a client will be notified at most once)
* io.to(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* io.to("room-101").to("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/ */
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> { public to(room: Room | Room[]) {
const rooms = new Set(this.rooms) const rooms = new Set(this.rooms)
if (Array.isArray(room)) { if (Array.isArray(room)) {
room.forEach((r) => rooms.add(r)) room.forEach((r) => rooms.add(r))
} else { } else {
rooms.add(room) rooms.add(room)
} }
return new BroadcastOperator( return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter, this.adapter,
rooms, rooms,
this.exceptRooms, this.exceptRooms,
@@ -45,31 +54,43 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
} }
/** /**
* Targets a room when emitting. * Targets a room when emitting. Similar to `to()`, but might feel clearer in some cases:
* *
* @param room * @example
* @return a new BroadcastOperator instance * // disconnect all clients in the "room-101" room
* @public * io.in("room-101").disconnectSockets();
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/ */
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> { public in(room: Room | Room[]) {
return this.to(room) return this.to(room)
} }
/** /**
* Excludes a room when emitting. * Excludes a room when emitting.
* *
* @param room * @example
* @return a new BroadcastOperator instance * // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room
* @public * io.except("room-101").emit("foo", "bar");
*
* // with an array of rooms
* io.except(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* io.except("room-101").except("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/ */
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> { public except(room: Room | Room[]) {
const exceptRooms = new Set(this.exceptRooms) const exceptRooms = new Set(this.exceptRooms)
if (Array.isArray(room)) { if (Array.isArray(room)) {
room.forEach((r) => exceptRooms.add(r)) room.forEach((r) => exceptRooms.add(r))
} else { } else {
exceptRooms.add(room) exceptRooms.add(room)
} }
return new BroadcastOperator( return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter, this.adapter,
this.rooms, this.rooms,
exceptRooms, exceptRooms,
@@ -80,13 +101,15 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/** /**
* Sets the compress flag. * Sets the compress flag.
* *
* @example
* io.compress(false).emit("hello");
*
* @param compress - if `true`, compresses the sending data * @param compress - if `true`, compresses the sending data
* @return a new BroadcastOperator instance * @return a new BroadcastOperator instance
* @public
*/ */
public compress(compress: boolean): BroadcastOperator<EmitEvents> { public compress(compress: boolean) {
const flags = Object.assign({}, this.flags, { compress }) const flags = Object.assign({}, this.flags, { compress })
return new BroadcastOperator( return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter, this.adapter,
this.rooms, this.rooms,
this.exceptRooms, this.exceptRooms,
@@ -99,12 +122,14 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
* receive messages (because of network slowness or other issues, or because theyre connected through long polling * receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle). * and is in the middle of a request-response cycle).
* *
* @example
* io.volatile.emit("hello"); // the clients may or may not receive it
*
* @return a new BroadcastOperator instance * @return a new BroadcastOperator instance
* @public
*/ */
public get volatile(): BroadcastOperator<EmitEvents> { public get volatile() {
const flags = Object.assign({}, this.flags, { volatile: true }) const flags = Object.assign({}, this.flags, { volatile: true })
return new BroadcastOperator( return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter, this.adapter,
this.rooms, this.rooms,
this.exceptRooms, this.exceptRooms,
@@ -115,12 +140,39 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/** /**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node. * Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
* *
* @return a new BroadcastOperator instance * @example
* @public * // the “foo” event will be broadcast to all connected clients on this node
* io.local.emit("foo", "bar");
*
* @return a new {@link BroadcastOperator} instance for chaining
*/ */
public get local(): BroadcastOperator<EmitEvents> { public get local() {
const flags = Object.assign({}, this.flags, { local: true }) const flags = Object.assign({}, this.flags, { local: true })
return new BroadcastOperator( return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
this.exceptRooms,
flags
)
}
/**
* Adds a timeout in milliseconds for the next operation
*
* @example
* io.timeout(1000).emit("some-event", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* @param timeout
*/
public timeout(timeout: number) {
const flags = Object.assign({}, this.flags, { timeout })
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter, this.adapter,
this.rooms, this.rooms,
this.exceptRooms, this.exceptRooms,
@@ -131,15 +183,30 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/** /**
* Emits to all clients. * Emits to all clients.
* *
* @example
* // the “foo” event will be broadcast to all connected clients
* io.emit("foo", "bar");
*
* // the “foo” event will be broadcast to all connected clients in the “room-101” room
* io.to("room-101").emit("foo", "bar");
*
* // with an acknowledgement expected from all connected clients
* io.timeout(1000).emit("some-event", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* @return Always true * @return Always true
* @public
*/ */
public emit<Ev extends EventNames<EmitEvents>>( public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev, ev: Ev,
...args: EventParams<EmitEvents, Ev> ...args: EventParams<EmitEvents, Ev>
): boolean { ): boolean {
if (RESERVED_EVENTS.has(ev)) { if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${ev}" is a reserved event name`) throw new Error(`"${String(ev)}" is a reserved event name`)
} }
// set up packet object // set up packet object
const data = [ev, ...args] const data = [ev, ...args]
@@ -148,14 +215,65 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
data: data, data: data,
} }
if ("function" == typeof data[data.length - 1]) { const withAck = typeof data[data.length - 1] === "function"
throw new Error("Callbacks are not supported when broadcasting")
if (!withAck) {
this.adapter.broadcast(packet, {
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
})
return true
} }
this.adapter.broadcast(packet, { const ack = data.pop() as (...args: any[]) => void
rooms: this.rooms, let timedOut = false
except: this.exceptRooms, let responses: any[] = []
flags: this.flags,
const timer = setTimeout(() => {
timedOut = true
ack.apply(this, [new Error("operation has timed out"), responses])
}, this.flags.timeout)
let expectedServerCount = -1
let actualServerCount = 0
let expectedClientCount = 0
const checkCompleteness = () => {
if (
!timedOut &&
expectedServerCount === actualServerCount &&
responses.length === expectedClientCount
) {
clearTimeout(timer)
ack.apply(this, [null, responses])
}
}
this.adapter.broadcastWithAck(
packet,
{
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
},
(clientCount) => {
// each Socket.IO server in the cluster sends the number of clients that were notified
expectedClientCount += clientCount
actualServerCount++
checkCompleteness()
},
(clientResponse) => {
// each client sends an acknowledgement
responses.push(clientResponse)
checkCompleteness()
}
)
this.adapter.serverCount().then((serverCount) => {
expectedServerCount = serverCount
checkCompleteness()
}) })
return true return true
@@ -164,7 +282,8 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/** /**
* Gets a list of clients. * Gets a list of clients.
* *
* @public * @deprecated this method will be removed in the next major release, please use {@link Server#serverSideEmit} or
* {@link fetchSockets} instead.
*/ */
public allSockets(): Promise<Set<SocketId>> { public allSockets(): Promise<Set<SocketId>> {
if (!this.adapter) { if (!this.adapter) {
@@ -176,71 +295,122 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
} }
/** /**
* Returns the matching socket instances * Returns the matching socket instances. This method works across a cluster of several Socket.IO servers.
* *
* @public * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* // return all Socket instances
* const sockets = await io.fetchSockets();
*
* // return all Socket instances in the "room1" room
* const sockets = await io.in("room1").fetchSockets();
*
* for (const socket of sockets) {
* console.log(socket.id);
* console.log(socket.handshake);
* console.log(socket.rooms);
* console.log(socket.data);
*
* socket.emit("hello");
* socket.join("room1");
* socket.leave("room2");
* socket.disconnect();
* }
*/ */
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> { public fetchSockets(): Promise<RemoteSocket<EmitEvents, SocketData>[]> {
return this.adapter return this.adapter
.fetchSockets({ .fetchSockets({
rooms: this.rooms, rooms: this.rooms,
except: this.exceptRooms, except: this.exceptRooms,
flags: this.flags,
}) })
.then((sockets) => { .then((sockets) => {
return sockets.map((socket) => { return sockets.map((socket) => {
if (socket instanceof Socket) { if (socket instanceof Socket) {
// FIXME the TypeScript compiler complains about missing private properties // FIXME the TypeScript compiler complains about missing private properties
return socket as unknown as RemoteSocket<EmitEvents> return socket as unknown as RemoteSocket<EmitEvents, SocketData>
} else { } else {
return new RemoteSocket(this.adapter, socket as SocketDetails) return new RemoteSocket(
this.adapter,
socket as SocketDetails<SocketData>
)
} }
}) })
}) })
} }
/** /**
* Makes the matching socket instances join the specified rooms * Makes the matching socket instances join the specified rooms.
* *
* @param room * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
* @public *
* @example
*
* // make all socket instances join the "room1" room
* io.socketsJoin("room1");
*
* // make all socket instances in the "room1" room join the "room2" and "room3" rooms
* io.in("room1").socketsJoin(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/ */
public socketsJoin(room: Room | Room[]): void { public socketsJoin(room: Room | Room[]): void {
this.adapter.addSockets( this.adapter.addSockets(
{ {
rooms: this.rooms, rooms: this.rooms,
except: this.exceptRooms, except: this.exceptRooms,
flags: this.flags,
}, },
Array.isArray(room) ? room : [room] Array.isArray(room) ? room : [room]
) )
} }
/** /**
* Makes the matching socket instances leave the specified rooms * Makes the matching socket instances leave the specified rooms.
* *
* @param room * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
* @public *
* @example
* // make all socket instances leave the "room1" room
* io.socketsLeave("room1");
*
* // make all socket instances in the "room1" room leave the "room2" and "room3" rooms
* io.in("room1").socketsLeave(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/ */
public socketsLeave(room: Room | Room[]): void { public socketsLeave(room: Room | Room[]): void {
this.adapter.delSockets( this.adapter.delSockets(
{ {
rooms: this.rooms, rooms: this.rooms,
except: this.exceptRooms, except: this.exceptRooms,
flags: this.flags,
}, },
Array.isArray(room) ? room : [room] Array.isArray(room) ? room : [room]
) )
} }
/** /**
* Makes the matching socket instances disconnect * Makes the matching socket instances disconnect.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* // make all socket instances disconnect (the connections might be kept alive for other namespaces)
* io.disconnectSockets();
*
* // make all socket instances in the "room1" room disconnect and close the underlying connections
* io.in("room1").disconnectSockets(true);
* *
* @param close - whether to close the underlying connection * @param close - whether to close the underlying connection
* @public
*/ */
public disconnectSockets(close: boolean = false): void { public disconnectSockets(close: boolean = false): void {
this.adapter.disconnectSockets( this.adapter.disconnectSockets(
{ {
rooms: this.rooms, rooms: this.rooms,
except: this.exceptRooms, except: this.exceptRooms,
flags: this.flags,
}, },
close close
) )
@@ -250,32 +420,35 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/** /**
* Format of the data when the Socket instance exists on another Socket.IO server * Format of the data when the Socket instance exists on another Socket.IO server
*/ */
interface SocketDetails { interface SocketDetails<SocketData> {
id: SocketId id: SocketId
handshake: Handshake handshake: Handshake
rooms: Room[] rooms: Room[]
data: any data: SocketData
} }
/** /**
* Expose of subset of the attributes and methods of the Socket class * Expose of subset of the attributes and methods of the Socket class
*/ */
export class RemoteSocket<EmitEvents extends EventsMap> export class RemoteSocket<EmitEvents extends EventsMap, SocketData>
implements TypedEventBroadcaster<EmitEvents> implements TypedEventBroadcaster<EmitEvents>
{ {
public readonly id: SocketId public readonly id: SocketId
public readonly handshake: Handshake public readonly handshake: Handshake
public readonly rooms: Set<Room> public readonly rooms: Set<Room>
public readonly data: any public readonly data: SocketData
private readonly operator: BroadcastOperator<EmitEvents> private readonly operator: BroadcastOperator<EmitEvents, SocketData>
constructor(adapter: Adapter, details: SocketDetails) { constructor(adapter: Adapter, details: SocketDetails<SocketData>) {
this.id = details.id this.id = details.id
this.handshake = details.handshake this.handshake = details.handshake
this.rooms = new Set(details.rooms) this.rooms = new Set(details.rooms)
this.data = details.data this.data = details.data
this.operator = new BroadcastOperator(adapter, new Set([this.id])) this.operator = new BroadcastOperator<EmitEvents, SocketData>(
adapter,
new Set([this.id])
)
} }
public emit<Ev extends EventNames<EmitEvents>>( public emit<Ev extends EventNames<EmitEvents>>(
@@ -289,7 +462,6 @@ export class RemoteSocket<EmitEvents extends EventsMap>
* Joins a room. * Joins a room.
* *
* @param {String|Array} room - room or array of rooms * @param {String|Array} room - room or array of rooms
* @public
*/ */
public join(room: Room | Room[]): void { public join(room: Room | Room[]): void {
return this.operator.socketsJoin(room) return this.operator.socketsJoin(room)
@@ -299,7 +471,6 @@ export class RemoteSocket<EmitEvents extends EventsMap>
* Leaves a room. * Leaves a room.
* *
* @param {String} room * @param {String} room
* @public
*/ */
public leave(room: Room): void { public leave(room: Room): void {
return this.operator.socketsLeave(room) return this.operator.socketsLeave(room)
@@ -310,8 +481,6 @@ export class RemoteSocket<EmitEvents extends EventsMap>
* *
* @param {Boolean} close - if `true`, closes the underlying connection * @param {Boolean} close - if `true`, closes the underlying connection
* @return {Socket} self * @return {Socket} self
*
* @public
*/ */
public disconnect(close = false): this { public disconnect(close = false): this {
this.operator.disconnectSockets(close) this.operator.disconnectSockets(close)

View File

@@ -9,9 +9,10 @@ import type { EventsMap } from "./typed-events"
import type { Socket } from "./socket" import type { Socket } from "./socket"
// import type { SocketId } from "socket.io-adapter" // import type { SocketId } from "socket.io-adapter"
import type { SocketId } from "../socket.io-adapter" import type { SocketId } from "../socket.io-adapter"
import type { Socket as EngineIOSocket } from '../engine.io/socket' import type { Socket as RawSocket } from '../engine.io/socket'
// const debug = debugModule("socket.io:client"); // const debug = debugModule("socket.io:client");
const debug = require('../debug')("socket.io:client")
interface WriteOptions { interface WriteOptions {
compress?: boolean compress?: boolean
@@ -20,28 +21,39 @@ interface WriteOptions {
wsPreEncoded?: string wsPreEncoded?: string
} }
type CloseReason =
| "transport error"
| "transport close"
| "forced close"
| "ping timeout"
| "parse error"
export class Client< export class Client<
ListenEvents extends EventsMap, ListenEvents extends EventsMap,
EmitEvents extends EventsMap, EmitEvents extends EventsMap,
ServerSideEvents extends EventsMap ServerSideEvents extends EventsMap,
> { SocketData = any
public readonly conn: EngineIOSocket > {
/** public readonly conn: RawSocket
* @private
*/ private readonly id: string
readonly id: string private readonly server: Server<
private readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents> ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>
private readonly encoder: Encoder private readonly encoder: Encoder
private readonly decoder: any private readonly decoder: Decoder
private sockets: Map< private sockets: Map<
SocketId, SocketId,
Socket<ListenEvents, EmitEvents, ServerSideEvents> Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map() > = new Map();
private nsps: Map< private nsps: Map<
string, string,
Socket<ListenEvents, EmitEvents, ServerSideEvents> Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map() > = new Map();
private connectTimeout: NodeJS.Timeout private connectTimeout?: NodeJS.Timeout
/** /**
* Client constructor. * Client constructor.
@@ -51,8 +63,8 @@ export class Client<
* @package * @package
*/ */
constructor( constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents>, server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
conn: EngineIOSocket conn: any
) { ) {
this.server = server this.server = server
this.conn = conn this.conn = conn
@@ -67,7 +79,8 @@ export class Client<
* *
* @public * @public
*/ */
public get request(): any/** IncomingMessage */ { // public get request(): IncomingMessage {
public get request(): any {
return this.conn.request return this.conn.request
} }
@@ -77,7 +90,6 @@ export class Client<
* @private * @private
*/ */
private setup() { private setup() {
console.debug(`socket.io client setup conn ${this.conn.id}`)
this.onclose = this.onclose.bind(this) this.onclose = this.onclose.bind(this)
this.ondata = this.ondata.bind(this) this.ondata = this.ondata.bind(this)
this.onerror = this.onerror.bind(this) this.onerror = this.onerror.bind(this)
@@ -91,10 +103,10 @@ export class Client<
this.connectTimeout = setTimeout(() => { this.connectTimeout = setTimeout(() => {
if (this.nsps.size === 0) { if (this.nsps.size === 0) {
console.debug(`no namespace joined yet, close the client ${this.id}`) debug("no namespace joined yet, close the client")
this.close() this.close()
} else { } else {
console.debug(`the client ${this.id} has already joined a namespace, nothing to do`) debug("the client has already joined a namespace, nothing to do")
} }
}, this.server._connectTimeout) }, this.server._connectTimeout)
} }
@@ -108,7 +120,7 @@ export class Client<
*/ */
private connect(name: string, auth: object = {}): void { private connect(name: string, auth: object = {}): void {
if (this.server._nsps.has(name)) { if (this.server._nsps.has(name)) {
console.debug(`socket.io client ${this.id} connecting to namespace ${name}`) debug("connecting to namespace %s", name)
return this.doConnect(name, auth) return this.doConnect(name, auth)
} }
@@ -117,14 +129,13 @@ export class Client<
auth, auth,
( (
dynamicNspName: dynamicNspName:
| Namespace<ListenEvents, EmitEvents, ServerSideEvents> | Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
| false | false
) => { ) => {
if (dynamicNspName) { if (dynamicNspName) {
console.debug(`dynamic namespace ${dynamicNspName} was created`)
this.doConnect(name, auth) this.doConnect(name, auth)
} else { } else {
console.debug(`creation of namespace ${name} was denied`) debug("creation of namespace %s was denied", name)
this._packet({ this._packet({
type: PacketType.CONNECT_ERROR, type: PacketType.CONNECT_ERROR,
nsp: name, nsp: name,
@@ -178,20 +189,16 @@ export class Client<
* *
* @private * @private
*/ */
_remove(socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>): void { _remove(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
): void {
if (this.sockets.has(socket.id)) { if (this.sockets.has(socket.id)) {
const nsp = this.sockets.get(socket.id)!.nsp.name const nsp = this.sockets.get(socket.id)!.nsp.name
this.sockets.delete(socket.id) this.sockets.delete(socket.id)
this.nsps.delete(nsp) this.nsps.delete(nsp)
} else { } else {
console.debug("ignoring remove for", socket.id) debug("ignoring remove for %s", socket.id)
} }
// @java-patch disconnect client when no live socket
process.nextTick(() => {
if (this.sockets.size == 0) {
this.onclose('no live socket')
}
})
} }
/** /**
@@ -200,43 +207,47 @@ export class Client<
* @private * @private
*/ */
private close(): void { private close(): void {
console.debug(`client ${this.id} close - reason: forcing transport close`)
if ("open" === this.conn.readyState) { if ("open" === this.conn.readyState) {
console.debug("forcing transport close") debug("forcing transport close")
this.conn.close() this.conn.close()
this.onclose("forced server close") this.onclose("forced server close")
} }
} }
/** /**
* Writes a packet to the transport. * Writes a packet to the transport.
* *
* @param {Object} packet object * @param {Object} packet object
* @param {Object} opts * @param {Object} opts
* @private * @private
*/ */
_packet(packet: Packet | any[], opts: WriteOptions = {}): void { _packet(packet: Packet | any[], opts: WriteOptions = {}): void {
if (this.conn.readyState !== "open") { if (this.conn.readyState !== "open") {
console.debug(`client ${this.id} ignoring packet write ${JSON.stringify(packet)}`) debug("ignoring packet write %j", packet)
return return
} }
const encodedPackets = opts.preEncoded const encodedPackets = opts.preEncoded
? (packet as any[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine() ? (packet as any[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine()
: this.encoder.encode(packet as Packet) : this.encoder.encode(packet as Packet)
for (const encodedPacket of encodedPackets) { this.writeToEngine(encodedPackets, opts)
this.writeToEngine(encodedPacket, opts)
}
} }
private writeToEngine( private writeToEngine(
encodedPacket: String | Buffer, encodedPackets: Array<String | Buffer>,
opts: WriteOptions opts: WriteOptions
): void { ): void {
if (opts.volatile && !this.conn.transport.writable) { if (opts.volatile && !this.conn.transport.writable) {
console.debug(`client ${this.id} volatile packet is discarded since the transport is not currently writable`) debug(
"volatile packet is discarded since the transport is not currently writable"
)
return return
} }
this.conn.write(encodedPacket, opts) const packets = Array.isArray(encodedPackets)
? encodedPackets
: [encodedPackets]
for (const encodedPacket of packets) {
this.conn.write(encodedPacket, opts)
}
} }
/** /**
@@ -248,8 +259,9 @@ export class Client<
// try/catch is needed for protocol violations (GH-1880) // try/catch is needed for protocol violations (GH-1880)
try { try {
this.decoder.add(data) this.decoder.add(data)
} catch (error: any) { } catch (e) {
this.onerror(error) debug("invalid packet format")
this.onerror(e)
} }
} }
@@ -259,22 +271,31 @@ export class Client<
* @private * @private
*/ */
private ondecoded(packet: Packet): void { private ondecoded(packet: Packet): void {
if (PacketType.CONNECT === packet.type) { let namespace: string
if (this.conn.protocol === 3) { let authPayload
const parsed = url.parse(packet.nsp, true) if (this.conn.protocol === 3) {
this.connect(parsed.pathname!, parsed.query) const parsed = url.parse(packet.nsp, true)
} else { namespace = parsed.pathname!
this.connect(packet.nsp, packet.data) authPayload = parsed.query
}
} else { } else {
const socket = this.nsps.get(packet.nsp) namespace = packet.nsp
if (socket) { authPayload = packet.data
process.nextTick(function () { }
socket._onpacket(packet) const socket = this.nsps.get(namespace)
})
} else { if (!socket && packet.type === PacketType.CONNECT) {
console.debug(`client ${this.id} no socket for namespace ${packet.nsp}.`) this.connect(namespace, authPayload)
} } else if (
socket &&
packet.type !== PacketType.CONNECT &&
packet.type !== PacketType.CONNECT_ERROR
) {
process.nextTick(function () {
socket._onpacket(packet)
})
} else {
debug("invalid state (packet type: %s)", packet.type)
this.close()
} }
} }
@@ -297,8 +318,8 @@ export class Client<
* @param reason * @param reason
* @private * @private
*/ */
private onclose(reason: string): void { private onclose(reason: CloseReason | "forced server close"): void {
console.debug(`client ${this.id} close with reason ${reason}`) debug("client close with reason %s", reason)
// ignore a potential subsequent `close` event // ignore a potential subsequent `close` event
this.destroy() this.destroy()

View File

@@ -1,24 +1,30 @@
// import http = require("http"); // import http = require("http");
// import type { Server as HTTPSServer } from "https";
// import type { Http2SecureServer } from "http2";
// import { createReadStream } from "fs"; // import { createReadStream } from "fs";
// import { createDeflate, createGzip, createBrotliCompress } from "zlib"; // import { createDeflate, createGzip, createBrotliCompress } from "zlib";
// import accepts = require("accepts"); // import accepts = require("accepts");
// import { pipeline } from "stream"; // import { pipeline } from "stream";
// import path = require("path"); // import path = require("path");
import engine = require("../engine.io") import {
import { Client } from './client' attach,
import { EventEmitter } from 'events' Server as Engine,
ServerOptions as EngineOptions,
AttachOptions,
// uServer,
} from "../engine.io"
import { Client } from "./client"
import { EventEmitter } from "events"
import { ExtendedError, Namespace, ServerReservedEventsMap } from "./namespace" import { ExtendedError, Namespace, ServerReservedEventsMap } from "./namespace"
import { ParentNamespace } from './parent-namespace' import { ParentNamespace } from "./parent-namespace"
// import { Adapter, Room, SocketId } from "socket.io-adapter" // import { Adapter, Room, SocketId } from "socket.io-adapter"
import { Adapter, Room, SocketId } from "../socket.io-adapter" import { Adapter, Room, SocketId } from "../socket.io-adapter"
// import * as parser from "socket.io-parser"; // import * as parser from "socket.io-parser"
import * as parser from "../socket.io-parser" import * as parser from "../socket.io-parser"
// import type { Encoder } from "socket.io-parser"; // import type { Encoder } from "socket.io-parser"
import type { Encoder } from "../socket.io-parser" import type { Encoder } from "../socket.io-parser"
// import debugModule from "debug"; // import debugModule from "debug"
import { Socket } from './socket' import { Socket } from "./socket"
// import type { CookieSerializeOptions } from "cookie";
// import type { CorsOptions } from "cors";
import type { BroadcastOperator, RemoteSocket } from "./broadcast-operator" import type { BroadcastOperator, RemoteSocket } from "./broadcast-operator"
import { import {
EventsMap, EventsMap,
@@ -27,13 +33,14 @@ import {
StrictEventEmitter, StrictEventEmitter,
EventNames, EventNames,
} from "./typed-events" } from "./typed-events"
// import { patchAdapter, restoreAdapter, serveFile } from "./uws"
import type { Socket as EngineIOSocket } from '../engine.io/socket' // const debug = debugModule("socket.io:server")
const debug = require('../debug')("socket.io:server")
// const clientVersion = require("../package.json").version // const clientVersion = require("../package.json").version
// const dotMapRegex = /\.map/ // const dotMapRegex = /\.map/
// type Transport = "polling" | "websocket";
type ParentNspNameMatchFn = ( type ParentNspNameMatchFn = (
name: string, name: string,
auth: { [key: string]: any }, auth: { [key: string]: any },
@@ -42,105 +49,7 @@ type ParentNspNameMatchFn = (
type AdapterConstructor = typeof Adapter | ((nsp: Namespace) => Adapter) type AdapterConstructor = typeof Adapter | ((nsp: Namespace) => Adapter)
interface EngineOptions { interface ServerOptions extends EngineOptions, AttachOptions {
/**
* how many ms without a pong packet to consider the connection closed
* @default 5000
*/
pingTimeout: number
/**
* how many ms before sending a new ping packet
* @default 25000
*/
pingInterval: number
/**
* how many ms before an uncompleted transport upgrade is cancelled
* @default 10000
*/
upgradeTimeout: number
/**
* how many bytes or characters a message can be, before closing the session (to avoid DoS).
* @default 1e5 (100 KB)
*/
maxHttpBufferSize: number
/**
* A function that receives a given handshake or upgrade request as its first parameter,
* and can decide whether to continue or not. The second argument is a function that needs
* to be called with the decided information: fn(err, success), where success is a boolean
* value where false means that the request is rejected, and err is an error code.
*/
// allowRequest: (
// req: http.IncomingMessage,
// fn: (err: string | null | undefined, success: boolean) => void
// ) => void
/**
* the low-level transports that are enabled
* @default ["polling", "websocket"]
*/
// transports: Transport[]
/**
* whether to allow transport upgrades
* @default true
*/
allowUpgrades: boolean
/**
* parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable.
* @default false
*/
perMessageDeflate: boolean | object
/**
* parameters of the http compression for the polling transports (see zlib api docs). Set to false to disable.
* @default true
*/
httpCompression: boolean | object
/**
* what WebSocket server implementation to use. Specified module must
* conform to the ws interface (see ws module api docs). Default value is ws.
* An alternative c++ addon is also available by installing uws module.
*/
wsEngine: string
/**
* an optional packet which will be concatenated to the handshake packet emitted by Engine.IO.
*/
initialPacket: any
/**
* configuration of the cookie that contains the client sid to send as part of handshake response headers. This cookie
* might be used for sticky-session. Defaults to not sending any cookie.
* @default false
*/
// cookie: CookieSerializeOptions | boolean
/**
* the options that will be forwarded to the cors module
*/
// cors: CorsOptions
/**
* whether to enable compatibility with Socket.IO v2 clients
* @default false
*/
allowEIO3: boolean
}
interface AttachOptions {
/**
* name of the path to capture
* @default "/engine.io"
*/
path: string
/**
* destroy unhandled upgrade requests
* @default true
*/
destroyUpgrade: boolean
/**
* milliseconds after which unhandled requests are ended
* @default 1000
*/
destroyUpgradeTimeout: number
}
interface EngineAttachOptions extends EngineOptions, AttachOptions { }
interface ServerOptions extends EngineAttachOptions {
/** /**
* name of the path to capture * name of the path to capture
* @default "/socket.io" * @default "/socket.io"
@@ -155,6 +64,7 @@ interface ServerOptions extends EngineAttachOptions {
* the adapter to use * the adapter to use
* @default the in-memory adapter (https://github.com/socketio/socket.io-adapter) * @default the in-memory adapter (https://github.com/socketio/socket.io-adapter)
*/ */
// adapter: AdapterConstructor
adapter: any adapter: any
/** /**
* the parser to use * the parser to use
@@ -168,31 +78,62 @@ interface ServerOptions extends EngineAttachOptions {
connectTimeout: number connectTimeout: number
} }
/**
* Represents a Socket.IO server.
*
* @example
* import { Server } from "socket.io";
*
* const io = new Server();
*
* io.on("connection", (socket) => {
* console.log(`socket ${socket.id} connected`);
*
* // send an event to the client
* socket.emit("foo", "bar");
*
* socket.on("foobar", () => {
* // an event was received from the client
* });
*
* // upon disconnection
* socket.on("disconnect", (reason) => {
* console.log(`socket ${socket.id} disconnected due to ${reason}`);
* });
* });
*
* io.listen(3000);
*/
export class Server< export class Server<
ListenEvents extends EventsMap = DefaultEventsMap, ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents, EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap ServerSideEvents extends EventsMap = DefaultEventsMap,
> extends StrictEventEmitter< SocketData = any
> extends StrictEventEmitter<
ServerSideEvents, ServerSideEvents,
EmitEvents, EmitEvents,
ServerReservedEventsMap<ListenEvents, EmitEvents, ServerSideEvents> ServerReservedEventsMap<
> { ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>
> {
public readonly sockets: Namespace< public readonly sockets: Namespace<
ListenEvents, ListenEvents,
EmitEvents, EmitEvents,
ServerSideEvents ServerSideEvents,
SocketData
> >
/** /**
* A reference to the underlying Engine.IO server. * A reference to the underlying Engine.IO server.
* *
* Example: * @example
* * const clientsCount = io.engine.clientsCount;
* <code>
* const clientsCount = io.engine.clientsCount;
* </code>
* *
*/ */
public engine: any public engine: any
/** @private */ /** @private */
readonly _parser: typeof parser readonly _parser: typeof parser
/** @private */ /** @private */
@@ -201,28 +142,62 @@ export class Server<
/** /**
* @private * @private
*/ */
_nsps: Map<string, Namespace<ListenEvents, EmitEvents, ServerSideEvents>> = _nsps: Map<
new Map(); string,
Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map();
private parentNsps: Map< private parentNsps: Map<
ParentNspNameMatchFn, ParentNspNameMatchFn,
ParentNamespace<ListenEvents, EmitEvents, ServerSideEvents> ParentNamespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map(); > = new Map();
private _adapter?: AdapterConstructor private _adapter?: AdapterConstructor
private _serveClient: boolean private _serveClient: boolean
private opts: Partial<EngineOptions> private opts: Partial<EngineOptions>
private eio private eio: Engine
private _path: string private _path: string
private clientPathRegex: RegExp private clientPathRegex: RegExp
/** /**
* @private * @private
*/ */
_connectTimeout: number _connectTimeout: number
// private httpServer: http.Server | HTTPSServer | Http2SecureServer
// private httpServer: http.Server /**
* Server constructor.
constructor(srv: any, opts: Partial<ServerOptions> = {}) { *
* @param srv http server, port, or options
* @param [opts]
*/
constructor(opts?: Partial<ServerOptions>)
constructor(
// srv?: http.Server | HTTPSServer | Http2SecureServer | number,
srv?: any,
opts?: Partial<ServerOptions>
)
constructor(
srv:
// | undefined
// | Partial<ServerOptions>
// | http.Server
// | HTTPSServer
// | Http2SecureServer
// | number,
any,
opts?: Partial<ServerOptions>
)
constructor(
srv:
// | undefined
// | Partial<ServerOptions>
// | http.Server
// | HTTPSServer
// | Http2SecureServer
// | number,
any,
opts: Partial<ServerOptions> = {}
) {
super() super()
if (!srv) { throw new Error('srv can\'t be undefiend!') }
// if ( // if (
// "object" === typeof srv && // "object" === typeof srv &&
// srv instanceof Object && // srv instanceof Object &&
@@ -237,8 +212,12 @@ export class Server<
this._parser = opts.parser || parser this._parser = opts.parser || parser
this.encoder = new this._parser.Encoder() this.encoder = new this._parser.Encoder()
this.adapter(opts.adapter || Adapter) this.adapter(opts.adapter || Adapter)
this.sockets = this.of('/') this.sockets = this.of("/")
// if (srv) this.attach(srv as http.Server); this.opts = opts
// if (srv || typeof srv == "number")
// this.attach(
// srv as http.Server | HTTPSServer | Http2SecureServer | number
// )
this.attach(srv, this.opts) this.attach(srv, this.opts)
} }
@@ -247,7 +226,6 @@ export class Server<
* *
* @param v - whether to serve client code * @param v - whether to serve client code
* @return self when setting or value when getting * @return self when setting or value when getting
* @public
*/ */
public serveClient(v: boolean): this public serveClient(v: boolean): this
public serveClient(): boolean public serveClient(): boolean
@@ -271,7 +249,9 @@ export class Server<
name: string, name: string,
auth: { [key: string]: any }, auth: { [key: string]: any },
fn: ( fn: (
nsp: Namespace<ListenEvents, EmitEvents, ServerSideEvents> | false nsp:
| Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
| false
) => void ) => void
): void { ): void {
if (this.parentNsps.size === 0) return fn(false) if (this.parentNsps.size === 0) return fn(false)
@@ -285,15 +265,18 @@ export class Server<
} }
nextFn.value(name, auth, (err, allow) => { nextFn.value(name, auth, (err, allow) => {
if (err || !allow) { if (err || !allow) {
run() return run()
} else {
const namespace = this.parentNsps
.get(nextFn.value)!
.createChild(name)
// @ts-ignore
this.sockets.emitReserved("new_namespace", namespace)
fn(namespace)
} }
if (this._nsps.has(name)) {
// the namespace was created in the meantime
debug("dynamic namespace %s already exists", name)
return fn(this._nsps.get(name) as Namespace)
}
const namespace = this.parentNsps.get(nextFn.value)!.createChild(name)
debug("dynamic namespace %s was created", name)
// @ts-ignore
this.sockets.emitReserved("new_namespace", namespace)
fn(namespace)
}) })
} }
@@ -305,7 +288,6 @@ export class Server<
* *
* @param {String} v pathname * @param {String} v pathname
* @return {Server|String} self when setting or value when getting * @return {Server|String} self when setting or value when getting
* @public
*/ */
public path(v: string): this public path(v: string): this
public path(): string public path(): string
@@ -319,7 +301,7 @@ export class Server<
this.clientPathRegex = new RegExp( this.clientPathRegex = new RegExp(
"^" + "^" +
escapedPath + escapedPath +
"/socket\\.io(\\.min|\\.msgpack\\.min)?\\.js(\\.map)?$" "/socket\\.io(\\.msgpack|\\.esm)?(\\.min)?\\.js(\\.map)?(?:\\?|$)"
) )
return this return this
} }
@@ -327,7 +309,6 @@ export class Server<
/** /**
* Set the delay after which a client without namespace is closed * Set the delay after which a client without namespace is closed
* @param v * @param v
* @public
*/ */
public connectTimeout(v: number): this public connectTimeout(v: number): this
public connectTimeout(): number public connectTimeout(): number
@@ -343,7 +324,6 @@ export class Server<
* *
* @param v pathname * @param v pathname
* @return self when setting or value when getting * @return self when setting or value when getting
* @public
*/ */
public adapter(): AdapterConstructor | undefined public adapter(): AdapterConstructor | undefined
public adapter(v: AdapterConstructor): this public adapter(v: AdapterConstructor): this
@@ -364,14 +344,14 @@ export class Server<
* @param srv - server or port * @param srv - server or port
* @param opts - options passed to engine.io * @param opts - options passed to engine.io
* @return self * @return self
* @public
*/ */
public listen( public listen(
srv: any,//http.Server | number, // srv: http.Server | HTTPSServer | Http2SecureServer | number,
srv: any,
opts: Partial<ServerOptions> = {} opts: Partial<ServerOptions> = {}
): this { ): this {
throw Error('Unsupport listen at MiaoScript Engine!') throw Error('Unsupport listen at MiaoScript Engine!')
//return this.attach(srv, opts) // return this.attach(srv, opts)
} }
/** /**
@@ -380,10 +360,10 @@ export class Server<
* @param srv - server or port * @param srv - server or port
* @param opts - options passed to engine.io * @param opts - options passed to engine.io
* @return self * @return self
* @public
*/ */
public attach( public attach(
srv: any,//http.Server | number, // srv: http.Server | HTTPSServer | Http2SecureServer | number,
srv: any,
opts: Partial<ServerOptions> = {} opts: Partial<ServerOptions> = {}
): this { ): this {
// if ("function" == typeof srv) { // if ("function" == typeof srv) {
@@ -418,6 +398,69 @@ export class Server<
return this return this
} }
// public attachApp(app /*: TemplatedApp */, opts: Partial<ServerOptions> = {}) {
// // merge the options passed to the Socket.IO server
// Object.assign(opts, this.opts)
// // set engine.io path to `/socket.io`
// opts.path = opts.path || this._path
// // initialize engine
// debug("creating uWebSockets.js-based engine with opts %j", opts)
// const engine = new uServer(opts)
// engine.attach(app, opts)
// // bind to engine events
// this.bind(engine)
// if (this._serveClient) {
// // attach static file serving
// app.get(`${this._path}/*`, (res, req) => {
// if (!this.clientPathRegex.test(req.getUrl())) {
// req.setYield(true)
// return
// }
// const filename = req
// .getUrl()
// .replace(this._path, "")
// .replace(/\?.*$/, "")
// .replace(/^\//, "")
// const isMap = dotMapRegex.test(filename)
// const type = isMap ? "map" : "source"
// // Per the standard, ETags must be quoted:
// // https://tools.ietf.org/html/rfc7232#section-2.3
// const expectedEtag = '"' + clientVersion + '"'
// const weakEtag = "W/" + expectedEtag
// const etag = req.getHeader("if-none-match")
// if (etag) {
// if (expectedEtag === etag || weakEtag === etag) {
// debug("serve client %s 304", type)
// res.writeStatus("304 Not Modified")
// res.end()
// return
// }
// }
// debug("serve client %s", type)
// res.writeHeader("cache-control", "public, max-age=0")
// res.writeHeader(
// "content-type",
// "application/" + (isMap ? "json" : "javascript")
// )
// res.writeHeader("etag", expectedEtag)
// const filepath = path.join(__dirname, "../client-dist/", filename)
// serveFile(res, filepath)
// })
// }
// patchAdapter(app)
// }
/** /**
* Initialize engine * Initialize engine
* *
@@ -425,10 +468,14 @@ export class Server<
* @param opts - options passed to engine.io * @param opts - options passed to engine.io
* @private * @private
*/ */
private initEngine(srv: any, opts: Partial<EngineAttachOptions>) { private initEngine(
// // initialize engine // srv: http.Server | HTTPSServer | Http2SecureServer,
console.debug("creating engine.io instance with opts", JSON.stringify(opts)) srv: any,
this.eio = engine.attach(srv, opts) opts: EngineOptions & AttachOptions
): void {
// initialize engine
debug("creating engine.io instance with opts %j", opts)
this.eio = attach(srv, opts)
// // attach static file serving // // attach static file serving
// if (this._serveClient) this.attachServe(srv) // if (this._serveClient) this.attachServe(srv)
@@ -446,13 +493,15 @@ export class Server<
// * @param srv http server // * @param srv http server
// * @private // * @private
// */ // */
// private attachServe(srv: http.Server): void { // private attachServe(
// srv: http.Server | HTTPSServer | Http2SecureServer
// ): void {
// debug("attaching client serving req handler") // debug("attaching client serving req handler")
// const evs = srv.listeners("request").slice(0) // const evs = srv.listeners("request").slice(0)
// srv.removeAllListeners("request") // srv.removeAllListeners("request")
// srv.on("request", (req, res) => { // srv.on("request", (req, res) => {
// if (this.clientPathRegex.test(req.url)) { // if (this.clientPathRegex.test(req.url!)) {
// this.serve(req, res) // this.serve(req, res)
// } else { // } else {
// for (let i = 0; i < evs.length; i++) { // for (let i = 0; i < evs.length; i++) {
@@ -470,7 +519,7 @@ export class Server<
// * @private // * @private
// */ // */
// private serve(req: http.IncomingMessage, res: http.ServerResponse): void { // private serve(req: http.IncomingMessage, res: http.ServerResponse): void {
// const filename = req.url!.replace(this._path, "") // const filename = req.url!.replace(this._path, "").replace(/\?.*$/, "")
// const isMap = dotMapRegex.test(filename) // const isMap = dotMapRegex.test(filename)
// const type = isMap ? "map" : "source" // const type = isMap ? "map" : "source"
@@ -547,11 +596,9 @@ export class Server<
* Binds socket.io to an engine.io instance. * Binds socket.io to an engine.io instance.
* *
* @param {engine.Server} engine engine.io (or compatible) server * @param {engine.Server} engine engine.io (or compatible) server
* @return {Server} self * @return self
* @public
*/ */
public bind(engine): Server { public bind(engine): this {
console.debug('engine.io', engine.constructor.name, 'bind to socket.io')
this.engine = engine this.engine = engine
this.engine.on("connection", this.onconnection.bind(this)) this.engine.on("connection", this.onconnection.bind(this))
return this return this
@@ -561,12 +608,12 @@ export class Server<
* Called with each incoming transport connection. * Called with each incoming transport connection.
* *
* @param {engine.Socket} conn * @param {engine.Socket} conn
* @return {Server} self * @return self
* @private * @private
*/ */
private onconnection(conn: EngineIOSocket): Server { private onconnection(conn): this {
console.debug(`socket.io index incoming connection with id ${conn.id}`) debug("incoming connection with id %s", conn.id)
let client = new Client(this, conn) const client = new Client(this, conn)
if (conn.protocol === 3) { if (conn.protocol === 3) {
// @ts-ignore // @ts-ignore
client.connect("/") client.connect("/")
@@ -577,17 +624,30 @@ export class Server<
/** /**
* Looks up a namespace. * Looks up a namespace.
* *
* @param {String|RegExp|Function} name nsp name * @example
* // with a simple string
* const myNamespace = io.of("/my-namespace");
*
* // with a regex
* const dynamicNsp = io.of(/^\/dynamic-\d+$/).on("connection", (socket) => {
* const namespace = socket.nsp; // newNamespace.name === "/dynamic-101"
*
* // broadcast to all clients in the given sub-namespace
* namespace.emit("hello");
* });
*
* @param name - nsp name
* @param fn optional, nsp `connection` ev handler * @param fn optional, nsp `connection` ev handler
* @public
*/ */
public of( public of(
name: string | RegExp | ParentNspNameMatchFn, name: string | RegExp | ParentNspNameMatchFn,
fn?: (socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>) => void fn?: (
): Namespace<ListenEvents, EmitEvents, ServerSideEvents> { socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void
): Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
if (typeof name === "function" || name instanceof RegExp) { if (typeof name === "function" || name instanceof RegExp) {
const parentNsp = new ParentNamespace(this) const parentNsp = new ParentNamespace(this)
console.debug(`initializing parent namespace ${parentNsp.name}`) debug("initializing parent namespace %s", parentNsp.name)
if (typeof name === "function") { if (typeof name === "function") {
this.parentNsps.set(name, parentNsp) this.parentNsps.set(name, parentNsp)
} else { } else {
@@ -607,7 +667,7 @@ export class Server<
let nsp = this._nsps.get(name) let nsp = this._nsps.get(name)
if (!nsp) { if (!nsp) {
console.debug("initializing namespace", name) debug("initializing namespace %s", name)
nsp = new Namespace(this, name) nsp = new Namespace(this, name)
this._nsps.set(name, nsp) this._nsps.set(name, nsp)
if (name !== "/") { if (name !== "/") {
@@ -623,7 +683,6 @@ export class Server<
* Closes server connection * Closes server connection
* *
* @param [fn] optional, called as `fn([err])` on error OR all conns closed * @param [fn] optional, called as `fn([err])` on error OR all conns closed
* @public
*/ */
public close(fn?: (err?: Error) => void): void { public close(fn?: (err?: Error) => void): void {
for (const socket of this.sockets.sockets.values()) { for (const socket of this.sockets.sockets.values()) {
@@ -632,6 +691,9 @@ export class Server<
this.engine.close() this.engine.close()
// // restore the Adapter prototype
// restoreAdapter()
// if (this.httpServer) { // if (this.httpServer) {
// this.httpServer.close(fn) // this.httpServer.close(fn)
// } else { // } else {
@@ -640,14 +702,19 @@ export class Server<
} }
/** /**
* Sets up namespace middleware. * Registers a middleware, which is a function that gets executed for every incoming {@link Socket}.
* *
* @return self * @example
* @public * io.use((socket, next) => {
* // ...
* next();
* });
*
* @param fn - the middleware function
*/ */
public use( public use(
fn: ( fn: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>, socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
next: (err?: ExtendedError) => void next: (err?: ExtendedError) => void
) => void ) => void
): this { ): this {
@@ -658,41 +725,71 @@ export class Server<
/** /**
* Targets a room when emitting. * Targets a room when emitting.
* *
* @param room * @example
* @return self * // the “foo” event will be broadcast to all connected clients in the “room-101” room
* @public * io.to("room-101").emit("foo", "bar");
*
* // with an array of rooms (a client will be notified at most once)
* io.to(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* io.to("room-101").to("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/ */
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> { public to(room: Room | Room[]) {
return this.sockets.to(room) return this.sockets.to(room)
} }
/** /**
* Targets a room when emitting. * Targets a room when emitting. Similar to `to()`, but might feel clearer in some cases:
* *
* @param room * @example
* @return self * // disconnect all clients in the "room-101" room
* @public * io.in("room-101").disconnectSockets();
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/ */
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> { public in(room: Room | Room[]) {
return this.sockets.in(room) return this.sockets.in(room)
} }
/** /**
* Excludes a room when emitting. * Excludes a room when emitting.
* *
* @param name * @example
* @return self * // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room
* @public * io.except("room-101").emit("foo", "bar");
*
* // with an array of rooms
* io.except(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* io.except("room-101").except("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/ */
public except(name: Room | Room[]): BroadcastOperator<EmitEvents> { public except(room: Room | Room[]) {
return this.sockets.except(name) return this.sockets.except(room)
} }
/** /**
* Sends a `message` event to all clients. * Sends a `message` event to all clients.
* *
* This method mimics the WebSocket.send() method.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
*
* @example
* io.send("hello");
*
* // this is equivalent to
* io.emit("message", "hello");
*
* @return self * @return self
* @public
*/ */
public send(...args: EventParams<EmitEvents, "message">): this { public send(...args: EventParams<EmitEvents, "message">): this {
this.sockets.emit("message", ...args) this.sockets.emit("message", ...args)
@@ -700,10 +797,9 @@ export class Server<
} }
/** /**
* Sends a `message` event to all clients. * Sends a `message` event to all clients. Alias of {@link send}.
* *
* @return self * @return self
* @public
*/ */
public write(...args: EventParams<EmitEvents, "message">): this { public write(...args: EventParams<EmitEvents, "message">): this {
this.sockets.emit("message", ...args) this.sockets.emit("message", ...args)
@@ -711,11 +807,30 @@ export class Server<
} }
/** /**
* Emit a packet to other Socket.IO servers * Sends a message to the other Socket.IO servers of the cluster.
*
* @example
* io.serverSideEmit("hello", "world");
*
* io.on("hello", (arg1) => {
* console.log(arg1); // prints "world"
* });
*
* // acknowledgements (without binary content) are supported too:
* io.serverSideEmit("ping", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* io.on("ping", (cb) => {
* cb("pong");
* });
* *
* @param ev - the event name * @param ev - the event name
* @param args - an array of arguments, which may include an acknowledgement callback at the end * @param args - an array of arguments, which may include an acknowledgement callback at the end
* @public
*/ */
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>( public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev, ev: Ev,
@@ -727,7 +842,8 @@ export class Server<
/** /**
* Gets a list of socket ids. * Gets a list of socket ids.
* *
* @public * @deprecated this method will be removed in the next major release, please use {@link Server#serverSideEmit} or
* {@link Server#fetchSockets} instead.
*/ */
public allSockets(): Promise<Set<SocketId>> { public allSockets(): Promise<Set<SocketId>> {
return this.sockets.allSockets() return this.sockets.allSockets()
@@ -736,11 +852,13 @@ export class Server<
/** /**
* Sets the compress flag. * Sets the compress flag.
* *
* @example
* io.compress(false).emit("hello");
*
* @param compress - if `true`, compresses the sending data * @param compress - if `true`, compresses the sending data
* @return self * @return a new {@link BroadcastOperator} instance for chaining
* @public
*/ */
public compress(compress: boolean): BroadcastOperator<EmitEvents> { public compress(compress: boolean) {
return this.sockets.compress(compress) return this.sockets.compress(compress)
} }
@@ -749,59 +867,126 @@ export class Server<
* receive messages (because of network slowness or other issues, or because theyre connected through long polling * receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle). * and is in the middle of a request-response cycle).
* *
* @return self * @example
* @public * io.volatile.emit("hello"); // the clients may or may not receive it
*
* @return a new {@link BroadcastOperator} instance for chaining
*/ */
public get volatile(): BroadcastOperator<EmitEvents> { public get volatile() {
return this.sockets.volatile return this.sockets.volatile
} }
/** /**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node. * Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
* *
* @return self * @example
* @public * // the “foo” event will be broadcast to all connected clients on this node
* io.local.emit("foo", "bar");
*
* @return a new {@link BroadcastOperator} instance for chaining
*/ */
public get local(): BroadcastOperator<EmitEvents> { public get local() {
return this.sockets.local return this.sockets.local
} }
/** /**
* Returns the matching socket instances * Adds a timeout in milliseconds for the next operation.
* *
* @public * @example
* io.timeout(1000).emit("some-event", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* @param timeout
*/ */
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> { public timeout(timeout: number) {
return this.sockets.timeout(timeout)
}
/**
* Returns the matching socket instances.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* // return all Socket instances
* const sockets = await io.fetchSockets();
*
* // return all Socket instances in the "room1" room
* const sockets = await io.in("room1").fetchSockets();
*
* for (const socket of sockets) {
* console.log(socket.id);
* console.log(socket.handshake);
* console.log(socket.rooms);
* console.log(socket.data);
*
* socket.emit("hello");
* socket.join("room1");
* socket.leave("room2");
* socket.disconnect();
* }
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents, SocketData>[]> {
return this.sockets.fetchSockets() return this.sockets.fetchSockets()
} }
/** /**
* Makes the matching socket instances join the specified rooms * Makes the matching socket instances join the specified rooms.
* *
* @param room * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
* @public *
* @example
*
* // make all socket instances join the "room1" room
* io.socketsJoin("room1");
*
* // make all socket instances in the "room1" room join the "room2" and "room3" rooms
* io.in("room1").socketsJoin(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/ */
public socketsJoin(room: Room | Room[]): void { public socketsJoin(room: Room | Room[]) {
return this.sockets.socketsJoin(room) return this.sockets.socketsJoin(room)
} }
/** /**
* Makes the matching socket instances leave the specified rooms * Makes the matching socket instances leave the specified rooms.
* *
* @param room * Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
* @public *
* @example
* // make all socket instances leave the "room1" room
* io.socketsLeave("room1");
*
* // make all socket instances in the "room1" room leave the "room2" and "room3" rooms
* io.in("room1").socketsLeave(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/ */
public socketsLeave(room: Room | Room[]): void { public socketsLeave(room: Room | Room[]) {
return this.sockets.socketsLeave(room) return this.sockets.socketsLeave(room)
} }
/** /**
* Makes the matching socket instances disconnect * Makes the matching socket instances disconnect.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* // make all socket instances disconnect (the connections might be kept alive for other namespaces)
* io.disconnectSockets();
*
* // make all socket instances in the "room1" room disconnect and close the underlying connections
* io.in("room1").disconnectSockets(true);
* *
* @param close - whether to close the underlying connection * @param close - whether to close the underlying connection
* @public
*/ */
public disconnectSockets(close: boolean = false): void { public disconnectSockets(close: boolean = false) {
return this.sockets.disconnectSockets(close) return this.sockets.disconnectSockets(close)
} }
} }
@@ -822,4 +1007,10 @@ emitterMethods.forEach(function (fn) {
} }
}) })
module.exports = (srv?, opts?) => new Server(srv, opts)
module.exports.Server = Server
module.exports.Namespace = Namespace
module.exports.Socket = Socket
export { Socket, ServerOptions, Namespace, BroadcastOperator, RemoteSocket } export { Socket, ServerOptions, Namespace, BroadcastOperator, RemoteSocket }
export { Event } from "./socket"

View File

@@ -1,4 +1,3 @@
import { Socket } from "./socket" import { Socket } from "./socket"
import type { Server } from "./index" import type { Server } from "./index"
import { import {
@@ -14,7 +13,8 @@ import type { Client } from "./client"
import type { Adapter, Room, SocketId } from "../socket.io-adapter" import type { Adapter, Room, SocketId } from "../socket.io-adapter"
import { BroadcastOperator, RemoteSocket } from "./broadcast-operator" import { BroadcastOperator, RemoteSocket } from "./broadcast-operator"
// const debug = debugModule("socket.io:namespace"); // const debug = debugModule("socket.io:namespace")
const debug = require('../debug')("socket.io:namespace")
export interface ExtendedError extends Error { export interface ExtendedError extends Error {
data?: any data?: any
@@ -23,56 +23,125 @@ export interface ExtendedError extends Error {
export interface NamespaceReservedEventsMap< export interface NamespaceReservedEventsMap<
ListenEvents extends EventsMap, ListenEvents extends EventsMap,
EmitEvents extends EventsMap, EmitEvents extends EventsMap,
ServerSideEvents extends EventsMap ServerSideEvents extends EventsMap,
> { SocketData
connect: (socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>) => void > {
connect: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void
connection: ( connection: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents> socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void ) => void
} }
export interface ServerReservedEventsMap< export interface ServerReservedEventsMap<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap,
ServerSideEvents extends EventsMap,
SocketData
> extends NamespaceReservedEventsMap<
ListenEvents, ListenEvents,
EmitEvents, EmitEvents,
ServerSideEvents ServerSideEvents,
> extends NamespaceReservedEventsMap< SocketData
ListenEvents, > {
EmitEvents,
ServerSideEvents
> {
new_namespace: ( new_namespace: (
namespace: Namespace<ListenEvents, EmitEvents, ServerSideEvents> namespace: Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void ) => void
} }
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set< export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
keyof ServerReservedEventsMap<never, never, never> keyof ServerReservedEventsMap<never, never, never, never>
>(<const>["connect", "connection", "new_namespace"]) >(<const>["connect", "connection", "new_namespace"])
/**
* A Namespace is a communication channel that allows you to split the logic of your application over a single shared
* connection.
*
* Each namespace has its own:
*
* - event handlers
*
* ```
* io.of("/orders").on("connection", (socket) => {
* socket.on("order:list", () => {});
* socket.on("order:create", () => {});
* });
*
* io.of("/users").on("connection", (socket) => {
* socket.on("user:list", () => {});
* });
* ```
*
* - rooms
*
* ```
* const orderNamespace = io.of("/orders");
*
* orderNamespace.on("connection", (socket) => {
* socket.join("room1");
* orderNamespace.to("room1").emit("hello");
* });
*
* const userNamespace = io.of("/users");
*
* userNamespace.on("connection", (socket) => {
* socket.join("room1"); // distinct from the room in the "orders" namespace
* userNamespace.to("room1").emit("holà");
* });
* ```
*
* - middlewares
*
* ```
* const orderNamespace = io.of("/orders");
*
* orderNamespace.use((socket, next) => {
* // ensure the socket has access to the "orders" namespace
* });
*
* const userNamespace = io.of("/users");
*
* userNamespace.use((socket, next) => {
* // ensure the socket has access to the "users" namespace
* });
* ```
*/
export class Namespace< export class Namespace<
ListenEvents extends EventsMap = DefaultEventsMap, ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents, EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap ServerSideEvents extends EventsMap = DefaultEventsMap,
> extends StrictEventEmitter< SocketData = any
> extends StrictEventEmitter<
ServerSideEvents, ServerSideEvents,
EmitEvents, EmitEvents,
NamespaceReservedEventsMap<ListenEvents, EmitEvents, ServerSideEvents> NamespaceReservedEventsMap<
> { ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>
> {
public readonly name: string public readonly name: string
public readonly sockets: Map< public readonly sockets: Map<
SocketId, SocketId,
Socket<ListenEvents, EmitEvents, ServerSideEvents> Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map(); > = new Map();
public adapter: Adapter public adapter: Adapter
/** @private */ /** @private */
readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents> readonly server: Server<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>
/** @private */ /** @private */
_fns: Array< _fns: Array<
( (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>, socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
next: (err?: ExtendedError) => void next: (err?: ExtendedError) => void
) => void ) => void
> = []; > = [];
@@ -87,7 +156,7 @@ export class Namespace<
* @param name * @param name
*/ */
constructor( constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents>, server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
name: string name: string
) { ) {
super() super()
@@ -103,20 +172,27 @@ export class Namespace<
* *
* @private * @private
*/ */
_initAdapter() { _initAdapter(): void {
// @ts-ignore // @ts-ignore
this.adapter = new (this.server.adapter()!)(this) this.adapter = new (this.server.adapter()!)(this)
} }
/** /**
* Sets up namespace middleware. * Registers a middleware, which is a function that gets executed for every incoming {@link Socket}.
* *
* @return self * @example
* @public * const myNamespace = io.of("/my-namespace");
*
* myNamespace.use((socket, next) => {
* // ...
* next();
* });
*
* @param fn - the middleware function
*/ */
public use( public use(
fn: ( fn: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>, socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
next: (err?: ExtendedError) => void next: (err?: ExtendedError) => void
) => void ) => void
): this { ): this {
@@ -132,7 +208,7 @@ export class Namespace<
* @private * @private
*/ */
private run( private run(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>, socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
fn: (err: ExtendedError | null) => void fn: (err: ExtendedError | null) => void
) { ) {
const fns = this._fns.slice(0) const fns = this._fns.slice(0)
@@ -157,34 +233,63 @@ export class Namespace<
/** /**
* Targets a room when emitting. * Targets a room when emitting.
* *
* @param room * @example
* @return self * const myNamespace = io.of("/my-namespace");
* @public *
* // the “foo” event will be broadcast to all connected clients in the “room-101” room
* myNamespace.to("room-101").emit("foo", "bar");
*
* // with an array of rooms (a client will be notified at most once)
* myNamespace.to(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* myNamespace.to("room-101").to("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/ */
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> { public to(room: Room | Room[]) {
return new BroadcastOperator(this.adapter).to(room) return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).to(room)
} }
/** /**
* Targets a room when emitting. * Targets a room when emitting. Similar to `to()`, but might feel clearer in some cases:
* *
* @param room * @example
* @return self * const myNamespace = io.of("/my-namespace");
* @public *
* // disconnect all clients in the "room-101" room
* myNamespace.in("room-101").disconnectSockets();
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/ */
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> { public in(room: Room | Room[]) {
return new BroadcastOperator(this.adapter).in(room) return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).in(room)
} }
/** /**
* Excludes a room when emitting. * Excludes a room when emitting.
* *
* @param room * @example
* @return self * const myNamespace = io.of("/my-namespace");
* @public *
* // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room
* myNamespace.except("room-101").emit("foo", "bar");
*
* // with an array of rooms
* myNamespace.except(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* myNamespace.except("room-101").except("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/ */
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> { public except(room: Room | Room[]) {
return new BroadcastOperator(this.adapter).except(room) return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).except(
room
)
} }
/** /**
@@ -197,41 +302,45 @@ export class Namespace<
client: Client<ListenEvents, EmitEvents, ServerSideEvents>, client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
query, query,
fn?: (socket: Socket) => void fn?: (socket: Socket) => void
): Socket<ListenEvents, EmitEvents, ServerSideEvents> { ): Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
const socket = new Socket(this, client, query || {}) debug("adding socket to nsp %s", this.name)
console.debug(`socket.io namespace client ${client.id} adding socket ${socket.id} to nsp ${this.name}`) const socket = new Socket(this, client, query)
this.run(socket, err => { this.run(socket, (err) => {
process.nextTick(() => { process.nextTick(() => {
if ("open" == client.conn.readyState) { if ("open" !== client.conn.readyState) {
if (err) { debug("next called after client was closed - ignoring socket")
if (client.conn.protocol === 3) { socket._cleanup()
return socket._error(err.data || err.message) return
} else {
return socket._error({
message: err.message,
data: err.data,
})
}
}
// track socket
this.sockets.set(socket.id, socket)
console.debug(`socket.io namespace ${this.name} track client ${client.id} socket ${socket.id}`)
// it's paramount that the internal `onconnect` logic
// fires before user-set events to prevent state order
// violations (such as a disconnection before the connection
// logic is complete)
socket._onconnect()
// @java-patch multi thread need direct callback socket
if (fn) fn(socket)
// fire user-set events
this.emitReserved("connect", socket)
this.emitReserved("connection", socket)
} else {
console.debug(`next called after client ${client.id} was closed - ignoring socket`)
} }
if (err) {
debug("middleware error, sending CONNECT_ERROR packet to the client")
socket._cleanup()
if (client.conn.protocol === 3) {
return socket._error(err.data || err.message)
} else {
return socket._error({
message: err.message,
data: err.data,
})
}
}
// track socket
this.sockets.set(socket.id, socket)
// it's paramount that the internal `onconnect` logic
// fires before user-set events to prevent state order
// violations (such as a disconnection before the connection
// logic is complete)
socket._onconnect()
// if (fn) fn()
// @java-patch multi thread need direct callback socket
if (fn) fn(socket)
// fire user-set events
this.emitReserved("connect", socket)
this.emitReserved("connection", socket)
}) })
}) })
return socket return socket
@@ -242,33 +351,64 @@ export class Namespace<
* *
* @private * @private
*/ */
_remove(socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>): void { _remove(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
): void {
if (this.sockets.has(socket.id)) { if (this.sockets.has(socket.id)) {
console.debug(`namespace ${this.name} remove socket ${socket.id}`)
this.sockets.delete(socket.id) this.sockets.delete(socket.id)
} else { } else {
console.debug(`namespace ${this.name} ignoring remove for ${socket.id}`) debug("ignoring remove for %s", socket.id)
} }
} }
/** /**
* Emits to all clients. * Emits to all connected clients.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.emit("hello", "world");
*
* // all serializable datastructures are supported (no need to call JSON.stringify)
* myNamespace.emit("hello", 1, "2", { 3: ["4"], 5: Uint8Array.from([6]) });
*
* // with an acknowledgement from the clients
* myNamespace.timeout(1000).emit("some-event", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
* *
* @return Always true * @return Always true
* @public
*/ */
public emit<Ev extends EventNames<EmitEvents>>( public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev, ev: Ev,
...args: EventParams<EmitEvents, Ev> ...args: EventParams<EmitEvents, Ev>
): boolean { ): boolean {
return new BroadcastOperator<EmitEvents>(this.adapter).emit(ev, ...args) return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).emit(
ev,
...args
)
} }
/** /**
* Sends a `message` event to all clients. * Sends a `message` event to all clients.
* *
* This method mimics the WebSocket.send() method.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.send("hello");
*
* // this is equivalent to
* myNamespace.emit("message", "hello");
*
* @return self * @return self
* @public
*/ */
public send(...args: EventParams<EmitEvents, "message">): this { public send(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args) this.emit("message", ...args)
@@ -276,10 +416,9 @@ export class Namespace<
} }
/** /**
* Sends a `message` event to all clients. * Sends a `message` event to all clients. Sends a `message` event. Alias of {@link send}.
* *
* @return self * @return self
* @public
*/ */
public write(...args: EventParams<EmitEvents, "message">): this { public write(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args) this.emit("message", ...args)
@@ -287,18 +426,39 @@ export class Namespace<
} }
/** /**
* Emit a packet to other Socket.IO servers * Sends a message to the other Socket.IO servers of the cluster.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.serverSideEmit("hello", "world");
*
* myNamespace.on("hello", (arg1) => {
* console.log(arg1); // prints "world"
* });
*
* // acknowledgements (without binary content) are supported too:
* myNamespace.serverSideEmit("ping", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* myNamespace.on("ping", (cb) => {
* cb("pong");
* });
* *
* @param ev - the event name * @param ev - the event name
* @param args - an array of arguments, which may include an acknowledgement callback at the end * @param args - an array of arguments, which may include an acknowledgement callback at the end
* @public
*/ */
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>( public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev, ev: Ev,
...args: EventParams<ServerSideEvents, Ev> ...args: EventParams<ServerSideEvents, Ev>
): boolean { ): boolean {
if (RESERVED_EVENTS.has(ev)) { if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${ev}" is a reserved event name`) throw new Error(`"${String(ev)}" is a reserved event name`)
} }
args.unshift(ev) args.unshift(ev)
this.adapter.serverSideEmit(args) this.adapter.serverSideEmit(args)
@@ -319,22 +479,30 @@ export class Namespace<
/** /**
* Gets a list of clients. * Gets a list of clients.
* *
* @return self * @deprecated this method will be removed in the next major release, please use {@link Namespace#serverSideEmit} or
* @public * {@link Namespace#fetchSockets} instead.
*/ */
public allSockets(): Promise<Set<SocketId>> { public allSockets(): Promise<Set<SocketId>> {
return new BroadcastOperator(this.adapter).allSockets() return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).allSockets()
} }
/** /**
* Sets the compress flag. * Sets the compress flag.
* *
* @param compress - if `true`, compresses the sending data * @example
* @return self * const myNamespace = io.of("/my-namespace");
* @public *
*/ * myNamespace.compress(false).emit("hello");
public compress(compress: boolean): BroadcastOperator<EmitEvents> { *
return new BroadcastOperator(this.adapter).compress(compress) * @param compress - if `true`, compresses the sending data
* @return self
*/
public compress(compress: boolean) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).compress(
compress
)
} }
/** /**
@@ -342,62 +510,153 @@ export class Namespace<
* receive messages (because of network slowness or other issues, or because theyre connected through long polling * receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle). * and is in the middle of a request-response cycle).
* *
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.volatile.emit("hello"); // the clients may or may not receive it
*
* @return self * @return self
* @public
*/ */
public get volatile(): BroadcastOperator<EmitEvents> { public get volatile() {
return new BroadcastOperator(this.adapter).volatile return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).volatile
} }
/** /**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node. * Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
* *
* @return self * @example
* @public * const myNamespace = io.of("/my-namespace");
*/
public get local(): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).local
}
/**
* Returns the matching socket instances
* *
* @public * // the “foo” event will be broadcast to all connected clients on this node
*/ * myNamespace.local.emit("foo", "bar");
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
return new BroadcastOperator(this.adapter).fetchSockets()
}
/**
* Makes the matching socket instances join the specified rooms
* *
* @param room * @return a new {@link BroadcastOperator} instance for chaining
* @public
*/ */
public socketsJoin(room: Room | Room[]): void { public get local() {
return new BroadcastOperator(this.adapter).socketsJoin(room) return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).local
} }
/** /**
* Makes the matching socket instances leave the specified rooms * Adds a timeout in milliseconds for the next operation.
* *
* @param room * @example
* @public * const myNamespace = io.of("/my-namespace");
*
* myNamespace.timeout(1000).emit("some-event", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* @param timeout
*/ */
public socketsLeave(room: Room | Room[]): void { public timeout(timeout: number) {
return new BroadcastOperator(this.adapter).socketsLeave(room) return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).timeout(
timeout
)
} }
/** /**
* Makes the matching socket instances disconnect * Returns the matching socket instances.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* // return all Socket instances
* const sockets = await myNamespace.fetchSockets();
*
* // return all Socket instances in the "room1" room
* const sockets = await myNamespace.in("room1").fetchSockets();
*
* for (const socket of sockets) {
* console.log(socket.id);
* console.log(socket.handshake);
* console.log(socket.rooms);
* console.log(socket.data);
*
* socket.emit("hello");
* socket.join("room1");
* socket.leave("room2");
* socket.disconnect();
* }
*/
public fetchSockets() {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).fetchSockets()
}
/**
* Makes the matching socket instances join the specified rooms.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* // make all socket instances join the "room1" room
* myNamespace.socketsJoin("room1");
*
* // make all socket instances in the "room1" room join the "room2" and "room3" rooms
* myNamespace.in("room1").socketsJoin(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/
public socketsJoin(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).socketsJoin(room)
}
/**
* Makes the matching socket instances leave the specified rooms.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* // make all socket instances leave the "room1" room
* myNamespace.socketsLeave("room1");
*
* // make all socket instances in the "room1" room leave the "room2" and "room3" rooms
* myNamespace.in("room1").socketsLeave(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/
public socketsLeave(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).socketsLeave(room)
}
/**
* Makes the matching socket instances disconnect.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* // make all socket instances disconnect (the connections might be kept alive for other namespaces)
* myNamespace.disconnectSockets();
*
* // make all socket instances in the "room1" room disconnect and close the underlying connections
* myNamespace.in("room1").disconnectSockets(true);
* *
* @param close - whether to close the underlying connection * @param close - whether to close the underlying connection
* @public
*/ */
public disconnectSockets(close: boolean = false): void { public disconnectSockets(close: boolean = false) {
return new BroadcastOperator(this.adapter).disconnectSockets(close) return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).disconnectSockets(close)
} }
// @java-patch
public close() { public close() {
RESERVED_EVENTS.forEach(event => this.removeAllListeners(event as any)) RESERVED_EVENTS.forEach(event => this.removeAllListeners(event as any))
this.server._nsps.delete(this.name) this.server._nsps.delete(this.name)

View File

@@ -1,5 +1,5 @@
import { Namespace } from "./namespace" import { Namespace } from "./namespace"
import type { Server } from "./index" import type { Server, RemoteSocket } from "./index"
import type { import type {
EventParams, EventParams,
EventNames, EventNames,
@@ -12,16 +12,24 @@ import type { BroadcastOptions } from "../socket.io-adapter"
export class ParentNamespace< export class ParentNamespace<
ListenEvents extends EventsMap = DefaultEventsMap, ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents, EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap ServerSideEvents extends EventsMap = DefaultEventsMap,
> extends Namespace<ListenEvents, EmitEvents, ServerSideEvents> { SocketData = any
> extends Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
private static count: number = 0; private static count: number = 0;
private children: Set<Namespace<ListenEvents, EmitEvents, ServerSideEvents>> = new Set(); private children: Set<
Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Set();
constructor(server: Server<ListenEvents, EmitEvents, ServerSideEvents>) { constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) {
super(server, "/_" + ParentNamespace.count++) super(server, "/_" + ParentNamespace.count++)
} }
_initAdapter() { /**
* @private
*/
_initAdapter(): void {
const broadcast = (packet: any, opts: BroadcastOptions) => { const broadcast = (packet: any, opts: BroadcastOptions) => {
this.children.forEach((nsp) => { this.children.forEach((nsp) => {
nsp.adapter.broadcast(packet, opts) nsp.adapter.broadcast(packet, opts)
@@ -42,21 +50,9 @@ export class ParentNamespace<
return true return true
} }
// public emit(...args: any[]): boolean {
// this.children.forEach(nsp => {
// nsp._rooms = this._rooms
// nsp._flags = this._flags
// nsp.emit.apply(nsp, args as any)
// })
// this._rooms.clear()
// this._flags = {}
// return true
// }
createChild( createChild(
name: string name: string
): Namespace<ListenEvents, EmitEvents, ServerSideEvents> { ): Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
const namespace = new Namespace(this.server, name) const namespace = new Namespace(this.server, name)
namespace._fns = this._fns.slice(0) namespace._fns = this._fns.slice(0)
this.listeners("connect").forEach((listener) => this.listeners("connect").forEach((listener) =>
@@ -69,4 +65,13 @@ export class ParentNamespace<
this.server._nsps.set(name, namespace) this.server._nsps.set(name, namespace)
return namespace return namespace
} }
fetchSockets(): Promise<RemoteSocket<EmitEvents, SocketData>[]> {
// note: we could make the fetchSockets() method work for dynamic namespaces created with a regex (by sending the
// regex to the other Socket.IO servers, and returning the sockets of each matching namespace for example), but
// the behavior for namespaces created with a function is less clear
// note²: we cannot loop over each children namespace, because with multiple Socket.IO servers, a given namespace
// may exist on one node but not exist on another (since it is created upon client connection)
throw new Error("fetchSockets() is not supported on parent namespaces")
}
} }

View File

@@ -1,7 +1,6 @@
// import { Packet, PacketType } from "socket.io-parser"
import { Packet, PacketType } from "../socket.io-parser" import { Packet, PacketType } from "../socket.io-parser"
import url = require("url") // import debugModule from "debug";
// import debugModule from "debug"
import type { Server } from "./index" import type { Server } from "./index"
import { import {
EventParams, EventParams,
@@ -12,24 +11,41 @@ import {
} from "./typed-events" } from "./typed-events"
import type { Client } from "./client" import type { Client } from "./client"
import type { Namespace, NamespaceReservedEventsMap } from "./namespace" import type { Namespace, NamespaceReservedEventsMap } from "./namespace"
// import type { IncomingMessage, IncomingHttpHeaders } from "http" // import type { IncomingMessage, IncomingHttpHeaders } from "http";
import type { import type {
Adapter, Adapter,
BroadcastFlags, BroadcastFlags,
Room, Room,
SocketId, SocketId,
} from "socket.io-adapter" } from "../socket.io-adapter"
// import base64id from "base64id" // import base64id from "base64id";
import type { ParsedUrlQuery } from "querystring" import type { ParsedUrlQuery } from "querystring"
import { BroadcastOperator } from "./broadcast-operator" import { BroadcastOperator } from "./broadcast-operator"
import * as url from "url"
// const debug = debugModule("socket.io:socket"); // const debug = debugModule("socket.io:socket");
const debug = require('../debug')("socket.io:socket")
type ClientReservedEvents = "connect_error" type ClientReservedEvents = "connect_error"
// TODO for next major release: cleanup disconnect reasons
export type DisconnectReason =
// Engine.IO close reasons
| "transport error"
| "transport close"
| "forced close"
| "ping timeout"
| "parse error"
// Socket.IO disconnect reasons
| "server shutting down"
| "forced server close"
| "client namespace disconnect"
| "server namespace disconnect"
| any
export interface SocketReservedEventsMap { export interface SocketReservedEventsMap {
disconnect: (reason: string) => void disconnect: (reason: DisconnectReason) => void
disconnecting: (reason: string) => void disconnecting: (reason: DisconnectReason) => void
error: (err: Error) => void error: (err: Error) => void
} }
@@ -47,7 +63,7 @@ export interface EventEmitterReservedEventsMap {
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set< export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
| ClientReservedEvents | ClientReservedEvents
| keyof NamespaceReservedEventsMap<never, never, never> | keyof NamespaceReservedEventsMap<never, never, never, never>
| keyof SocketReservedEventsMap | keyof SocketReservedEventsMap
| keyof EventEmitterReservedEventsMap | keyof EventEmitterReservedEventsMap
>(<const>[ >(<const>[
@@ -66,7 +82,8 @@ export interface Handshake {
/** /**
* The headers sent as part of the handshake * The headers sent as part of the handshake
*/ */
headers: any//IncomingHttpHeaders // headers: IncomingHttpHeaders;
headers: any
/** /**
* The date of creation (as string) * The date of creation (as string)
@@ -109,33 +126,94 @@ export interface Handshake {
auth: { [key: string]: any } auth: { [key: string]: any }
} }
/**
* `[eventName, ...args]`
*/
export type Event = [string, ...any[]]
function noop() { }
/**
* This is the main object for interacting with a client.
*
* A Socket belongs to a given {@link Namespace} and uses an underlying {@link Client} to communicate.
*
* Within each {@link Namespace}, you can also define arbitrary channels (called "rooms") that the {@link Socket} can
* join and leave. That provides a convenient way to broadcast to a group of socket instances.
*
* @example
* io.on("connection", (socket) => {
* console.log(`socket ${socket.id} connected`);
*
* // send an event to the client
* socket.emit("foo", "bar");
*
* socket.on("foobar", () => {
* // an event was received from the client
* });
*
* // join the room named "room1"
* socket.join("room1");
*
* // broadcast to everyone in the room named "room1"
* io.to("room1").emit("hello");
*
* // upon disconnection
* socket.on("disconnect", (reason) => {
* console.log(`socket ${socket.id} disconnected due to ${reason}`);
* });
* });
*/
export class Socket< export class Socket<
ListenEvents extends EventsMap = DefaultEventsMap, ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents, EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap ServerSideEvents extends EventsMap = DefaultEventsMap,
> extends StrictEventEmitter< SocketData = any
> extends StrictEventEmitter<
ListenEvents, ListenEvents,
EmitEvents, EmitEvents,
SocketReservedEventsMap SocketReservedEventsMap
> { > {
public readonly id: SocketId
public readonly handshake: Handshake
/** /**
* Additional information that can be attached to the Socket instance and which will be used in the fetchSockets method * An unique identifier for the session.
*/ */
public data: any = {}; public readonly id: SocketId
/**
* The handshake details.
*/
public readonly handshake: Handshake
/**
* Additional information that can be attached to the Socket instance and which will be used in the
* {@link Server.fetchSockets()} method.
*/
public data: Partial<SocketData> = {};
/**
* Whether the socket is currently connected or not.
*
* @example
* io.use((socket, next) => {
* console.log(socket.connected); // false
* next();
* });
*
* io.on("connection", (socket) => {
* console.log(socket.connected); // true
* });
*/
public connected: boolean = false;
public connected: boolean private readonly server: Server<
public disconnected: boolean ListenEvents,
EmitEvents,
private readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents> ServerSideEvents,
SocketData
>
private readonly adapter: Adapter private readonly adapter: Adapter
private acks: Map<number, () => void> = new Map(); private acks: Map<number, () => void> = new Map();
private fns: Array<(event: Array<any>, next: (err?: Error) => void) => void> = private fns: Array<(event: Event, next: (err?: Error) => void) => void> = [];
[];
private flags: BroadcastFlags = {}; private flags: BroadcastFlags = {};
private _anyListeners?: Array<(...args: any[]) => void> private _anyListeners?: Array<(...args: any[]) => void>
private _anyOutgoingListeners?: Array<(...args: any[]) => void>
/** /**
* Interface to a `Client` for a given `Namespace`. * Interface to a `Client` for a given `Namespace`.
@@ -151,23 +229,23 @@ export class Socket<
auth: object auth: object
) { ) {
super() super()
this.nsp = nsp
this.server = nsp.server this.server = nsp.server
this.adapter = this.nsp.adapter this.adapter = this.nsp.adapter
// if (client.conn.protocol === 3) { // if (client.conn.protocol === 3) {
// // @ts-ignore // @ts-ignore
this.id = nsp.name !== "/" ? nsp.name + "#" + client.id : client.id this.id = nsp.name !== "/" ? nsp.name + "#" + client.id : client.id
// } else { // } else {
// this.id = base64id.generateId() // don't reuse the Engine.IO id because it's sensitive information // this.id = base64id.generateId() // don't reuse the Engine.IO id because it's sensitive information
// } // }
this.client = client
this.acks = new Map()
this.connected = true
this.disconnected = false
this.handshake = this.buildHandshake(auth) this.handshake = this.buildHandshake(auth)
} }
buildHandshake(auth): Handshake { /**
* Builds the `handshake` BC object
*
* @private
*/
private buildHandshake(auth: object): Handshake {
return { return {
headers: this.request.headers, headers: this.request.headers,
time: new Date() + "", time: new Date() + "",
@@ -177,6 +255,7 @@ export class Socket<
secure: !!this.request.connection.encrypted, secure: !!this.request.connection.encrypted,
issued: +new Date(), issued: +new Date(),
url: this.request.url!, url: this.request.url!,
// @ts-ignore
query: url.parse(this.request.url!, true).query, query: url.parse(this.request.url!, true).query,
auth, auth,
} }
@@ -185,15 +264,27 @@ export class Socket<
/** /**
* Emits to this client. * Emits to this client.
* *
* @example
* io.on("connection", (socket) => {
* socket.emit("hello", "world");
*
* // all serializable datastructures are supported (no need to call JSON.stringify)
* socket.emit("hello", 1, "2", { 3: ["4"], 5: Buffer.from([6]) });
*
* // with an acknowledgement from the client
* socket.emit("hello", "world", (val) => {
* // ...
* });
* });
*
* @return Always returns `true`. * @return Always returns `true`.
* @public
*/ */
public emit<Ev extends EventNames<EmitEvents>>( public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev, ev: Ev,
...args: EventParams<EmitEvents, Ev> ...args: EventParams<EmitEvents, Ev>
): boolean { ): boolean {
if (RESERVED_EVENTS.has(ev)) { if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${ev}" is a reserved event name`) throw new Error(`"${String(ev)}" is a reserved event name`)
} }
const data: any[] = [ev, ...args] const data: any[] = [ev, ...args]
const packet: any = { const packet: any = {
@@ -203,57 +294,124 @@ export class Socket<
// access last argument to see if it's an ACK callback // access last argument to see if it's an ACK callback
if (typeof data[data.length - 1] === "function") { if (typeof data[data.length - 1] === "function") {
console.trace("emitting packet with ack id %d", this.nsp._ids) const id = this.nsp._ids++
this.acks.set(this.nsp._ids, data.pop()) debug("emitting packet with ack id %d", id)
packet.id = this.nsp._ids++
this.registerAckCallback(id, data.pop())
packet.id = id
} }
const flags = Object.assign({}, this.flags) const flags = Object.assign({}, this.flags)
this.flags = {} this.flags = {}
this.notifyOutgoingListeners(packet)
this.packet(packet, flags) this.packet(packet, flags)
return true return true
} }
/** /**
* Targets a room when broadcasting. * @private
*
* @param room
* @return self
* @public
*/ */
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> { private registerAckCallback(id: number, ack: (...args: any[]) => void): void {
return this.newBroadcastOperator().to(room) const timeout = this.flags.timeout
if (timeout === undefined) {
this.acks.set(id, ack)
return
}
const timer = setTimeout(() => {
debug("event with ack id %d has timed out after %d ms", id, timeout)
this.acks.delete(id)
ack.call(this, new Error("operation has timed out"))
}, timeout)
this.acks.set(id, (...args) => {
clearTimeout(timer)
ack.apply(this, [null, ...args])
})
} }
/** /**
* Targets a room when broadcasting. * Targets a room when broadcasting.
* *
* @param room * @example
* @return self * io.on("connection", (socket) => {
* @public * // the “foo” event will be broadcast to all connected clients in the “room-101” room, except this socket
* socket.to("room-101").emit("foo", "bar");
*
* // the code above is equivalent to:
* io.to("room-101").except(socket.id).emit("foo", "bar");
*
* // with an array of rooms (a client will be notified at most once)
* socket.to(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* socket.to("room-101").to("room-102").emit("foo", "bar");
* });
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/ */
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> { public to(room: Room | Room[]) {
return this.newBroadcastOperator().to(room)
}
/**
* Targets a room when broadcasting. Similar to `to()`, but might feel clearer in some cases:
*
* @example
* io.on("connection", (socket) => {
* // disconnect all clients in the "room-101" room, except this socket
* socket.in("room-101").disconnectSockets();
* });
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public in(room: Room | Room[]) {
return this.newBroadcastOperator().in(room) return this.newBroadcastOperator().in(room)
} }
/** /**
* Excludes a room when broadcasting. * Excludes a room when broadcasting.
* *
* @param room * @example
* @return self * io.on("connection", (socket) => {
* @public * // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room
* // and this socket
* socket.except("room-101").emit("foo", "bar");
*
* // with an array of rooms
* socket.except(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* socket.except("room-101").except("room-102").emit("foo", "bar");
* });
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/ */
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> { public except(room: Room | Room[]) {
return this.newBroadcastOperator().except(room) return this.newBroadcastOperator().except(room)
} }
/** /**
* Sends a `message` event. * Sends a `message` event.
* *
* This method mimics the WebSocket.send() method.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
*
* @example
* io.on("connection", (socket) => {
* socket.send("hello");
*
* // this is equivalent to
* socket.emit("message", "hello");
* });
*
* @return self * @return self
* @public
*/ */
public send(...args: EventParams<EmitEvents, "message">): this { public send(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args) this.emit("message", ...args)
@@ -261,10 +419,9 @@ export class Socket<
} }
/** /**
* Sends a `message` event. * Sends a `message` event. Alias of {@link send}.
* *
* @return self * @return self
* @public
*/ */
public write(...args: EventParams<EmitEvents, "message">): this { public write(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args) this.emit("message", ...args)
@@ -290,12 +447,20 @@ export class Socket<
/** /**
* Joins a room. * Joins a room.
* *
* @example
* io.on("connection", (socket) => {
* // join a single room
* socket.join("room1");
*
* // join multiple rooms
* socket.join(["room1", "room2"]);
* });
*
* @param {String|Array} rooms - room or array of rooms * @param {String|Array} rooms - room or array of rooms
* @return a Promise or nothing, depending on the adapter * @return a Promise or nothing, depending on the adapter
* @public
*/ */
public join(rooms: Room | Array<Room>): Promise<void> | void { public join(rooms: Room | Array<Room>): Promise<void> | void {
console.debug(`join room ${rooms}`) debug("join room %s", rooms)
return this.adapter.addAll( return this.adapter.addAll(
this.id, this.id,
@@ -306,12 +471,20 @@ export class Socket<
/** /**
* Leaves a room. * Leaves a room.
* *
* @example
* io.on("connection", (socket) => {
* // leave a single room
* socket.leave("room1");
*
* // leave multiple rooms
* socket.leave("room1").leave("room2");
* });
*
* @param {String} room * @param {String} room
* @return a Promise or nothing, depending on the adapter * @return a Promise or nothing, depending on the adapter
* @public
*/ */
public leave(room: string): Promise<void> | void { public leave(room: string): Promise<void> | void {
console.debug(`leave room ${room}`) debug("leave room %s", room)
return this.adapter.del(this.id, room) return this.adapter.del(this.id, room)
} }
@@ -334,7 +507,8 @@ export class Socket<
* @private * @private
*/ */
_onconnect(): void { _onconnect(): void {
console.debug(`socket ${this.id} connected - writing packet`) debug("socket connected - writing packet")
this.connected = true
this.join(this.id) this.join(this.id)
if (this.conn.protocol === 3) { if (this.conn.protocol === 3) {
this.packet({ type: PacketType.CONNECT }) this.packet({ type: PacketType.CONNECT })
@@ -350,7 +524,7 @@ export class Socket<
* @private * @private
*/ */
_onpacket(packet: Packet): void { _onpacket(packet: Packet): void {
console.trace("got packet", JSON.stringify(packet)) debug("got packet %j", packet)
switch (packet.type) { switch (packet.type) {
case PacketType.EVENT: case PacketType.EVENT:
this.onevent(packet) this.onevent(packet)
@@ -371,9 +545,6 @@ export class Socket<
case PacketType.DISCONNECT: case PacketType.DISCONNECT:
this.ondisconnect() this.ondisconnect()
break break
case PacketType.CONNECT_ERROR:
this._onerror(new Error(packet.data))
} }
} }
@@ -385,10 +556,10 @@ export class Socket<
*/ */
private onevent(packet: Packet): void { private onevent(packet: Packet): void {
const args = packet.data || [] const args = packet.data || []
console.trace("emitting event", JSON.stringify(args)) debug("emitting event %j", args)
if (null != packet.id) { if (null != packet.id) {
console.trace("attaching ack callback to event") debug("attaching ack callback to event")
args.push(this.ack(packet.id)) args.push(this.ack(packet.id))
} }
@@ -414,7 +585,7 @@ export class Socket<
// prevent double callbacks // prevent double callbacks
if (sent) return if (sent) return
const args = Array.prototype.slice.call(arguments) const args = Array.prototype.slice.call(arguments)
console.trace("sending ack", JSON.stringify(args)) debug("sending ack %j", args)
self.packet({ self.packet({
id: id, id: id,
@@ -434,11 +605,11 @@ export class Socket<
private onack(packet: Packet): void { private onack(packet: Packet): void {
const ack = this.acks.get(packet.id!) const ack = this.acks.get(packet.id!)
if ("function" == typeof ack) { if ("function" == typeof ack) {
console.trace(`socket ${this.id} calling ack ${packet.id} with ${packet.data}`) debug("calling ack %s with %j", packet.id, packet.data)
ack.apply(this, packet.data) ack.apply(this, packet.data)
this.acks.delete(packet.id!) this.acks.delete(packet.id!)
} else { } else {
console.debug(`socket ${this.id} bad ack`, packet.id) debug("bad ack %s", packet.id)
} }
} }
@@ -448,7 +619,7 @@ export class Socket<
* @private * @private
*/ */
private ondisconnect(): void { private ondisconnect(): void {
console.debug(`socket ${this.id} got disconnect packet`) debug("got disconnect packet")
this._onclose("client namespace disconnect") this._onclose("client namespace disconnect")
} }
@@ -474,19 +645,28 @@ export class Socket<
* *
* @private * @private
*/ */
_onclose(reason: string): this | undefined { _onclose(reason: DisconnectReason): this | undefined {
if (!this.connected) return this if (!this.connected) return this
console.debug(`closing socket ${this.id} - reason: ${reason}`) debug("closing socket - reason %s", reason)
this.emitReserved("disconnecting", reason) this.emitReserved("disconnecting", reason)
this.leaveAll() this._cleanup()
this.nsp._remove(this) this.nsp._remove(this)
this.client._remove(this) this.client._remove(this)
this.connected = false this.connected = false
this.disconnected = true
this.emitReserved("disconnect", reason) this.emitReserved("disconnect", reason)
return return
} }
/**
* Makes the socket leave all the rooms it was part of and prevents it from joining any other room
*
* @private
*/
_cleanup() {
this.leaveAll()
this.join = noop
}
/** /**
* Produces an `error` packet. * Produces an `error` packet.
* *
@@ -501,10 +681,17 @@ export class Socket<
/** /**
* Disconnects this client. * Disconnects this client.
* *
* @param {Boolean} close - if `true`, closes the underlying connection * @example
* @return {Socket} self * io.on("connection", (socket) => {
* // disconnect this socket (the connection might be kept alive for other namespaces)
* socket.disconnect();
* *
* @public * // disconnect this socket and close the underlying connection
* socket.disconnect(true);
* })
*
* @param {Boolean} close - if `true`, closes the underlying connection
* @return self
*/ */
public disconnect(close = false): this { public disconnect(close = false): this {
if (!this.connected) return this if (!this.connected) return this
@@ -520,9 +707,13 @@ export class Socket<
/** /**
* Sets the compress flag. * Sets the compress flag.
* *
* @example
* io.on("connection", (socket) => {
* socket.compress(false).emit("hello");
* });
*
* @param {Boolean} compress - if `true`, compresses the sending data * @param {Boolean} compress - if `true`, compresses the sending data
* @return {Socket} self * @return {Socket} self
* @public
*/ */
public compress(compress: boolean): this { public compress(compress: boolean): this {
this.flags.compress = compress this.flags.compress = compress
@@ -530,13 +721,17 @@ export class Socket<
} }
/** /**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to * Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
* receive messages (because of network slowness or other issues, or because theyre connected through long polling * receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle). * and is in the middle of a request-response cycle).
* *
* @return {Socket} self * @example
* @public * io.on("connection", (socket) => {
*/ * socket.volatile.emit("hello"); // the client may or may not receive it
* });
*
* @return {Socket} self
*/
public get volatile(): this { public get volatile(): this {
this.flags.volatile = true this.flags.volatile = true
return this return this
@@ -546,31 +741,61 @@ export class Socket<
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to every sockets but the * Sets a modifier for a subsequent event emission that the event data will only be broadcast to every sockets but the
* sender. * sender.
* *
* @return {Socket} self * @example
* @public * io.on("connection", (socket) => {
* // the “foo” event will be broadcast to all connected clients, except this socket
* socket.broadcast.emit("foo", "bar");
* });
*
* @return a new {@link BroadcastOperator} instance for chaining
*/ */
public get broadcast(): BroadcastOperator<EmitEvents> { public get broadcast() {
return this.newBroadcastOperator() return this.newBroadcastOperator()
} }
/** /**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node. * Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
* *
* @return {Socket} self * @example
* @public * io.on("connection", (socket) => {
* // the “foo” event will be broadcast to all connected clients on this node, except this socket
* socket.local.emit("foo", "bar");
* });
*
* @return a new {@link BroadcastOperator} instance for chaining
*/ */
public get local(): BroadcastOperator<EmitEvents> { public get local() {
return this.newBroadcastOperator().local return this.newBroadcastOperator().local
} }
/**
* 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 client:
*
* @example
* io.on("connection", (socket) => {
* socket.timeout(5000).emit("my-event", (err) => {
* if (err) {
* // the client did not acknowledge the event in the given delay
* }
* });
* });
*
* @returns self
*/
public timeout(timeout: number): this {
this.flags.timeout = timeout
return this
}
/** /**
* Dispatch incoming event to socket listeners. * Dispatch incoming event to socket listeners.
* *
* @param {Array} event - event that will get emitted * @param {Array} event - event that will get emitted
* @private * @private
*/ */
private dispatch(event: [eventName: string, ...args: any[]]): void { private dispatch(event: Event): void {
console.trace("dispatching an event", JSON.stringify(event)) debug("dispatching an event %j", event)
this.run(event, (err) => { this.run(event, (err) => {
process.nextTick(() => { process.nextTick(() => {
if (err) { if (err) {
@@ -579,7 +804,7 @@ export class Socket<
if (this.connected) { if (this.connected) {
super.emitUntyped.apply(this, event) super.emitUntyped.apply(this, event)
} else { } else {
console.debug("ignore packet received after disconnection") debug("ignore packet received after disconnection")
} }
}) })
}) })
@@ -588,13 +813,27 @@ export class Socket<
/** /**
* Sets up socket middleware. * Sets up socket middleware.
* *
* @example
* io.on("connection", (socket) => {
* socket.use(([event, ...args], next) => {
* if (isUnauthorized(event)) {
* return next(new Error("unauthorized event"));
* }
* // do not forget to call next
* next();
* });
*
* socket.on("error", (err) => {
* if (err && err.message === "unauthorized event") {
* socket.disconnect();
* }
* });
* });
*
* @param {Function} fn - middleware function (event, next) * @param {Function} fn - middleware function (event, next)
* @return {Socket} self * @return {Socket} self
* @public
*/ */
public use( public use(fn: (event: Event, next: (err?: Error) => void) => void): this {
fn: (event: Array<any>, next: (err?: Error) => void) => void
): this {
this.fns.push(fn) this.fns.push(fn)
return this return this
} }
@@ -606,10 +845,7 @@ export class Socket<
* @param {Function} fn - last fn call in the middleware * @param {Function} fn - last fn call in the middleware
* @private * @private
*/ */
private run( private run(event: Event, fn: (err: Error | null) => void): void {
event: [eventName: string, ...args: any[]],
fn: (err: Error | null) => void
): void {
const fns = this.fns.slice(0) const fns = this.fns.slice(0)
if (!fns.length) return fn(null) if (!fns.length) return fn(null)
@@ -629,10 +865,15 @@ export class Socket<
run(0) run(0)
} }
/**
* Whether the socket is currently disconnected
*/
public get disconnected() {
return !this.connected
}
/** /**
* A reference to the request that originated the underlying Engine.IO Socket. * A reference to the request that originated the underlying Engine.IO Socket.
*
* @public
*/ */
public get request(): any /** IncomingMessage */ { public get request(): any /** IncomingMessage */ {
return this.client.request return this.client.request
@@ -641,25 +882,47 @@ export class Socket<
/** /**
* A reference to the underlying Client transport connection (Engine.IO Socket object). * A reference to the underlying Client transport connection (Engine.IO Socket object).
* *
* @public * @example
* io.on("connection", (socket) => {
* console.log(socket.conn.transport.name); // prints "polling" or "websocket"
*
* socket.conn.once("upgrade", () => {
* console.log(socket.conn.transport.name); // prints "websocket"
* });
* });
*/ */
public get conn() { public get conn() {
return this.client.conn return this.client.conn
} }
/** /**
* @public * Returns the rooms the socket is currently in.
*
* @example
* io.on("connection", (socket) => {
* console.log(socket.rooms); // Set { <socket.id> }
*
* socket.join("room1");
*
* console.log(socket.rooms); // Set { <socket.id>, "room1" }
* });
*/ */
public get rooms(): Set<Room> { public get rooms(): Set<Room> {
return this.adapter.socketRooms(this.id) || new Set() return this.adapter.socketRooms(this.id) || new Set()
} }
/** /**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the * Adds a listener that will be fired when any event is received. The event name is passed as the first argument to
* callback. * the callback.
*
* @example
* io.on("connection", (socket) => {
* socket.onAny((event, ...args) => {
* console.log(`got event ${event}`);
* });
* });
* *
* @param listener * @param listener
* @public
*/ */
public onAny(listener: (...args: any[]) => void): this { public onAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || [] this._anyListeners = this._anyListeners || []
@@ -668,11 +931,10 @@ 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 * Adds a listener that will be fired when any event is received. The event name is passed as the first argument to
* callback. The listener is added to the beginning of the listeners array. * the callback. The listener is added to the beginning of the listeners array.
* *
* @param listener * @param listener
* @public
*/ */
public prependAny(listener: (...args: any[]) => void): this { public prependAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || [] this._anyListeners = this._anyListeners || []
@@ -681,10 +943,24 @@ export class Socket<
} }
/** /**
* Removes the listener that will be fired when any event is emitted. * Removes the listener that will be fired when any event is received.
*
* @example
* io.on("connection", (socket) => {
* 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 * @param listener
* @public
*/ */
public offAny(listener?: (...args: any[]) => void): this { public offAny(listener?: (...args: any[]) => void): this {
if (!this._anyListeners) { if (!this._anyListeners) {
@@ -707,17 +983,117 @@ export class Socket<
/** /**
* Returns an array of listeners that are listening for any event that is specified. This array can be manipulated, * Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
* e.g. to remove listeners. * e.g. to remove listeners.
*
* @public
*/ */
public listenersAny() { public listenersAny() {
return this._anyListeners || [] return this._anyListeners || []
} }
private newBroadcastOperator(): BroadcastOperator<EmitEvents> { /**
* Adds a listener that will be fired when any event is sent. The event name is passed as the first argument to
* the callback.
*
* Note: acknowledgements sent to the client are not included.
*
* @example
* io.on("connection", (socket) => {
* 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.
*
* @example
* io.on("connection", (socket) => {
* 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 sent.
*
* @example
* io.on("connection", (socket) => {
* 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
*/
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 (emit or broadcast)
*
* @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)
}
}
}
private newBroadcastOperator() {
const flags = Object.assign({}, this.flags) const flags = Object.assign({}, this.flags)
this.flags = {} this.flags = {}
return new BroadcastOperator( return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter, this.adapter,
new Set<Room>(), new Set<Room>(),
new Set<Room>([this.id]), new Set<Room>([this.id]),