feat: upgrade socket.io to v4
Signed-off-by: MiaoWoo <admin@yumc.pw>
This commit is contained in:
parent
e563e1b507
commit
df0d246136
2134
packages/polyfill/src/buffer.ts
Normal file
2134
packages/polyfill/src/buffer.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
}
|
||||
|
12
packages/websocket/src/engine.io-client/contrib/has-cors.ts
Normal file
12
packages/websocket/src/engine.io-client/contrib/has-cors.ts
Normal 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;
|
38
packages/websocket/src/engine.io-client/contrib/parseqs.ts
Normal file
38
packages/websocket/src/engine.io-client/contrib/parseqs.ts
Normal 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
|
||||
}
|
68
packages/websocket/src/engine.io-client/contrib/parseuri.ts
Normal file
68
packages/websocket/src/engine.io-client/contrib/parseuri.ts
Normal 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
|
||||
}
|
62
packages/websocket/src/engine.io-client/contrib/yeast.ts
Normal file
62
packages/websocket/src/engine.io-client/contrib/yeast.ts
Normal 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
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { WS } from "./websocket"
|
||||
export default {
|
||||
'websocket': WS
|
||||
import { WS } from "./websocket.js"
|
||||
|
||||
export const transports = {
|
||||
websocket: WS,
|
||||
}
|
||||
|
@ -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
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
484
packages/websocket/src/engine.io/parser-v3/index.ts
Normal file
484
packages/websocket/src/engine.io/parser-v3/index.ts
Normal 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)
|
||||
}
|
||||
}
|
210
packages/websocket/src/engine.io/parser-v3/utf8.ts
Normal file
210
packages/websocket/src/engine.io/parser-v3/utf8.ts
Normal 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, it’s 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
|
||||
}
|
@ -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 */
|
||||
|
||||
// /**
|
||||
|
@ -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"))
|
||||
}
|
||||
|
@ -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?)
|
||||
}
|
||||
|
@ -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"]
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
77
packages/websocket/src/socket.io-client/contrib/backo2.ts
Normal file
77
packages/websocket/src/socket.io-client/contrib/backo2.ts
Normal 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
|
||||
}
|
@ -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,
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
>[]
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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 they’re 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)
|
||||
|
@ -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()
|
||||
|
@ -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 they’re 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"
|
||||
|
@ -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 they’re 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)
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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 they’re 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 they’re 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]),
|
||||
|
Loading…
Reference in New Issue
Block a user