feat: upgrade socket.io to v4

Signed-off-by: MiaoWoo <admin@yumc.pw>
This commit is contained in:
MiaoWoo 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())
global.setGlobal('Proxy', require('./proxy').Proxy)
global.setGlobal('XMLHttpRequest', require('./xml-http-request').XMLHttpRequest)
global.setGlobal('Buffer', require('./buffer').Buffer)
global.setGlobal('Blob', require('blob-polyfill').Blob)
console.i18n("ms.polyfill.completed", { time: (new Date().getTime() - polyfillStartTime) / 1000 })
export default true

View File

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

View File

@ -38,7 +38,7 @@ export class WebSocket extends EventEmitter {
private client: Transport
constructor(url: string, subProtocol: string = '', headers: WebSocketHeader = {}) {
constructor(url: string, subProtocol: string | string[] = '', headers: WebSocketHeader = {}) {
super()
this.manager = manager
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)
/**
* Expose deps for legacy compatibility
* and standalone browser access.
*/
const protocol = Socket.protocol // this is an int
export { Socket, protocol }
// 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'
export { Socket }
export { SocketOptions } from "./socket"
export const protocol = Socket.protocol
export { Transport } from "./transport"
export { transports } from "./transports/index"
export { installTimerFunctions } from "./util"
export { parse } from "./contrib/parseuri"
export { nextTick } from "./transports/websocket-constructor.js"

View File

@ -1,21 +1,278 @@
import transports from "./transports"
// const transports = require("./transports/index")
const Emitter = require("component-emitter")
const debug = (...args: any) => console.debug('engine.io-client:socket', ...args)//require("debug")("engine.io-client:socket")
import parser from "../engine.io-parser"
const parseuri = require("parseuri")
const parseqs = require("parseqs")
import { installTimerFunctions } from "./util"
// import { transports } from "./transports/index.js";
import { transports } from "./transports"
import { installTimerFunctions, byteLength } from "./util"
import { decode } from "./contrib/parseqs"
import { parse } from "./contrib/parseuri"
// import debugModule from "debug"; // debug()
import { Emitter } from "@socket.io/component-emitter"
// 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.
*
* @param {String|Object} uri or options
* @param {Object} options
* @param {Object} opts - options
* @api public
*/
constructor(uri, opts: any = {}) {
constructor(uri, opts: Partial<SocketOptions> = {}) {
super()
if (uri && "object" === typeof uri) {
@ -24,13 +281,13 @@ export class Socket extends Emitter {
}
if (uri) {
uri = parseuri(uri)
uri = parse(uri)
opts.hostname = uri.host
opts.secure = uri.protocol === "https" || uri.protocol === "wss"
opts.port = uri.port
if (uri.query) opts.query = uri.query
} else if (opts.host) {
opts.hostname = parseuri(opts.host).host
opts.hostname = parse(opts.host).host
}
installTimerFunctions(this, opts)
@ -53,10 +310,10 @@ export class Socket extends Emitter {
(typeof location !== "undefined" && location.port
? location.port
: this.secure
? 443
: 80)
? "443"
: "80")
this.transports = ["websocket"]
this.transports = opts.transports || ["polling", "websocket"]
this.readyState = ""
this.writeBuffer = []
this.prevBufferLen = 0
@ -67,7 +324,6 @@ export class Socket extends Emitter {
agent: false,
withCredentials: false,
upgrade: true,
jsonp: true,
timestampParam: "t",
rememberUpgrade: false,
rejectUnauthorized: true,
@ -83,7 +339,7 @@ export class Socket extends Emitter {
this.opts.path = this.opts.path.replace(/\/$/, "") + "/"
if (typeof this.opts.query === "string") {
this.opts.query = parseqs.decode(this.opts.query)
this.opts.query = decode(this.opts.query)
}
// 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
// ensures every browser behaves the same (no "disconnect" event at the Socket.IO level when the page is
// closed/reloaded)
addEventListener(
"beforeunload",
() => {
if (this.transport) {
// silently close the transport
this.transport.removeAllListeners()
this.transport.close()
}
},
false
)
this.beforeunloadEventListener = () => {
if (this.transport) {
// silently close the transport
this.transport.removeAllListeners()
this.transport.close()
}
}
addEventListener("beforeunload", this.beforeunloadEventListener, false)
}
if (this.hostname !== "localhost") {
this.offlineEventListener = () => {
this.onClose("transport close")
this.onClose("transport close", {
description: "network connection lost"
})
}
addEventListener("offline", this.offlineEventListener, false)
}
@ -130,15 +385,12 @@ export class Socket extends Emitter {
* @return {Transport}
* @api private
*/
createTransport(name, opt?) {
if (name != 'websocket') {
throw new Error('Only Support WebSocket in MiaoScript!')
}
private createTransport(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
query.EIO = parser.protocol
query.EIO = protocol
// transport name
query.transport = name
@ -159,8 +411,8 @@ export class Socket extends Emitter {
}
)
debug("options: %j", JSON.stringify(opts))
debug("new func", transports[name])
debug("options: %j", opts)
return new transports[name](opts)
}
@ -169,7 +421,7 @@ export class Socket extends Emitter {
*
* @api private
*/
open() {
private open() {
let transport
if (
this.opts.rememberUpgrade &&
@ -180,7 +432,7 @@ export class Socket extends Emitter {
} else if (0 === this.transports.length) {
// Emit error on next tick so it can be listened to
this.setTimeoutFn(() => {
this.emit("error", "No transports available")
this.emitReserved("error", "No transports available")
}, 0)
return
} else {
@ -191,8 +443,8 @@ export class Socket extends Emitter {
// Retry with the next transport if the transport is disabled (jsonp: false)
try {
transport = this.createTransport(transport)
} catch (error: any) {
debug("error while creating transport: %s", error)
} catch (e) {
debug("error while creating transport: %s", e)
this.transports.shift()
this.open()
return
@ -207,7 +459,7 @@ export class Socket extends Emitter {
*
* @api private
*/
setTransport(transport) {
private setTransport(transport) {
debug("setting transport %s", transport.name)
if (this.transport) {
@ -223,9 +475,7 @@ export class Socket extends Emitter {
.on("drain", this.onDrain.bind(this))
.on("packet", this.onPacket.bind(this))
.on("error", this.onError.bind(this))
.on("close", () => {
this.onClose("transport close")
})
.on("close", reason => this.onClose("transport close", reason))
}
/**
@ -234,9 +484,9 @@ export class Socket extends Emitter {
* @param {String} transport name
* @api private
*/
probe(name) {
private probe(name) {
debug('probing transport "%s"', name)
let transport = this.createTransport(name, { probe: 1 })
let transport = this.createTransport(name)
let failed = false
Socket.priorWebsocketSuccess = false
@ -251,7 +501,7 @@ export class Socket extends Emitter {
if ("pong" === msg.type && "probe" === msg.data) {
debug('probe transport "%s" pong', name)
this.upgrading = true
this.emit("upgrading", transport)
this.emitReserved("upgrading", transport)
if (!transport) return
Socket.priorWebsocketSuccess = "websocket" === transport.name
@ -265,16 +515,17 @@ export class Socket extends Emitter {
this.setTransport(transport)
transport.send([{ type: "upgrade" }])
this.emit("upgrade", transport)
this.emitReserved("upgrade", transport)
transport = null
this.upgrading = false
this.flush()
})
} else {
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
this.emit("upgradeError", err)
this.emitReserved("upgradeError", err)
}
})
}
@ -293,14 +544,15 @@ export class Socket extends Emitter {
// Handle any error that happens while probing
const onerror = err => {
const error: any = new Error("probe error: " + err)
const error = new Error("probe error: " + err)
// @ts-ignore
error.transport = transport.name
freezeTransport()
debug('probe transport "%s" failed because of error: %s', name, err)
this.emit("upgradeError", error)
this.emitReserved("upgradeError", error)
}
function onTransportClose() {
@ -325,8 +577,8 @@ export class Socket extends Emitter {
transport.removeListener("open", onTransportOpen)
transport.removeListener("error", onerror)
transport.removeListener("close", onTransportClose)
this.removeListener("close", onclose)
this.removeListener("upgrading", onupgrade)
this.off("close", onclose)
this.off("upgrading", onupgrade)
}
transport.once("open", onTransportOpen)
@ -342,13 +594,13 @@ export class Socket extends Emitter {
/**
* Called when connection is deemed open.
*
* @api public
* @api private
*/
onOpen() {
private onOpen() {
debug("socket open")
this.readyState = "open"
Socket.priorWebsocketSuccess = "websocket" === this.transport.name
this.emit("open")
this.emitReserved("open")
this.flush()
// we check for `readyState` in case an `open`
@ -372,7 +624,7 @@ export class Socket extends Emitter {
*
* @api private
*/
onPacket(packet) {
private onPacket(packet) {
if (
"opening" === 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)
this.emit("packet", packet)
this.emitReserved("packet", packet)
// Socket is live - any packet counts
this.emit("heartbeat")
this.emitReserved("heartbeat")
switch (packet.type) {
case "open":
@ -393,19 +645,20 @@ export class Socket extends Emitter {
case "ping":
this.resetPingTimeout()
this.sendPacket("pong")
this.emit("ping")
this.emit("pong")
this.emitReserved("ping")
this.emitReserved("pong")
break
case "error":
const err: any = new Error("server error")
const err = new Error("server error")
// @ts-ignore
err.code = packet.data
this.onError(err)
break
case "message":
this.emit("data", packet.data)
this.emit("message", packet.data)
this.emitReserved("data", packet.data)
this.emitReserved("message", packet.data)
break
}
} else {
@ -416,16 +669,17 @@ export class Socket extends Emitter {
/**
* Called upon handshake completion.
*
* @param {Object} handshake obj
* @param {Object} data - handshake obj
* @api private
*/
onHandshake(data) {
this.emit("handshake", data)
private onHandshake(data) {
this.emitReserved("handshake", data)
this.id = data.sid
this.transport.query.sid = data.sid
this.upgrades = this.filterUpgrades(data.upgrades)
this.pingInterval = data.pingInterval
this.pingTimeout = data.pingTimeout
this.maxPayload = data.maxPayload
this.onOpen()
// In case open handler closes socket
if ("closed" === this.readyState) return
@ -437,7 +691,7 @@ export class Socket extends Emitter {
*
* @api private
*/
resetPingTimeout() {
private resetPingTimeout() {
this.clearTimeoutFn(this.pingTimeoutTimer)
this.pingTimeoutTimer = this.setTimeoutFn(() => {
this.onClose("ping timeout")
@ -452,7 +706,7 @@ export class Socket extends Emitter {
*
* @api private
*/
onDrain() {
private onDrain() {
this.writeBuffer.splice(0, this.prevBufferLen)
// setting prevBufferLen = 0 is very important
@ -461,7 +715,7 @@ export class Socket extends Emitter {
this.prevBufferLen = 0
if (0 === this.writeBuffer.length) {
this.emit("drain")
this.emitReserved("drain")
} else {
this.flush()
}
@ -472,22 +726,53 @@ export class Socket extends Emitter {
*
* @api private
*/
flush() {
private flush() {
if (
"closed" !== this.readyState &&
this.transport.writable &&
!this.upgrading &&
this.writeBuffer.length
) {
debug("flushing %d packets in socket", this.writeBuffer.length)
this.transport.send(this.writeBuffer)
const packets = this.getWritablePackets()
debug("flushing %d packets in socket", packets.length)
this.transport.send(packets)
// keep track of current length of writeBuffer
// splice writeBuffer and callbackBuffer on `drain`
this.prevBufferLen = this.writeBuffer.length
this.emit("flush")
this.prevBufferLen = packets.length
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.
*
@ -497,12 +782,12 @@ export class Socket extends Emitter {
* @return {Socket} for chaining.
* @api public
*/
write(msg, options, fn) {
public write(msg, options, fn?) {
this.sendPacket("message", msg, options, fn)
return this
}
send(msg, options, fn) {
public send(msg, options, fn?) {
this.sendPacket("message", msg, options, fn)
return this
}
@ -516,7 +801,7 @@ export class Socket extends Emitter {
* @param {Function} callback function.
* @api private
*/
sendPacket(type, data?, options?, fn?) {
private sendPacket(type, data?, options?, fn?) {
if ("function" === typeof data) {
fn = data
data = undefined
@ -539,7 +824,7 @@ export class Socket extends Emitter {
data: data,
options: options
}
this.emit("packetCreate", packet)
this.emitReserved("packetCreate", packet)
this.writeBuffer.push(packet)
if (fn) this.once("flush", fn)
this.flush()
@ -548,9 +833,9 @@ export class Socket extends Emitter {
/**
* Closes the connection.
*
* @api private
* @api public
*/
close() {
public close() {
const close = () => {
this.onClose("forced close")
debug("socket closing - telling transport to close")
@ -558,8 +843,8 @@ export class Socket extends Emitter {
}
const cleanupAndClose = () => {
this.removeListener("upgrade", cleanupAndClose)
this.removeListener("upgradeError", cleanupAndClose)
this.off("upgrade", cleanupAndClose)
this.off("upgradeError", cleanupAndClose)
close()
}
@ -595,10 +880,10 @@ export class Socket extends Emitter {
*
* @api private
*/
onError(err) {
private onError(err) {
debug("socket error %j", err)
Socket.priorWebsocketSuccess = false
this.emit("error", err)
this.emitReserved("error", err)
this.onClose("transport error", err)
}
@ -607,7 +892,7 @@ export class Socket extends Emitter {
*
* @api private
*/
onClose(reason, desc?) {
private onClose(reason: string, description?: CloseDetails | Error) {
if (
"opening" === this.readyState ||
"open" === this.readyState ||
@ -616,7 +901,6 @@ export class Socket extends Emitter {
debug('socket close with reason: "%s"', reason)
// clear timers
this.clearTimeoutFn(this.pingIntervalTimer)
this.clearTimeoutFn(this.pingTimeoutTimer)
// stop event from firing again for transport
@ -629,6 +913,11 @@ export class Socket extends Emitter {
this.transport.removeAllListeners()
if (typeof removeEventListener === "function") {
removeEventListener(
"beforeunload",
this.beforeunloadEventListener,
false
)
removeEventListener("offline", this.offlineEventListener, false)
}
@ -639,7 +928,7 @@ export class Socket extends Emitter {
this.id = null
// emit close event
this.emit("close", reason, desc)
this.emitReserved("close", reason, description)
// clean buffers after, so users can still
// grab the buffers on `close` event
@ -655,7 +944,7 @@ export class Socket extends Emitter {
* @api private
*
*/
filterUpgrades(upgrades) {
private filterUpgrades(upgrades) {
const filteredUpgrades = []
let i = 0
const j = upgrades.length
@ -666,23 +955,3 @@ export class Socket extends Emitter {
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"
const Emitter = require("component-emitter")
// import { decodePacket, Packet, RawData } from "engine.io-parser"
import { decodePacket, Packet, RawData } from "../engine.io-parser"
import { Emitter } from "@socket.io/component-emitter"
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.
*
* @param {Object} options.
* @api private
*/
* Transport abstract constructor.
*
* @param {Object} options.
* @api private
*/
constructor(opts) {
super()
installTimerFunctions(this, opts)
@ -23,15 +67,17 @@ export class Transport extends Emitter {
/**
* Emits an error.
*
* @param {String} str
* @param {String} reason
* @param description
* @param context - the error context
* @return {Transport} for chaining
* @api public
* @api protected
*/
onError(msg, desc) {
const err: any = new Error(msg)
err.type = "TransportError"
err.description = desc
this.emit("error", err)
protected onError(reason: string, description: any, context?: any) {
super.emitReserved(
"error",
new TransportError(reason, description, context)
)
return this
}
@ -40,7 +86,7 @@ export class Transport extends Emitter {
*
* @api public
*/
open() {
private open() {
if ("closed" === this.readyState || "" === this.readyState) {
this.readyState = "opening"
this.doOpen()
@ -52,9 +98,9 @@ export class Transport extends Emitter {
/**
* Closes the transport.
*
* @api private
* @api public
*/
close() {
public close() {
if ("opening" === this.readyState || "open" === this.readyState) {
this.doClose()
this.onClose()
@ -64,12 +110,12 @@ export class Transport extends Emitter {
}
/**
* Sends multiple packets.
*
* @param {Array} packets
* @api private
*/
send(packets) {
* Sends multiple packets.
*
* @param {Array} packets
* @api public
*/
public send(packets) {
if ("open" === this.readyState) {
this.write(packets)
} else {
@ -81,39 +127,45 @@ export class Transport extends Emitter {
/**
* Called upon open
*
* @api private
* @api protected
*/
onOpen() {
protected onOpen() {
this.readyState = "open"
this.writable = true
this.emit("open")
super.emitReserved("open")
}
/**
* Called with data.
*
* @param {String} data
* @api private
* @api protected
*/
onData(data) {
const packet = parser.decodePacket(data, this.socket.binaryType)
protected onData(data: RawData) {
const packet = decodePacket(data, this.socket.binaryType)
this.onPacket(packet)
}
/**
* Called with a decoded packet.
*
* @api protected
*/
onPacket(packet) {
this.emit("packet", packet)
protected onPacket(packet: Packet) {
super.emitReserved("packet", packet)
}
/**
* Called upon close.
*
* @api private
* @api protected
*/
onClose() {
protected onClose(details?: CloseDetails) {
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"
export default {
'websocket': WS
import { WS } from "./websocket.js"
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'
// const Transport = require("../transport")
import parser from '../../engine.io-parser'
// const parser = require("../engine.io-parser")
const parseqs = require("parseqs")
const yeast = require("yeast")
import { pick } from '../util'
// const { pick } = require("../util")
import { WebSocket } from '../../client'
const usingBrowserWebSocket = true
// const {
// WebSocket,
// usingBrowserWebSocket,
// defaultBinaryType,
// nextTick
// } = require("./websocket-constructor")
import { Transport } from "../transport"
import { encode } from "../contrib/parseqs"
import { yeast } from "../contrib/yeast"
import { pick } from "../util"
import {
defaultBinaryType,
nextTick,
usingBrowserWebSocket,
WebSocket
} from "./websocket-constructor"
// import debugModule from "debug" // debug()
import { encodePacket } from "../../engine.io-parser"
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
const isReactNative =
@ -24,6 +21,8 @@ const isReactNative =
navigator.product.toLowerCase() === "reactnative"
export class WS extends Transport {
private ws: any
/**
* WebSocket transport constructor.
*
@ -86,17 +85,17 @@ export class WS extends Transport {
}
try {
this.ws = new WebSocket(uri, protocols)
// usingBrowserWebSocket && !isReactNative
// ? protocols
// ? new WebSocket(uri, protocols)
// : new WebSocket(uri)
// : new WebSocket(uri, protocols, opts)
} catch (err) {
return this.emit("error", err)
this.ws =
usingBrowserWebSocket && !isReactNative
? protocols
? new WebSocket(uri, protocols)
: new WebSocket(uri)
: new WebSocket(uri, protocols, opts)
} catch (err: any) {
return this.emitReserved("error", err)
}
this.ws.binaryType = this.socket.binaryType || 'arraybuffer'
this.ws.binaryType = this.socket.binaryType || defaultBinaryType
this.addEventListeners()
}
@ -113,7 +112,11 @@ export class WS extends Transport {
}
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.onerror = e => this.onError("websocket error", e)
}
@ -133,9 +136,9 @@ export class WS extends Transport {
const packet = packets[i]
const lastPacket = i === packets.length - 1
parser.encodePacket(packet, this.supportsBinary, data => {
encodePacket(packet, this.supportsBinary, data => {
// always create a new object (GH-437)
const opts: any = {}
const opts: { compress?: boolean } = {}
if (!usingBrowserWebSocket) {
if (packet.options) {
opts.compress = packet.options.compress
@ -143,6 +146,7 @@ export class WS extends Transport {
if (this.opts.perMessageDeflate) {
const len =
// @ts-ignore
"string" === typeof data ? Buffer.byteLength(data) : data.length
if (len < this.opts.perMessageDeflate.threshold) {
opts.compress = false
@ -160,31 +164,22 @@ export class WS extends Transport {
} else {
this.ws.send(data, opts)
}
} catch (error: any) {
} catch (e) {
debug("websocket closed before onclose event")
}
if (lastPacket) {
// fake drain
// defer to next tick to allow Socket to clear writeBuffer
process.nextTick(() => {
nextTick(() => {
this.writable = true
this.emit("drain")
this.emitReserved("drain")
}, this.setTimeoutFn)
}
})
}
}
/**
* Called upon close
*
* @api private
*/
onClose() {
Transport.prototype.onClose.call(this)
}
/**
* Closes socket.
*
@ -203,7 +198,7 @@ export class WS extends Transport {
* @api private
*/
uri() {
let query = this.query || {}
let query: { b64?: number } = this.query || {}
const schema = this.opts.secure ? "wss" : "ws"
let port = ""
@ -226,21 +221,16 @@ export class WS extends Transport {
query.b64 = 1
}
query = parseqs.encode(query)
// prepend ? to query
if (query.length) {
query = "?" + query
}
const encodedQuery = encode(query)
const ipv6 = this.opts.hostname.indexOf(":") !== -1
return (
schema +
"://" +
(ipv6 ? "[" + this.opts.hostname + "]" : this.opts.hostname) +
port +
this.opts.path +
query
(encodedQuery.length ? "?" + encodedQuery : "")
)
}
@ -251,9 +241,6 @@ export class WS extends Transport {
* @api public
*/
check() {
return (
!!WebSocket &&
!("__initialize" in WebSocket && this.name === WS.prototype.name)
)
return !!WebSocket
}
}

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) => {
if (obj.hasOwnProperty(k)) {
acc[k] = obj[k]
@ -11,7 +13,7 @@ const pick = (obj, ...attr) => {
const NATIVE_SET_TIMEOUT = setTimeout
const NATIVE_CLEAR_TIMEOUT = clearTimeout
const installTimerFunctions = (obj, opts) => {
export function installTimerFunctions(obj, opts) {
if (opts.useNativeTimers) {
obj.setTimeoutFn = NATIVE_SET_TIMEOUT.bind(globalThis)
obj.clearTimeoutFn = NATIVE_CLEAR_TIMEOUT.bind(globalThis)
@ -20,4 +22,34 @@ const installTimerFunctions = (obj, opts) => {
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
})
const ERROR_PACKET = { type: "error", data: "parser error" }
const ERROR_PACKET: Packet = { type: "error", data: "parser error" }
export = {
PACKET_TYPES,
PACKET_TYPES_REVERSE,
ERROR_PACKET
export { PACKET_TYPES, PACKET_TYPES_REVERSE, ERROR_PACKET }
export type PacketType =
| "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") {
return {
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) {
case "arraybuffer":
return Buffer.isBuffer(data) ? toArrayBuffer(data) : data
return isBuffer ? toArrayBuffer(data) : data
case "nodebuffer":
default:
return data // assuming the data is already a Buffer
}
}
const toArrayBuffer = buffer => {
const toArrayBuffer = (buffer: Buffer): ArrayBuffer => {
const arrayBuffer = new ArrayBuffer(buffer.length)
const view = new Uint8Array(arrayBuffer)
for (let i = 0; i < buffer.length; i++) {
@ -46,3 +56,5 @@ const toArrayBuffer = buffer => {
}
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) => {
console.trace('encodePacket', type, JSON.stringify(data))
const encodePacket = (
{ type, data }: Packet,
supportsBinary: boolean,
callback: (encodedPacket: RawData) => void
) => {
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
const buffer = toBuffer(data)
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
const encodeBuffer = (data, supportsBinary) => {
const encodeBuffer = (data: Buffer, supportsBinary: boolean): RawData => {
return supportsBinary ? data : "b" + data.toString("base64")
}
export default encodePacket

View File

@ -1,9 +1,13 @@
import { encodePacket } from "./encodePacket"
import { decodePacket } from "./decodePacket"
import encodePacket from "./encodePacket.js"
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 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
const length = packets.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 packets = []
for (let i = 0; i < encodedPackets.length; i++) {
@ -33,10 +40,14 @@ const decodePayload = (encodedPayload, binaryType) => {
return packets
}
export default {
protocol: 4,
export const protocol = 4
export {
encodePacket,
encodePayload,
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")
// const Server = require("./server")
import { Server } from './server'
// function listen(port, options: AttachOptions & ServerOptions, fn) {
// if ("function" === typeof options) {
// 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.
@ -15,12 +50,8 @@ import { Server } from './server'
* @api public
*/
function attach(srv, options) {
function attach(server, options: AttachOptions & ServerOptions) {
const engine = new Server(options)
engine.attach(srv, options)
engine.attach(server, options)
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")
const parse = require("url").parse
import * as qs from "querystring"
import { parse } from "url"
// const base64id = require("base64id")
import transports from './transports'
import { EventEmitter } from 'events'
// const EventEmitter = require("events").EventEmitter
import { Socket } from './socket'
// const debug = require("debug")("engine")
const debug = function (...args) { }
// const cookieMod = require("cookie")
// const DEFAULT_WS_ENGINE = require("ws").Server;
import { WebSocketServer } from '../server'
import { Transport } from './transport'
const DEFAULT_WS_ENGINE = WebSocketServer
import transports from "./transports"
import { EventEmitter } from "events"
import { Socket } from "./socket"
// import debugModule from "debug"
// import { serialize } from "cookie"
// import { Server as DEFAULT_WS_ENGINE } from "ws"
import { WebSocketServer as DEFAULT_WS_ENGINE } from "../server"
// import { IncomingMessage, Server as HttpServer } from "http"
// import { CookieSerializeOptions } from "cookie"
// import { CorsOptions, CorsOptionsDelegate } from "cors"
// const debug = debugModule("engine");
const debug = require("../debug")("engine")
import { Request } from '../server/request'
import { WebSocketClient } from '../server/client'
export class Server extends EventEmitter {
public static errors = {
UNKNOWN_TRANSPORT: 0,
UNKNOWN_SID: 1,
BAD_HANDSHAKE_METHOD: 2,
BAD_REQUEST: 3,
FORBIDDEN: 4,
UNSUPPORTED_PROTOCOL_VERSION: 5
}
type Transport = "polling" | "websocket"
public static errorMessages = {
0: "Transport unknown",
1: "Session ID unknown",
2: "Bad handshake method",
3: "Bad request",
4: "Forbidden",
5: "Unsupported protocol version"
}
export 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
}
private clients = {}
private clientsCount = 0
public opts: any
export interface ServerOptions {
/**
* 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
private perMessageDeflate: any
protected clients: any
private clientsCount: number
protected corsMiddleware: Function
constructor(opts: any = {}) {
/**
* Server constructor.
*
* @param {Object} opts - options
* @api public
*/
constructor(opts: ServerOptions = {}) {
super()
this.clients = {}
this.clientsCount = 0
this.opts = Object.assign(
{
wsEngine: DEFAULT_WS_ENGINE,
@ -70,6 +159,7 @@ export class Server extends EventEmitter {
// {
// name: "io",
// path: "/",
// // @ts-ignore
// httpOnly: opts.cookie.path !== false,
// sameSite: "lax"
// },
@ -93,42 +183,7 @@ export class Server extends EventEmitter {
// this.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]}`)
// })
// })
// }
// }
protected abstract init()
/**
* Returns a list of available transports for upgrade given a certain transport.
@ -136,7 +191,7 @@ export class Server extends EventEmitter {
* @return {Array}
* @api public
*/
upgrades(transport): Array<any> {
public upgrades(transport) {
if (!this.opts.allowUpgrades) return []
return transports[transport].upgradesTo || []
}
@ -148,7 +203,7 @@ export class Server extends EventEmitter {
// * @return {Boolean} whether the request is valid
// * @api private
// */
// verify(req, upgrade, fn) {
// protected verify(req, upgrade, fn) {
// // transport check
// const transport = req._query.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()
// return this.opts.allowRequest(req, (message, success) => {
@ -209,36 +271,234 @@ export class Server extends EventEmitter {
// 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.
*
* @api public
*/
close() {
public close() {
debug("closing all open clients")
for (let i in this.clients) {
if (this.clients.hasOwnProperty(i)) {
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) {
debug("closing webSocketServer")
this.ws.close()
// 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
// * @api public
// */
// handleRequest(req, res) {
// public handleRequest(req, res) {
// debug('handling "%s" http request "%s"', req.method, req.url)
// this.prepare(req)
// 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.
// *
// * @api public
// */
// handleUpgrade(req, socket, upgradeHead) {
// public handleUpgrade(req, socket, upgradeHead) {
// this.prepare(req)
// this.verify(req, true, (errorCode, errorContext) => {
@ -423,7 +564,7 @@ export class Server extends EventEmitter {
// return
// }
// const head = Buffer.from(upgradeHead) // eslint-disable-line node/no-deprecated-api
// const head = Buffer.from(upgradeHead)
// upgradeHead = null
// // delegate to ws
@ -439,14 +580,14 @@ export class Server extends EventEmitter {
* @param {ws.Socket} websocket
* @api private
*/
onWebSocket(req: Request, socket, websocket: WebSocketClient) {
private onWebSocket(req: Request, socket, websocket: WebSocketClient) {
websocket.on("error", onUpgradeError)
if (
transports[req._query.transport] !== undefined &&
!transports[req._query.transport].prototype.handlesUpgrades
) {
console.debug("transport doesnt handle upgraded requests")
debug("transport doesnt handle upgraded requests")
websocket.close()
return
}
@ -460,40 +601,37 @@ export class Server extends EventEmitter {
if (id) {
const client = this.clients[id]
if (!client) {
console.debug("upgrade attempt for closed client")
debug("upgrade attempt for closed client")
websocket.close()
} else if (client.upgrading) {
console.debug("transport has already been trying to upgrade")
debug("transport has already been trying to upgrade")
websocket.close()
} else if (client.upgraded) {
console.debug("transport had already been upgraded")
debug("transport had already been upgraded")
websocket.close()
} else {
console.debug("upgrading existing transport")
debug("upgrading existing transport")
// transport error handling takes over
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) {
transport.supportsBinary = false
} else {
transport.supportsBinary = true
}
transport.perMessageDeflate = this.perMessageDeflate
transport.perMessageDeflate = this.opts.perMessageDeflate
client.maybeUpgrade(transport)
}
} else {
// transport error handling takes over
websocket.removeListener("error", onUpgradeError)
// const closeConnection = (errorCode, errorContext) =>
// abortUpgrade(socket, errorCode, errorContext)
this.handshake(req._query.transport, req, () => { })
}
function onUpgradeError() {
console.debug("websocket error before upgrade")
function onUpgradeError(...args) {
debug("websocket error before upgrade %s", ...args)
// websocket.close() not needed
}
}
@ -505,7 +643,9 @@ export class Server extends EventEmitter {
* @param {Object} options
* @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(/\/$/, "")
// const destroyUpgradeTimeout = options.destroyUpgradeTimeout || 1000
@ -514,7 +654,7 @@ export class Server extends EventEmitter {
// path += "/"
// function check(req) {
// return path === req.url.substr(0, path.length)
// return path === req.url.slice(0, path.length)
// }
// cache and clean up listeners
@ -555,7 +695,11 @@ export class Server extends EventEmitter {
// // and if no eio thing handles the upgrade
// // then the socket needs to die!
// setTimeout(function () {
// // @ts-ignore
// if (socket.writable && socket.bytesWritten <= 0) {
// socket.on("error", e => {
// debug("error while destroying upgrade: %s", e.message)
// })
// return socket.end()
// }
// }, destroyUpgradeTimeout)
@ -601,7 +745,11 @@ export class Server extends EventEmitter {
// * @api private
// */
// function abortUpgrade(socket, errorCode, errorContext: any = {}) {
// function abortUpgrade(
// socket,
// errorCode,
// errorContext: { message?: string } = {}
// ) {
// socket.on("error", () => {
// debug("ignoring error from closed connection")
// })
@ -622,8 +770,6 @@ export class Server extends EventEmitter {
// socket.destroy()
// }
// module.exports = Server
/* eslint-disable */
// /**

View File

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

View File

@ -1,6 +1,13 @@
import { EventEmitter } from 'events'
import parser_v4 from "../engine.io-parser"
import type { WebSocketClient } from '../server/client'
import { EventEmitter } from "events"
import * as parser_v4 from "../engine.io-parser"
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.
*
@ -11,15 +18,28 @@ function noop() { }
export abstract class Transport extends EventEmitter {
public sid: string
public req /**http.IncomingMessage */
public socket: WebSocketClient
public writable: boolean
public readyState: string
public discarded: boolean
public protocol: Number
public parser: any
public perMessageDeflate: any
public supportsBinary: boolean = false
public protocol: number
protected _readyState: string
protected discarded: boolean
protected parser: any
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.
@ -32,7 +52,7 @@ export abstract class Transport extends EventEmitter {
this.readyState = "open"
this.discarded = false
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.
*
* @param {http.IncomingMessage} request
* @api private
* @api protected
*/
onRequest(req) {
console.debug(`engine.io transport ${this.socket.id} setting request`, JSON.stringify(req))
protected onRequest(req) {
debug("setting request")
this.req = req
}
@ -72,16 +92,18 @@ export abstract class Transport extends EventEmitter {
*
* @param {String} message error
* @param {Object} error description
* @api private
* @api protected
*/
onError(msg: string, desc?: string) {
protected onError(msg: string, desc?) {
if (this.listeners("error").length) {
const err: any = new Error(msg)
const err = new Error(msg)
// @ts-ignore
err.type = "TransportError"
// @ts-ignore
err.description = desc
this.emit("error", err)
} 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.
*
* @param {Object} packet
* @api private
* @api protected
*/
onPacket(packet) {
protected onPacket(packet: Packet) {
this.emit("packet", packet)
}
@ -99,23 +121,24 @@ export abstract class Transport extends EventEmitter {
* Called with the encoded packet data.
*
* @param {String} data
* @api private
* @api protected
*/
onData(data) {
protected onData(data) {
this.onPacket(this.parser.decodePacket(data))
}
/**
* Called upon transport close.
*
* @api private
* @api protected
*/
onClose() {
protected onClose() {
this.readyState = "closed"
this.emit("close")
}
abstract get supportsFraming()
abstract get name()
abstract send(...args: any[])
abstract doClose(d: Function)
abstract send(packets)
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 {
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'
// const debug = require("debug")("engine:ws")
import { Transport } from "../transport"
// import debugModule from "debug";
const debug = require('../../debug')("engine:ws")
export class WebSocket extends Transport {
public perMessageDeflate: any
protected perMessageDeflate: any
private socket: any
/**
* WebSocket transport
@ -13,7 +16,11 @@ export class WebSocket extends Transport {
constructor(req) {
super(req)
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.on("error", this.onError.bind(this))
this.writable = true
@ -21,10 +28,10 @@ export class WebSocket extends Transport {
}
/**
* Transport name
*
* @api public
*/
* Transport name
*
* @api public
*/
get name() {
return "websocket"
}
@ -47,17 +54,6 @@ export class WebSocket extends Transport {
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.
*
@ -65,7 +61,6 @@ export class WebSocket extends Transport {
* @api private
*/
send(packets) {
// console.log('WebSocket send packets', JSON.stringify(packets))
const packet = packets.shift()
if (typeof packet === "undefined") {
this.writable = true
@ -74,7 +69,7 @@ export class WebSocket extends Transport {
}
// always creates a new object since ws modifies it
const opts: any = {}
const opts: { compress?: boolean } = {}
if (packet.options) {
opts.compress = packet.options.compress
}
@ -87,7 +82,7 @@ export class WebSocket extends Transport {
opts.compress = false
}
}
console.trace('writing', data)
debug('writing "%s"', data)
this.writable = false
this.socket.send(data, opts, err => {
@ -109,7 +104,7 @@ export class WebSocket extends Transport {
* @api private
*/
doClose(fn) {
// debug("closing")
debug("closing")
this.socket.close()
fn && fn()
}

View File

@ -1,6 +1,8 @@
import { EventEmitter } from "events"
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 interface BroadcastFlags {
@ -9,11 +11,12 @@ export interface BroadcastFlags {
local?: boolean
broadcast?: boolean
binary?: boolean
timeout?: number
}
export interface BroadcastOptions {
rooms: Set<Room>
except?: Set<SocketId>
except?: Set<Room>
flags?: BroadcastFlags
}
@ -42,6 +45,15 @@ export class Adapter extends EventEmitter {
*/
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.
*
@ -82,14 +94,14 @@ export class Adapter extends EventEmitter {
this._del(room, id)
}
private _del(room, id) {
if (this.rooms.has(room)) {
const deleted = this.rooms.get(room).delete(id)
private _del(room: Room, id: SocketId) {
const _room = this.rooms.get(room)
if (_room != null) {
const deleted = _room.delete(id)
if (deleted) {
this.emit("leave-room", room, id)
}
if (this.rooms.get(room).size === 0) {
this.rooms.delete(room)
if (_room.size === 0 && this.rooms.delete(room)) {
this.emit("delete-room", room)
}
}
@ -126,7 +138,7 @@ export class Adapter extends EventEmitter {
*/
public broadcast(packet: any, opts: BroadcastOptions): void {
const flags = opts.flags || {}
const basePacketOpts = {
const packetOpts = {
preEncoded: true,
volatile: flags.volatile,
compress: flags.compress
@ -135,22 +147,65 @@ export class Adapter extends EventEmitter {
packet.nsp = this.nsp.name
const encodedPackets = this.encoder.encode(packet)
const packetOpts = encodedPackets.map(encodedPacket => {
if (typeof encodedPacket === "string") {
return {
...basePacketOpts,
wsPreEncoded: "4" + encodedPacket // "4" being the "message" packet type in Engine.IO
}
} else {
return basePacketOpts
this.apply(opts, socket => {
if (typeof socket.notifyOutgoingListeners === "function") {
socket.notifyOutgoingListeners(packet)
}
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 => {
for (let i = 0; i < encodedPackets.length; i++) {
socket.client.writeToEngine(encodedPackets[i], packetOpts[i])
// track the total number of acknowledgements that are expected
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
*/
public serverSideEmit(packet: any[]): void {
throw new Error(
console.warn(
"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")
/**
* Module exports.
*/
module.exports = exports = lookup
/**
* Managers cache.
*/
const cache: Record<string, Manager> = (exports.managers = {})
const cache: Record<string, Manager> = {}
/**
* Looks up an existing `Manager` for multiplexing.
@ -76,6 +70,15 @@ function lookup(
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.
*
@ -84,22 +87,18 @@ function lookup(
export { protocol } from "../socket.io-parser"
/**
* `connect`.
*
* @param {String} uri
* @public
*/
exports.connect = lookup
/**
* Expose constructors for standalone build.
*
* @public
*/
export { Manager, ManagerOptions } from "./manager"
export { Socket } from "./socket"
export { lookup as io, SocketOptions }
export default lookup
export {
Manager,
ManagerOptions,
Socket,
SocketOptions,
lookup as io,
lookup as connect,
lookup as default,
}

View File

@ -1,210 +1,26 @@
import eio from "../engine.io-client"
import { Socket, SocketOptions } from "./socket"
import {
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 { Decoder, Encoder, Packet } from "socket.io-parser"
import { Decoder, Encoder, Packet } from "../socket.io-parser"
import { on } from "./on"
import * as Backoff from "backo2"
import { on } from "./on.js"
import { Backoff } from "./contrib/backo2"
import {
DefaultEventsMap,
EventsMap,
StrictEventEmitter,
} from "./typed-events"
Emitter,
} 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")
interface EngineOptions {
/**
* The host that we're connecting to. Set from the URI passed when connecting
*/
host: string
/**
* The hostname for our connection. Set from the URI passed when connecting
*/
hostname: string
/**
* If this is a secure connection. Set from the URI passed when connecting
*/
secure: boolean
/**
* The port for our connection. Set from the URI passed when connecting
*/
port: string
/**
* Any query parameters in our uri. Set from the URI passed when connecting
*/
query: { [key: string]: string }
/**
* `http.Agent` to use, defaults to `false` (NodeJS only)
*/
agent: string | boolean
/**
* Whether the client should try to upgrade the transport from
* long-polling to something better.
* @default true
*/
upgrade: boolean
/**
* Forces JSONP for polling transport.
*/
forceJSONP: boolean
/**
* Determines whether to use JSONP when necessary for polling. If
* disabled (by settings to false) an error will be emitted (saying
* "No transports available") if no other transports are available.
* If another transport is available for opening a connection (e.g.
* WebSocket) that transport will be used instead.
* @default true
*/
jsonp: boolean
/**
* Forces base 64 encoding for polling transport even when XHR2
* responseType is available and WebSocket even if the used standard
* supports binary.
*/
forceBase64: boolean
/**
* Enables XDomainRequest for IE8 to avoid loading bar flashing with
* click sound. default to `false` because XDomainRequest has a flaw
* of not sending cookie.
* @default false
*/
enablesXDR: boolean
/**
* The param name to use as our timestamp key
* @default 't'
*/
timestampParam: string
/**
* Whether to add the timestamp with each transport request. Note: this
* is ignored if the browser is IE or Android, in which case requests
* are always stamped
* @default false
*/
timestampRequests: boolean
/**
* A list of transports to try (in order). Engine.io always attempts to
* connect directly with the first one, provided the feature detection test
* for it passes.
* @default ['polling','websocket']
*/
transports: string[]
/**
* The port the policy server listens on
* @default 843
*/
policyPost: number
/**
* If true and if the previous websocket connection to the server succeeded,
* the connection attempt will bypass the normal upgrade process and will
* initially try websocket. A connection attempt following a transport error
* will use the normal upgrade process. It is recommended you turn this on
* only when using SSL/TLS connections, or if you know that your network does
* not block websockets.
* @default false
*/
rememberUpgrade: boolean
/**
* Are we only interested in transports that support binary?
*/
onlyBinaryUpgrades: boolean
/**
* Timeout for xhr-polling requests in milliseconds (0) (only for polling transport)
*/
requestTimeout: number
/**
* Transport options for Node.js client (headers etc)
*/
transportOptions: Object
/**
* (SSL) Certificate, Private key and CA certificates to use for SSL.
* Can be used in Node.js client environment to manually specify
* certificate information.
*/
pfx: string
/**
* (SSL) Private key to use for SSL. Can be used in Node.js client
* environment to manually specify certificate information.
*/
key: string
/**
* (SSL) A string or passphrase for the private key or pfx. Can be
* used in Node.js client environment to manually specify certificate
* information.
*/
passphrase: string
/**
* (SSL) Public x509 certificate to use. Can be used in Node.js client
* environment to manually specify certificate information.
*/
cert: string
/**
* (SSL) An authority certificate or array of authority certificates to
* check the remote host against.. Can be used in Node.js client
* environment to manually specify certificate information.
*/
ca: string | string[]
/**
* (SSL) A string describing the ciphers to use or exclude. Consult the
* [cipher format list]
* (http://www.openssl.org/docs/apps/ciphers.html#CIPHER_LIST_FORMAT) for
* details on the format.. Can be used in Node.js client environment to
* manually specify certificate information.
*/
ciphers: string
/**
* (SSL) If true, the server certificate is verified against the list of
* supplied CAs. An 'error' event is emitted if verification fails.
* Verification happens at the connection level, before the HTTP request
* is sent. Can be used in Node.js client environment to manually specify
* certificate information.
*/
rejectUnauthorized: boolean
/**
* Headers that will be passed for each request to the server (via xhr-polling and via websockets).
* These values then can be used during handshake or for special proxies.
*/
extraHeaders?: { [header: string]: string }
/**
* Whether to include credentials (cookies, authorization headers, TLS
* client certificates, etc.) with cross-origin XHR polling requests
* @default false
*/
withCredentials: boolean
/**
* Whether to automatically close the connection whenever the beforeunload event is received.
* @default true
*/
closeOnBeforeunload: boolean
}
export interface ManagerOptions extends EngineOptions {
/**
* Should we force a new Manager for this connection?
@ -267,13 +83,6 @@ export interface ManagerOptions extends EngineOptions {
*/
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.
*/
@ -285,7 +94,7 @@ interface ManagerReservedEvents {
error: (err: Error) => void
ping: () => void
packet: (packet: Packet) => void
close: (reason: string) => void
close: (reason: string, description?: DisconnectDescription) => void
reconnect_failed: () => void
reconnect_attempt: (attempt: number) => void
reconnect_error: (err: Error) => void
@ -295,13 +104,13 @@ interface ManagerReservedEvents {
export class Manager<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents
> extends StrictEventEmitter<{}, {}, ManagerReservedEvents> {
> extends Emitter<{}, {}, ManagerReservedEvents> {
/**
* The Engine.IO client instance
*
* @public
*/
public engine: any
public engine: Engine
/**
* @private
*/
@ -320,7 +129,9 @@ export class Manager<
private nsps: Record<string, Socket> = {};
private subs: Array<ReturnType<typeof on>> = [];
// @ts-ignore
private backoff: Backoff
private setTimeoutFn: typeof setTimeout
private _reconnection: boolean
private _reconnectionAttempts: number
private _reconnectionDelay: number
@ -358,6 +169,7 @@ export class Manager<
opts.path = opts.path || "/socket.io"
this.opts = opts
installTimerFunctions(this, opts)
this.reconnection(opts.reconnection !== false)
this.reconnectionAttempts(opts.reconnectionAttempts || Infinity)
this.reconnectionDelay(opts.reconnectionDelay || 1000)
@ -507,8 +319,7 @@ export class Manager<
if (~this._readyState.indexOf("open")) return this
debug("opening %s", this.uri)
// @ts-ignore
this.engine = eio(this.uri, this.opts)
this.engine = new Engine(this.uri, this.opts)
const socket = this.engine
const self = this
this._readyState = "opening"
@ -543,10 +354,11 @@ export class Manager<
}
// set timer
const timer = setTimeout(() => {
const timer = this.setTimeoutFn(() => {
debug("connect attempt timed out after %d", timeout)
openSubDestroy()
socket.close()
// @ts-ignore
socket.emit("error", new Error("timeout"))
}, timeout)
@ -616,7 +428,11 @@ export class Manager<
* @private
*/
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 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")
this.skipReconnect = true
this._reconnecting = false
if ("opening" === this._readyState) {
// `onclose` will not fire because
// an open event never happened
this.cleanup()
}
this.backoff.reset()
this._readyState = "closed"
this.onclose("forced close")
if (this.engine) this.engine.close()
}
@ -737,13 +550,13 @@ export class Manager<
*
* @private
*/
private onclose(reason: string): void {
debug("onclose")
private onclose(reason: string, description?: DisconnectDescription): void {
debug("closed due to %s", reason)
this.cleanup()
this.backoff.reset()
this._readyState = "closed"
this.emitReserved("close", reason)
this.emitReserved("close", reason, description)
if (this._reconnection && !this.skipReconnect) {
this.reconnect()
@ -770,7 +583,7 @@ export class Manager<
debug("will wait %dms before reconnect attempt", delay)
this._reconnecting = true
const timer = setTimeout(() => {
const timer = this.setTimeoutFn(() => {
if (self.skipReconnect) return
debug("attempting reconnect")

View File

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

View File

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

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")
type ParsedUrl = {
@ -67,7 +70,7 @@ export function url(
// parse
debug("parse %s", uri)
obj = parseuri(uri) as ParsedUrl
obj = parse(uri) as ParsedUrl
}
// 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.
@ -33,7 +33,7 @@ function _deconstructPacket(data, buffers) {
} else if (typeof data === "object" && !(data instanceof Date)) {
const newData = {}
for (const key in data) {
if (data.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
newData[key] = _deconstructPacket(data[key], buffers)
}
}
@ -60,15 +60,23 @@ export function reconstructPacket(packet, buffers) {
function _reconstructPacket(data, buffers) {
if (!data) return data
if (data && data._placeholder) {
return buffers[data.num] // appropriate buffer (should be natural order anyway)
if (data && data._placeholder === true) {
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)) {
for (let i = 0; i < data.length; i++) {
data[i] = _reconstructPacket(data[i], buffers)
}
} else if (typeof data === "object") {
for (const key in data) {
if (data.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
data[key] = _reconstructPacket(data[key], buffers)
}
}

View File

@ -1,8 +1,10 @@
import EventEmitter = require("events")
import { deconstructPacket, reconstructPacket } from "./binary"
import { isBinary, hasBinary } from "./is-binary"
import { Emitter } from "@socket.io/component-emitter"
import { deconstructPacket, reconstructPacket } from "./binary.js"
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.
@ -35,6 +37,12 @@ export interface Packet {
*/
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
* buffer sequence, depending on packet type.
@ -42,7 +50,7 @@ export class Encoder {
* @param {Object} obj - packet object
*/
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 (hasBinary(obj)) {
@ -85,10 +93,10 @@ export class Encoder {
// json 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
}
@ -108,15 +116,24 @@ export class Encoder {
}
}
interface DecoderReservedEvents {
decoded: (packet: Packet) => void
}
/**
* A socket.io Decoder instance
*
* @return {Object} decoder
*/
export class Decoder extends EventEmitter {
export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
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()
}
@ -129,6 +146,9 @@ export class Decoder extends EventEmitter {
public add(obj: any) {
let packet
if (typeof obj === "string") {
if (this.reconstructor) {
throw new Error("got plaintext data when reconstructing a packet")
}
packet = this.decodeString(obj)
if (
packet.type === PacketType.BINARY_EVENT ||
@ -139,11 +159,11 @@ export class Decoder extends EventEmitter {
// no attachments, labeled binary but no binary data to follow
if (packet.attachments === 0) {
super.emit("decoded", packet)
super.emitReserved("decoded", packet)
}
} else {
// non-binary full packet
super.emit("decoded", packet)
super.emitReserved("decoded", packet)
}
} else if (isBinary(obj) || obj.base64) {
// raw binary data
@ -154,7 +174,7 @@ export class Decoder extends EventEmitter {
if (packet) {
// received final buffer
this.reconstructor = null
super.emit("decoded", packet)
super.emitReserved("decoded", packet)
}
}
} else {
@ -223,7 +243,7 @@ export class Decoder extends EventEmitter {
// look up json data
if (str.charAt(++i)) {
const payload = tryParse(str.substr(i))
const payload = this.tryParse(str.substr(i))
if (Decoder.isPayloadValid(p.type, payload)) {
p.data = payload
} 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
}
private tryParse(str) {
try {
return JSON.parse(str, this.reviver)
} catch (e) {
return false
}
}
private static isPayloadValid(type: PacketType, payload: any): boolean {
switch (type) {
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
* 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 withNativeBlob = false
// typeof Blob === "function" ||
// (typeof Blob !== "undefined" &&
// toString.call(Blob) === "[object BlobConstructor]")
const withNativeFile = false
// typeof File === "function" ||
// (typeof File !== "undefined" &&
// toString.call(File) === "[object FileConstructor]")
const withNativeBlob =
typeof Blob === "function" ||
(typeof Blob !== "undefined" &&
toString.call(Blob) === "[object BlobConstructor]")
const withNativeFile =
typeof File === "function" ||
(typeof File !== "undefined" &&
toString.call(File) === "[object FileConstructor]")
/**
* 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) {
return (
(withNativeArrayBuffer && (obj instanceof ArrayBuffer || isView(obj)))
// || (withNativeBlob && obj instanceof Blob) || (withNativeFile && obj instanceof File)
(withNativeArrayBuffer && (obj instanceof ArrayBuffer || isView(obj))) ||
(withNativeBlob && obj instanceof Blob) ||
(withNativeFile && obj instanceof File)
)
}

View File

@ -12,7 +12,7 @@ import type {
TypedEventBroadcaster,
} from "./typed-events"
export class BroadcastOperator<EmitEvents extends EventsMap>
export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
implements TypedEventBroadcaster<EmitEvents>
{
constructor(
@ -25,18 +25,27 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/**
* Targets a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
* @example
* // the “foo” event will be broadcast to all connected clients in the “room-101” room
* 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)
if (Array.isArray(room)) {
room.forEach((r) => rooms.add(r))
} else {
rooms.add(room)
}
return new BroadcastOperator(
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
rooms,
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
* @return a new BroadcastOperator instance
* @public
* @example
* // disconnect all clients in the "room-101" room
* 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)
}
/**
* Excludes a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
* @example
* // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room
* 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)
if (Array.isArray(room)) {
room.forEach((r) => exceptRooms.add(r))
} else {
exceptRooms.add(room)
}
return new BroadcastOperator(
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
exceptRooms,
@ -80,13 +101,15 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/**
* Sets the compress flag.
*
* @example
* io.compress(false).emit("hello");
*
* @param compress - if `true`, compresses the sending data
* @return a new BroadcastOperator instance
* @public
*/
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
public compress(compress: boolean) {
const flags = Object.assign({}, this.flags, { compress })
return new BroadcastOperator(
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
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
* 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
* @public
*/
public get volatile(): BroadcastOperator<EmitEvents> {
public get volatile() {
const flags = Object.assign({}, this.flags, { volatile: true })
return new BroadcastOperator(
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
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.
*
* @return a new BroadcastOperator instance
* @public
* @example
* // 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 })
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.rooms,
this.exceptRooms,
@ -131,15 +183,30 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/**
* 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
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
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
const data = [ev, ...args]
@ -148,14 +215,65 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
data: data,
}
if ("function" == typeof data[data.length - 1]) {
throw new Error("Callbacks are not supported when broadcasting")
const withAck = typeof data[data.length - 1] === "function"
if (!withAck) {
this.adapter.broadcast(packet, {
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
})
return true
}
this.adapter.broadcast(packet, {
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
const ack = data.pop() as (...args: any[]) => void
let timedOut = false
let responses: any[] = []
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
@ -164,7 +282,8 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/**
* 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>> {
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
.fetchSockets({
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
})
.then((sockets) => {
return sockets.map((socket) => {
if (socket instanceof Socket) {
// 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 {
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
* @public
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @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 {
this.adapter.addSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
},
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
* @public
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @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 {
this.adapter.delSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
},
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
* @public
*/
public disconnectSockets(close: boolean = false): void {
this.adapter.disconnectSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
},
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
*/
interface SocketDetails {
interface SocketDetails<SocketData> {
id: SocketId
handshake: Handshake
rooms: Room[]
data: any
data: SocketData
}
/**
* 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>
{
public readonly id: SocketId
public readonly handshake: Handshake
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.handshake = details.handshake
this.rooms = new Set(details.rooms)
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>>(
@ -289,7 +462,6 @@ export class RemoteSocket<EmitEvents extends EventsMap>
* Joins a room.
*
* @param {String|Array} room - room or array of rooms
* @public
*/
public join(room: Room | Room[]): void {
return this.operator.socketsJoin(room)
@ -299,7 +471,6 @@ export class RemoteSocket<EmitEvents extends EventsMap>
* Leaves a room.
*
* @param {String} room
* @public
*/
public leave(room: Room): void {
return this.operator.socketsLeave(room)
@ -310,8 +481,6 @@ export class RemoteSocket<EmitEvents extends EventsMap>
*
* @param {Boolean} close - if `true`, closes the underlying connection
* @return {Socket} self
*
* @public
*/
public disconnect(close = false): this {
this.operator.disconnectSockets(close)

View File

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

View File

@ -1,24 +1,30 @@
// import http = require("http");
// import type { Server as HTTPSServer } from "https";
// import type { Http2SecureServer } from "http2";
// import { createReadStream } from "fs";
// import { createDeflate, createGzip, createBrotliCompress } from "zlib";
// import accepts = require("accepts");
// import { pipeline } from "stream";
// import path = require("path");
import engine = require("../engine.io")
import { Client } from './client'
import { EventEmitter } from 'events'
import {
attach,
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 { 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 * 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 debugModule from "debug";
import { Socket } from './socket'
// import type { CookieSerializeOptions } from "cookie";
// import type { CorsOptions } from "cors";
// import debugModule from "debug"
import { Socket } from "./socket"
import type { BroadcastOperator, RemoteSocket } from "./broadcast-operator"
import {
EventsMap,
@ -27,13 +33,14 @@ import {
StrictEventEmitter,
EventNames,
} 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 dotMapRegex = /\.map/
// type Transport = "polling" | "websocket";
type ParentNspNameMatchFn = (
name: string,
auth: { [key: string]: any },
@ -42,105 +49,7 @@ type ParentNspNameMatchFn = (
type AdapterConstructor = typeof Adapter | ((nsp: Namespace) => Adapter)
interface EngineOptions {
/**
* 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 {
interface ServerOptions extends EngineOptions, AttachOptions {
/**
* name of the path to capture
* @default "/socket.io"
@ -155,6 +64,7 @@ interface ServerOptions extends EngineAttachOptions {
* the adapter to use
* @default the in-memory adapter (https://github.com/socketio/socket.io-adapter)
*/
// adapter: AdapterConstructor
adapter: any
/**
* the parser to use
@ -168,31 +78,62 @@ interface ServerOptions extends EngineAttachOptions {
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<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap
> extends StrictEventEmitter<
ServerSideEvents extends EventsMap = DefaultEventsMap,
SocketData = any
> extends StrictEventEmitter<
ServerSideEvents,
EmitEvents,
ServerReservedEventsMap<ListenEvents, EmitEvents, ServerSideEvents>
> {
ServerReservedEventsMap<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>
> {
public readonly sockets: Namespace<
ListenEvents,
EmitEvents,
ServerSideEvents
ServerSideEvents,
SocketData
>
/**
* A reference to the underlying Engine.IO server.
*
* Example:
*
* <code>
* const clientsCount = io.engine.clientsCount;
* </code>
* @example
* const clientsCount = io.engine.clientsCount;
*
*/
public engine: any
/** @private */
readonly _parser: typeof parser
/** @private */
@ -201,28 +142,62 @@ export class Server<
/**
* @private
*/
_nsps: Map<string, Namespace<ListenEvents, EmitEvents, ServerSideEvents>> =
new Map();
_nsps: Map<
string,
Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map();
private parentNsps: Map<
ParentNspNameMatchFn,
ParentNamespace<ListenEvents, EmitEvents, ServerSideEvents>
ParentNamespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map();
private _adapter?: AdapterConstructor
private _serveClient: boolean
private opts: Partial<EngineOptions>
private eio
private eio: Engine
private _path: string
private clientPathRegex: RegExp
/**
* @private
*/
_connectTimeout: number
// private httpServer: http.Server | HTTPSServer | Http2SecureServer
// private httpServer: http.Server
constructor(srv: any, opts: Partial<ServerOptions> = {}) {
/**
* Server constructor.
*
* @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()
if (!srv) { throw new Error('srv can\'t be undefiend!') }
// if (
// "object" === typeof srv &&
// srv instanceof Object &&
@ -237,8 +212,12 @@ export class Server<
this._parser = opts.parser || parser
this.encoder = new this._parser.Encoder()
this.adapter(opts.adapter || Adapter)
this.sockets = this.of('/')
// if (srv) this.attach(srv as http.Server);
this.sockets = this.of("/")
this.opts = opts
// if (srv || typeof srv == "number")
// this.attach(
// srv as http.Server | HTTPSServer | Http2SecureServer | number
// )
this.attach(srv, this.opts)
}
@ -247,7 +226,6 @@ export class Server<
*
* @param v - whether to serve client code
* @return self when setting or value when getting
* @public
*/
public serveClient(v: boolean): this
public serveClient(): boolean
@ -271,7 +249,9 @@ export class Server<
name: string,
auth: { [key: string]: any },
fn: (
nsp: Namespace<ListenEvents, EmitEvents, ServerSideEvents> | false
nsp:
| Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
| false
) => void
): void {
if (this.parentNsps.size === 0) return fn(false)
@ -285,15 +265,18 @@ export class Server<
}
nextFn.value(name, auth, (err, allow) => {
if (err || !allow) {
run()
} else {
const namespace = this.parentNsps
.get(nextFn.value)!
.createChild(name)
// @ts-ignore
this.sockets.emitReserved("new_namespace", namespace)
fn(namespace)
return run()
}
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
* @return {Server|String} self when setting or value when getting
* @public
*/
public path(v: string): this
public path(): string
@ -319,7 +301,7 @@ export class Server<
this.clientPathRegex = new RegExp(
"^" +
escapedPath +
"/socket\\.io(\\.min|\\.msgpack\\.min)?\\.js(\\.map)?$"
"/socket\\.io(\\.msgpack|\\.esm)?(\\.min)?\\.js(\\.map)?(?:\\?|$)"
)
return this
}
@ -327,7 +309,6 @@ export class Server<
/**
* Set the delay after which a client without namespace is closed
* @param v
* @public
*/
public connectTimeout(v: number): this
public connectTimeout(): number
@ -343,7 +324,6 @@ export class Server<
*
* @param v pathname
* @return self when setting or value when getting
* @public
*/
public adapter(): AdapterConstructor | undefined
public adapter(v: AdapterConstructor): this
@ -364,14 +344,14 @@ export class Server<
* @param srv - server or port
* @param opts - options passed to engine.io
* @return self
* @public
*/
public listen(
srv: any,//http.Server | number,
// srv: http.Server | HTTPSServer | Http2SecureServer | number,
srv: any,
opts: Partial<ServerOptions> = {}
): this {
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 opts - options passed to engine.io
* @return self
* @public
*/
public attach(
srv: any,//http.Server | number,
// srv: http.Server | HTTPSServer | Http2SecureServer | number,
srv: any,
opts: Partial<ServerOptions> = {}
): this {
// if ("function" == typeof srv) {
@ -418,6 +398,69 @@ export class Server<
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
*
@ -425,10 +468,14 @@ export class Server<
* @param opts - options passed to engine.io
* @private
*/
private initEngine(srv: any, opts: Partial<EngineAttachOptions>) {
// // initialize engine
console.debug("creating engine.io instance with opts", JSON.stringify(opts))
this.eio = engine.attach(srv, opts)
private initEngine(
// srv: http.Server | HTTPSServer | Http2SecureServer,
srv: any,
opts: EngineOptions & AttachOptions
): void {
// initialize engine
debug("creating engine.io instance with opts %j", opts)
this.eio = attach(srv, opts)
// // attach static file serving
// if (this._serveClient) this.attachServe(srv)
@ -446,13 +493,15 @@ export class Server<
// * @param srv http server
// * @private
// */
// private attachServe(srv: http.Server): void {
// private attachServe(
// srv: http.Server | HTTPSServer | Http2SecureServer
// ): void {
// debug("attaching client serving req handler")
// const evs = srv.listeners("request").slice(0)
// srv.removeAllListeners("request")
// srv.on("request", (req, res) => {
// if (this.clientPathRegex.test(req.url)) {
// if (this.clientPathRegex.test(req.url!)) {
// this.serve(req, res)
// } else {
// for (let i = 0; i < evs.length; i++) {
@ -470,7 +519,7 @@ export class Server<
// * @private
// */
// 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 type = isMap ? "map" : "source"
@ -547,11 +596,9 @@ export class Server<
* Binds socket.io to an engine.io instance.
*
* @param {engine.Server} engine engine.io (or compatible) server
* @return {Server} self
* @public
* @return self
*/
public bind(engine): Server {
console.debug('engine.io', engine.constructor.name, 'bind to socket.io')
public bind(engine): this {
this.engine = engine
this.engine.on("connection", this.onconnection.bind(this))
return this
@ -561,12 +608,12 @@ export class Server<
* Called with each incoming transport connection.
*
* @param {engine.Socket} conn
* @return {Server} self
* @return self
* @private
*/
private onconnection(conn: EngineIOSocket): Server {
console.debug(`socket.io index incoming connection with id ${conn.id}`)
let client = new Client(this, conn)
private onconnection(conn): this {
debug("incoming connection with id %s", conn.id)
const client = new Client(this, conn)
if (conn.protocol === 3) {
// @ts-ignore
client.connect("/")
@ -577,17 +624,30 @@ export class Server<
/**
* 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
* @public
*/
public of(
name: string | RegExp | ParentNspNameMatchFn,
fn?: (socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>) => void
): Namespace<ListenEvents, EmitEvents, ServerSideEvents> {
fn?: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void
): Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
if (typeof name === "function" || name instanceof RegExp) {
const parentNsp = new ParentNamespace(this)
console.debug(`initializing parent namespace ${parentNsp.name}`)
debug("initializing parent namespace %s", parentNsp.name)
if (typeof name === "function") {
this.parentNsps.set(name, parentNsp)
} else {
@ -607,7 +667,7 @@ export class Server<
let nsp = this._nsps.get(name)
if (!nsp) {
console.debug("initializing namespace", name)
debug("initializing namespace %s", name)
nsp = new Namespace(this, name)
this._nsps.set(name, nsp)
if (name !== "/") {
@ -623,7 +683,6 @@ export class Server<
* Closes server connection
*
* @param [fn] optional, called as `fn([err])` on error OR all conns closed
* @public
*/
public close(fn?: (err?: Error) => void): void {
for (const socket of this.sockets.sockets.values()) {
@ -632,6 +691,9 @@ export class Server<
this.engine.close()
// // restore the Adapter prototype
// restoreAdapter()
// if (this.httpServer) {
// this.httpServer.close(fn)
// } 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
* @public
* @example
* io.use((socket, next) => {
* // ...
* next();
* });
*
* @param fn - the middleware function
*/
public use(
fn: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>,
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
next: (err?: ExtendedError) => void
) => void
): this {
@ -658,41 +725,71 @@ export class Server<
/**
* Targets a room when emitting.
*
* @param room
* @return self
* @public
* @example
* // the “foo” event will be broadcast to all connected clients in the “room-101” room
* 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)
}
/**
* Targets a room when emitting.
* Targets a room when emitting. Similar to `to()`, but might feel clearer in some cases:
*
* @param room
* @return self
* @public
* @example
* // disconnect all clients in the "room-101" room
* 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)
}
/**
* Excludes a room when emitting.
*
* @param name
* @return self
* @public
* @example
* // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room
* 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> {
return this.sockets.except(name)
public except(room: Room | Room[]) {
return this.sockets.except(room)
}
/**
* 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
* @public
*/
public send(...args: EventParams<EmitEvents, "message">): this {
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
* @public
*/
public write(...args: EventParams<EmitEvents, "message">): this {
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 args - an array of arguments, which may include an acknowledgement callback at the end
* @public
*/
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
@ -727,7 +842,8 @@ export class Server<
/**
* 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>> {
return this.sockets.allSockets()
@ -736,11 +852,13 @@ export class Server<
/**
* Sets the compress flag.
*
* @example
* io.compress(false).emit("hello");
*
* @param compress - if `true`, compresses the sending data
* @return self
* @public
* @return a new {@link BroadcastOperator} instance for chaining
*/
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
public compress(compress: boolean) {
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
* and is in the middle of a request-response cycle).
*
* @return self
* @public
* @example
* 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
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return self
* @public
* @example
* // 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
}
/**
* 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()
}
/**
* Makes the matching socket instances join the specified rooms
* Makes the matching socket instances join the specified rooms.
*
* @param room
* @public
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @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)
}
/**
* Makes the matching socket instances leave the specified rooms
* Makes the matching socket instances leave the specified rooms.
*
* @param room
* @public
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @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)
}
/**
* 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
* @public
*/
public disconnectSockets(close: boolean = false): void {
public disconnectSockets(close: boolean = false) {
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 { Event } from "./socket"

View File

@ -1,4 +1,3 @@
import { Socket } from "./socket"
import type { Server } from "./index"
import {
@ -14,7 +13,8 @@ import type { Client } from "./client"
import type { Adapter, Room, SocketId } from "../socket.io-adapter"
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 {
data?: any
@ -23,56 +23,125 @@ export interface ExtendedError extends Error {
export interface NamespaceReservedEventsMap<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap,
ServerSideEvents extends EventsMap
> {
connect: (socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>) => void
ServerSideEvents extends EventsMap,
SocketData
> {
connect: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void
connection: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void
}
export interface ServerReservedEventsMap<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap,
ServerSideEvents extends EventsMap,
SocketData
> extends NamespaceReservedEventsMap<
ListenEvents,
EmitEvents,
ServerSideEvents
> extends NamespaceReservedEventsMap<
ListenEvents,
EmitEvents,
ServerSideEvents
> {
ServerSideEvents,
SocketData
> {
new_namespace: (
namespace: Namespace<ListenEvents, EmitEvents, ServerSideEvents>
namespace: Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void
}
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"])
/**
* 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<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap
> extends StrictEventEmitter<
ServerSideEvents extends EventsMap = DefaultEventsMap,
SocketData = any
> extends StrictEventEmitter<
ServerSideEvents,
EmitEvents,
NamespaceReservedEventsMap<ListenEvents, EmitEvents, ServerSideEvents>
> {
NamespaceReservedEventsMap<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>
> {
public readonly name: string
public readonly sockets: Map<
SocketId,
Socket<ListenEvents, EmitEvents, ServerSideEvents>
Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map();
public adapter: Adapter
/** @private */
readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents>
readonly server: Server<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>
/** @private */
_fns: Array<
(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>,
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
next: (err?: ExtendedError) => void
) => void
> = [];
@ -87,7 +156,7 @@ export class Namespace<
* @param name
*/
constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents>,
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
name: string
) {
super()
@ -103,20 +172,27 @@ export class Namespace<
*
* @private
*/
_initAdapter() {
_initAdapter(): void {
// @ts-ignore
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
* @public
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.use((socket, next) => {
* // ...
* next();
* });
*
* @param fn - the middleware function
*/
public use(
fn: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>,
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
next: (err?: ExtendedError) => void
) => void
): this {
@ -132,7 +208,7 @@ export class Namespace<
* @private
*/
private run(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>,
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
fn: (err: ExtendedError | null) => void
) {
const fns = this._fns.slice(0)
@ -157,34 +233,63 @@ export class Namespace<
/**
* Targets a room when emitting.
*
* @param room
* @return self
* @public
* @example
* const myNamespace = io.of("/my-namespace");
*
* // 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> {
return new BroadcastOperator(this.adapter).to(room)
public to(room: Room | 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
* @return self
* @public
* @example
* const myNamespace = io.of("/my-namespace");
*
* // 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> {
return new BroadcastOperator(this.adapter).in(room)
public in(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).in(room)
}
/**
* Excludes a room when emitting.
*
* @param room
* @return self
* @public
* @example
* const myNamespace = io.of("/my-namespace");
*
* // 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> {
return new BroadcastOperator(this.adapter).except(room)
public except(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).except(
room
)
}
/**
@ -197,41 +302,45 @@ export class Namespace<
client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
query,
fn?: (socket: Socket) => void
): Socket<ListenEvents, EmitEvents, ServerSideEvents> {
const socket = new Socket(this, client, query || {})
console.debug(`socket.io namespace client ${client.id} adding socket ${socket.id} to nsp ${this.name}`)
this.run(socket, err => {
): Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
debug("adding socket to nsp %s", this.name)
const socket = new Socket(this, client, query)
this.run(socket, (err) => {
process.nextTick(() => {
if ("open" == client.conn.readyState) {
if (err) {
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)
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 ("open" !== client.conn.readyState) {
debug("next called after client was closed - ignoring socket")
socket._cleanup()
return
}
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
@ -242,33 +351,64 @@ export class Namespace<
*
* @private
*/
_remove(socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>): void {
_remove(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
): void {
if (this.sockets.has(socket.id)) {
console.debug(`namespace ${this.name} remove socket ${socket.id}`)
this.sockets.delete(socket.id)
} 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
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): 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.
*
* 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
* @public
*/
public send(...args: EventParams<EmitEvents, "message">): this {
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
* @public
*/
public write(...args: EventParams<EmitEvents, "message">): this {
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 args - an array of arguments, which may include an acknowledgement callback at the end
* @public
*/
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
...args: EventParams<ServerSideEvents, Ev>
): boolean {
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)
this.adapter.serverSideEmit(args)
@ -319,22 +479,30 @@ export class Namespace<
/**
* Gets a list of clients.
*
* @return self
* @public
* @deprecated this method will be removed in the next major release, please use {@link Namespace#serverSideEmit} or
* {@link Namespace#fetchSockets} instead.
*/
public allSockets(): Promise<Set<SocketId>> {
return new BroadcastOperator(this.adapter).allSockets()
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).allSockets()
}
/**
* Sets the compress flag.
*
* @param compress - if `true`, compresses the sending data
* @return self
* @public
*/
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).compress(compress)
* Sets the compress flag.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.compress(false).emit("hello");
*
* @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
* 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
* @public
*/
public get volatile(): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).volatile
public get 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.
*
* @return self
* @public
*/
public get local(): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).local
}
/**
* Returns the matching socket instances
* @example
* const myNamespace = io.of("/my-namespace");
*
* @public
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
return new BroadcastOperator(this.adapter).fetchSockets()
}
/**
* Makes the matching socket instances join the specified rooms
* // the “foo” event will be broadcast to all connected clients on this node
* myNamespace.local.emit("foo", "bar");
*
* @param room
* @public
* @return a new {@link BroadcastOperator} instance for chaining
*/
public socketsJoin(room: Room | Room[]): void {
return new BroadcastOperator(this.adapter).socketsJoin(room)
public get local() {
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
* @public
* @example
* 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 {
return new BroadcastOperator(this.adapter).socketsLeave(room)
public timeout(timeout: number) {
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
* @public
*/
public disconnectSockets(close: boolean = false): void {
return new BroadcastOperator(this.adapter).disconnectSockets(close)
public disconnectSockets(close: boolean = false) {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).disconnectSockets(close)
}
// @java-patch
public close() {
RESERVED_EVENTS.forEach(event => this.removeAllListeners(event as any))
this.server._nsps.delete(this.name)

View File

@ -1,5 +1,5 @@
import { Namespace } from "./namespace"
import type { Server } from "./index"
import type { Server, RemoteSocket } from "./index"
import type {
EventParams,
EventNames,
@ -12,16 +12,24 @@ import type { BroadcastOptions } from "../socket.io-adapter"
export class ParentNamespace<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap
> extends Namespace<ListenEvents, EmitEvents, ServerSideEvents> {
ServerSideEvents extends EventsMap = DefaultEventsMap,
SocketData = any
> extends Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
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++)
}
_initAdapter() {
/**
* @private
*/
_initAdapter(): void {
const broadcast = (packet: any, opts: BroadcastOptions) => {
this.children.forEach((nsp) => {
nsp.adapter.broadcast(packet, opts)
@ -42,21 +50,9 @@ export class ParentNamespace<
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(
name: string
): Namespace<ListenEvents, EmitEvents, ServerSideEvents> {
): Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
const namespace = new Namespace(this.server, name)
namespace._fns = this._fns.slice(0)
this.listeners("connect").forEach((listener) =>
@ -69,4 +65,13 @@ export class ParentNamespace<
this.server._nsps.set(name, 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 url = require("url")
// import debugModule from "debug"
// import debugModule from "debug";
import type { Server } from "./index"
import {
EventParams,
@ -12,24 +11,41 @@ import {
} from "./typed-events"
import type { Client } from "./client"
import type { Namespace, NamespaceReservedEventsMap } from "./namespace"
// import type { IncomingMessage, IncomingHttpHeaders } from "http"
// import type { IncomingMessage, IncomingHttpHeaders } from "http";
import type {
Adapter,
BroadcastFlags,
Room,
SocketId,
} from "socket.io-adapter"
// import base64id from "base64id"
} from "../socket.io-adapter"
// import base64id from "base64id";
import type { ParsedUrlQuery } from "querystring"
import { BroadcastOperator } from "./broadcast-operator"
import * as url from "url"
// const debug = debugModule("socket.io:socket");
const debug = require('../debug')("socket.io:socket")
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 {
disconnect: (reason: string) => void
disconnecting: (reason: string) => void
disconnect: (reason: DisconnectReason) => void
disconnecting: (reason: DisconnectReason) => void
error: (err: Error) => void
}
@ -47,7 +63,7 @@ export interface EventEmitterReservedEventsMap {
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
| ClientReservedEvents
| keyof NamespaceReservedEventsMap<never, never, never>
| keyof NamespaceReservedEventsMap<never, never, never, never>
| keyof SocketReservedEventsMap
| keyof EventEmitterReservedEventsMap
>(<const>[
@ -66,7 +82,8 @@ export interface Handshake {
/**
* The headers sent as part of the handshake
*/
headers: any//IncomingHttpHeaders
// headers: IncomingHttpHeaders;
headers: any
/**
* The date of creation (as string)
@ -109,33 +126,94 @@ export interface Handshake {
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<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap
> extends StrictEventEmitter<
ServerSideEvents extends EventsMap = DefaultEventsMap,
SocketData = any
> extends StrictEventEmitter<
ListenEvents,
EmitEvents,
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
public disconnected: boolean
private readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents>
private readonly server: Server<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>
private readonly adapter: Adapter
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 _anyListeners?: Array<(...args: any[]) => void>
private _anyOutgoingListeners?: Array<(...args: any[]) => void>
/**
* Interface to a `Client` for a given `Namespace`.
@ -151,23 +229,23 @@ export class Socket<
auth: object
) {
super()
this.nsp = nsp
this.server = nsp.server
this.adapter = this.nsp.adapter
// if (client.conn.protocol === 3) {
// // @ts-ignore
// @ts-ignore
this.id = nsp.name !== "/" ? nsp.name + "#" + client.id : client.id
// } 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)
}
buildHandshake(auth): Handshake {
/**
* Builds the `handshake` BC object
*
* @private
*/
private buildHandshake(auth: object): Handshake {
return {
headers: this.request.headers,
time: new Date() + "",
@ -177,6 +255,7 @@ export class Socket<
secure: !!this.request.connection.encrypted,
issued: +new Date(),
url: this.request.url!,
// @ts-ignore
query: url.parse(this.request.url!, true).query,
auth,
}
@ -185,15 +264,27 @@ export class Socket<
/**
* 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`.
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
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 packet: any = {
@ -203,57 +294,124 @@ export class Socket<
// access last argument to see if it's an ACK callback
if (typeof data[data.length - 1] === "function") {
console.trace("emitting packet with ack id %d", this.nsp._ids)
this.acks.set(this.nsp._ids, data.pop())
packet.id = this.nsp._ids++
const id = this.nsp._ids++
debug("emitting packet with ack id %d", id)
this.registerAckCallback(id, data.pop())
packet.id = id
}
const flags = Object.assign({}, this.flags)
this.flags = {}
this.notifyOutgoingListeners(packet)
this.packet(packet, flags)
return true
}
/**
* Targets a room when broadcasting.
*
* @param room
* @return self
* @public
* @private
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.newBroadcastOperator().to(room)
private registerAckCallback(id: number, ack: (...args: any[]) => void): void {
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.
*
* @param room
* @return self
* @public
* @example
* io.on("connection", (socket) => {
* // 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)
}
/**
* Excludes a room when broadcasting.
*
* @param room
* @return self
* @public
* @example
* io.on("connection", (socket) => {
* // 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)
}
/**
* 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
* @public
*/
public send(...args: EventParams<EmitEvents, "message">): this {
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
* @public
*/
public write(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args)
@ -290,12 +447,20 @@ export class Socket<
/**
* 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
* @return a Promise or nothing, depending on the adapter
* @public
*/
public join(rooms: Room | Array<Room>): Promise<void> | void {
console.debug(`join room ${rooms}`)
debug("join room %s", rooms)
return this.adapter.addAll(
this.id,
@ -306,12 +471,20 @@ export class Socket<
/**
* 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
* @return a Promise or nothing, depending on the adapter
* @public
*/
public leave(room: string): Promise<void> | void {
console.debug(`leave room ${room}`)
debug("leave room %s", room)
return this.adapter.del(this.id, room)
}
@ -334,7 +507,8 @@ export class Socket<
* @private
*/
_onconnect(): void {
console.debug(`socket ${this.id} connected - writing packet`)
debug("socket connected - writing packet")
this.connected = true
this.join(this.id)
if (this.conn.protocol === 3) {
this.packet({ type: PacketType.CONNECT })
@ -350,7 +524,7 @@ export class Socket<
* @private
*/
_onpacket(packet: Packet): void {
console.trace("got packet", JSON.stringify(packet))
debug("got packet %j", packet)
switch (packet.type) {
case PacketType.EVENT:
this.onevent(packet)
@ -371,9 +545,6 @@ export class Socket<
case PacketType.DISCONNECT:
this.ondisconnect()
break
case PacketType.CONNECT_ERROR:
this._onerror(new Error(packet.data))
}
}
@ -385,10 +556,10 @@ export class Socket<
*/
private onevent(packet: Packet): void {
const args = packet.data || []
console.trace("emitting event", JSON.stringify(args))
debug("emitting event %j", args)
if (null != packet.id) {
console.trace("attaching ack callback to event")
debug("attaching ack callback to event")
args.push(this.ack(packet.id))
}
@ -414,7 +585,7 @@ export class Socket<
// prevent double callbacks
if (sent) return
const args = Array.prototype.slice.call(arguments)
console.trace("sending ack", JSON.stringify(args))
debug("sending ack %j", args)
self.packet({
id: id,
@ -434,11 +605,11 @@ export class Socket<
private onack(packet: Packet): void {
const ack = this.acks.get(packet.id!)
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)
this.acks.delete(packet.id!)
} 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 ondisconnect(): void {
console.debug(`socket ${this.id} got disconnect packet`)
debug("got disconnect packet")
this._onclose("client namespace disconnect")
}
@ -474,19 +645,28 @@ export class Socket<
*
* @private
*/
_onclose(reason: string): this | undefined {
_onclose(reason: DisconnectReason): this | undefined {
if (!this.connected) return this
console.debug(`closing socket ${this.id} - reason: ${reason}`)
debug("closing socket - reason %s", reason)
this.emitReserved("disconnecting", reason)
this.leaveAll()
this._cleanup()
this.nsp._remove(this)
this.client._remove(this)
this.connected = false
this.disconnected = true
this.emitReserved("disconnect", reason)
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.
*
@ -501,10 +681,17 @@ export class Socket<
/**
* Disconnects this client.
*
* @param {Boolean} close - if `true`, closes the underlying connection
* @return {Socket} self
* @example
* 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 {
if (!this.connected) return this
@ -520,9 +707,13 @@ export class Socket<
/**
* Sets the compress flag.
*
* @example
* io.on("connection", (socket) => {
* socket.compress(false).emit("hello");
* });
*
* @param {Boolean} compress - if `true`, compresses the sending data
* @return {Socket} self
* @public
*/
public compress(compress: boolean): this {
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
* 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).
*
* @return {Socket} self
* @public
*/
* 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
* and is in the middle of a request-response cycle).
*
* @example
* io.on("connection", (socket) => {
* socket.volatile.emit("hello"); // the client may or may not receive it
* });
*
* @return {Socket} self
*/
public get volatile(): this {
this.flags.volatile = true
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
* sender.
*
* @return {Socket} self
* @public
* @example
* 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()
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return {Socket} self
* @public
* @example
* 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
}
/**
* 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.
*
* @param {Array} event - event that will get emitted
* @private
*/
private dispatch(event: [eventName: string, ...args: any[]]): void {
console.trace("dispatching an event", JSON.stringify(event))
private dispatch(event: Event): void {
debug("dispatching an event %j", event)
this.run(event, (err) => {
process.nextTick(() => {
if (err) {
@ -579,7 +804,7 @@ export class Socket<
if (this.connected) {
super.emitUntyped.apply(this, event)
} 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.
*
* @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)
* @return {Socket} self
* @public
*/
public use(
fn: (event: Array<any>, next: (err?: Error) => void) => void
): this {
public use(fn: (event: Event, next: (err?: Error) => void) => void): this {
this.fns.push(fn)
return this
}
@ -606,10 +845,7 @@ export class Socket<
* @param {Function} fn - last fn call in the middleware
* @private
*/
private run(
event: [eventName: string, ...args: any[]],
fn: (err: Error | null) => void
): void {
private run(event: Event, fn: (err: Error | null) => void): void {
const fns = this.fns.slice(0)
if (!fns.length) return fn(null)
@ -629,10 +865,15 @@ export class Socket<
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.
*
* @public
*/
public get request(): any /** IncomingMessage */ {
return this.client.request
@ -641,25 +882,47 @@ export class Socket<
/**
* 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() {
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> {
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
* callback.
* Adds a listener that will be fired when any event is received. The event name is passed as the first argument to
* the callback.
*
* @example
* io.on("connection", (socket) => {
* socket.onAny((event, ...args) => {
* console.log(`got event ${event}`);
* });
* });
*
* @param listener
* @public
*/
public onAny(listener: (...args: any[]) => void): this {
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
* callback. The listener is added to the beginning of the listeners array.
* Adds a listener that will be fired when any event is received. The event name is passed as the first argument to
* the callback. The listener is added to the beginning of the listeners array.
*
* @param listener
* @public
*/
public prependAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || []
@ -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
* @public
*/
public offAny(listener?: (...args: any[]) => void): this {
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,
* e.g. to remove listeners.
*
* @public
*/
public listenersAny() {
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)
this.flags = {}
return new BroadcastOperator(
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
new Set<Room>(),
new Set<Room>([this.id]),