feat: 同步 socket.io 上游代码

Signed-off-by: MiaoWoo <admin@yumc.pw>
This commit is contained in:
MiaoWoo 2023-02-09 13:49:48 +08:00
parent 359aeb9d63
commit 7b85ff5b7c
35 changed files with 7612 additions and 6519 deletions

View File

@ -0,0 +1,4 @@
{
"tabWidth": 2,
"semi": true
}

View File

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

View File

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

View File

@ -7,4 +7,4 @@ export { Transport } from "./transport"
export { transports } from "./transports/index" export { transports } from "./transports/index"
export { installTimerFunctions } from "./util" export { installTimerFunctions } from "./util"
export { parse } from "./contrib/parseuri" export { parse } from "./contrib/parseuri"
export { nextTick } from "./transports/websocket-constructor.js" export { nextTick } from "./transports/websocket-constructor"

View File

@ -7,7 +7,8 @@ import { parse } from "./contrib/parseuri"
import { Emitter } from "@socket.io/component-emitter" import { Emitter } from "@socket.io/component-emitter"
// import { protocol } from "engine.io-parser"; // import { protocol } from "engine.io-parser";
import { protocol } from "../engine.io-parser" import { protocol } from "../engine.io-parser"
import { CloseDetails } from "./transport" import type { Packet, BinaryType, PacketType, RawData } from "../engine.io-parser"
import { CloseDetails, Transport } from "./transport"
// const debug = debugModule("engine.io-client:socket"); // debug() // const debug = debugModule("engine.io-client:socket"); // debug()
const debug = require('../debug')('engine.io-client:socket') const debug = require('../debug')('engine.io-client:socket')
@ -209,6 +210,12 @@ export interface SocketOptions {
*/ */
path: string path: string
/**
* Whether we should add a trailing slash to the request path.
* @default true
*/
addTrailingSlash: boolean
/** /**
* Either a single protocol string or an array of protocol strings. These strings are used to indicate sub-protocols, * 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 * so that a single server can implement multiple WebSocket sub-protocols (for example, you might want one server to
@ -218,11 +225,19 @@ export interface SocketOptions {
protocols: string | string[] protocols: string | string[]
} }
interface HandshakeData {
sid: string
upgrades: string[]
pingInterval: number
pingTimeout: number
maxPayload: number
}
interface SocketReservedEvents { interface SocketReservedEvents {
open: () => void open: () => void
handshake: (data) => void handshake: (data: HandshakeData) => void
packet: (packet) => void packet: (packet: Packet) => void
packetCreate: (packet) => void packetCreate: (packet: Packet) => void
data: (data) => void data: (data) => void
message: (data) => void message: (data) => void
drain: () => void drain: () => void
@ -237,13 +252,15 @@ interface SocketReservedEvents {
close: (reason: string, description?: CloseDetails | Error) => void close: (reason: string, description?: CloseDetails | Error) => void
} }
type SocketState = "opening" | "open" | "closing" | "closed"
export class Socket extends Emitter<{}, {}, SocketReservedEvents> { export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
public id: string public id: string
public transport: any public transport: Transport
public binaryType: string public binaryType: BinaryType
public readyState: SocketState
public writeBuffer: Packet[] = [];
private readyState: string
private writeBuffer
private prevBufferLen: number private prevBufferLen: number
private upgrades private upgrades
private pingInterval: number private pingInterval: number
@ -314,7 +331,6 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
: "80") : "80")
this.transports = opts.transports || ["polling", "websocket"] this.transports = opts.transports || ["polling", "websocket"]
this.readyState = ""
this.writeBuffer = [] this.writeBuffer = []
this.prevBufferLen = 0 this.prevBufferLen = 0
@ -326,6 +342,7 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
upgrade: true, upgrade: true,
timestampParam: "t", timestampParam: "t",
rememberUpgrade: false, rememberUpgrade: false,
addTrailingSlash: true,
rejectUnauthorized: true, rejectUnauthorized: true,
perMessageDeflate: { perMessageDeflate: {
threshold: 1024 threshold: 1024
@ -336,7 +353,9 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
opts opts
) )
this.opts.path = this.opts.path.replace(/\/$/, "") + "/" this.opts.path =
this.opts.path.replace(/\/$/, "") +
(this.opts.addTrailingSlash ? "/" : "")
if (typeof this.opts.query === "string") { if (typeof this.opts.query === "string") {
this.opts.query = decode(this.opts.query) this.opts.query = decode(this.opts.query)
@ -368,7 +387,7 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
if (this.hostname !== "localhost") { if (this.hostname !== "localhost") {
this.offlineEventListener = () => { this.offlineEventListener = () => {
this.onClose("transport close", { this.onClose("transport close", {
description: "network connection lost" description: "network connection lost",
}) })
} }
addEventListener("offline", this.offlineEventListener, false) addEventListener("offline", this.offlineEventListener, false)
@ -381,9 +400,9 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
/** /**
* Creates transport of the given type. * Creates transport of the given type.
* *
* @param {String} transport name * @param {String} name - transport name
* @return {Transport} * @return {Transport}
* @api private * @private
*/ */
private createTransport(name) { private createTransport(name) {
debug('creating transport "%s"', name) debug('creating transport "%s"', name)
@ -407,7 +426,7 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
socket: this, socket: this,
hostname: this.hostname, hostname: this.hostname,
secure: this.secure, secure: this.secure,
port: this.port port: this.port,
} }
) )
@ -419,7 +438,7 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
/** /**
* Initializes transport to use and starts probe. * Initializes transport to use and starts probe.
* *
* @api private * @private
*/ */
private open() { private open() {
let transport let transport
@ -457,7 +476,7 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
/** /**
* Sets the current transport. Disables the existing one (if any). * Sets the current transport. Disables the existing one (if any).
* *
* @api private * @private
*/ */
private setTransport(transport) { private setTransport(transport) {
debug("setting transport %s", transport.name) debug("setting transport %s", transport.name)
@ -475,7 +494,7 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
.on("drain", this.onDrain.bind(this)) .on("drain", this.onDrain.bind(this))
.on("packet", this.onPacket.bind(this)) .on("packet", this.onPacket.bind(this))
.on("error", this.onError.bind(this)) .on("error", this.onError.bind(this))
.on("close", reason => this.onClose("transport close", reason)) .on("close", (reason) => this.onClose("transport close", reason))
} }
/** /**

View File

@ -22,7 +22,7 @@ class TransportError extends Error {
export interface CloseDetails { export interface CloseDetails {
description: string description: string
context?: CloseEvent | XMLHttpRequest context?: unknown // context should be typed as CloseEvent | XMLHttpRequest, but these types are not available on non-browser platforms
} }
interface TransportReservedEvents { interface TransportReservedEvents {
@ -35,24 +35,27 @@ interface TransportReservedEvents {
drain: () => void drain: () => void
} }
type TransportState = "opening" | "open" | "closed" | "pausing" | "paused"
export abstract class Transport extends Emitter< export abstract class Transport extends Emitter<
{}, Record<never, never>,
{}, Record<never, never>,
TransportReservedEvents TransportReservedEvents
> { > {
public query: Record<string, string>
public writable: boolean = false;
protected opts: SocketOptions protected opts: SocketOptions
protected supportsBinary: boolean protected supportsBinary: boolean
protected query: object protected readyState: TransportState
protected readyState: string
protected writable: boolean = false;
protected socket: any protected socket: any
protected setTimeoutFn: typeof setTimeout protected setTimeoutFn: typeof setTimeout
/** /**
* Transport abstract constructor. * Transport abstract constructor.
* *
* @param {Object} options. * @param {Object} opts - options
* @api private * @protected
*/ */
constructor(opts) { constructor(opts) {
super() super()
@ -60,7 +63,6 @@ export abstract class Transport extends Emitter<
this.opts = opts this.opts = opts
this.query = opts.query this.query = opts.query
this.readyState = ""
this.socket = opts.socket this.socket = opts.socket
} }
@ -71,7 +73,7 @@ export abstract class Transport extends Emitter<
* @param description * @param description
* @param context - the error context * @param context - the error context
* @return {Transport} for chaining * @return {Transport} for chaining
* @api protected * @protected
*/ */
protected onError(reason: string, description: any, context?: any) { protected onError(reason: string, description: any, context?: any) {
super.emitReserved( super.emitReserved(
@ -83,25 +85,19 @@ export abstract class Transport extends Emitter<
/** /**
* Opens the transport. * Opens the transport.
*
* @api public
*/ */
private open() { public open() {
if ("closed" === this.readyState || "" === this.readyState) {
this.readyState = "opening" this.readyState = "opening"
this.doOpen() this.doOpen()
}
return this return this
} }
/** /**
* Closes the transport. * Closes the transport.
*
* @api public
*/ */
public close() { public close() {
if ("opening" === this.readyState || "open" === this.readyState) { if (this.readyState === "opening" || this.readyState === "open") {
this.doClose() this.doClose()
this.onClose() this.onClose()
} }
@ -113,10 +109,9 @@ export abstract class Transport extends Emitter<
* Sends multiple packets. * Sends multiple packets.
* *
* @param {Array} packets * @param {Array} packets
* @api public
*/ */
public send(packets) { public send(packets) {
if ("open" === this.readyState) { if (this.readyState === "open") {
this.write(packets) this.write(packets)
} else { } else {
// this might happen if the transport was silently closed in the beforeunload event handler // this might happen if the transport was silently closed in the beforeunload event handler
@ -127,7 +122,7 @@ export abstract class Transport extends Emitter<
/** /**
* Called upon open * Called upon open
* *
* @api protected * @protected
*/ */
protected onOpen() { protected onOpen() {
this.readyState = "open" this.readyState = "open"
@ -139,17 +134,18 @@ export abstract class Transport extends Emitter<
* Called with data. * Called with data.
* *
* @param {String} data * @param {String} data
* @api protected * @protected
*/ */
protected onData(data: RawData) { protected onData(data: RawData) {
const packet = decodePacket(data, this.socket.binaryType) const packet = decodePacket(data, this.socket.binaryType)
this.onPacket(packet) this.onPacket(packet)
} }
/** /**
* Called with a decoded packet. * Called with a decoded packet.
* *
* @api protected * @protected
*/ */
protected onPacket(packet: Packet) { protected onPacket(packet: Packet) {
super.emitReserved("packet", packet) super.emitReserved("packet", packet)
@ -158,14 +154,26 @@ export abstract class Transport extends Emitter<
/** /**
* Called upon close. * Called upon close.
* *
* @api protected * @protected
*/ */
protected onClose(details?: CloseDetails) { protected onClose(details?: CloseDetails) {
this.readyState = "closed" this.readyState = "closed"
super.emitReserved("close", details) super.emitReserved("close", details)
} }
/**
* The name of the transport
*/
public abstract get name(): string
/**
* Pauses the transport, in order not to lose packets during an upgrade.
*
* @param onPause
*/
public pause(onPause: () => void) { }
protected abstract doOpen() protected abstract doOpen()
protected abstract doClose() protected abstract doClose()
protected abstract write(packets) protected abstract write(packets: Packet[])
} }

View File

@ -26,8 +26,8 @@ export class WS extends Transport {
/** /**
* WebSocket transport constructor. * WebSocket transport constructor.
* *
* @api {Object} connection options * @param {Object} opts - connection options
* @api public * @protected
*/ */
constructor(opts) { constructor(opts) {
super(opts) super(opts)
@ -35,21 +35,11 @@ export class WS extends Transport {
this.supportsBinary = !opts.forceBase64 this.supportsBinary = !opts.forceBase64
} }
/** override get name() {
* Transport name.
*
* @api public
*/
get name() {
return "websocket" return "websocket"
} }
/** override doOpen() {
* Opens socket.
*
* @api private
*/
doOpen() {
if (!this.check()) { if (!this.check()) {
// let probe timeout // let probe timeout
return return
@ -103,31 +93,25 @@ export class WS extends Transport {
/** /**
* Adds event listeners to the socket * Adds event listeners to the socket
* *
* @api private * @private
*/ */
addEventListeners() { private addEventListeners() {
this.ws.onopen = () => { this.ws.onopen = () => {
if (this.opts.autoUnref) { if (this.opts.autoUnref) {
this.ws._socket.unref() this.ws._socket.unref()
} }
this.onOpen() this.onOpen()
} }
this.ws.onclose = closeEvent => this.ws.onclose = (closeEvent) =>
this.onClose({ this.onClose({
description: "websocket connection closed", description: "websocket connection closed",
context: closeEvent context: closeEvent,
}) })
this.ws.onmessage = ev => this.onData(ev.data) this.ws.onmessage = (ev) => this.onData(ev.data)
this.ws.onerror = e => this.onError("websocket error", e) this.ws.onerror = (e) => this.onError("websocket error", e)
} }
/** override write(packets) {
* Writes data to socket.
*
* @param {Array} array of packets.
* @api private
*/
write(packets) {
this.writable = false this.writable = false
// encodePacket efficient as it uses WS framing // encodePacket efficient as it uses WS framing
@ -136,7 +120,7 @@ export class WS extends Transport {
const packet = packets[i] const packet = packets[i]
const lastPacket = i === packets.length - 1 const lastPacket = i === packets.length - 1
encodePacket(packet, this.supportsBinary, data => { encodePacket(packet, this.supportsBinary, (data) => {
// always create a new object (GH-437) // always create a new object (GH-437)
const opts: { compress?: boolean } = {} const opts: { compress?: boolean } = {}
if (!usingBrowserWebSocket) { if (!usingBrowserWebSocket) {
@ -180,12 +164,7 @@ export class WS extends Transport {
} }
} }
/** override doClose() {
* Closes socket.
*
* @api private
*/
doClose() {
if (typeof this.ws !== "undefined") { if (typeof this.ws !== "undefined") {
this.ws.close() this.ws.close()
this.ws = null this.ws = null
@ -195,7 +174,7 @@ export class WS extends Transport {
/** /**
* Generates uri for connection. * Generates uri for connection.
* *
* @api private * @private
*/ */
uri() { uri() {
let query: { b64?: number } = this.query || {} let query: { b64?: number } = this.query || {}
@ -238,9 +217,9 @@ export class WS extends Transport {
* Feature detection for WebSocket. * Feature detection for WebSocket.
* *
* @return {Boolean} whether this transport is available. * @return {Boolean} whether this transport is available.
* @api public * @private
*/ */
check() { private check() {
return !!WebSocket return !!WebSocket
} }
} }

View File

@ -10,16 +10,16 @@ export function pick(obj, ...attr) {
} }
// Keep a reference to the real timeout functions so they can be used when overridden // Keep a reference to the real timeout functions so they can be used when overridden
const NATIVE_SET_TIMEOUT = setTimeout const NATIVE_SET_TIMEOUT = globalThis.setTimeout
const NATIVE_CLEAR_TIMEOUT = clearTimeout const NATIVE_CLEAR_TIMEOUT = globalThis.clearTimeout
export function installTimerFunctions(obj, opts) { export function installTimerFunctions(obj, opts) {
if (opts.useNativeTimers) { if (opts.useNativeTimers) {
obj.setTimeoutFn = NATIVE_SET_TIMEOUT.bind(globalThis) obj.setTimeoutFn = NATIVE_SET_TIMEOUT.bind(globalThis)
obj.clearTimeoutFn = NATIVE_CLEAR_TIMEOUT.bind(globalThis) obj.clearTimeoutFn = NATIVE_CLEAR_TIMEOUT.bind(globalThis)
} else { } else {
obj.setTimeoutFn = setTimeout.bind(globalThis) obj.setTimeoutFn = globalThis.setTimeout.bind(globalThis)
obj.clearTimeoutFn = clearTimeout.bind(globalThis) obj.clearTimeoutFn = globalThis.clearTimeout.bind(globalThis)
} }
} }

View File

@ -1,20 +1,20 @@
const PACKET_TYPES = Object.create(null) // no Map = no polyfill const PACKET_TYPES = Object.create(null); // no Map = no polyfill
PACKET_TYPES["open"] = "0" PACKET_TYPES["open"] = "0";
PACKET_TYPES["close"] = "1" PACKET_TYPES["close"] = "1";
PACKET_TYPES["ping"] = "2" PACKET_TYPES["ping"] = "2";
PACKET_TYPES["pong"] = "3" PACKET_TYPES["pong"] = "3";
PACKET_TYPES["message"] = "4" PACKET_TYPES["message"] = "4";
PACKET_TYPES["upgrade"] = "5" PACKET_TYPES["upgrade"] = "5";
PACKET_TYPES["noop"] = "6" PACKET_TYPES["noop"] = "6";
const PACKET_TYPES_REVERSE = Object.create(null) const PACKET_TYPES_REVERSE = Object.create(null);
Object.keys(PACKET_TYPES).forEach(key => { Object.keys(PACKET_TYPES).forEach(key => {
PACKET_TYPES_REVERSE[PACKET_TYPES[key]] = key PACKET_TYPES_REVERSE[PACKET_TYPES[key]] = key;
}) });
const ERROR_PACKET: 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 = export type PacketType =
| "open" | "open"
@ -24,16 +24,16 @@ export type PacketType =
| "message" | "message"
| "upgrade" | "upgrade"
| "noop" | "noop"
| "error" | "error";
// RawData should be "string | Buffer | ArrayBuffer | ArrayBufferView | Blob", but Blob does not exist in Node.js and // 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 // requires to add the dom lib in tsconfig.json
export type RawData = any export type RawData = any;
export interface Packet { export interface Packet {
type: PacketType type: PacketType;
options?: { compress: boolean } options?: { compress: boolean };
data?: RawData data?: RawData;
} }
export type BinaryType = "nodebuffer" | "arraybuffer" | "blob" export type BinaryType = "nodebuffer" | "arraybuffer" | "blob";

View File

@ -4,7 +4,7 @@ import {
Packet, Packet,
BinaryType, BinaryType,
RawData RawData
} from "./commons.js" } from "./commons.js";
const decodePacket = ( const decodePacket = (
encodedPacket: RawData, encodedPacket: RawData,
@ -14,18 +14,18 @@ const decodePacket = (
return { return {
type: "message", type: "message",
data: mapBinary(encodedPacket, binaryType) data: mapBinary(encodedPacket, binaryType)
};
} }
} const type = encodedPacket.charAt(0);
const type = encodedPacket.charAt(0)
if (type === "b") { if (type === "b") {
const buffer = Buffer.from(encodedPacket.substring(1), "base64") const buffer = Buffer.from(encodedPacket.substring(1), "base64");
return { return {
type: "message", type: "message",
data: mapBinary(buffer, binaryType) data: mapBinary(buffer, binaryType)
} };
} }
if (!PACKET_TYPES_REVERSE[type]) { if (!PACKET_TYPES_REVERSE[type]) {
return ERROR_PACKET return ERROR_PACKET;
} }
return encodedPacket.length > 1 return encodedPacket.length > 1
? { ? {
@ -34,27 +34,27 @@ const decodePacket = (
} }
: { : {
type: PACKET_TYPES_REVERSE[type] type: PACKET_TYPES_REVERSE[type]
} };
} };
const mapBinary = (data: RawData, binaryType?: BinaryType) => { const mapBinary = (data: RawData, binaryType?: BinaryType) => {
const isBuffer = Buffer.isBuffer(data) const isBuffer = Buffer.isBuffer(data);
switch (binaryType) { switch (binaryType) {
case "arraybuffer": case "arraybuffer":
return isBuffer ? toArrayBuffer(data) : data return isBuffer ? toArrayBuffer(data) : data;
case "nodebuffer": case "nodebuffer":
default: default:
return data // assuming the data is already a Buffer return data; // assuming the data is already a Buffer
} }
} };
const toArrayBuffer = (buffer: Buffer): ArrayBuffer => { const toArrayBuffer = (buffer: Buffer): ArrayBuffer => {
const arrayBuffer = new ArrayBuffer(buffer.length) const arrayBuffer = new ArrayBuffer(buffer.length);
const view = new Uint8Array(arrayBuffer) const view = new Uint8Array(arrayBuffer);
for (let i = 0; i < buffer.length; i++) { for (let i = 0; i < buffer.length; i++) {
view[i] = buffer[i] view[i] = buffer[i];
} }
return arrayBuffer return arrayBuffer;
} };
export default decodePacket export default decodePacket;

View File

@ -1,4 +1,4 @@
import { PACKET_TYPES, Packet, RawData } from "./commons.js" import { PACKET_TYPES, Packet, RawData } from "./commons.js";
const encodePacket = ( const encodePacket = (
{ type, data }: Packet, { type, data }: Packet,
@ -6,26 +6,26 @@ const encodePacket = (
callback: (encodedPacket: RawData) => void callback: (encodedPacket: RawData) => void
) => { ) => {
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) { if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
const buffer = toBuffer(data) const buffer = toBuffer(data);
return callback(encodeBuffer(buffer, supportsBinary)) return callback(encodeBuffer(buffer, supportsBinary));
} }
// plain string // plain string
return callback(PACKET_TYPES[type] + (data || "")) return callback(PACKET_TYPES[type] + (data || ""));
} };
const toBuffer = data => { const toBuffer = data => {
if (Buffer.isBuffer(data)) { if (Buffer.isBuffer(data)) {
return data return data;
} else if (data instanceof ArrayBuffer) { } else if (data instanceof ArrayBuffer) {
return Buffer.from(data) return Buffer.from(data);
} else { } else {
return Buffer.from(data.buffer, data.byteOffset, data.byteLength) return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
} }
} };
// only 'message' packets can contain binary, so the type prefix is not needed // only 'message' packets can contain binary, so the type prefix is not needed
const encodeBuffer = (data: Buffer, supportsBinary: boolean): RawData => { const encodeBuffer = (data: Buffer, supportsBinary: boolean): RawData => {
return supportsBinary ? data : "b" + data.toString("base64") return supportsBinary ? data : "b" + data.toString("base64");
} };
export default encodePacket export default encodePacket;

View File

@ -1,46 +1,46 @@
import encodePacket from "./encodePacket.js" import encodePacket from "./encodePacket.js";
import decodePacket from "./decodePacket.js" import decodePacket from "./decodePacket.js";
import { Packet, PacketType, RawData, BinaryType } from "./commons.js" import { Packet, PacketType, RawData, BinaryType } from "./commons.js";
const SEPARATOR = String.fromCharCode(30) // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text const SEPARATOR = String.fromCharCode(30); // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
const encodePayload = ( const encodePayload = (
packets: Packet[], packets: Packet[],
callback: (encodedPayload: string) => void callback: (encodedPayload: string) => void
) => { ) => {
// some packets may be added to the array while encoding, so the initial length must be saved // some packets may be added to the array while encoding, so the initial length must be saved
const length = packets.length const length = packets.length;
const encodedPackets = new Array(length) const encodedPackets = new Array(length);
let count = 0 let count = 0;
packets.forEach((packet, i) => { packets.forEach((packet, i) => {
// force base64 encoding for binary packets // force base64 encoding for binary packets
encodePacket(packet, false, encodedPacket => { encodePacket(packet, false, encodedPacket => {
encodedPackets[i] = encodedPacket encodedPackets[i] = encodedPacket;
if (++count === length) { if (++count === length) {
callback(encodedPackets.join(SEPARATOR)) callback(encodedPackets.join(SEPARATOR));
} }
}) });
}) });
} };
const decodePayload = ( const decodePayload = (
encodedPayload: string, encodedPayload: string,
binaryType?: BinaryType binaryType?: BinaryType
): Packet[] => { ): Packet[] => {
const encodedPackets = encodedPayload.split(SEPARATOR) const encodedPackets = encodedPayload.split(SEPARATOR);
const packets = [] const packets = [];
for (let i = 0; i < encodedPackets.length; i++) { for (let i = 0; i < encodedPackets.length; i++) {
const decodedPacket = decodePacket(encodedPackets[i], binaryType) const decodedPacket = decodePacket(encodedPackets[i], binaryType);
packets.push(decodedPacket) packets.push(decodedPacket);
if (decodedPacket.type === "error") { if (decodedPacket.type === "error") {
break break;
} }
} }
return packets return packets;
} };
export const protocol = 4 export const protocol = 4;
export { export {
encodePacket, encodePacket,
encodePayload, encodePayload,
@ -50,4 +50,4 @@ export {
PacketType, PacketType,
RawData, RawData,
BinaryType BinaryType
} };

View File

@ -1,26 +1,26 @@
// import { createServer } from "http" // import { createServer } from "http"
import { Server, AttachOptions, ServerOptions } from "./server" import { Server, AttachOptions, ServerOptions } from "./server";
import transports from "./transports/index" import transports from "./transports/index";
import * as parser from "../engine.io-parser" import * as parser from "../engine.io-parser";
// export { Server, transports, listen, attach, parser } // export { Server, transports, listen, attach, parser }
export { Server, transports, attach, parser } export { Server, transports, attach, parser };
export { AttachOptions, ServerOptions } from "./server" export { AttachOptions, ServerOptions } from "./server";
// export { uServer } from "./userver"; // export { uServer } from "./userver";
export { Socket } from "./socket" export { Socket } from "./socket";
export { Transport } from "./transport" export { Transport } from "./transport";
export const protocol = parser.protocol export const protocol = parser.protocol;
/**
* 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
*/
// /**
// * 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
// */
//
// function listen(port, options: AttachOptions & ServerOptions, fn) { // function listen(port, options: AttachOptions & ServerOptions, fn) {
// if ("function" === typeof options) { // if ("function" === typeof options) {
// fn = options; // fn = options;
@ -51,7 +51,7 @@ export const protocol = parser.protocol
*/ */
function attach(server, options: AttachOptions & ServerOptions) { function attach(server, options: AttachOptions & ServerOptions) {
const engine = new Server(options) const engine = new Server(options);
engine.attach(server, options) engine.attach(server, options);
return engine return engine;
} }

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,45 +1,45 @@
import { EventEmitter } from "events" import { EventEmitter } from "events";
// import debugModule from "debug" // import debugModule from "debug"
// import { IncomingMessage } from "http" // import { IncomingMessage } from "http"
import { Transport } from "./transport" import { Transport } from "./transport";
import { Server } from "./server" import { Server } from "./server";
// import { setTimeout, clearTimeout } from "timers" // import { setTimeout, clearTimeout } from "timers"
// import { Packet, PacketType, RawData } from "engine.io-parser" // import { Packet, PacketType, RawData } from "engine.io-parser"
import { Packet, PacketType, RawData } from "../engine.io-parser" import { Packet, PacketType, RawData } from "../engine.io-parser";
// const debug = debugModule("engine:socket") // const debug = debugModule("engine:socket")
const debug = require('../debug')("engine:socket") const debug = require("../debug")("engine:socket");
export class Socket extends EventEmitter { export class Socket extends EventEmitter {
public readonly protocol: number public readonly protocol: number;
// public readonly request: IncomingMessage // public readonly request: IncomingMessage
public readonly request: any public readonly request: any;
public readonly remoteAddress: string public readonly remoteAddress: string;
public _readyState: string public _readyState: string;
public transport: Transport public transport: Transport;
private server: Server private server: Server;
private upgrading: boolean private upgrading: boolean;
private upgraded: boolean private upgraded: boolean;
private writeBuffer: Packet[] private writeBuffer: Packet[];
private packetsFn: any[] private packetsFn: any[];
private sentCallbackFn: any[] private sentCallbackFn: any[];
private cleanupFn: any[] private cleanupFn: any[];
private checkIntervalTimer private checkIntervalTimer;
private upgradeTimeoutTimer private upgradeTimeoutTimer;
private pingTimeoutTimer private pingTimeoutTimer;
private pingIntervalTimer private pingIntervalTimer;
private readonly id: string private readonly id: string;
get readyState() { get readyState() {
return this._readyState return this._readyState;
} }
set readyState(state) { set readyState(state) {
debug("readyState updated from %s to %s", this._readyState, state) debug("readyState updated from %s to %s", this._readyState, state);
this._readyState = state this._readyState = state;
} }
/** /**
@ -48,33 +48,33 @@ export class Socket extends EventEmitter {
* @api private * @api private
*/ */
constructor(id, server, transport, req, protocol) { constructor(id, server, transport, req, protocol) {
super() super();
this.id = id this.id = id;
this.server = server this.server = server;
this.upgrading = false this.upgrading = false;
this.upgraded = false this.upgraded = false;
this.readyState = "opening" this.readyState = "opening";
this.writeBuffer = [] this.writeBuffer = [];
this.packetsFn = [] this.packetsFn = [];
this.sentCallbackFn = [] this.sentCallbackFn = [];
this.cleanupFn = [] this.cleanupFn = [];
this.request = req this.request = req;
this.protocol = protocol this.protocol = protocol;
// Cache IP since it might not be in the req later // Cache IP since it might not be in the req later
if (req.websocket && req.websocket._socket) { if (req.websocket && req.websocket._socket) {
this.remoteAddress = req.websocket._socket.remoteAddress this.remoteAddress = req.websocket._socket.remoteAddress;
} else { } else {
this.remoteAddress = req.connection.remoteAddress this.remoteAddress = req.connection.remoteAddress;
} }
this.checkIntervalTimer = null this.checkIntervalTimer = null;
this.upgradeTimeoutTimer = null this.upgradeTimeoutTimer = null;
this.pingTimeoutTimer = null this.pingTimeoutTimer = null;
this.pingIntervalTimer = null this.pingIntervalTimer = null;
this.setTransport(transport) this.setTransport(transport);
this.onOpen() this.onOpen();
} }
/** /**
@ -83,10 +83,10 @@ export class Socket extends EventEmitter {
* @api private * @api private
*/ */
private onOpen() { private onOpen() {
this.readyState = "open" this.readyState = "open";
// sends an `open` packet // sends an `open` packet
this.transport.sid = this.id this.transport.sid = this.id;
this.sendPacket( this.sendPacket(
"open", "open",
JSON.stringify({ JSON.stringify({
@ -94,24 +94,24 @@ export class Socket extends EventEmitter {
upgrades: this.getAvailableUpgrades(), upgrades: this.getAvailableUpgrades(),
pingInterval: this.server.opts.pingInterval, pingInterval: this.server.opts.pingInterval,
pingTimeout: this.server.opts.pingTimeout, pingTimeout: this.server.opts.pingTimeout,
maxPayload: this.server.opts.maxHttpBufferSize maxPayload: this.server.opts.maxHttpBufferSize,
}) })
) );
if (this.server.opts.initialPacket) { if (this.server.opts.initialPacket) {
this.sendPacket("message", this.server.opts.initialPacket) this.sendPacket("message", this.server.opts.initialPacket);
} }
this.emit("open") this.emit("open");
if (this.protocol === 3) { if (this.protocol === 3) {
// in protocol v3, the client sends a ping, and the server answers with a pong // in protocol v3, the client sends a ping, and the server answers with a pong
this.resetPingTimeout( this.resetPingTimeout(
this.server.opts.pingInterval + this.server.opts.pingTimeout this.server.opts.pingInterval + this.server.opts.pingTimeout
) );
} else { } else {
// in protocol v4, the server sends a ping, and the client answers with a pong // in protocol v4, the server sends a ping, and the client answers with a pong
this.schedulePing() this.schedulePing();
} }
} }
@ -123,48 +123,48 @@ export class Socket extends EventEmitter {
*/ */
private onPacket(packet: Packet) { private onPacket(packet: Packet) {
if ("open" !== this.readyState) { if ("open" !== this.readyState) {
return debug("packet received with closed socket") return debug("packet received with closed socket");
} }
// export packet event // export packet event
debug(`received packet ${packet.type}`) debug(`received packet ${packet.type}`);
this.emit("packet", packet) this.emit("packet", packet);
// Reset ping timeout on any packet, incoming data is a good sign of // Reset ping timeout on any packet, incoming data is a good sign of
// other side's liveness // other side's liveness
this.resetPingTimeout( this.resetPingTimeout(
this.server.opts.pingInterval + this.server.opts.pingTimeout this.server.opts.pingInterval + this.server.opts.pingTimeout
) );
switch (packet.type) { switch (packet.type) {
case "ping": case "ping":
if (this.transport.protocol !== 3) { if (this.transport.protocol !== 3) {
this.onError("invalid heartbeat direction") this.onError("invalid heartbeat direction");
return return;
} }
debug("got ping") debug("got ping");
this.sendPacket("pong") this.sendPacket("pong");
this.emit("heartbeat") this.emit("heartbeat");
break break;
case "pong": case "pong":
if (this.transport.protocol === 3) { if (this.transport.protocol === 3) {
this.onError("invalid heartbeat direction") this.onError("invalid heartbeat direction");
return return;
} }
debug("got pong") debug("got pong");
// this.pingIntervalTimer.refresh() // this.pingIntervalTimer.refresh()
this.schedulePing() this.schedulePing();
this.emit("heartbeat") this.emit("heartbeat");
break break;
case "error": case "error":
this.onClose("parse error") this.onClose("parse error");
break break;
case "message": case "message":
this.emit("data", packet.data) this.emit("data", packet.data);
this.emit("message", packet.data) this.emit("message", packet.data);
break break;
} }
} }
@ -175,8 +175,8 @@ export class Socket extends EventEmitter {
* @api private * @api private
*/ */
private onError(err) { private onError(err) {
debug("transport error") debug("transport error");
this.onClose("transport error", err) this.onClose("transport error", err);
} }
/** /**
@ -190,10 +190,10 @@ export class Socket extends EventEmitter {
debug( debug(
"writing ping packet - expecting pong within %sms", "writing ping packet - expecting pong within %sms",
this.server.opts.pingTimeout this.server.opts.pingTimeout
) );
this.sendPacket("ping") this.sendPacket("ping");
this.resetPingTimeout(this.server.opts.pingTimeout) this.resetPingTimeout(this.server.opts.pingTimeout);
}, this.server.opts.pingInterval) }, this.server.opts.pingInterval);
} }
/** /**
@ -202,11 +202,11 @@ export class Socket extends EventEmitter {
* @api private * @api private
*/ */
private resetPingTimeout(timeout) { private resetPingTimeout(timeout) {
clearTimeout(this.pingTimeoutTimer) clearTimeout(this.pingTimeoutTimer);
this.pingTimeoutTimer = setTimeout(() => { this.pingTimeoutTimer = setTimeout(() => {
if (this.readyState === "closed") return if (this.readyState === "closed") return;
this.onClose("ping timeout") this.onClose("ping timeout");
}, timeout) }, timeout);
} }
/** /**
@ -216,25 +216,25 @@ export class Socket extends EventEmitter {
* @api private * @api private
*/ */
private setTransport(transport) { private setTransport(transport) {
const onError = this.onError.bind(this) const onError = this.onError.bind(this);
const onPacket = this.onPacket.bind(this) const onPacket = this.onPacket.bind(this);
const flush = this.flush.bind(this) const flush = this.flush.bind(this);
const onClose = this.onClose.bind(this, "transport close") const onClose = this.onClose.bind(this, "transport close");
this.transport = transport this.transport = transport;
this.transport.once("error", onError) this.transport.once("error", onError);
this.transport.on("packet", onPacket) this.transport.on("packet", onPacket);
this.transport.on("drain", flush) this.transport.on("drain", flush);
this.transport.once("close", onClose) this.transport.once("close", onClose);
// this function will manage packet events (also message callbacks) // this function will manage packet events (also message callbacks)
this.setupSendCallback() this.setupSendCallback();
this.cleanupFn.push(function () { this.cleanupFn.push(function () {
transport.removeListener("error", onError) transport.removeListener("error", onError);
transport.removeListener("packet", onPacket) transport.removeListener("packet", onPacket);
transport.removeListener("drain", flush) transport.removeListener("drain", flush);
transport.removeListener("close", onClose) transport.removeListener("close", onClose);
}) });
} }
/** /**
@ -248,89 +248,89 @@ export class Socket extends EventEmitter {
'might upgrade socket transport from "%s" to "%s"', 'might upgrade socket transport from "%s" to "%s"',
this.transport.name, this.transport.name,
transport.name transport.name
) );
this.upgrading = true this.upgrading = true;
// set transport upgrade timer // set transport upgrade timer
this.upgradeTimeoutTimer = setTimeout(() => { this.upgradeTimeoutTimer = setTimeout(() => {
debug("client did not complete upgrade - closing transport") debug("client did not complete upgrade - closing transport");
cleanup() cleanup();
if ("open" === transport.readyState) { if ("open" === transport.readyState) {
transport.close() transport.close();
} }
}, this.server.opts.upgradeTimeout) }, this.server.opts.upgradeTimeout);
const onPacket = packet => { const onPacket = (packet) => {
if ("ping" === packet.type && "probe" === packet.data) { if ("ping" === packet.type && "probe" === packet.data) {
debug("got probe ping packet, sending pong") debug("got probe ping packet, sending pong");
transport.send([{ type: "pong", data: "probe" }]) transport.send([{ type: "pong", data: "probe" }]);
this.emit("upgrading", transport) this.emit("upgrading", transport);
clearInterval(this.checkIntervalTimer) clearInterval(this.checkIntervalTimer);
this.checkIntervalTimer = setInterval(check, 100) this.checkIntervalTimer = setInterval(check, 100);
} else if ("upgrade" === packet.type && this.readyState !== "closed") { } else if ("upgrade" === packet.type && this.readyState !== "closed") {
debug("got upgrade packet - upgrading") debug("got upgrade packet - upgrading");
cleanup() cleanup();
this.transport.discard() this.transport.discard();
this.upgraded = true this.upgraded = true;
this.clearTransport() this.clearTransport();
this.setTransport(transport) this.setTransport(transport);
this.emit("upgrade", transport) this.emit("upgrade", transport);
this.flush() this.flush();
if (this.readyState === "closing") { if (this.readyState === "closing") {
transport.close(() => { transport.close(() => {
this.onClose("forced close") this.onClose("forced close");
}) });
} }
} else { } else {
cleanup() cleanup();
transport.close() transport.close();
}
} }
};
// we force a polling cycle to ensure a fast upgrade // we force a polling cycle to ensure a fast upgrade
const check = () => { const check = () => {
if ("polling" === this.transport.name && this.transport.writable) { if ("polling" === this.transport.name && this.transport.writable) {
debug("writing a noop packet to polling for fast upgrade") debug("writing a noop packet to polling for fast upgrade");
this.transport.send([{ type: "noop" }]) this.transport.send([{ type: "noop" }]);
}
} }
};
const cleanup = () => { const cleanup = () => {
this.upgrading = false this.upgrading = false;
clearInterval(this.checkIntervalTimer) clearInterval(this.checkIntervalTimer);
this.checkIntervalTimer = null this.checkIntervalTimer = null;
clearTimeout(this.upgradeTimeoutTimer) clearTimeout(this.upgradeTimeoutTimer);
this.upgradeTimeoutTimer = null this.upgradeTimeoutTimer = null;
transport.removeListener("packet", onPacket) transport.removeListener("packet", onPacket);
transport.removeListener("close", onTransportClose) transport.removeListener("close", onTransportClose);
transport.removeListener("error", onError) transport.removeListener("error", onError);
this.removeListener("close", onClose) this.removeListener("close", onClose);
} };
const onError = err => { const onError = (err) => {
debug("client did not complete upgrade - %s", err) debug("client did not complete upgrade - %s", err);
cleanup() cleanup();
transport.close() transport.close();
transport = null transport = null;
} };
const onTransportClose = () => { const onTransportClose = () => {
onError("transport closed") onError("transport closed");
} };
const onClose = () => { const onClose = () => {
onError("socket closed") onError("socket closed");
} };
transport.on("packet", onPacket) transport.on("packet", onPacket);
transport.once("close", onTransportClose) transport.once("close", onTransportClose);
transport.once("error", onError) transport.once("error", onError);
this.once("close", onClose) this.once("close", onClose);
} }
/** /**
@ -339,24 +339,24 @@ export class Socket extends EventEmitter {
* @api private * @api private
*/ */
private clearTransport() { private clearTransport() {
let cleanup let cleanup;
const toCleanUp = this.cleanupFn.length const toCleanUp = this.cleanupFn.length;
for (let i = 0; i < toCleanUp; i++) { for (let i = 0; i < toCleanUp; i++) {
cleanup = this.cleanupFn.shift() cleanup = this.cleanupFn.shift();
cleanup() cleanup();
} }
// silence further transport errors and prevent uncaught exceptions // silence further transport errors and prevent uncaught exceptions
this.transport.on("error", function () { this.transport.on("error", function () {
debug("error triggered by discarded transport") debug("error triggered by discarded transport");
}) });
// ensure transport won't stay open // ensure transport won't stay open
this.transport.close() this.transport.close();
clearTimeout(this.pingTimeoutTimer) clearTimeout(this.pingTimeoutTimer);
} }
/** /**
@ -366,24 +366,24 @@ export class Socket extends EventEmitter {
*/ */
private onClose(reason: string, description?) { private onClose(reason: string, description?) {
if ("closed" !== this.readyState) { if ("closed" !== this.readyState) {
this.readyState = "closed" this.readyState = "closed";
// clear timers // clear timers
clearTimeout(this.pingIntervalTimer) clearTimeout(this.pingIntervalTimer);
clearTimeout(this.pingTimeoutTimer) clearTimeout(this.pingTimeoutTimer);
clearInterval(this.checkIntervalTimer) clearInterval(this.checkIntervalTimer);
this.checkIntervalTimer = null this.checkIntervalTimer = null;
clearTimeout(this.upgradeTimeoutTimer) clearTimeout(this.upgradeTimeoutTimer);
// clean writeBuffer in next tick, so developers can still // clean writeBuffer in next tick, so developers can still
// grab the writeBuffer on 'close' event // grab the writeBuffer on 'close' event
process.nextTick(() => { process.nextTick(() => {
this.writeBuffer = [] this.writeBuffer = [];
}) });
this.packetsFn = [] this.packetsFn = [];
this.sentCallbackFn = [] this.sentCallbackFn = [];
this.clearTransport() this.clearTransport();
this.emit("close", reason, description) this.emit("close", reason, description);
} }
} }
@ -396,28 +396,28 @@ export class Socket extends EventEmitter {
// the message was sent successfully, execute the callback // the message was sent successfully, execute the callback
const onDrain = () => { const onDrain = () => {
if (this.sentCallbackFn.length > 0) { if (this.sentCallbackFn.length > 0) {
const seqFn = this.sentCallbackFn.splice(0, 1)[0] const seqFn = this.sentCallbackFn.splice(0, 1)[0];
if ("function" === typeof seqFn) { if ("function" === typeof seqFn) {
debug("executing send callback") debug("executing send callback");
seqFn(this.transport) seqFn(this.transport);
} else if (Array.isArray(seqFn)) { } else if (Array.isArray(seqFn)) {
debug("executing batch send callback") debug("executing batch send callback");
const l = seqFn.length const l = seqFn.length;
let i = 0 let i = 0;
for (; i < l; i++) { for (; i < l; i++) {
if ("function" === typeof seqFn[i]) { if ("function" === typeof seqFn[i]) {
seqFn[i](this.transport) seqFn[i](this.transport);
}
} }
} }
} }
} }
};
this.transport.on("drain", onDrain) this.transport.on("drain", onDrain);
this.cleanupFn.push(() => { this.cleanupFn.push(() => {
this.transport.removeListener("drain", onDrain) this.transport.removeListener("drain", onDrain);
}) });
} }
/** /**
@ -430,13 +430,13 @@ export class Socket extends EventEmitter {
* @api public * @api public
*/ */
public send(data, options, callback?) { public send(data, options, callback?) {
this.sendPacket("message", data, options, callback) this.sendPacket("message", data, options, callback);
return this return this;
} }
public write(data, options, callback?) { public write(data, options, callback?) {
this.sendPacket("message", data, options, callback) this.sendPacket("message", data, options, callback);
return this return this;
} }
/** /**
@ -451,32 +451,32 @@ export class Socket extends EventEmitter {
*/ */
private sendPacket(type: PacketType, data?: RawData, options?, callback?) { private sendPacket(type: PacketType, data?: RawData, options?, callback?) {
if ("function" === typeof options) { if ("function" === typeof options) {
callback = options callback = options;
options = null options = null;
} }
options = options || {} options = options || {};
options.compress = false !== options.compress options.compress = false !== options.compress;
if ("closing" !== this.readyState && "closed" !== this.readyState) { if ("closing" !== this.readyState && "closed" !== this.readyState) {
debug('sending packet "%s" (%s)', type, data) debug('sending packet "%s" (%s)', type, data);
const packet: Packet = { const packet: Packet = {
type, type,
options options,
} };
if (data) packet.data = data if (data) packet.data = data;
// exports packetCreate event // exports packetCreate event
this.emit("packetCreate", packet) this.emit("packetCreate", packet);
this.writeBuffer.push(packet) this.writeBuffer.push(packet);
// add send callback to object, if defined // add send callback to object, if defined
if (callback) this.packetsFn.push(callback) if (callback) this.packetsFn.push(callback);
this.flush() this.flush();
} }
} }
@ -491,20 +491,20 @@ export class Socket extends EventEmitter {
this.transport.writable && this.transport.writable &&
this.writeBuffer.length this.writeBuffer.length
) { ) {
debug("flushing buffer to transport") debug("flushing buffer to transport");
this.emit("flush", this.writeBuffer) this.emit("flush", this.writeBuffer);
this.server.emit("flush", this, this.writeBuffer) this.server.emit("flush", this, this.writeBuffer);
const wbuf = this.writeBuffer const wbuf = this.writeBuffer;
this.writeBuffer = [] this.writeBuffer = [];
if (!this.transport.supportsFraming) { if (!this.transport.supportsFraming) {
this.sentCallbackFn.push(this.packetsFn) this.sentCallbackFn.push(this.packetsFn);
} else { } else {
this.sentCallbackFn.push.apply(this.sentCallbackFn, this.packetsFn) this.sentCallbackFn.push.apply(this.sentCallbackFn, this.packetsFn);
} }
this.packetsFn = [] this.packetsFn = [];
this.transport.send(wbuf) this.transport.send(wbuf);
this.emit("drain") this.emit("drain");
this.server.emit("drain", this) this.server.emit("drain", this);
} }
} }
@ -514,17 +514,17 @@ export class Socket extends EventEmitter {
* @api private * @api private
*/ */
private getAvailableUpgrades() { private getAvailableUpgrades() {
const availableUpgrades = [] const availableUpgrades = [];
const allUpgrades = this.server.upgrades(this.transport.name) const allUpgrades = this.server.upgrades(this.transport.name);
let i = 0 let i = 0;
const l = allUpgrades.length const l = allUpgrades.length;
for (; i < l; ++i) { for (; i < l; ++i) {
const upg = allUpgrades[i] const upg = allUpgrades[i];
if (this.server.opts.transports.indexOf(upg) !== -1) { if (this.server.opts.transports.indexOf(upg) !== -1) {
availableUpgrades.push(upg) availableUpgrades.push(upg);
} }
} }
return availableUpgrades return availableUpgrades;
} }
/** /**
@ -535,16 +535,16 @@ export class Socket extends EventEmitter {
* @api public * @api public
*/ */
public close(discard?: boolean) { public close(discard?: boolean) {
if ("open" !== this.readyState) return if ("open" !== this.readyState) return;
this.readyState = "closing" this.readyState = "closing";
if (this.writeBuffer.length) { if (this.writeBuffer.length) {
this.once("drain", this.closeTransport.bind(this, discard)) this.once("drain", this.closeTransport.bind(this, discard));
return return;
} }
this.closeTransport(discard) this.closeTransport(discard);
} }
/** /**
@ -554,7 +554,7 @@ export class Socket extends EventEmitter {
* @api private * @api private
*/ */
private closeTransport(discard) { private closeTransport(discard) {
if (discard) this.transport.discard() if (discard) this.transport.discard();
this.transport.close(this.onClose.bind(this, "forced close")) this.transport.close(this.onClose.bind(this, "forced close"));
} }
} }

View File

@ -1,12 +1,12 @@
import { EventEmitter } from "events" import { EventEmitter } from "events";
import * as parser_v4 from "../engine.io-parser" import * as parser_v4 from "../engine.io-parser";
import * as parser_v3 from "./parser-v3" import * as parser_v3 from "./parser-v3";
// import debugModule from "debug" // import debugModule from "debug"
// import { IncomingMessage } from "http" // import { IncomingMessage } from "http"
import { Packet } from "../engine.io-parser" import { Packet } from "../engine.io-parser";
// const debug = debugModule("engine:transport") // const debug = debugModule("engine:transport")
const debug = require('../debug')("engine:transport") const debug = require("../debug")("engine:transport");
/** /**
* Noop function. * Noop function.
@ -14,22 +14,22 @@ const debug = require('../debug')("engine:transport")
* @api private * @api private
*/ */
function noop() { } function noop() {}
export abstract class Transport extends EventEmitter { export abstract class Transport extends EventEmitter {
public sid: string public sid: string;
public writable: boolean public writable: boolean;
public protocol: number public protocol: number;
protected _readyState: string protected _readyState: string;
protected discarded: boolean protected discarded: boolean;
protected parser: any protected parser: any;
// protected req: IncomingMessage & { cleanup: Function } // protected req: IncomingMessage & { cleanup: Function }
protected req: { cleanup: Function } protected req: { cleanup: Function };
protected supportsBinary: boolean protected supportsBinary: boolean;
get readyState() { get readyState() {
return this._readyState return this._readyState;
} }
set readyState(state) { set readyState(state) {
@ -38,8 +38,8 @@ export abstract class Transport extends EventEmitter {
this._readyState, this._readyState,
state, state,
this.name this.name
) );
this._readyState = state this._readyState = state;
} }
/** /**
@ -49,11 +49,11 @@ export abstract class Transport extends EventEmitter {
* @api public * @api public
*/ */
constructor(req) { constructor(req) {
super() super();
this.readyState = "open" this.readyState = "open";
this.discarded = false this.discarded = false;
this.protocol = req._query.EIO === "4" ? 4 : 3 // 3rd revision by default this.protocol = req._query.EIO === "4" ? 4 : 3; // 3rd revision by default
this.parser = this.protocol === 4 ? parser_v4 : parser_v3 this.parser = this.protocol === 4 ? parser_v4 : parser_v3;
} }
/** /**
@ -62,7 +62,7 @@ export abstract class Transport extends EventEmitter {
* @api private * @api private
*/ */
discard() { discard() {
this.discarded = true this.discarded = true;
} }
/** /**
@ -72,8 +72,8 @@ export abstract class Transport extends EventEmitter {
* @api protected * @api protected
*/ */
protected onRequest(req) { protected onRequest(req) {
debug("setting request") debug("setting request");
this.req = req this.req = req;
} }
/** /**
@ -82,10 +82,10 @@ export abstract class Transport extends EventEmitter {
* @api private * @api private
*/ */
close(fn?) { close(fn?) {
if ("closed" === this.readyState || "closing" === this.readyState) return if ("closed" === this.readyState || "closing" === this.readyState) return;
this.readyState = "closing" this.readyState = "closing";
this.doClose(fn || noop) this.doClose(fn || noop);
} }
/** /**
@ -97,14 +97,14 @@ export abstract class Transport extends EventEmitter {
*/ */
protected onError(msg: string, desc?) { protected onError(msg: string, desc?) {
if (this.listeners("error").length) { if (this.listeners("error").length) {
const err = new Error(msg) const err = new Error(msg);
// @ts-ignore // @ts-ignore
err.type = "TransportError" err.type = "TransportError";
// @ts-ignore // @ts-ignore
err.description = desc err.description = desc;
this.emit("error", err) this.emit("error", err);
} else { } else {
debug("ignored transport error %s (%s)", msg, desc) debug("ignored transport error %s (%s)", msg, desc);
} }
} }
@ -115,7 +115,7 @@ export abstract class Transport extends EventEmitter {
* @api protected * @api protected
*/ */
protected onPacket(packet: Packet) { protected onPacket(packet: Packet) {
this.emit("packet", packet) this.emit("packet", packet);
} }
/** /**
@ -125,7 +125,7 @@ export abstract class Transport extends EventEmitter {
* @api protected * @api protected
*/ */
protected onData(data) { protected onData(data) {
this.onPacket(this.parser.decodePacket(data)) this.onPacket(this.parser.decodePacket(data));
} }
/** /**
@ -134,12 +134,12 @@ export abstract class Transport extends EventEmitter {
* @api protected * @api protected
*/ */
protected onClose() { protected onClose() {
this.readyState = "closed" this.readyState = "closed";
this.emit("close") this.emit("close");
} }
abstract get supportsFraming() abstract get supportsFraming();
abstract get name() abstract get name();
abstract send(packets) abstract send(packets);
abstract doClose(fn?) abstract doClose(fn?);
} }

View File

@ -1,11 +1,11 @@
// import { Polling as XHR } from "./polling" // import { Polling as XHR } from "./polling"
// import { JSONP } from "./polling-jsonp" // import { JSONP } from "./polling-jsonp"
import { WebSocket } from "./websocket" import { WebSocket } from "./websocket";
export default { export default {
// polling: polling, // polling: polling,
websocket: WebSocket websocket: WebSocket,
} };
// /** // /**
// * Polling polymorphic constructor. // * Polling polymorphic constructor.

View File

@ -1,11 +1,11 @@
import { Transport } from "../transport" import { Transport } from "../transport";
// import debugModule from "debug"; // import debugModule from "debug";
const debug = require('../../debug')("engine:ws") const debug = require("../../debug")("engine:ws");
export class WebSocket extends Transport { export class WebSocket extends Transport {
protected perMessageDeflate: any protected perMessageDeflate: any;
private socket: any private socket: any;
/** /**
* WebSocket transport * WebSocket transport
@ -14,17 +14,17 @@ export class WebSocket extends Transport {
* @api public * @api public
*/ */
constructor(req) { constructor(req) {
super(req) super(req);
this.socket = req.websocket this.socket = req.websocket;
this.socket.on("message", (data, isBinary) => { this.socket.on("message", (data, isBinary) => {
const message = isBinary ? data : data.toString() const message = isBinary ? data : data.toString();
debug('received "%s"', message) debug('received "%s"', message);
super.onData(message) super.onData(message);
}) });
this.socket.once("close", this.onClose.bind(this)) this.socket.once("close", this.onClose.bind(this));
this.socket.on("error", this.onError.bind(this)) this.socket.on("error", this.onError.bind(this));
this.writable = true this.writable = true;
this.perMessageDeflate = null this.perMessageDeflate = null;
} }
/** /**
@ -33,7 +33,7 @@ export class WebSocket extends Transport {
* @api public * @api public
*/ */
get name() { get name() {
return "websocket" return "websocket";
} }
/** /**
@ -42,7 +42,7 @@ export class WebSocket extends Transport {
* @api public * @api public
*/ */
get handlesUpgrades() { get handlesUpgrades() {
return true return true;
} }
/** /**
@ -51,7 +51,7 @@ export class WebSocket extends Transport {
* @api public * @api public
*/ */
get supportsFraming() { get supportsFraming() {
return true return true;
} }
/** /**
@ -61,40 +61,40 @@ export class WebSocket extends Transport {
* @api private * @api private
*/ */
send(packets) { send(packets) {
const packet = packets.shift() const packet = packets.shift();
if (typeof packet === "undefined") { if (typeof packet === "undefined") {
this.writable = true this.writable = true;
this.emit("drain") this.emit("drain");
return return;
} }
// always creates a new object since ws modifies it // always creates a new object since ws modifies it
const opts: { compress?: boolean } = {} const opts: { compress?: boolean } = {};
if (packet.options) { if (packet.options) {
opts.compress = packet.options.compress opts.compress = packet.options.compress;
} }
const send = data => { const send = data => {
if (this.perMessageDeflate) { if (this.perMessageDeflate) {
const len = const len =
"string" === typeof data ? Buffer.byteLength(data) : data.length "string" === typeof data ? Buffer.byteLength(data) : data.length;
if (len < this.perMessageDeflate.threshold) { if (len < this.perMessageDeflate.threshold) {
opts.compress = false opts.compress = false;
} }
} }
debug('writing "%s"', data) debug('writing "%s"', data);
this.writable = false this.writable = false;
this.socket.send(data, opts, err => { this.socket.send(data, opts, err => {
if (err) return this.onError("write error", err.stack) if (err) return this.onError("write error", err.stack);
this.send(packets) this.send(packets);
}) });
} };
if (packet.options && typeof packet.options.wsPreEncoded === "string") { if (packet.options && typeof packet.options.wsPreEncoded === "string") {
send(packet.options.wsPreEncoded) send(packet.options.wsPreEncoded);
} else { } else {
this.parser.encodePacket(packet, this.supportsBinary, send) this.parser.encodePacket(packet, this.supportsBinary, send);
} }
} }
@ -104,8 +104,8 @@ export class WebSocket extends Transport {
* @api private * @api private
*/ */
doClose(fn) { doClose(fn) {
debug("closing") debug("closing");
this.socket.close() this.socket.close();
fn && fn() fn && fn();
} }
} }

View File

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

View File

@ -1,29 +1,51 @@
import { EventEmitter } from "events" import { EventEmitter } from "events";
import { yeast } from "./contrib/yeast";
// import WebSocket = require("ws");
// const canPreComputeFrame = typeof WebSocket?.Sender?.frame === "function";
/**
* A public ID, sent by the server at the beginning of the Socket.IO session and which can be used for private messaging
*/
export type SocketId = string;
/**
* A private ID, sent by the server at the beginning of the Socket.IO session and used for connection state recovery
* upon reconnection
*/
export type PrivateSessionId = string;
export type SocketId = string
// we could extend the Room type to "string | number", but that would be a breaking change // 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 // related: https://github.com/socketio/socket.io-redis-adapter/issues/418
export type Room = string export type Room = string;
export interface BroadcastFlags { export interface BroadcastFlags {
volatile?: boolean volatile?: boolean;
compress?: boolean compress?: boolean;
local?: boolean local?: boolean;
broadcast?: boolean broadcast?: boolean;
binary?: boolean binary?: boolean;
timeout?: number timeout?: number;
} }
export interface BroadcastOptions { export interface BroadcastOptions {
rooms: Set<Room> rooms: Set<Room>;
except?: Set<Room> except?: Set<Room>;
flags?: BroadcastFlags flags?: BroadcastFlags;
} }
interface SessionToPersist {
sid: SocketId;
pid: PrivateSessionId;
rooms: Room[];
data: unknown;
}
export type Session = SessionToPersist & { missedPackets: unknown[][] };
export class Adapter extends EventEmitter { export class Adapter extends EventEmitter {
public rooms: Map<Room, Set<SocketId>> = new Map(); public rooms: Map<Room, Set<SocketId>> = new Map();
public sids: Map<SocketId, Set<Room>> = new Map(); public sids: Map<SocketId, Set<Room>> = new Map();
private readonly encoder private readonly encoder;
/** /**
* In-memory adapter constructor. * In-memory adapter constructor.
@ -31,19 +53,19 @@ export class Adapter extends EventEmitter {
* @param {Namespace} nsp * @param {Namespace} nsp
*/ */
constructor(readonly nsp: any) { constructor(readonly nsp: any) {
super() super();
this.encoder = nsp.server.encoder this.encoder = nsp.server.encoder;
} }
/** /**
* To be overridden * To be overridden
*/ */
public init(): Promise<void> | void { } public init(): Promise<void> | void {}
/** /**
* To be overridden * To be overridden
*/ */
public close(): Promise<void> | void { } public close(): Promise<void> | void {}
/** /**
* Returns the number of Socket.IO servers in the cluster * Returns the number of Socket.IO servers in the cluster
@ -51,7 +73,7 @@ export class Adapter extends EventEmitter {
* @public * @public
*/ */
public serverCount(): Promise<number> { public serverCount(): Promise<number> {
return Promise.resolve(1) return Promise.resolve(1);
} }
/** /**
@ -63,19 +85,19 @@ export class Adapter extends EventEmitter {
*/ */
public addAll(id: SocketId, rooms: Set<Room>): Promise<void> | void { public addAll(id: SocketId, rooms: Set<Room>): Promise<void> | void {
if (!this.sids.has(id)) { if (!this.sids.has(id)) {
this.sids.set(id, new Set()) this.sids.set(id, new Set());
} }
for (const room of rooms) { for (const room of rooms) {
this.sids.get(id).add(room) this.sids.get(id).add(room);
if (!this.rooms.has(room)) { if (!this.rooms.has(room)) {
this.rooms.set(room, new Set()) this.rooms.set(room, new Set());
this.emit("create-room", room) this.emit("create-room", room);
} }
if (!this.rooms.get(room).has(id)) { if (!this.rooms.get(room).has(id)) {
this.rooms.get(room).add(id) this.rooms.get(room).add(id);
this.emit("join-room", room, id) this.emit("join-room", room, id);
} }
} }
} }
@ -88,21 +110,21 @@ export class Adapter extends EventEmitter {
*/ */
public del(id: SocketId, room: Room): Promise<void> | void { public del(id: SocketId, room: Room): Promise<void> | void {
if (this.sids.has(id)) { if (this.sids.has(id)) {
this.sids.get(id).delete(room) this.sids.get(id).delete(room);
} }
this._del(room, id) this._del(room, id);
} }
private _del(room: Room, id: SocketId) { private _del(room: Room, id: SocketId) {
const _room = this.rooms.get(room) const _room = this.rooms.get(room);
if (_room != null) { if (_room != null) {
const deleted = _room.delete(id) const deleted = _room.delete(id);
if (deleted) { if (deleted) {
this.emit("leave-room", room, id) this.emit("leave-room", room, id);
} }
if (_room.size === 0 && this.rooms.delete(room)) { if (_room.size === 0 && this.rooms.delete(room)) {
this.emit("delete-room", room) this.emit("delete-room", room);
} }
} }
} }
@ -114,14 +136,14 @@ export class Adapter extends EventEmitter {
*/ */
public delAll(id: SocketId): void { public delAll(id: SocketId): void {
if (!this.sids.has(id)) { if (!this.sids.has(id)) {
return return;
} }
for (const room of this.sids.get(id)) { for (const room of this.sids.get(id)) {
this._del(room, id) this._del(room, id);
} }
this.sids.delete(id) this.sids.delete(id);
} }
/** /**
@ -137,23 +159,23 @@ export class Adapter extends EventEmitter {
* @public * @public
*/ */
public broadcast(packet: any, opts: BroadcastOptions): void { public broadcast(packet: any, opts: BroadcastOptions): void {
const flags = opts.flags || {} const flags = opts.flags || {};
const packetOpts = { const packetOpts = {
preEncoded: true, preEncoded: true,
volatile: flags.volatile, volatile: flags.volatile,
compress: flags.compress compress: flags.compress,
} };
packet.nsp = this.nsp.name packet.nsp = this.nsp.name;
const encodedPackets = this.encoder.encode(packet) const encodedPackets = this._encode(packet, packetOpts);
this.apply(opts, socket => { this.apply(opts, (socket) => {
if (typeof socket.notifyOutgoingListeners === "function") { if (typeof socket.notifyOutgoingListeners === "function") {
socket.notifyOutgoingListeners(packet) socket.notifyOutgoingListeners(packet);
} }
socket.client.writeToEngine(encodedPackets, packetOpts) socket.client.writeToEngine(encodedPackets, packetOpts);
}) });
} }
/** /**
@ -177,35 +199,58 @@ export class Adapter extends EventEmitter {
clientCountCallback: (clientCount: number) => void, clientCountCallback: (clientCount: number) => void,
ack: (...args: any[]) => void ack: (...args: any[]) => void
) { ) {
const flags = opts.flags || {} const flags = opts.flags || {};
const packetOpts = { const packetOpts = {
preEncoded: true, preEncoded: true,
volatile: flags.volatile, volatile: flags.volatile,
compress: flags.compress compress: flags.compress,
} };
packet.nsp = this.nsp.name packet.nsp = this.nsp.name;
// we can use the same id for each packet, since the _ids counter is common (no duplicate) // we can use the same id for each packet, since the _ids counter is common (no duplicate)
packet.id = this.nsp._ids++ packet.id = this.nsp._ids++;
const encodedPackets = this.encoder.encode(packet) const encodedPackets = this._encode(packet, packetOpts);
let clientCount = 0 let clientCount = 0;
this.apply(opts, socket => { this.apply(opts, (socket) => {
// track the total number of acknowledgements that are expected // track the total number of acknowledgements that are expected
clientCount++ clientCount++;
// call the ack callback for each client response // call the ack callback for each client response
socket.acks.set(packet.id, ack) socket.acks.set(packet.id, ack);
if (typeof socket.notifyOutgoingListeners === "function") { if (typeof socket.notifyOutgoingListeners === "function") {
socket.notifyOutgoingListeners(packet) socket.notifyOutgoingListeners(packet);
} }
socket.client.writeToEngine(encodedPackets, packetOpts) socket.client.writeToEngine(encodedPackets, packetOpts);
}) });
clientCountCallback(clientCount) clientCountCallback(clientCount);
}
private _encode(packet: unknown, packetOpts: Record<string, unknown>) {
const encodedPackets = this.encoder.encode(packet);
// if (
// canPreComputeFrame &&
// encodedPackets.length === 1 &&
// typeof encodedPackets[0] === "string"
// ) {
// // "4" being the "message" packet type in the Engine.IO protocol
// const data = Buffer.from("4" + encodedPackets[0]);
// // see https://github.com/websockets/ws/issues/617#issuecomment-283002469
// packetOpts.wsPreEncodedFrame = WebSocket.Sender.frame(data, {
// readOnly: false,
// mask: false,
// rsv1: false,
// opcode: 1,
// fin: true,
// });
// }
return encodedPackets;
} }
/** /**
@ -214,13 +259,13 @@ export class Adapter extends EventEmitter {
* @param {Set<Room>} rooms the explicit set of rooms to check. * @param {Set<Room>} rooms the explicit set of rooms to check.
*/ */
public sockets(rooms: Set<Room>): Promise<Set<SocketId>> { public sockets(rooms: Set<Room>): Promise<Set<SocketId>> {
const sids = new Set<SocketId>() const sids = new Set<SocketId>();
this.apply({ rooms }, socket => { this.apply({ rooms }, (socket) => {
sids.add(socket.id) sids.add(socket.id);
}) });
return Promise.resolve(sids) return Promise.resolve(sids);
} }
/** /**
@ -229,7 +274,7 @@ export class Adapter extends EventEmitter {
* @param {SocketId} id the socket id * @param {SocketId} id the socket id
*/ */
public socketRooms(id: SocketId): Set<Room> | undefined { public socketRooms(id: SocketId): Set<Room> | undefined {
return this.sids.get(id) return this.sids.get(id);
} }
/** /**
@ -238,13 +283,13 @@ export class Adapter extends EventEmitter {
* @param opts - the filters to apply * @param opts - the filters to apply
*/ */
public fetchSockets(opts: BroadcastOptions): Promise<any[]> { public fetchSockets(opts: BroadcastOptions): Promise<any[]> {
const sockets = [] const sockets = [];
this.apply(opts, socket => { this.apply(opts, (socket) => {
sockets.push(socket) sockets.push(socket);
}) });
return Promise.resolve(sockets) return Promise.resolve(sockets);
} }
/** /**
@ -254,9 +299,9 @@ export class Adapter extends EventEmitter {
* @param rooms - the rooms to join * @param rooms - the rooms to join
*/ */
public addSockets(opts: BroadcastOptions, rooms: Room[]): void { public addSockets(opts: BroadcastOptions, rooms: Room[]): void {
this.apply(opts, socket => { this.apply(opts, (socket) => {
socket.join(rooms) socket.join(rooms);
}) });
} }
/** /**
@ -266,9 +311,9 @@ export class Adapter extends EventEmitter {
* @param rooms - the rooms to leave * @param rooms - the rooms to leave
*/ */
public delSockets(opts: BroadcastOptions, rooms: Room[]): void { public delSockets(opts: BroadcastOptions, rooms: Room[]): void {
this.apply(opts, socket => { this.apply(opts, (socket) => {
rooms.forEach(room => socket.leave(room)) rooms.forEach((room) => socket.leave(room));
}) });
} }
/** /**
@ -278,48 +323,48 @@ export class Adapter extends EventEmitter {
* @param close - whether to close the underlying connection * @param close - whether to close the underlying connection
*/ */
public disconnectSockets(opts: BroadcastOptions, close: boolean): void { public disconnectSockets(opts: BroadcastOptions, close: boolean): void {
this.apply(opts, socket => { this.apply(opts, (socket) => {
socket.disconnect(close) socket.disconnect(close);
}) });
} }
private apply(opts: BroadcastOptions, callback: (socket) => void): void { private apply(opts: BroadcastOptions, callback: (socket) => void): void {
const rooms = opts.rooms const rooms = opts.rooms;
const except = this.computeExceptSids(opts.except) const except = this.computeExceptSids(opts.except);
if (rooms.size) { if (rooms.size) {
const ids = new Set() const ids = new Set();
for (const room of rooms) { for (const room of rooms) {
if (!this.rooms.has(room)) continue if (!this.rooms.has(room)) continue;
for (const id of this.rooms.get(room)) { for (const id of this.rooms.get(room)) {
if (ids.has(id) || except.has(id)) continue if (ids.has(id) || except.has(id)) continue;
const socket = this.nsp.sockets.get(id) const socket = this.nsp.sockets.get(id);
if (socket) { if (socket) {
callback(socket) callback(socket);
ids.add(id) ids.add(id);
} }
} }
} }
} else { } else {
for (const [id] of this.sids) { for (const [id] of this.sids) {
if (except.has(id)) continue if (except.has(id)) continue;
const socket = this.nsp.sockets.get(id) const socket = this.nsp.sockets.get(id);
if (socket) callback(socket) if (socket) callback(socket);
} }
} }
} }
private computeExceptSids(exceptRooms?: Set<Room>) { private computeExceptSids(exceptRooms?: Set<Room>) {
const exceptSids = new Set() const exceptSids = new Set();
if (exceptRooms && exceptRooms.size > 0) { if (exceptRooms && exceptRooms.size > 0) {
for (const room of exceptRooms) { for (const room of exceptRooms) {
if (this.rooms.has(room)) { if (this.rooms.has(room)) {
this.rooms.get(room).forEach(sid => exceptSids.add(sid)) this.rooms.get(room).forEach((sid) => exceptSids.add(sid));
} }
} }
} }
return exceptSids return exceptSids;
} }
/** /**
@ -329,6 +374,134 @@ export class Adapter extends EventEmitter {
public serverSideEmit(packet: any[]): void { public serverSideEmit(packet: any[]): void {
console.warn( console.warn(
"this adapter does not support the serverSideEmit() functionality" "this adapter does not support the serverSideEmit() functionality"
) );
}
/**
* Save the client session in order to restore it upon reconnection.
*/
public persistSession(session: SessionToPersist) {}
/**
* Restore the session and find the packets that were missed by the client.
* @param pid
* @param offset
*/
public restoreSession(
pid: PrivateSessionId,
offset: string
): Session {
return null;
} }
} }
interface PersistedPacket {
id: string;
emittedAt: number;
data: unknown[];
opts: BroadcastOptions;
}
type SessionWithTimestamp = SessionToPersist & { disconnectedAt: number };
export class SessionAwareAdapter extends Adapter {
private readonly maxDisconnectionDuration: number;
private sessions: Map<PrivateSessionId, SessionWithTimestamp> = new Map();
private packets: PersistedPacket[] = [];
constructor(readonly nsp: any) {
super(nsp);
this.maxDisconnectionDuration =
nsp.server.opts.connectionStateRecovery.maxDisconnectionDuration;
const timer = setInterval(() => {
const threshold = Date.now() - this.maxDisconnectionDuration;
this.sessions.forEach((session, sessionId) => {
const hasExpired = session.disconnectedAt < threshold;
if (hasExpired) {
this.sessions.delete(sessionId);
}
});
for (let i = this.packets.length - 1; i >= 0; i--) {
const hasExpired = this.packets[i].emittedAt < threshold;
if (hasExpired) {
this.packets.splice(0, i + 1);
break;
}
}
}, 60 * 1000);
// prevents the timer from keeping the process alive
timer.unref();
}
override persistSession(session: SessionToPersist) {
(session as SessionWithTimestamp).disconnectedAt = Date.now();
this.sessions.set(session.pid, session as SessionWithTimestamp);
}
override restoreSession(
pid: PrivateSessionId,
offset: string
): Session {
const session = this.sessions.get(pid);
if (!session) {
// the session may have expired
return null;
}
const hasExpired =
session.disconnectedAt + this.maxDisconnectionDuration < Date.now();
if (hasExpired) {
// the session has expired
this.sessions.delete(pid);
return null;
}
const index = this.packets.findIndex((packet) => packet.id === offset);
if (index === -1) {
// the offset may be too old
return null;
}
const missedPackets = [];
for (let i = index + 1; i < this.packets.length; i++) {
const packet = this.packets[i];
if (shouldIncludePacket(session.rooms, packet.opts)) {
missedPackets.push(packet.data);
}
}
return {
...session,
missedPackets,
};
}
override broadcast(packet: any, opts: BroadcastOptions) {
const isEventPacket = packet.type === 2;
// packets with acknowledgement are not stored because the acknowledgement function cannot be serialized and
// restored on another server upon reconnection
const withoutAcknowledgement = packet.id === undefined;
const notVolatile = opts.flags?.volatile === undefined;
if (isEventPacket && withoutAcknowledgement && notVolatile) {
const id = yeast();
// the offset is stored at the end of the data array, so the client knows the ID of the last packet it has
// processed (and the format is backward-compatible)
packet.data.push(id);
this.packets.push({
id,
opts,
data: packet.data,
emittedAt: Date.now(),
});
}
super.broadcast(packet, opts);
}
}
function shouldIncludePacket(
sessionRooms: Room[],
opts: BroadcastOptions
): boolean {
const included =
opts.rooms.size === 0 || sessionRooms.some((room) => opts.rooms.has(room));
const notExcluded = sessionRooms.every((room) => !opts.except.has(room));
return included && notExcluded;
}

View File

@ -1,6 +1,7 @@
import { url } from "./url" import { url } from "./url"
import { Manager, ManagerOptions } from "./manager" import { Manager, ManagerOptions } from "./manager"
import { Socket, SocketOptions } from "./socket" import { Socket, SocketOptions } from "./socket"
// import debugModule from "debug"; // debug()
const debug = require("../debug")("socket.io-client") const debug = require("../debug")("socket.io-client")

View File

@ -10,7 +10,7 @@ import * as parser from "../socket.io-parser"
// import { Decoder, Encoder, Packet } from "socket.io-parser" // import { Decoder, Encoder, Packet } from "socket.io-parser"
import { Decoder, Encoder, Packet } from "../socket.io-parser" import { Decoder, Encoder, Packet } from "../socket.io-parser"
import { on } from "./on.js" import { on } from "./on.js"
import { Backoff } from "./contrib/backo2" import { Backoff } from "./contrib/backo2.js"
import { import {
DefaultEventsMap, DefaultEventsMap,
EventsMap, EventsMap,
@ -470,6 +470,10 @@ export class Manager<
this.nsps[nsp] = socket this.nsps[nsp] = socket
} }
if (this._autoConnect) {
socket.connect()
}
return socket return socket
} }

View File

@ -14,11 +14,69 @@ import {
// const debug = debugModule("socket.io-client:socket") // debug() // const debug = debugModule("socket.io-client:socket") // debug()
const debug = require("../debug")("socket.io-client") const debug = require("../debug")("socket.io-client")
type PrependTimeoutError<T extends any[]> = {
[K in keyof T]: T[K] extends (...args: infer Params) => infer Result
? (err: Error, ...args: Params) => Result
: T[K]
}
/**
* Utility type to decorate the acknowledgement callbacks with a timeout error.
*
* This is needed because the timeout() flag breaks the symmetry between the sender and the receiver:
*
* @example
* interface Events {
* "my-event": (val: string) => void;
* }
*
* socket.on("my-event", (cb) => {
* cb("123"); // one single argument here
* });
*
* socket.timeout(1000).emit("my-event", (err, val) => {
* // two arguments there (the "err" argument is not properly typed)
* });
*
*/
export type DecorateAcknowledgements<E> = {
[K in keyof E]: E[K] extends (...args: infer Params) => infer Result
? (...args: PrependTimeoutError<Params>) => Result
: E[K]
}
export type Last<T extends any[]> = T extends [...infer H, infer L] ? L : any
export type AllButLast<T extends any[]> = T extends [...infer H, infer L]
? H
: any[]
export type FirstArg<T> = T extends (arg: infer Param) => infer Result
? Param
: any
export interface SocketOptions { export interface SocketOptions {
/** /**
* the authentication payload sent when connecting to the Namespace * the authentication payload sent when connecting to the Namespace
*/ */
auth: { [key: string]: any } | ((cb: (data: object) => void) => void) auth?: { [key: string]: any } | ((cb: (data: object) => void) => void)
/**
* The maximum number of retries. Above the limit, the packet will be discarded.
*
* Using `Infinity` means the delivery guarantee is "at-least-once" (instead of "at-most-once" by default), but a
* smaller value like 10 should be sufficient in practice.
*/
retries?: number
/**
* The default timeout in milliseconds used when waiting for an acknowledgement.
*/
ackTimeout?: number
}
type QueuedPacket = {
id: number
args: unknown[]
flags: Flags
pending: boolean
tryCount: number
} }
/** /**
@ -39,13 +97,14 @@ interface Flags {
compress?: boolean compress?: boolean
volatile?: boolean volatile?: boolean
timeout?: number timeout?: number
fromQueue?: boolean
} }
export type DisconnectDescription = export type DisconnectDescription =
| Error | Error
| { | {
description: string description: string
context?: CloseEvent | XMLHttpRequest context?: unknown // context should be typed as CloseEvent | XMLHttpRequest, but these types are not available on non-browser platforms
} }
interface SocketReservedEvents { interface SocketReservedEvents {
@ -101,6 +160,20 @@ export class Socket<
*/ */
public id: string public id: string
/**
* The session ID used for connection state recovery, which must not be shared (unlike {@link id}).
*
* @private
*/
private _pid: string
/**
* The offset of the last received packet, which will be sent upon reconnection to allow for the recovery of the connection state.
*
* @private
*/
private _lastOffset: string
/** /**
* Whether the socket is currently connected to the server. * Whether the socket is currently connected to the server.
* *
@ -116,7 +189,11 @@ export class Socket<
* }); * });
*/ */
public connected: boolean = false; public connected: boolean = false;
/**
* Whether the connection state was recovered after a temporary disconnection. In that case, any missed packets will
* be transmitted by the server.
*/
public recovered: boolean = false;
/** /**
* Credentials that are sent when accessing a namespace. * Credentials that are sent when accessing a namespace.
* *
@ -143,8 +220,16 @@ export class Socket<
* Buffer for packets that will be sent once the socket is connected * Buffer for packets that will be sent once the socket is connected
*/ */
public sendBuffer: Array<Packet> = []; public sendBuffer: Array<Packet> = [];
/**
* The queue of packets to be sent with retry in case of failure.
*
* Packets are sent one by one, each waiting for the server acknowledgement, in order to guarantee the delivery order.
* @private
*/
private _queue: Array<QueuedPacket> = [];
private readonly nsp: string private readonly nsp: string
private readonly _opts: SocketOptions
private ids: number = 0; private ids: number = 0;
private acks: object = {}; private acks: object = {};
@ -163,6 +248,7 @@ export class Socket<
if (opts && opts.auth) { if (opts && opts.auth) {
this.auth = opts.auth this.auth = opts.auth
} }
this._opts = Object.assign({}, opts)
if (this.io._autoConnect) this.open() if (this.io._autoConnect) this.open()
} }
@ -296,6 +382,12 @@ export class Socket<
} }
args.unshift(ev) args.unshift(ev)
if (this._opts.retries && !this.flags.fromQueue && !this.flags.volatile) {
this._addToQueue(args)
return this
}
const packet: any = { const packet: any = {
type: PacketType.EVENT, type: PacketType.EVENT,
data: args, data: args,
@ -339,7 +431,7 @@ export class Socket<
* @private * @private
*/ */
private _registerAckCallback(id: number, ack: Function) { private _registerAckCallback(id: number, ack: Function) {
const timeout = this.flags.timeout const timeout = this.flags.timeout ?? this._opts.ackTimeout
if (timeout === undefined) { if (timeout === undefined) {
this.acks[id] = ack this.acks[id] = ack
return return
@ -365,6 +457,122 @@ export class Socket<
} }
} }
/**
* Emits an event and waits for an acknowledgement
*
* @example
* // without timeout
* const response = await socket.emitWithAck("hello", "world");
*
* // with a specific timeout
* try {
* const response = await socket.timeout(1000).emitWithAck("hello", "world");
* } catch (err) {
* // the server did not acknowledge the event in the given delay
* }
*
* @return a Promise that will be fulfilled when the server acknowledges the event
*/
public emitWithAck<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: AllButLast<EventParams<EmitEvents, Ev>>
): Promise<FirstArg<Last<EventParams<EmitEvents, Ev>>>> {
// the timeout flag is optional
const withErr =
this.flags.timeout !== undefined || this._opts.ackTimeout !== undefined
return new Promise((resolve, reject) => {
args.push((arg1, arg2) => {
if (withErr) {
return arg1 ? reject(arg1) : resolve(arg2)
} else {
return resolve(arg1)
}
})
this.emit(ev, ...(args as any[] as EventParams<EmitEvents, Ev>))
})
}
/**
* Add the packet to the queue.
* @param args
* @private
*/
private _addToQueue(args: unknown[]) {
let ack
if (typeof args[args.length - 1] === "function") {
ack = args.pop()
}
const packet = {
id: this.ids++,
tryCount: 0,
pending: false,
args,
flags: Object.assign({ fromQueue: true }, this.flags),
}
args.push((err, ...responseArgs) => {
if (packet !== this._queue[0]) {
// the packet has already been acknowledged
return
}
const hasError = err !== null
if (hasError) {
if (packet.tryCount > this._opts.retries) {
debug(
"packet [%d] is discarded after %d tries",
packet.id,
packet.tryCount
)
this._queue.shift()
if (ack) {
ack(err)
}
}
} else {
debug("packet [%d] was successfully sent", packet.id)
this._queue.shift()
if (ack) {
ack(null, ...responseArgs)
}
}
packet.pending = false
return this._drainQueue()
})
this._queue.push(packet)
this._drainQueue()
}
/**
* Send the first packet of the queue, and wait for an acknowledgement from the server.
* @private
*/
private _drainQueue() {
debug("draining queue")
if (this._queue.length === 0) {
return
}
const packet = this._queue[0]
if (packet.pending) {
debug(
"packet [%d] has already been sent and is waiting for an ack",
packet.id
)
return
}
packet.pending = true
packet.tryCount++
debug("sending packet [%d] (try n°%d)", packet.id, packet.tryCount)
const currentId = this.ids
this.ids = packet.id // the same id is reused for consecutive retries, in order to allow deduplication on the server side
this.flags = packet.flags
// @ts-ignore
this.emit.apply(this, packet.args)
this.ids = currentId // restore offset
}
/** /**
* Sends a packet. * Sends a packet.
* *
@ -385,13 +593,28 @@ export class Socket<
debug("transport is open - connecting") debug("transport is open - connecting")
if (typeof this.auth == "function") { if (typeof this.auth == "function") {
this.auth((data) => { this.auth((data) => {
this.packet({ type: PacketType.CONNECT, data }) this._sendConnectPacket(data as Record<string, unknown>)
}) })
} else { } else {
this.packet({ type: PacketType.CONNECT, data: this.auth }) this._sendConnectPacket(this.auth)
} }
} }
/**
* Sends a CONNECT packet to initiate the Socket.IO session.
*
* @param data
* @private
*/
private _sendConnectPacket(data: Record<string, unknown>) {
this.packet({
type: PacketType.CONNECT,
data: this._pid
? Object.assign({ pid: this._pid, offset: this._lastOffset }, data)
: data,
})
}
/** /**
* Called upon engine or manager `error`. * Called upon engine or manager `error`.
* *
@ -435,8 +658,7 @@ export class Socket<
switch (packet.type) { switch (packet.type) {
case PacketType.CONNECT: case PacketType.CONNECT:
if (packet.data && packet.data.sid) { if (packet.data && packet.data.sid) {
const id = packet.data.sid this.onconnect(packet.data.sid, packet.data.pid)
this.onconnect(id)
} else { } else {
this.emitReserved( this.emitReserved(
"connect_error", "connect_error",
@ -503,6 +725,9 @@ export class Socket<
} }
// @ts-ignore // @ts-ignore
super.emit.apply(this, args) super.emit.apply(this, args)
if (this._pid && args.length && typeof args[args.length - 1] === "string") {
this._lastOffset = args[args.length - 1]
}
} }
/** /**
@ -549,9 +774,11 @@ export class Socket<
* *
* @private * @private
*/ */
private onconnect(id: string): void { private onconnect(id: string, pid: string) {
debug("socket connected with id %s", id) debug("socket connected with id %s", id)
this.id = id this.id = id
this.recovered = pid && this._pid === pid
this._pid = pid // defined only if connection state recovery is enabled
this.connected = true this.connected = true
this.emitBuffered() this.emitBuffered()
this.emitReserved("connect") this.emitReserved("connect")
@ -682,7 +909,9 @@ export class Socket<
* *
* @returns self * @returns self
*/ */
public timeout(timeout: number): this { public timeout(
timeout: number
): Socket<ListenEvents, DecorateAcknowledgements<EmitEvents>> {
this.flags.timeout = timeout this.flags.timeout = timeout
return this return this
} }

View File

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

View File

@ -1,10 +1,9 @@
import { Emitter } from "@socket.io/component-emitter" import { Emitter } from "@socket.io/component-emitter";
import { deconstructPacket, reconstructPacket } from "./binary.js" import { deconstructPacket, reconstructPacket } from "./binary.js";
import { isBinary, hasBinary } from "./is-binary.js" import { isBinary, hasBinary } from "./is-binary.js";
// import debugModule from "debug" // debug() // import debugModule from "debug" // debug()
// const debug = debugModule("socket.io-parser") // debug() const debug = require("../debug")("socket.io-parser");
const debug = require("../debug")("socket.io-client")
/** /**
* Protocol version. * Protocol version.
@ -12,7 +11,7 @@ const debug = require("../debug")("socket.io-client")
* @public * @public
*/ */
export const protocol: number = 5 export const protocol: number = 5;
export enum PacketType { export enum PacketType {
CONNECT, CONNECT,
@ -25,11 +24,11 @@ export enum PacketType {
} }
export interface Packet { export interface Packet {
type: PacketType type: PacketType;
nsp: string nsp: string;
data?: any data?: any;
id?: number id?: number;
attachments?: number attachments?: number;
} }
/** /**
@ -42,7 +41,7 @@ export class Encoder {
* *
* @param {function} replacer - custom replacer to pass down to JSON.parse * @param {function} replacer - custom replacer to pass down to JSON.parse
*/ */
constructor(private replacer?: (this: any, key: string, value: any) => any) { } constructor(private replacer?: (this: any, key: string, value: any) => any) {}
/** /**
* Encode a packet as a single string if non-binary, or as a * Encode a packet as a single string if non-binary, or as a
* buffer sequence, depending on packet type. * buffer sequence, depending on packet type.
@ -50,18 +49,22 @@ export class Encoder {
* @param {Object} obj - packet object * @param {Object} obj - packet object
*/ */
public encode(obj: Packet) { public encode(obj: Packet) {
debug("encoding packet %j", obj) debug("encoding packet %j", obj);
if (obj.type === PacketType.EVENT || obj.type === PacketType.ACK) { if (obj.type === PacketType.EVENT || obj.type === PacketType.ACK) {
if (hasBinary(obj)) { if (hasBinary(obj)) {
obj.type = return this.encodeAsBinary({
type:
obj.type === PacketType.EVENT obj.type === PacketType.EVENT
? PacketType.BINARY_EVENT ? PacketType.BINARY_EVENT
: PacketType.BINARY_ACK : PacketType.BINARY_ACK,
return this.encodeAsBinary(obj) nsp: obj.nsp,
data: obj.data,
id: obj.id,
});
} }
} }
return [this.encodeAsString(obj)] return [this.encodeAsString(obj)];
} }
/** /**
@ -70,34 +73,34 @@ export class Encoder {
private encodeAsString(obj: Packet) { private encodeAsString(obj: Packet) {
// first is type // first is type
let str = "" + obj.type let str = "" + obj.type;
// attachments if we have them // attachments if we have them
if ( if (
obj.type === PacketType.BINARY_EVENT || obj.type === PacketType.BINARY_EVENT ||
obj.type === PacketType.BINARY_ACK obj.type === PacketType.BINARY_ACK
) { ) {
str += obj.attachments + "-" str += obj.attachments + "-";
} }
// if we have a namespace other than `/` // if we have a namespace other than `/`
// we append it followed by a comma `,` // we append it followed by a comma `,`
if (obj.nsp && "/" !== obj.nsp) { if (obj.nsp && "/" !== obj.nsp) {
str += obj.nsp + "," str += obj.nsp + ",";
} }
// immediately followed by the id // immediately followed by the id
if (null != obj.id) { if (null != obj.id) {
str += obj.id str += obj.id;
} }
// json data // json data
if (null != obj.data) { if (null != obj.data) {
str += JSON.stringify(obj.data, this.replacer) str += JSON.stringify(obj.data, this.replacer);
} }
debug("encoded %j as %s", obj, str) debug("encoded %j as %s", obj, str);
return str return str;
} }
/** /**
@ -107,17 +110,17 @@ export class Encoder {
*/ */
private encodeAsBinary(obj: Packet) { private encodeAsBinary(obj: Packet) {
const deconstruction = deconstructPacket(obj) const deconstruction = deconstructPacket(obj);
const pack = this.encodeAsString(deconstruction.packet) const pack = this.encodeAsString(deconstruction.packet);
const buffers = deconstruction.buffers const buffers = deconstruction.buffers;
buffers.unshift(pack) // add packet info to beginning of data list buffers.unshift(pack); // add packet info to beginning of data list
return buffers // write all the buffers return buffers; // write all the buffers
} }
} }
interface DecoderReservedEvents { interface DecoderReservedEvents {
decoded: (packet: Packet) => void decoded: (packet: Packet) => void;
} }
/** /**
@ -126,7 +129,7 @@ interface DecoderReservedEvents {
* @return {Object} decoder * @return {Object} decoder
*/ */
export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> { export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
private reconstructor: BinaryReconstructor private reconstructor: BinaryReconstructor;
/** /**
* Decoder constructor * Decoder constructor
@ -134,7 +137,7 @@ export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
* @param {function} reviver - custom reviver to pass down to JSON.stringify * @param {function} reviver - custom reviver to pass down to JSON.stringify
*/ */
constructor(private reviver?: (this: any, key: string, value: any) => any) { constructor(private reviver?: (this: any, key: string, value: any) => any) {
super() super();
} }
/** /**
@ -144,41 +147,40 @@ export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
*/ */
public add(obj: any) { public add(obj: any) {
let packet let packet;
if (typeof obj === "string") { if (typeof obj === "string") {
if (this.reconstructor) { if (this.reconstructor) {
throw new Error("got plaintext data when reconstructing a packet") throw new Error("got plaintext data when reconstructing a packet");
} }
packet = this.decodeString(obj) packet = this.decodeString(obj);
if ( const isBinaryEvent = packet.type === PacketType.BINARY_EVENT;
packet.type === PacketType.BINARY_EVENT || if (isBinaryEvent || packet.type === PacketType.BINARY_ACK) {
packet.type === PacketType.BINARY_ACK packet.type = isBinaryEvent ? PacketType.EVENT : PacketType.ACK;
) {
// binary packet's json // binary packet's json
this.reconstructor = new BinaryReconstructor(packet) this.reconstructor = new BinaryReconstructor(packet);
// no attachments, labeled binary but no binary data to follow // no attachments, labeled binary but no binary data to follow
if (packet.attachments === 0) { if (packet.attachments === 0) {
super.emitReserved("decoded", packet) super.emitReserved("decoded", packet);
} }
} else { } else {
// non-binary full packet // non-binary full packet
super.emitReserved("decoded", packet) super.emitReserved("decoded", packet);
} }
} else if (isBinary(obj) || obj.base64) { } else if (isBinary(obj) || obj.base64) {
// raw binary data // raw binary data
if (!this.reconstructor) { if (!this.reconstructor) {
throw new Error("got binary data when not reconstructing a packet") throw new Error("got binary data when not reconstructing a packet");
} else { } else {
packet = this.reconstructor.takeBinaryData(obj) packet = this.reconstructor.takeBinaryData(obj);
if (packet) { if (packet) {
// received final buffer // received final buffer
this.reconstructor = null this.reconstructor = null;
super.emitReserved("decoded", packet) super.emitReserved("decoded", packet);
} }
} }
} else { } else {
throw new Error("Unknown type: " + obj) throw new Error("Unknown type: " + obj);
} }
} }
@ -189,14 +191,14 @@ export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
* @return {Object} packet * @return {Object} packet
*/ */
private decodeString(str): Packet { private decodeString(str): Packet {
let i = 0 let i = 0;
// look up type // look up type
const p: any = { const p: any = {
type: Number(str.charAt(0)), type: Number(str.charAt(0)),
} };
if (PacketType[p.type] === undefined) { if (PacketType[p.type] === undefined) {
throw new Error("unknown packet type " + p.type) throw new Error("unknown packet type " + p.type);
} }
// look up attachments if type binary // look up attachments if type binary
@ -204,79 +206,79 @@ export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
p.type === PacketType.BINARY_EVENT || p.type === PacketType.BINARY_EVENT ||
p.type === PacketType.BINARY_ACK p.type === PacketType.BINARY_ACK
) { ) {
const start = i + 1 const start = i + 1;
while (str.charAt(++i) !== "-" && i != str.length) { } while (str.charAt(++i) !== "-" && i != str.length) {}
const buf = str.substring(start, i) const buf = str.substring(start, i);
if (buf != Number(buf) || str.charAt(i) !== "-") { if (buf != Number(buf) || str.charAt(i) !== "-") {
throw new Error("Illegal attachments") throw new Error("Illegal attachments");
} }
p.attachments = Number(buf) p.attachments = Number(buf);
} }
// look up namespace (if any) // look up namespace (if any)
if ("/" === str.charAt(i + 1)) { if ("/" === str.charAt(i + 1)) {
const start = i + 1 const start = i + 1;
while (++i) { while (++i) {
const c = str.charAt(i) const c = str.charAt(i);
if ("," === c) break if ("," === c) break;
if (i === str.length) break if (i === str.length) break;
} }
p.nsp = str.substring(start, i) p.nsp = str.substring(start, i);
} else { } else {
p.nsp = "/" p.nsp = "/";
} }
// look up id // look up id
const next = str.charAt(i + 1) const next = str.charAt(i + 1);
if ("" !== next && Number(next) == next) { if ("" !== next && Number(next) == next) {
const start = i + 1 const start = i + 1;
while (++i) { while (++i) {
const c = str.charAt(i) const c = str.charAt(i);
if (null == c || Number(c) != c) { if (null == c || Number(c) != c) {
--i --i;
break break;
} }
if (i === str.length) break if (i === str.length) break;
} }
p.id = Number(str.substring(start, i + 1)) p.id = Number(str.substring(start, i + 1));
} }
// look up json data // look up json data
if (str.charAt(++i)) { if (str.charAt(++i)) {
const payload = this.tryParse(str.substr(i)) const payload = this.tryParse(str.substr(i));
if (Decoder.isPayloadValid(p.type, payload)) { if (Decoder.isPayloadValid(p.type, payload)) {
p.data = payload p.data = payload;
} else { } else {
throw new Error("invalid payload") throw new Error("invalid payload");
} }
} }
debug("decoded %s as %j", str, p) debug("decoded %s as %j", str, p);
return p return p;
} }
private tryParse(str) { private tryParse(str) {
try { try {
return JSON.parse(str, this.reviver) return JSON.parse(str, this.reviver);
} catch (e) { } catch (e) {
return false return false;
} }
} }
private static isPayloadValid(type: PacketType, payload: any): boolean { private static isPayloadValid(type: PacketType, payload: any): boolean {
switch (type) { switch (type) {
case PacketType.CONNECT: case PacketType.CONNECT:
return typeof payload === "object" return typeof payload === "object";
case PacketType.DISCONNECT: case PacketType.DISCONNECT:
return payload === undefined return payload === undefined;
case PacketType.CONNECT_ERROR: case PacketType.CONNECT_ERROR:
return typeof payload === "string" || typeof payload === "object" return typeof payload === "string" || typeof payload === "object";
case PacketType.EVENT: case PacketType.EVENT:
case PacketType.BINARY_EVENT: case PacketType.BINARY_EVENT:
return Array.isArray(payload) && payload.length > 0 return Array.isArray(payload) && payload.length > 0;
case PacketType.ACK: case PacketType.ACK:
case PacketType.BINARY_ACK: case PacketType.BINARY_ACK:
return Array.isArray(payload) return Array.isArray(payload);
} }
} }
@ -285,7 +287,8 @@ export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
*/ */
public destroy() { public destroy() {
if (this.reconstructor) { if (this.reconstructor) {
this.reconstructor.finishedReconstruction() this.reconstructor.finishedReconstruction();
this.reconstructor = null;
} }
} }
} }
@ -300,11 +303,11 @@ export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
*/ */
class BinaryReconstructor { class BinaryReconstructor {
private reconPack private reconPack;
private buffers: Array<Buffer | ArrayBuffer> = []; private buffers: Array<Buffer | ArrayBuffer> = [];
constructor(readonly packet: Packet) { constructor(readonly packet: Packet) {
this.reconPack = packet this.reconPack = packet;
} }
/** /**
@ -316,21 +319,21 @@ class BinaryReconstructor {
* a reconstructed packet object if all buffers have been received. * a reconstructed packet object if all buffers have been received.
*/ */
public takeBinaryData(binData) { public takeBinaryData(binData) {
this.buffers.push(binData) this.buffers.push(binData);
if (this.buffers.length === this.reconPack.attachments) { if (this.buffers.length === this.reconPack.attachments) {
// done with buffer list // done with buffer list
const packet = reconstructPacket(this.reconPack, this.buffers) const packet = reconstructPacket(this.reconPack, this.buffers);
this.finishedReconstruction() this.finishedReconstruction();
return packet return packet;
} }
return null return null;
} }
/** /**
* Cleans up binary packet reconstruction variables. * Cleans up binary packet reconstruction variables.
*/ */
public finishedReconstruction() { public finishedReconstruction() {
this.reconPack = null this.reconPack = null;
this.buffers = [] this.buffers = [];
} }
} }

View File

@ -1,20 +1,20 @@
const withNativeArrayBuffer: boolean = typeof ArrayBuffer === "function" const withNativeArrayBuffer: boolean = typeof ArrayBuffer === "function";
const isView = (obj: any) => { const isView = (obj: any) => {
return typeof ArrayBuffer.isView === "function" return typeof ArrayBuffer.isView === "function"
? ArrayBuffer.isView(obj) ? ArrayBuffer.isView(obj)
: obj.buffer instanceof ArrayBuffer : obj.buffer instanceof ArrayBuffer;
} };
const toString = Object.prototype.toString const toString = Object.prototype.toString;
const withNativeBlob = const withNativeBlob =
typeof Blob === "function" || typeof Blob === "function" ||
(typeof Blob !== "undefined" && (typeof Blob !== "undefined" &&
toString.call(Blob) === "[object BlobConstructor]") toString.call(Blob) === "[object BlobConstructor]");
const withNativeFile = const withNativeFile =
typeof File === "function" || typeof File === "function" ||
(typeof File !== "undefined" && (typeof File !== "undefined" &&
toString.call(File) === "[object FileConstructor]") toString.call(File) === "[object FileConstructor]");
/** /**
* Returns true if obj is a Buffer, an ArrayBuffer, a Blob or a File. * Returns true if obj is a Buffer, an ArrayBuffer, a Blob or a File.
@ -27,25 +27,25 @@ export function isBinary(obj: any) {
(withNativeArrayBuffer && (obj instanceof ArrayBuffer || isView(obj))) || (withNativeArrayBuffer && (obj instanceof ArrayBuffer || isView(obj))) ||
(withNativeBlob && obj instanceof Blob) || (withNativeBlob && obj instanceof Blob) ||
(withNativeFile && obj instanceof File) (withNativeFile && obj instanceof File)
) );
} }
export function hasBinary(obj: any, toJSON?: boolean) { export function hasBinary(obj: any, toJSON?: boolean) {
if (!obj || typeof obj !== "object") { if (!obj || typeof obj !== "object") {
return false return false;
} }
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
for (let i = 0, l = obj.length; i < l; i++) { for (let i = 0, l = obj.length; i < l; i++) {
if (hasBinary(obj[i])) { if (hasBinary(obj[i])) {
return true return true;
} }
} }
return false return false;
} }
if (isBinary(obj)) { if (isBinary(obj)) {
return true return true;
} }
if ( if (
@ -53,14 +53,14 @@ export function hasBinary(obj: any, toJSON?: boolean) {
typeof obj.toJSON === "function" && typeof obj.toJSON === "function" &&
arguments.length === 1 arguments.length === 1
) { ) {
return hasBinary(obj.toJSON(), true) return hasBinary(obj.toJSON(), true);
} }
for (const key in obj) { for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key) && hasBinary(obj[key])) { if (Object.prototype.hasOwnProperty.call(obj, key) && hasBinary(obj[key])) {
return true return true;
} }
} }
return false return false;
} }

View File

@ -1,16 +1,18 @@
// import type { BroadcastFlags, Room, SocketId } from "socket.io-adapter" import type { BroadcastFlags, Room, SocketId } from "../socket.io-adapter";
import type { BroadcastFlags, Room, SocketId } from "../socket.io-adapter" import { Handshake, RESERVED_EVENTS, Socket } from "./socket";
import { Handshake, RESERVED_EVENTS, Socket } from "./socket" import { PacketType } from "../socket.io-parser";
// import { PacketType } from "socket.io-parser" import type { Adapter } from "../socket.io-adapter";
import { PacketType } from "../socket.io-parser"
// import type { Adapter } from "socket.io-adapter"
import type { Adapter } from "../socket.io-adapter"
import type { import type {
EventParams, EventParams,
EventNames, EventNames,
EventsMap, EventsMap,
TypedEventBroadcaster, TypedEventBroadcaster,
} from "./typed-events" DecorateAcknowledgements,
DecorateAcknowledgementsWithTimeoutAndMultipleResponses,
AllButLast,
Last,
SecondArg,
} from "./typed-events";
export class BroadcastOperator<EmitEvents extends EventsMap, SocketData> export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
implements TypedEventBroadcaster<EmitEvents> implements TypedEventBroadcaster<EmitEvents>
@ -19,8 +21,10 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
private readonly adapter: Adapter, private readonly adapter: Adapter,
private readonly rooms: Set<Room> = new Set<Room>(), private readonly rooms: Set<Room> = new Set<Room>(),
private readonly exceptRooms: Set<Room> = new Set<Room>(), private readonly exceptRooms: Set<Room> = new Set<Room>(),
private readonly flags: BroadcastFlags = {} private readonly flags: BroadcastFlags & {
) { } expectSingleResponse?: boolean;
} = {}
) {}
/** /**
* Targets a room when emitting. * Targets a room when emitting.
@ -39,18 +43,18 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
* @return a new {@link BroadcastOperator} instance for chaining * @return a new {@link BroadcastOperator} instance for chaining
*/ */
public to(room: Room | Room[]) { public to(room: Room | Room[]) {
const rooms = new Set(this.rooms) const rooms = new Set(this.rooms);
if (Array.isArray(room)) { if (Array.isArray(room)) {
room.forEach((r) => rooms.add(r)) room.forEach((r) => rooms.add(r));
} else { } else {
rooms.add(room) rooms.add(room);
} }
return new BroadcastOperator<EmitEvents, SocketData>( return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter, this.adapter,
rooms, rooms,
this.exceptRooms, this.exceptRooms,
this.flags this.flags
) );
} }
/** /**
@ -64,7 +68,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
* @return a new {@link BroadcastOperator} instance for chaining * @return a new {@link BroadcastOperator} instance for chaining
*/ */
public in(room: Room | Room[]) { public in(room: Room | Room[]) {
return this.to(room) return this.to(room);
} }
/** /**
@ -84,18 +88,18 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
* @return a new {@link BroadcastOperator} instance for chaining * @return a new {@link BroadcastOperator} instance for chaining
*/ */
public except(room: Room | Room[]) { public except(room: Room | Room[]) {
const exceptRooms = new Set(this.exceptRooms) const exceptRooms = new Set(this.exceptRooms);
if (Array.isArray(room)) { if (Array.isArray(room)) {
room.forEach((r) => exceptRooms.add(r)) room.forEach((r) => exceptRooms.add(r));
} else { } else {
exceptRooms.add(room) exceptRooms.add(room);
} }
return new BroadcastOperator<EmitEvents, SocketData>( return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter, this.adapter,
this.rooms, this.rooms,
exceptRooms, exceptRooms,
this.flags this.flags
) );
} }
/** /**
@ -108,13 +112,13 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
* @return a new BroadcastOperator instance * @return a new BroadcastOperator instance
*/ */
public compress(compress: boolean) { public compress(compress: boolean) {
const flags = Object.assign({}, this.flags, { compress }) const flags = Object.assign({}, this.flags, { compress });
return new BroadcastOperator<EmitEvents, SocketData>( return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter, this.adapter,
this.rooms, this.rooms,
this.exceptRooms, this.exceptRooms,
flags flags
) );
} }
/** /**
@ -128,13 +132,13 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
* @return a new BroadcastOperator instance * @return a new BroadcastOperator instance
*/ */
public get volatile() { public get volatile() {
const flags = Object.assign({}, this.flags, { volatile: true }) const flags = Object.assign({}, this.flags, { volatile: true });
return new BroadcastOperator<EmitEvents, SocketData>( return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter, this.adapter,
this.rooms, this.rooms,
this.exceptRooms, this.exceptRooms,
flags flags
) );
} }
/** /**
@ -147,13 +151,13 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
* @return a new {@link BroadcastOperator} instance for chaining * @return a new {@link BroadcastOperator} instance for chaining
*/ */
public get local() { public get local() {
const flags = Object.assign({}, this.flags, { local: true }) const flags = Object.assign({}, this.flags, { local: true });
return new BroadcastOperator<EmitEvents, SocketData>( return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter, this.adapter,
this.rooms, this.rooms,
this.exceptRooms, this.exceptRooms,
flags flags
) );
} }
/** /**
@ -171,13 +175,11 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
* @param timeout * @param timeout
*/ */
public timeout(timeout: number) { public timeout(timeout: number) {
const flags = Object.assign({}, this.flags, { timeout }) const flags = Object.assign({}, this.flags, { timeout });
return new BroadcastOperator<EmitEvents, SocketData>( return new BroadcastOperator<
this.adapter, DecorateAcknowledgementsWithTimeoutAndMultipleResponses<EmitEvents>,
this.rooms, SocketData
this.exceptRooms, >(this.adapter, this.rooms, this.exceptRooms, flags);
flags
)
} }
/** /**
@ -206,39 +208,42 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
...args: EventParams<EmitEvents, Ev> ...args: EventParams<EmitEvents, Ev>
): boolean { ): boolean {
if (RESERVED_EVENTS.has(ev)) { if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${String(ev)}" is a reserved event name`) throw new Error(`"${String(ev)}" is a reserved event name`);
} }
// set up packet object // set up packet object
const data = [ev, ...args] const data = [ev, ...args];
const packet = { const packet = {
type: PacketType.EVENT, type: PacketType.EVENT,
data: data, data: data,
} };
const withAck = typeof data[data.length - 1] === "function" const withAck = typeof data[data.length - 1] === "function";
if (!withAck) { if (!withAck) {
this.adapter.broadcast(packet, { this.adapter.broadcast(packet, {
rooms: this.rooms, rooms: this.rooms,
except: this.exceptRooms, except: this.exceptRooms,
flags: this.flags, flags: this.flags,
}) });
return true return true;
} }
const ack = data.pop() as (...args: any[]) => void const ack = data.pop() as (...args: any[]) => void;
let timedOut = false let timedOut = false;
let responses: any[] = [] let responses: any[] = [];
const timer = setTimeout(() => { const timer = setTimeout(() => {
timedOut = true timedOut = true;
ack.apply(this, [new Error("operation has timed out"), responses]) ack.apply(this, [
}, this.flags.timeout) new Error("operation has timed out"),
this.flags.expectSingleResponse ? null : responses,
]);
}, this.flags.timeout);
let expectedServerCount = -1 let expectedServerCount = -1;
let actualServerCount = 0 let actualServerCount = 0;
let expectedClientCount = 0 let expectedClientCount = 0;
const checkCompleteness = () => { const checkCompleteness = () => {
if ( if (
@ -246,10 +251,13 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
expectedServerCount === actualServerCount && expectedServerCount === actualServerCount &&
responses.length === expectedClientCount responses.length === expectedClientCount
) { ) {
clearTimeout(timer) clearTimeout(timer);
ack.apply(this, [null, responses]) ack.apply(this, [
} null,
this.flags.expectSingleResponse ? null : responses,
]);
} }
};
this.adapter.broadcastWithAck( this.adapter.broadcastWithAck(
packet, packet,
@ -260,23 +268,53 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
}, },
(clientCount) => { (clientCount) => {
// each Socket.IO server in the cluster sends the number of clients that were notified // each Socket.IO server in the cluster sends the number of clients that were notified
expectedClientCount += clientCount expectedClientCount += clientCount;
actualServerCount++ actualServerCount++;
checkCompleteness() checkCompleteness();
}, },
(clientResponse) => { (clientResponse) => {
// each client sends an acknowledgement // each client sends an acknowledgement
responses.push(clientResponse) responses.push(clientResponse);
checkCompleteness() checkCompleteness();
} }
) );
this.adapter.serverCount().then((serverCount) => { this.adapter.serverCount().then((serverCount) => {
expectedServerCount = serverCount expectedServerCount = serverCount;
checkCompleteness() checkCompleteness();
}) });
return true return true;
}
/**
* Emits an event and waits for an acknowledgement from all clients.
*
* @example
* try {
* const responses = await io.timeout(1000).emitWithAck("some-event");
* console.log(responses); // one response per client
* } catch (e) {
* // some clients did not acknowledge the event in the given delay
* }
*
* @return a Promise that will be fulfilled when all clients have acknowledged the event
*/
public emitWithAck<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: AllButLast<EventParams<EmitEvents, Ev>>
): Promise<SecondArg<Last<EventParams<EmitEvents, Ev>>>> {
return new Promise((resolve, reject) => {
args.push((err, responses) => {
if (err) {
err.responses = responses;
return reject(err);
} else {
return resolve(responses);
}
});
this.emit(ev, ...(args as any[] as EventParams<EmitEvents, Ev>));
});
} }
/** /**
@ -289,9 +327,9 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
if (!this.adapter) { if (!this.adapter) {
throw new Error( throw new Error(
"No adapter for this namespace, are you trying to get the list of clients of a dynamic namespace?" "No adapter for this namespace, are you trying to get the list of clients of a dynamic namespace?"
) );
} }
return this.adapter.sockets(this.rooms) return this.adapter.sockets(this.rooms);
} }
/** /**
@ -329,15 +367,15 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
return sockets.map((socket) => { return sockets.map((socket) => {
if (socket instanceof Socket) { if (socket instanceof Socket) {
// FIXME the TypeScript compiler complains about missing private properties // FIXME the TypeScript compiler complains about missing private properties
return socket as unknown as RemoteSocket<EmitEvents, SocketData> return socket as unknown as RemoteSocket<EmitEvents, SocketData>;
} else { } else {
return new RemoteSocket( return new RemoteSocket(
this.adapter, this.adapter,
socket as SocketDetails<SocketData> socket as SocketDetails<SocketData>
) );
} }
}) });
}) });
} }
/** /**
@ -363,7 +401,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
flags: this.flags, flags: this.flags,
}, },
Array.isArray(room) ? room : [room] Array.isArray(room) ? room : [room]
) );
} }
/** /**
@ -388,7 +426,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
flags: this.flags, flags: this.flags,
}, },
Array.isArray(room) ? room : [room] Array.isArray(room) ? room : [room]
) );
} }
/** /**
@ -413,7 +451,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
flags: this.flags, flags: this.flags,
}, },
close close
) );
} }
} }
@ -421,10 +459,10 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
* Format of the data when the Socket instance exists on another Socket.IO server * Format of the data when the Socket instance exists on another Socket.IO server
*/ */
interface SocketDetails<SocketData> { interface SocketDetails<SocketData> {
id: SocketId id: SocketId;
handshake: Handshake handshake: Handshake;
rooms: Room[] rooms: Room[];
data: SocketData data: SocketData;
} }
/** /**
@ -433,29 +471,63 @@ interface SocketDetails<SocketData> {
export class RemoteSocket<EmitEvents extends EventsMap, SocketData> export class RemoteSocket<EmitEvents extends EventsMap, SocketData>
implements TypedEventBroadcaster<EmitEvents> implements TypedEventBroadcaster<EmitEvents>
{ {
public readonly id: SocketId public readonly id: SocketId;
public readonly handshake: Handshake public readonly handshake: Handshake;
public readonly rooms: Set<Room> public readonly rooms: Set<Room>;
public readonly data: SocketData public readonly data: SocketData;
private readonly operator: BroadcastOperator<EmitEvents, SocketData> private readonly operator: BroadcastOperator<EmitEvents, SocketData>;
constructor(adapter: Adapter, details: SocketDetails<SocketData>) { constructor(adapter: Adapter, details: SocketDetails<SocketData>) {
this.id = details.id this.id = details.id;
this.handshake = details.handshake this.handshake = details.handshake;
this.rooms = new Set(details.rooms) this.rooms = new Set(details.rooms);
this.data = details.data this.data = details.data;
this.operator = new BroadcastOperator<EmitEvents, SocketData>( this.operator = new BroadcastOperator<EmitEvents, SocketData>(
adapter, adapter,
new Set([this.id]) new Set([this.id]),
) new Set(),
{
expectSingleResponse: true, // so that remoteSocket.emit() with acknowledgement behaves like socket.emit()
}
);
}
/**
* Adds a timeout in milliseconds for the next operation.
*
* @example
* const sockets = await io.fetchSockets();
*
* for (const socket of sockets) {
* if (someCondition) {
* socket.timeout(1000).emit("some-event", (err) => {
* if (err) {
* // the client did not acknowledge the event in the given delay
* }
* });
* }
* }
*
* // note: if possible, using a room instead of looping over all sockets is preferable
* io.timeout(1000).to(someConditionRoom).emit("some-event", (err, responses) => {
* // ...
* });
*
* @param timeout
*/
public timeout(timeout: number) {
return this.operator.timeout(timeout) as BroadcastOperator<
DecorateAcknowledgements<EmitEvents>,
SocketData
>;
} }
public emit<Ev extends EventNames<EmitEvents>>( public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev, ev: Ev,
...args: EventParams<EmitEvents, Ev> ...args: EventParams<EmitEvents, Ev>
): boolean { ): boolean {
return this.operator.emit(ev, ...args) return this.operator.emit(ev, ...args);
} }
/** /**
@ -464,7 +536,7 @@ export class RemoteSocket<EmitEvents extends EventsMap, SocketData>
* @param {String|Array} room - room or array of rooms * @param {String|Array} room - room or array of rooms
*/ */
public join(room: Room | Room[]): void { public join(room: Room | Room[]): void {
return this.operator.socketsJoin(room) return this.operator.socketsJoin(room);
} }
/** /**
@ -473,7 +545,7 @@ export class RemoteSocket<EmitEvents extends EventsMap, SocketData>
* @param {String} room * @param {String} room
*/ */
public leave(room: Room): void { public leave(room: Room): void {
return this.operator.socketsLeave(room) return this.operator.socketsLeave(room);
} }
/** /**
@ -483,7 +555,7 @@ export class RemoteSocket<EmitEvents extends EventsMap, SocketData>
* @return {Socket} self * @return {Socket} self
*/ */
public disconnect(close = false): this { public disconnect(close = false): this {
this.operator.disconnectSockets(close) this.operator.disconnectSockets(close);
return this return this;
} }
} }

View File

@ -1,24 +1,21 @@
// import { Decoder, Encoder, Packet, PacketType } from "socket.io-parser" import { Decoder, Encoder, Packet, PacketType } from "../socket.io-parser";
import { Decoder, Encoder, Packet, PacketType } from "../socket.io-parser"
// import debugModule = require("debug") // import debugModule = require("debug")
import url = require("url") import url = require("url");
// import type { IncomingMessage } from "http" // import type { IncomingMessage } from "http"
import type { Server } from "./index" import type { Server } from "./index";
import type { Namespace } from "./namespace" import type { Namespace } from "./namespace";
import type { EventsMap } from "./typed-events" import type { EventsMap } from "./typed-events";
import type { Socket } from "./socket" import type { Socket } from "./socket";
// import type { SocketId } from "socket.io-adapter" import type { SocketId } from "../socket.io-adapter";
import type { SocketId } from "../socket.io-adapter" import type { Socket as RawSocket } 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");
const debug = require('../debug')("socket.io:client")
interface WriteOptions { interface WriteOptions {
compress?: boolean compress?: boolean;
volatile?: boolean volatile?: boolean;
preEncoded?: boolean preEncoded?: boolean;
wsPreEncoded?: string wsPreEncoded?: string;
} }
type CloseReason = type CloseReason =
@ -26,7 +23,7 @@ type CloseReason =
| "transport close" | "transport close"
| "forced close" | "forced close"
| "ping timeout" | "ping timeout"
| "parse error" | "parse error";
export class Client< export class Client<
ListenEvents extends EventsMap, ListenEvents extends EventsMap,
@ -34,17 +31,17 @@ export class Client<
ServerSideEvents extends EventsMap, ServerSideEvents extends EventsMap,
SocketData = any SocketData = any
> { > {
public readonly conn: RawSocket public readonly conn: RawSocket;
private readonly id: string private readonly id: string;
private readonly server: Server< private readonly server: Server<
ListenEvents, ListenEvents,
EmitEvents, EmitEvents,
ServerSideEvents, ServerSideEvents,
SocketData SocketData
> >;
private readonly encoder: Encoder private readonly encoder: Encoder;
private readonly decoder: Decoder private readonly decoder: Decoder;
private sockets: Map< private sockets: Map<
SocketId, SocketId,
Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData> Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
@ -53,7 +50,7 @@ export class Client<
string, string,
Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData> Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map(); > = new Map();
private connectTimeout?: NodeJS.Timeout private connectTimeout?: NodeJS.Timeout;
/** /**
* Client constructor. * Client constructor.
@ -66,12 +63,12 @@ export class Client<
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>, server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
conn: any conn: any
) { ) {
this.server = server this.server = server;
this.conn = conn this.conn = conn;
this.encoder = server.encoder this.encoder = server.encoder;
this.decoder = new server._parser.Decoder() this.decoder = new server._parser.Decoder();
this.id = conn.id this.id = conn.id;
this.setup() this.setup();
} }
/** /**
@ -81,7 +78,7 @@ export class Client<
*/ */
// public get request(): IncomingMessage { // public get request(): IncomingMessage {
public get request(): any { public get request(): any {
return this.conn.request return this.conn.request;
} }
/** /**
@ -90,25 +87,25 @@ export class Client<
* @private * @private
*/ */
private setup() { private setup() {
this.onclose = this.onclose.bind(this) this.onclose = this.onclose.bind(this);
this.ondata = this.ondata.bind(this) this.ondata = this.ondata.bind(this);
this.onerror = this.onerror.bind(this) this.onerror = this.onerror.bind(this);
this.ondecoded = this.ondecoded.bind(this) this.ondecoded = this.ondecoded.bind(this);
// @ts-ignore // @ts-ignore
this.decoder.on("decoded", this.ondecoded) this.decoder.on("decoded", this.ondecoded);
this.conn.on("data", this.ondata) this.conn.on("data", this.ondata);
this.conn.on("error", this.onerror) this.conn.on("error", this.onerror);
this.conn.on("close", this.onclose) this.conn.on("close", this.onclose);
this.connectTimeout = setTimeout(() => { this.connectTimeout = setTimeout(() => {
if (this.nsps.size === 0) { if (this.nsps.size === 0) {
debug("no namespace joined yet, close the client") debug("no namespace joined yet, close the client");
this.close() this.close();
} else { } else {
debug("the client has already joined a namespace, nothing to do") debug("the client has already joined a namespace, nothing to do");
} }
}, this.server._connectTimeout) }, this.server._connectTimeout);
} }
/** /**
@ -118,10 +115,10 @@ export class Client<
* @param {Object} auth - the auth parameters * @param {Object} auth - the auth parameters
* @private * @private
*/ */
private connect(name: string, auth: object = {}): void { private connect(name: string, auth: Record<string, unknown> = {}): void {
if (this.server._nsps.has(name)) { if (this.server._nsps.has(name)) {
debug("connecting to namespace %s", name) debug("connecting to namespace %s", name);
return this.doConnect(name, auth) return this.doConnect(name, auth);
} }
this.server._checkNamespace( this.server._checkNamespace(
@ -133,19 +130,19 @@ export class Client<
| false | false
) => { ) => {
if (dynamicNspName) { if (dynamicNspName) {
this.doConnect(name, auth) this.doConnect(name, auth);
} else { } else {
debug("creation of namespace %s was denied", name) debug("creation of namespace %s was denied", name);
this._packet({ this._packet({
type: PacketType.CONNECT_ERROR, type: PacketType.CONNECT_ERROR,
nsp: name, nsp: name,
data: { data: {
message: "Invalid namespace", message: "Invalid namespace",
}, },
}) });
} }
} }
) );
} }
/** /**
@ -156,19 +153,19 @@ export class Client<
* *
* @private * @private
*/ */
private doConnect(name: string, auth: object): void { private doConnect(name: string, auth: Record<string, unknown>): void {
const nsp = this.server.of(name) const nsp = this.server.of(name);
// @java-patch multi thread need direct callback socket // @java-patch multi thread need direct callback socket
const socket = nsp._add(this, auth, (socket) => { const socket = nsp._add(this, auth, (socket) => {
this.sockets.set(socket.id, socket) this.sockets.set(socket.id, socket);
this.nsps.set(nsp.name, socket) this.nsps.set(nsp.name, socket);
if (this.connectTimeout) { if (this.connectTimeout) {
clearTimeout(this.connectTimeout) clearTimeout(this.connectTimeout);
this.connectTimeout = undefined this.connectTimeout = undefined;
} }
}) });
} }
/** /**
@ -178,10 +175,10 @@ export class Client<
*/ */
_disconnect(): void { _disconnect(): void {
for (const socket of this.sockets.values()) { for (const socket of this.sockets.values()) {
socket.disconnect() socket.disconnect();
} }
this.sockets.clear() this.sockets.clear();
this.close() this.close();
} }
/** /**
@ -193,11 +190,11 @@ export class Client<
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData> socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
): void { ): void {
if (this.sockets.has(socket.id)) { if (this.sockets.has(socket.id)) {
const nsp = this.sockets.get(socket.id)!.nsp.name const nsp = this.sockets.get(socket.id)!.nsp.name;
this.sockets.delete(socket.id) this.sockets.delete(socket.id);
this.nsps.delete(nsp) this.nsps.delete(nsp);
} else { } else {
debug("ignoring remove for %s", socket.id) debug("ignoring remove for %s", socket.id);
} }
} }
@ -208,9 +205,9 @@ export class Client<
*/ */
private close(): void { private close(): void {
if ("open" === this.conn.readyState) { if ("open" === this.conn.readyState) {
debug("forcing transport close") debug("forcing transport close");
this.conn.close() this.conn.close();
this.onclose("forced server close") this.onclose("forced server close");
} }
} }
@ -223,30 +220,30 @@ export class Client<
*/ */
_packet(packet: Packet | any[], opts: WriteOptions = {}): void { _packet(packet: Packet | any[], opts: WriteOptions = {}): void {
if (this.conn.readyState !== "open") { if (this.conn.readyState !== "open") {
debug("ignoring packet write %j", packet) debug("ignoring packet write %j", packet);
return return;
} }
const encodedPackets = opts.preEncoded const encodedPackets = opts.preEncoded
? (packet as any[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine() ? (packet as any[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine()
: this.encoder.encode(packet as Packet) : this.encoder.encode(packet as Packet);
this.writeToEngine(encodedPackets, opts) this.writeToEngine(encodedPackets, opts);
} }
private writeToEngine( private writeToEngine(
encodedPackets: Array<String | Buffer>, encodedPackets: Array<string | Buffer>,
opts: WriteOptions opts: WriteOptions
): void { ): void {
if (opts.volatile && !this.conn.transport.writable) { if (opts.volatile && !this.conn.transport.writable) {
debug( debug(
"volatile packet is discarded since the transport is not currently writable" "volatile packet is discarded since the transport is not currently writable"
) );
return return;
} }
const packets = Array.isArray(encodedPackets) const packets = Array.isArray(encodedPackets)
? encodedPackets ? encodedPackets
: [encodedPackets] : [encodedPackets];
for (const encodedPacket of packets) { for (const encodedPacket of packets) {
this.conn.write(encodedPacket, opts) this.conn.write(encodedPacket, opts);
} }
} }
@ -258,10 +255,10 @@ export class Client<
private ondata(data): void { private ondata(data): void {
// try/catch is needed for protocol violations (GH-1880) // try/catch is needed for protocol violations (GH-1880)
try { try {
this.decoder.add(data) this.decoder.add(data);
} catch (e) { } catch (e) {
debug("invalid packet format") debug("invalid packet format");
this.onerror(e) this.onerror(e);
} }
} }
@ -271,31 +268,31 @@ export class Client<
* @private * @private
*/ */
private ondecoded(packet: Packet): void { private ondecoded(packet: Packet): void {
let namespace: string let namespace: string;
let authPayload let authPayload: Record<string, unknown>;
if (this.conn.protocol === 3) { if (this.conn.protocol === 3) {
const parsed = url.parse(packet.nsp, true) const parsed = url.parse(packet.nsp, true);
namespace = parsed.pathname! namespace = parsed.pathname!;
authPayload = parsed.query authPayload = parsed.query;
} else { } else {
namespace = packet.nsp namespace = packet.nsp;
authPayload = packet.data authPayload = packet.data;
} }
const socket = this.nsps.get(namespace) const socket = this.nsps.get(namespace);
if (!socket && packet.type === PacketType.CONNECT) { if (!socket && packet.type === PacketType.CONNECT) {
this.connect(namespace, authPayload) this.connect(namespace, authPayload);
} else if ( } else if (
socket && socket &&
packet.type !== PacketType.CONNECT && packet.type !== PacketType.CONNECT &&
packet.type !== PacketType.CONNECT_ERROR packet.type !== PacketType.CONNECT_ERROR
) { ) {
process.nextTick(function () { process.nextTick(function () {
socket._onpacket(packet) socket._onpacket(packet);
}) });
} else { } else {
debug("invalid state (packet type: %s)", packet.type) debug("invalid state (packet type: %s)", packet.type);
this.close() this.close();
} }
} }
@ -307,30 +304,34 @@ export class Client<
*/ */
private onerror(err): void { private onerror(err): void {
for (const socket of this.sockets.values()) { for (const socket of this.sockets.values()) {
socket._onerror(err) socket._onerror(err);
} }
this.conn.close() this.conn.close();
} }
/** /**
* Called upon transport close. * Called upon transport close.
* *
* @param reason * @param reason
* @param description
* @private * @private
*/ */
private onclose(reason: CloseReason | "forced server close"): void { private onclose(
debug("client close with reason %s", reason) reason: CloseReason | "forced server close",
description?: any
): void {
debug("client close with reason %s", reason);
// ignore a potential subsequent `close` event // ignore a potential subsequent `close` event
this.destroy() this.destroy();
// `nsps` and `sockets` are cleaned up seamlessly // `nsps` and `sockets` are cleaned up seamlessly
for (const socket of this.sockets.values()) { for (const socket of this.sockets.values()) {
socket._onclose(reason) socket._onclose(reason, description);
} }
this.sockets.clear() this.sockets.clear();
this.decoder.destroy() // clean up decoder this.decoder.destroy(); // clean up decoder
} }
/** /**
@ -338,15 +339,15 @@ export class Client<
* @private * @private
*/ */
private destroy(): void { private destroy(): void {
this.conn.removeListener("data", this.ondata) this.conn.removeListener("data", this.ondata);
this.conn.removeListener("error", this.onerror) this.conn.removeListener("error", this.onerror);
this.conn.removeListener("close", this.onclose) this.conn.removeListener("close", this.onclose);
// @ts-ignore // @ts-ignore
this.decoder.removeListener("decoded", this.ondecoded) this.decoder.removeListener("decoded", this.ondecoded);
if (this.connectTimeout) { if (this.connectTimeout) {
clearTimeout(this.connectTimeout) clearTimeout(this.connectTimeout);
this.connectTimeout = undefined this.connectTimeout = undefined;
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,26 @@
import { Socket } from "./socket" import { Socket } from "./socket";
import type { Server } from "./index" import type { Server } from "./index";
import { import {
EventParams, EventParams,
EventNames, EventNames,
EventsMap, EventsMap,
StrictEventEmitter, StrictEventEmitter,
DefaultEventsMap, DefaultEventsMap,
} from "./typed-events" DecorateAcknowledgementsWithTimeoutAndMultipleResponses,
import type { Client } from "./client" AllButLast,
Last,
FirstArg,
SecondArg,
} from "./typed-events";
import type { Client } from "./client";
// import debugModule from "debug" // import debugModule from "debug"
// import type { Adapter, Room, SocketId } from "socket.io-adapter" import type { Adapter, Room, SocketId } from "../socket.io-adapter";
import type { Adapter, Room, SocketId } from "../socket.io-adapter" import { BroadcastOperator } from "./broadcast-operator";
import { BroadcastOperator, RemoteSocket } from "./broadcast-operator"
// const debug = debugModule("socket.io:namespace") const debug = require("../debug")("socket.io:namespace");
const debug = require('../debug')("socket.io:namespace")
export interface ExtendedError extends Error { export interface ExtendedError extends Error {
data?: any data?: any;
} }
export interface NamespaceReservedEventsMap< export interface NamespaceReservedEventsMap<
@ -28,10 +31,10 @@ export interface NamespaceReservedEventsMap<
> { > {
connect: ( connect: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData> socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void ) => void;
connection: ( connection: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData> socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void ) => void;
} }
export interface ServerReservedEventsMap< export interface ServerReservedEventsMap<
@ -44,15 +47,15 @@ export interface ServerReservedEventsMap<
EmitEvents, EmitEvents,
ServerSideEvents, ServerSideEvents,
SocketData SocketData
> { > {
new_namespace: ( new_namespace: (
namespace: Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> namespace: Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void ) => void;
} }
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set< export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
keyof ServerReservedEventsMap<never, never, never, never> keyof ServerReservedEventsMap<never, never, never, never>
>(<const>["connect", "connection", "new_namespace"]) >(<const>["connect", "connection", "new_namespace"]);
/** /**
* A Namespace is a communication channel that allows you to split the logic of your application over a single shared * A Namespace is a communication channel that allows you to split the logic of your application over a single shared
@ -122,13 +125,13 @@ export class Namespace<
SocketData SocketData
> >
> { > {
public readonly name: string public readonly name: string;
public readonly sockets: Map< public readonly sockets: Map<
SocketId, SocketId,
Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData> Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map(); > = new Map();
public adapter: Adapter public adapter: Adapter;
/** @private */ /** @private */
readonly server: Server< readonly server: Server<
@ -136,7 +139,7 @@ export class Namespace<
EmitEvents, EmitEvents,
ServerSideEvents, ServerSideEvents,
SocketData SocketData
> >;
/** @private */ /** @private */
_fns: Array< _fns: Array<
@ -159,10 +162,10 @@ export class Namespace<
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>, server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
name: string name: string
) { ) {
super() super();
this.server = server this.server = server;
this.name = name this.name = name;
this._initAdapter() this._initAdapter();
} }
/** /**
@ -174,7 +177,7 @@ export class Namespace<
*/ */
_initAdapter(): void { _initAdapter(): void {
// @ts-ignore // @ts-ignore
this.adapter = new (this.server.adapter()!)(this) this.adapter = new (this.server.adapter()!)(this);
} }
/** /**
@ -196,8 +199,8 @@ export class Namespace<
next: (err?: ExtendedError) => void next: (err?: ExtendedError) => void
) => void ) => void
): this { ): this {
this._fns.push(fn) this._fns.push(fn);
return this return this;
} }
/** /**
@ -211,23 +214,23 @@ export class Namespace<
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>, socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
fn: (err: ExtendedError | null) => void fn: (err: ExtendedError | null) => void
) { ) {
const fns = this._fns.slice(0) const fns = this._fns.slice(0);
if (!fns.length) return fn(null) if (!fns.length) return fn(null);
function run(i: number) { function run(i: number) {
fns[i](socket, function (err) { fns[i](socket, function (err) {
// upon error, short-circuit // upon error, short-circuit
if (err) return fn(err) if (err) return fn(err);
// if no middleware left, summon callback // if no middleware left, summon callback
if (!fns[i + 1]) return fn(null) if (!fns[i + 1]) return fn(null);
// go on to next // go on to next
run(i + 1) run(i + 1);
}) });
} }
run(0) run(0);
} }
/** /**
@ -249,7 +252,7 @@ export class Namespace<
* @return a new {@link BroadcastOperator} instance for chaining * @return a new {@link BroadcastOperator} instance for chaining
*/ */
public to(room: Room | Room[]) { public to(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).to(room) return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).to(room);
} }
/** /**
@ -265,7 +268,7 @@ export class Namespace<
* @return a new {@link BroadcastOperator} instance for chaining * @return a new {@link BroadcastOperator} instance for chaining
*/ */
public in(room: Room | Room[]) { public in(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).in(room) return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).in(room);
} }
/** /**
@ -289,7 +292,7 @@ export class Namespace<
public except(room: Room | Room[]) { public except(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).except( return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).except(
room room
) );
} }
/** /**
@ -298,52 +301,95 @@ export class Namespace<
* @return {Socket} * @return {Socket}
* @private * @private
*/ */
// @java-patch sync
_add( _add(
client: Client<ListenEvents, EmitEvents, ServerSideEvents>, client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
query, auth: Record<string, unknown>,
fn?: (socket: Socket) => void fn: (
): Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData> { socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
debug("adding socket to nsp %s", this.name) ) => void
const socket = new Socket(this, client, query) ) {
debug("adding socket to nsp %s", this.name);
const socket = this._createSocket(client, auth);
if (
// @ts-ignore
this.server.opts.connectionStateRecovery?.skipMiddlewares &&
socket.recovered &&
client.conn.readyState === "open"
) {
return this._doConnect(socket, fn);
}
this.run(socket, (err) => { this.run(socket, (err) => {
process.nextTick(() => { process.nextTick(() => {
if ("open" !== client.conn.readyState) { if ("open" !== client.conn.readyState) {
debug("next called after client was closed - ignoring socket") debug("next called after client was closed - ignoring socket");
socket._cleanup() socket._cleanup();
return return;
} }
if (err) { if (err) {
debug("middleware error, sending CONNECT_ERROR packet to the client") debug("middleware error, sending CONNECT_ERROR packet to the client");
socket._cleanup() socket._cleanup();
if (client.conn.protocol === 3) { if (client.conn.protocol === 3) {
return socket._error(err.data || err.message) return socket._error(err.data || err.message);
} else { } else {
return socket._error({ return socket._error({
message: err.message, message: err.message,
data: err.data, data: err.data,
}) });
} }
} }
this._doConnect(socket, fn);
});
});
}
// @java-patch sync
private _createSocket(
client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
auth: Record<string, unknown>
) {
const sessionId = auth.pid;
const offset = auth.offset;
if (
// @ts-ignore
this.server.opts.connectionStateRecovery &&
typeof sessionId === "string" &&
typeof offset === "string"
) {
const session = this.adapter.restoreSession(sessionId, offset);
if (session) {
debug("connection state recovered for sid %s", session.sid);
return new Socket(this, client, auth, session);
} else {
debug("unable to restore session state");
}
}
return new Socket(this, client, auth);
}
private _doConnect(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
fn: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void
) {
// track socket // track socket
this.sockets.set(socket.id, socket) this.sockets.set(socket.id, socket);
// it's paramount that the internal `onconnect` logic // it's paramount that the internal `onconnect` logic
// fires before user-set events to prevent state order // fires before user-set events to prevent state order
// violations (such as a disconnection before the connection // violations (such as a disconnection before the connection
// logic is complete) // logic is complete)
socket._onconnect() socket._onconnect();
// if (fn) fn() if (fn) fn(socket);
// @java-patch multi thread need direct callback socket
if (fn) fn(socket)
// fire user-set events // fire user-set events
this.emitReserved("connect", socket) this.emitReserved("connect", socket);
this.emitReserved("connection", socket) this.emitReserved("connection", socket);
})
})
return socket
} }
/** /**
@ -355,9 +401,9 @@ export class Namespace<
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData> socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
): void { ): void {
if (this.sockets.has(socket.id)) { if (this.sockets.has(socket.id)) {
this.sockets.delete(socket.id) this.sockets.delete(socket.id);
} else { } else {
debug("ignoring remove for %s", socket.id) debug("ignoring remove for %s", socket.id);
} }
} }
@ -390,7 +436,31 @@ export class Namespace<
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).emit( return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).emit(
ev, ev,
...args ...args
) );
}
/**
* Emits an event and waits for an acknowledgement from all clients.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* try {
* const responses = await myNamespace.timeout(1000).emitWithAck("some-event");
* console.log(responses); // one response per client
* } catch (e) {
* // some clients did not acknowledge the event in the given delay
* }
*
* @return a Promise that will be fulfilled when all clients have acknowledged the event
*/
public emitWithAck<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: AllButLast<EventParams<EmitEvents, Ev>>
): Promise<SecondArg<Last<EventParams<EmitEvents, Ev>>>> {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).emitWithAck(ev, ...args);
} }
/** /**
@ -411,8 +481,8 @@ export class Namespace<
* @return self * @return self
*/ */
public send(...args: EventParams<EmitEvents, "message">): this { public send(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args) this.emit("message", ...args);
return this return this;
} }
/** /**
@ -421,8 +491,8 @@ export class Namespace<
* @return self * @return self
*/ */
public write(...args: EventParams<EmitEvents, "message">): this { public write(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args) this.emit("message", ...args);
return this return this;
} }
/** /**
@ -440,9 +510,9 @@ export class Namespace<
* // acknowledgements (without binary content) are supported too: * // acknowledgements (without binary content) are supported too:
* myNamespace.serverSideEmit("ping", (err, responses) => { * myNamespace.serverSideEmit("ping", (err, responses) => {
* if (err) { * if (err) {
* // some clients did not acknowledge the event in the given delay * // some servers did not acknowledge the event in the given delay
* } else { * } else {
* console.log(responses); // one response per client * console.log(responses); // one response per server (except the current one)
* } * }
* }); * });
* *
@ -455,14 +525,55 @@ export class Namespace<
*/ */
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>( public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev, ev: Ev,
...args: EventParams<ServerSideEvents, Ev> ...args: EventParams<
DecorateAcknowledgementsWithTimeoutAndMultipleResponses<ServerSideEvents>,
Ev
>
): boolean { ): boolean {
if (RESERVED_EVENTS.has(ev)) { if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${String(ev)}" is a reserved event name`) throw new Error(`"${String(ev)}" is a reserved event name`);
} }
args.unshift(ev) args.unshift(ev);
this.adapter.serverSideEmit(args) this.adapter.serverSideEmit(args);
return true return true;
}
/**
* Sends a message and expect an acknowledgement from the other Socket.IO servers of the cluster.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* try {
* const responses = await myNamespace.serverSideEmitWithAck("ping");
* console.log(responses); // one response per server (except the current one)
* } catch (e) {
* // some servers did not acknowledge the event in the given delay
* }
*
* @param ev - the event name
* @param args - an array of arguments
*
* @return a Promise that will be fulfilled when all servers have acknowledged the event
*/
public serverSideEmitWithAck<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
...args: AllButLast<EventParams<ServerSideEvents, Ev>>
): Promise<FirstArg<Last<EventParams<ServerSideEvents, Ev>>>[]> {
return new Promise((resolve, reject) => {
args.push((err, responses) => {
if (err) {
err.responses = responses;
return reject(err);
} else {
return resolve(responses);
}
});
this.serverSideEmit(
ev,
...(args as any[] as EventParams<ServerSideEvents, Ev>)
);
});
} }
/** /**
@ -473,7 +584,7 @@ export class Namespace<
* @private * @private
*/ */
_onServerSideEmit(args: [string, ...any[]]) { _onServerSideEmit(args: [string, ...any[]]) {
super.emitUntyped.apply(this, args) super.emitUntyped.apply(this, args);
} }
/** /**
@ -485,7 +596,7 @@ export class Namespace<
public allSockets(): Promise<Set<SocketId>> { public allSockets(): Promise<Set<SocketId>> {
return new BroadcastOperator<EmitEvents, SocketData>( return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter this.adapter
).allSockets() ).allSockets();
} }
/** /**
@ -502,7 +613,7 @@ export class Namespace<
public compress(compress: boolean) { public compress(compress: boolean) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).compress( return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).compress(
compress compress
) );
} }
/** /**
@ -518,7 +629,7 @@ export class Namespace<
* @return self * @return self
*/ */
public get volatile() { public get volatile() {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).volatile return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).volatile;
} }
/** /**
@ -533,7 +644,7 @@ export class Namespace<
* @return a new {@link BroadcastOperator} instance for chaining * @return a new {@link BroadcastOperator} instance for chaining
*/ */
public get local() { public get local() {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).local return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).local;
} }
/** /**
@ -555,7 +666,7 @@ export class Namespace<
public timeout(timeout: number) { public timeout(timeout: number) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).timeout( return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).timeout(
timeout timeout
) );
} }
/** /**
@ -587,7 +698,7 @@ export class Namespace<
public fetchSockets() { public fetchSockets() {
return new BroadcastOperator<EmitEvents, SocketData>( return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter this.adapter
).fetchSockets() ).fetchSockets();
} }
/** /**
@ -609,7 +720,7 @@ export class Namespace<
public socketsJoin(room: Room | Room[]) { public socketsJoin(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>( return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter this.adapter
).socketsJoin(room) ).socketsJoin(room);
} }
/** /**
@ -631,7 +742,7 @@ export class Namespace<
public socketsLeave(room: Room | Room[]) { public socketsLeave(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>( return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter this.adapter
).socketsLeave(room) ).socketsLeave(room);
} }
/** /**
@ -653,14 +764,16 @@ export class Namespace<
public disconnectSockets(close: boolean = false) { public disconnectSockets(close: boolean = false) {
return new BroadcastOperator<EmitEvents, SocketData>( return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter this.adapter
).disconnectSockets(close) ).disconnectSockets(close);
} }
// @java-patch // @java-patch
public close() { public close() {
RESERVED_EVENTS.forEach(event => this.removeAllListeners(event as any)) RESERVED_EVENTS.forEach((event) => this.removeAllListeners(event as any));
this.server._nsps.delete(this.name) this.server._nsps.delete(this.name);
// @java-patch close all socket when namespace close // @java-patch close all socket when namespace close
this.sockets.forEach(socket => socket._onclose(`namepsace ${this.name} close`)) this.sockets.forEach((socket) =>
socket._onclose(`namepsace ${this.name} close` as any)
);
} }
} }

View File

@ -1,13 +1,15 @@
import { Namespace } from "./namespace" import { Namespace } from "./namespace";
import type { Server, RemoteSocket } from "./index" import type { Server, RemoteSocket } from "./index";
import type { import type {
EventParams, EventParams,
EventNames, EventNames,
EventsMap, EventsMap,
DefaultEventsMap, DefaultEventsMap,
} from "./typed-events" } from "./typed-events";
// import type { BroadcastOptions } from "socket.io-adapter" import type { BroadcastOptions } from "../socket.io-adapter";
import type { BroadcastOptions } from "../socket.io-adapter" // import debugModule from "debug";
const debug = require("../debug")("socket.io:parent-namespace");
export class ParentNamespace< export class ParentNamespace<
ListenEvents extends EventsMap = DefaultEventsMap, ListenEvents extends EventsMap = DefaultEventsMap,
@ -23,7 +25,7 @@ export class ParentNamespace<
constructor( constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData> server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) { ) {
super(server, "/_" + ParentNamespace.count++) super(server, "/_" + ParentNamespace.count++);
} }
/** /**
@ -32,11 +34,11 @@ export class ParentNamespace<
_initAdapter(): void { _initAdapter(): void {
const broadcast = (packet: any, opts: BroadcastOptions) => { const broadcast = (packet: any, opts: BroadcastOptions) => {
this.children.forEach((nsp) => { this.children.forEach((nsp) => {
nsp.adapter.broadcast(packet, opts) nsp.adapter.broadcast(packet, opts);
}) });
} };
// @ts-ignore FIXME is there a way to declare an inner class in TypeScript? // @ts-ignore FIXME is there a way to declare an inner class in TypeScript?
this.adapter = { broadcast } this.adapter = { broadcast };
} }
public emit<Ev extends EventNames<EmitEvents>>( public emit<Ev extends EventNames<EmitEvents>>(
@ -44,26 +46,42 @@ export class ParentNamespace<
...args: EventParams<EmitEvents, Ev> ...args: EventParams<EmitEvents, Ev>
): boolean { ): boolean {
this.children.forEach((nsp) => { this.children.forEach((nsp) => {
nsp.emit(ev, ...args) nsp.emit(ev, ...args);
}) });
return true return true;
} }
createChild( createChild(
name: string name: string
): Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> { ): Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
const namespace = new Namespace(this.server, name) debug("creating child namespace %s", name);
namespace._fns = this._fns.slice(0) const namespace = new Namespace(this.server, name);
namespace._fns = this._fns.slice(0);
this.listeners("connect").forEach((listener) => this.listeners("connect").forEach((listener) =>
namespace.on("connect", listener) namespace.on("connect", listener)
) );
this.listeners("connection").forEach((listener) => this.listeners("connection").forEach((listener) =>
namespace.on("connection", listener) namespace.on("connection", listener)
) );
this.children.add(namespace) this.children.add(namespace);
this.server._nsps.set(name, namespace)
return namespace if (this.server._opts.cleanupEmptyChildNamespaces) {
const remove = namespace._remove;
namespace._remove = (socket) => {
remove.call(namespace, socket);
if (namespace.sockets.size === 0) {
debug("closing child namespace %s", name);
namespace.adapter.close();
this.server._nsps.delete(namespace.name);
this.children.delete(namespace);
}
};
}
this.server._nsps.set(name, namespace);
return namespace;
} }
fetchSockets(): Promise<RemoteSocket<EmitEvents, SocketData>[]> { fetchSockets(): Promise<RemoteSocket<EmitEvents, SocketData>[]> {
@ -72,6 +90,6 @@ export class ParentNamespace<
// the behavior for namespaces created with a function is less clear // 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 // 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) // 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") throw new Error("fetchSockets() is not supported on parent namespaces");
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
import { EventEmitter } from "events" import { EventEmitter } from "events";
/** /**
* An events map is an interface that maps event names to their value, which * An events map is an interface that maps event names to their value, which
* represents the type of the `on` listener. * represents the type of the `on` listener.
*/ */
export interface EventsMap { export interface EventsMap {
[event: string]: any [event: string]: any;
} }
/** /**
@ -13,19 +13,19 @@ export interface EventsMap {
* is equivalent to accepting all event names, and any data. * is equivalent to accepting all event names, and any data.
*/ */
export interface DefaultEventsMap { export interface DefaultEventsMap {
[event: string]: (...args: any[]) => void [event: string]: (...args: any[]) => void;
} }
/** /**
* Returns a union type containing all the keys of an event map. * Returns a union type containing all the keys of an event map.
*/ */
export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol) export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol);
/** The tuple type representing the parameters of an event listener */ /** The tuple type representing the parameters of an event listener */
export type EventParams< export type EventParams<
Map extends EventsMap, Map extends EventsMap,
Ev extends EventNames<Map> Ev extends EventNames<Map>
> = Parameters<Map[Ev]> > = Parameters<Map[Ev]>;
/** /**
* The event names that are either in ReservedEvents or in UserEvents * The event names that are either in ReservedEvents or in UserEvents
@ -33,7 +33,7 @@ export type EventParams<
export type ReservedOrUserEventNames< export type ReservedOrUserEventNames<
ReservedEventsMap extends EventsMap, ReservedEventsMap extends EventsMap,
UserEvents extends EventsMap UserEvents extends EventsMap
> = EventNames<ReservedEventsMap> | EventNames<UserEvents> > = EventNames<ReservedEventsMap> | EventNames<UserEvents>;
/** /**
* Type of a listener of a user event or a reserved event. If `Ev` is in * Type of a listener of a user event or a reserved event. If `Ev` is in
@ -43,13 +43,13 @@ export type ReservedOrUserListener<
ReservedEvents extends EventsMap, ReservedEvents extends EventsMap,
UserEvents extends EventsMap, UserEvents extends EventsMap,
Ev extends ReservedOrUserEventNames<ReservedEvents, UserEvents> Ev extends ReservedOrUserEventNames<ReservedEvents, UserEvents>
> = FallbackToUntypedListener< > = FallbackToUntypedListener<
Ev extends EventNames<ReservedEvents> Ev extends EventNames<ReservedEvents>
? ReservedEvents[Ev] ? ReservedEvents[Ev]
: Ev extends EventNames<UserEvents> : Ev extends EventNames<UserEvents>
? UserEvents[Ev] ? UserEvents[Ev]
: never : never
> >;
/** /**
* Returns an untyped listener type if `T` is `never`; otherwise, returns `T`. * Returns an untyped listener type if `T` is `never`; otherwise, returns `T`.
@ -59,7 +59,7 @@ export type ReservedOrUserListener<
*/ */
type FallbackToUntypedListener<T> = [T] extends [never] type FallbackToUntypedListener<T> = [T] extends [never]
? (...args: any[]) => void ? (...args: any[]) => void
: T : T;
/** /**
* Interface for classes that aren't `EventEmitter`s, but still expose a * Interface for classes that aren't `EventEmitter`s, but still expose a
@ -69,7 +69,7 @@ export interface TypedEventBroadcaster<EmitEvents extends EventsMap> {
emit<Ev extends EventNames<EmitEvents>>( emit<Ev extends EventNames<EmitEvents>>(
ev: Ev, ev: Ev,
...args: EventParams<EmitEvents, Ev> ...args: EventParams<EmitEvents, Ev>
): boolean ): boolean;
} }
/** /**
@ -103,7 +103,7 @@ export abstract class StrictEventEmitter<
ev: Ev, ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev> listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this { ): this {
return super.on(ev, listener) return super.on(ev, listener);
} }
/** /**
@ -116,7 +116,7 @@ export abstract class StrictEventEmitter<
ev: Ev, ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev> listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this { ): this {
return super.once(ev, listener) return super.once(ev, listener);
} }
/** /**
@ -129,7 +129,7 @@ export abstract class StrictEventEmitter<
ev: Ev, ev: Ev,
...args: EventParams<EmitEvents, Ev> ...args: EventParams<EmitEvents, Ev>
): boolean { ): boolean {
return super.emit(ev, ...args) return super.emit(ev, ...args);
} }
/** /**
@ -145,7 +145,7 @@ export abstract class StrictEventEmitter<
ev: Ev, ev: Ev,
...args: EventParams<ReservedEvents, Ev> ...args: EventParams<ReservedEvents, Ev>
): boolean { ): boolean {
return super.emit(ev, ...args) return super.emit(ev, ...args);
} }
/** /**
@ -159,7 +159,7 @@ export abstract class StrictEventEmitter<
* @param args Arguments to emit along with the event * @param args Arguments to emit along with the event
*/ */
protected emitUntyped(ev: string, ...args: any[]): boolean { protected emitUntyped(ev: string, ...args: any[]): boolean {
return super.emit(ev, ...args) return super.emit(ev, ...args);
} }
/** /**
@ -175,6 +175,69 @@ export abstract class StrictEventEmitter<
ReservedEvents, ReservedEvents,
ListenEvents, ListenEvents,
Ev Ev
>[] >[];
} }
} }
export type Last<T extends any[]> = T extends [...infer H, infer L] ? L : any;
export type AllButLast<T extends any[]> = T extends [...infer H, infer L]
? H
: any[];
export type FirstArg<T> = T extends (arg: infer Param) => infer Result
? Param
: any;
export type SecondArg<T> = T extends (
err: Error,
arg: infer Param
) => infer Result
? Param
: any;
type PrependTimeoutError<T extends any[]> = {
[K in keyof T]: T[K] extends (...args: infer Params) => infer Result
? (err: Error, ...args: Params) => Result
: T[K];
};
type ExpectMultipleResponses<T extends any[]> = {
[K in keyof T]: T[K] extends (err: Error, arg: infer Param) => infer Result
? (err: Error, arg: Param[]) => Result
: T[K];
};
/**
* Utility type to decorate the acknowledgement callbacks with a timeout error.
*
* This is needed because the timeout() flag breaks the symmetry between the sender and the receiver:
*
* @example
* interface Events {
* "my-event": (val: string) => void;
* }
*
* socket.on("my-event", (cb) => {
* cb("123"); // one single argument here
* });
*
* socket.timeout(1000).emit("my-event", (err, val) => {
* // two arguments there (the "err" argument is not properly typed)
* });
*
*/
export type DecorateAcknowledgements<E> = {
[K in keyof E]: E[K] extends (...args: infer Params) => infer Result
? (...args: PrependTimeoutError<Params>) => Result
: E[K];
};
export type DecorateAcknowledgementsWithTimeoutAndMultipleResponses<E> = {
[K in keyof E]: E[K] extends (...args: infer Params) => infer Result
? (...args: ExpectMultipleResponses<PrependTimeoutError<Params>>) => Result
: E[K];
};
export type DecorateAcknowledgementsWithMultipleResponses<E> = {
[K in keyof E]: E[K] extends (...args: infer Params) => infer Result
? (...args: ExpectMultipleResponses<Params>) => Result
: E[K];
};