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

View File

@ -7,4 +7,4 @@ export { Transport } from "./transport"
export { transports } from "./transports/index"
export { installTimerFunctions } from "./util"
export { parse } from "./contrib/parseuri"
export { nextTick } from "./transports/websocket-constructor.js"
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 { 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 = require('../debug')('engine.io-client:socket')
@ -209,6 +210,12 @@ export interface SocketOptions {
*/
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,
* 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[]
}
interface HandshakeData {
sid: string
upgrades: string[]
pingInterval: number
pingTimeout: number
maxPayload: number
}
interface SocketReservedEvents {
open: () => void
handshake: (data) => void
packet: (packet) => void
packetCreate: (packet) => void
handshake: (data: HandshakeData) => void
packet: (packet: Packet) => void
packetCreate: (packet: Packet) => void
data: (data) => void
message: (data) => void
drain: () => void
@ -237,13 +252,15 @@ interface SocketReservedEvents {
close: (reason: string, description?: CloseDetails | Error) => void
}
type SocketState = "opening" | "open" | "closing" | "closed"
export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
public id: string
public transport: any
public binaryType: string
public transport: Transport
public binaryType: BinaryType
public readyState: SocketState
public writeBuffer: Packet[] = [];
private readyState: string
private writeBuffer
private prevBufferLen: number
private upgrades
private pingInterval: number
@ -314,7 +331,6 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
: "80")
this.transports = opts.transports || ["polling", "websocket"]
this.readyState = ""
this.writeBuffer = []
this.prevBufferLen = 0
@ -326,6 +342,7 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
upgrade: true,
timestampParam: "t",
rememberUpgrade: false,
addTrailingSlash: true,
rejectUnauthorized: true,
perMessageDeflate: {
threshold: 1024
@ -336,7 +353,9 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
opts
)
this.opts.path = this.opts.path.replace(/\/$/, "") + "/"
this.opts.path =
this.opts.path.replace(/\/$/, "") +
(this.opts.addTrailingSlash ? "/" : "")
if (typeof this.opts.query === "string") {
this.opts.query = decode(this.opts.query)
@ -368,7 +387,7 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
if (this.hostname !== "localhost") {
this.offlineEventListener = () => {
this.onClose("transport close", {
description: "network connection lost"
description: "network connection lost",
})
}
addEventListener("offline", this.offlineEventListener, false)
@ -381,9 +400,9 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
/**
* Creates transport of the given type.
*
* @param {String} transport name
* @param {String} name - transport name
* @return {Transport}
* @api private
* @private
*/
private createTransport(name) {
debug('creating transport "%s"', name)
@ -407,7 +426,7 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
socket: this,
hostname: this.hostname,
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.
*
* @api private
* @private
*/
private open() {
let transport
@ -457,7 +476,7 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
/**
* Sets the current transport. Disables the existing one (if any).
*
* @api private
* @private
*/
private setTransport(transport) {
debug("setting transport %s", transport.name)
@ -475,7 +494,7 @@ export class Socket extends Emitter<{}, {}, SocketReservedEvents> {
.on("drain", this.onDrain.bind(this))
.on("packet", this.onPacket.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 {
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 {
@ -35,24 +35,27 @@ interface TransportReservedEvents {
drain: () => void
}
type TransportState = "opening" | "open" | "closed" | "pausing" | "paused"
export abstract class Transport extends Emitter<
{},
{},
Record<never, never>,
Record<never, never>,
TransportReservedEvents
> {
public query: Record<string, string>
public writable: boolean = false;
protected opts: SocketOptions
protected supportsBinary: boolean
protected query: object
protected readyState: string
protected writable: boolean = false;
protected readyState: TransportState
protected socket: any
protected setTimeoutFn: typeof setTimeout
/**
* Transport abstract constructor.
*
* @param {Object} options.
* @api private
* @param {Object} opts - options
* @protected
*/
constructor(opts) {
super()
@ -60,7 +63,6 @@ export abstract class Transport extends Emitter<
this.opts = opts
this.query = opts.query
this.readyState = ""
this.socket = opts.socket
}
@ -71,7 +73,7 @@ export abstract class Transport extends Emitter<
* @param description
* @param context - the error context
* @return {Transport} for chaining
* @api protected
* @protected
*/
protected onError(reason: string, description: any, context?: any) {
super.emitReserved(
@ -83,25 +85,19 @@ export abstract class Transport extends Emitter<
/**
* Opens the transport.
*
* @api public
*/
private open() {
if ("closed" === this.readyState || "" === this.readyState) {
public open() {
this.readyState = "opening"
this.doOpen()
}
return this
}
/**
* Closes the transport.
*
* @api public
*/
public close() {
if ("opening" === this.readyState || "open" === this.readyState) {
if (this.readyState === "opening" || this.readyState === "open") {
this.doClose()
this.onClose()
}
@ -113,10 +109,9 @@ export abstract class Transport extends Emitter<
* Sends multiple packets.
*
* @param {Array} packets
* @api public
*/
public send(packets) {
if ("open" === this.readyState) {
if (this.readyState === "open") {
this.write(packets)
} else {
// 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
*
* @api protected
* @protected
*/
protected onOpen() {
this.readyState = "open"
@ -139,17 +134,18 @@ export abstract class Transport extends Emitter<
* Called with data.
*
* @param {String} data
* @api protected
* @protected
*/
protected onData(data: RawData) {
const packet = decodePacket(data, this.socket.binaryType)
this.onPacket(packet)
}
/**
* Called with a decoded packet.
*
* @api protected
* @protected
*/
protected onPacket(packet: Packet) {
super.emitReserved("packet", packet)
@ -158,14 +154,26 @@ export abstract class Transport extends Emitter<
/**
* Called upon close.
*
* @api protected
* @protected
*/
protected onClose(details?: CloseDetails) {
this.readyState = "closed"
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 doClose()
protected abstract write(packets)
protected abstract write(packets: Packet[])
}

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import {
Packet,
BinaryType,
RawData
} from "./commons.js"
} from "./commons.js";
const decodePacket = (
encodedPacket: RawData,
@ -14,18 +14,18 @@ const decodePacket = (
return {
type: "message",
data: mapBinary(encodedPacket, binaryType)
};
}
}
const type = encodedPacket.charAt(0)
const type = encodedPacket.charAt(0);
if (type === "b") {
const buffer = Buffer.from(encodedPacket.substring(1), "base64")
const buffer = Buffer.from(encodedPacket.substring(1), "base64");
return {
type: "message",
data: mapBinary(buffer, binaryType)
}
};
}
if (!PACKET_TYPES_REVERSE[type]) {
return ERROR_PACKET
return ERROR_PACKET;
}
return encodedPacket.length > 1
? {
@ -34,27 +34,27 @@ const decodePacket = (
}
: {
type: PACKET_TYPES_REVERSE[type]
}
}
};
};
const mapBinary = (data: RawData, binaryType?: BinaryType) => {
const isBuffer = Buffer.isBuffer(data)
const isBuffer = Buffer.isBuffer(data);
switch (binaryType) {
case "arraybuffer":
return isBuffer ? toArrayBuffer(data) : data
return isBuffer ? toArrayBuffer(data) : data;
case "nodebuffer":
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 arrayBuffer = new ArrayBuffer(buffer.length)
const view = new Uint8Array(arrayBuffer)
const arrayBuffer = new ArrayBuffer(buffer.length);
const view = new Uint8Array(arrayBuffer);
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 = (
{ type, data }: Packet,
@ -6,26 +6,26 @@ const encodePacket = (
callback: (encodedPacket: RawData) => void
) => {
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
const buffer = toBuffer(data)
return callback(encodeBuffer(buffer, supportsBinary))
const buffer = toBuffer(data);
return callback(encodeBuffer(buffer, supportsBinary));
}
// plain string
return callback(PACKET_TYPES[type] + (data || ""))
}
return callback(PACKET_TYPES[type] + (data || ""));
};
const toBuffer = data => {
if (Buffer.isBuffer(data)) {
return data
return data;
} else if (data instanceof ArrayBuffer) {
return Buffer.from(data)
return Buffer.from(data);
} 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
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 decodePacket from "./decodePacket.js"
import { Packet, PacketType, RawData, BinaryType } from "./commons.js"
import encodePacket from "./encodePacket.js";
import decodePacket from "./decodePacket.js";
import { Packet, PacketType, RawData, BinaryType } from "./commons.js";
const SEPARATOR = String.fromCharCode(30) // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
const SEPARATOR = String.fromCharCode(30); // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
const encodePayload = (
packets: Packet[],
callback: (encodedPayload: string) => void
) => {
// some packets may be added to the array while encoding, so the initial length must be saved
const length = packets.length
const encodedPackets = new Array(length)
let count = 0
const length = packets.length;
const encodedPackets = new Array(length);
let count = 0;
packets.forEach((packet, i) => {
// force base64 encoding for binary packets
encodePacket(packet, false, encodedPacket => {
encodedPackets[i] = encodedPacket
encodedPackets[i] = encodedPacket;
if (++count === length) {
callback(encodedPackets.join(SEPARATOR))
callback(encodedPackets.join(SEPARATOR));
}
})
})
}
});
});
};
const decodePayload = (
encodedPayload: string,
binaryType?: BinaryType
): Packet[] => {
const encodedPackets = encodedPayload.split(SEPARATOR)
const packets = []
const encodedPackets = encodedPayload.split(SEPARATOR);
const packets = [];
for (let i = 0; i < encodedPackets.length; i++) {
const decodedPacket = decodePacket(encodedPackets[i], binaryType)
packets.push(decodedPacket)
const decodedPacket = decodePacket(encodedPackets[i], binaryType);
packets.push(decodedPacket);
if (decodedPacket.type === "error") {
break
break;
}
}
return packets
}
return packets;
};
export const protocol = 4
export const protocol = 4;
export {
encodePacket,
encodePayload,
@ -50,4 +50,4 @@ export {
PacketType,
RawData,
BinaryType
}
};

View File

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

View File

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

View File

@ -1,50 +1,50 @@
/*! https://mths.be/utf8js v2.1.2 by @mathias */
var stringFromCharCode = String.fromCharCode
var stringFromCharCode = String.fromCharCode;
// Taken from https://mths.be/punycode
function ucs2decode(string) {
var output = []
var counter = 0
var length = string.length
var value
var extra
var output = [];
var counter = 0;
var length = string.length;
var value;
var extra;
while (counter < length) {
value = string.charCodeAt(counter++)
value = string.charCodeAt(counter++);
if (value >= 0xD800 && value <= 0xDBFF && counter < length) {
// high surrogate, and there is a next character
extra = string.charCodeAt(counter++)
extra = string.charCodeAt(counter++);
if ((extra & 0xFC00) == 0xDC00) { // low surrogate
output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000)
output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000);
} else {
// unmatched surrogate; only append this code unit, in case the next
// code unit is the high surrogate of a surrogate pair
output.push(value)
counter--
output.push(value);
counter--;
}
} else {
output.push(value)
output.push(value);
}
}
return output
return output;
}
// Taken from https://mths.be/punycode
function ucs2encode(array) {
var length = array.length
var index = -1
var value
var output = ''
var length = array.length;
var index = -1;
var value;
var output = '';
while (++index < length) {
value = array[index]
value = array[index];
if (value > 0xFFFF) {
value -= 0x10000
output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800)
value = 0xDC00 | value & 0x3FF
value -= 0x10000;
output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800);
value = 0xDC00 | value & 0x3FF;
}
output += stringFromCharCode(value)
output += stringFromCharCode(value);
}
return output
return output;
}
function checkScalarValue(codePoint, strict) {
@ -53,158 +53,158 @@ function checkScalarValue(codePoint, strict) {
throw Error(
'Lone surrogate U+' + codePoint.toString(16).toUpperCase() +
' is not a scalar value'
)
);
}
return false
return false;
}
return true
return true;
}
/*--------------------------------------------------------------------------*/
function createByte(codePoint, shift) {
return stringFromCharCode(((codePoint >> shift) & 0x3F) | 0x80)
return stringFromCharCode(((codePoint >> shift) & 0x3F) | 0x80);
}
function encodeCodePoint(codePoint, strict) {
if ((codePoint & 0xFFFFFF80) == 0) { // 1-byte sequence
return stringFromCharCode(codePoint)
return stringFromCharCode(codePoint);
}
var symbol = ''
var symbol = '';
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
if (!checkScalarValue(codePoint, strict)) {
codePoint = 0xFFFD
codePoint = 0xFFFD;
}
symbol = stringFromCharCode(((codePoint >> 12) & 0x0F) | 0xE0)
symbol += createByte(codePoint, 6)
symbol = stringFromCharCode(((codePoint >> 12) & 0x0F) | 0xE0);
symbol += createByte(codePoint, 6);
}
else if ((codePoint & 0xFFE00000) == 0) { // 4-byte sequence
symbol = stringFromCharCode(((codePoint >> 18) & 0x07) | 0xF0)
symbol += createByte(codePoint, 12)
symbol += createByte(codePoint, 6)
symbol = stringFromCharCode(((codePoint >> 18) & 0x07) | 0xF0);
symbol += createByte(codePoint, 12);
symbol += createByte(codePoint, 6);
}
symbol += stringFromCharCode((codePoint & 0x3F) | 0x80)
return symbol
symbol += stringFromCharCode((codePoint & 0x3F) | 0x80);
return symbol;
}
function utf8encode(string, opts) {
opts = opts || {}
var strict = false !== opts.strict
opts = opts || {};
var strict = false !== opts.strict;
var codePoints = ucs2decode(string)
var length = codePoints.length
var index = -1
var codePoint
var byteString = ''
var codePoints = ucs2decode(string);
var length = codePoints.length;
var index = -1;
var codePoint;
var byteString = '';
while (++index < length) {
codePoint = codePoints[index]
byteString += encodeCodePoint(codePoint, strict)
codePoint = codePoints[index];
byteString += encodeCodePoint(codePoint, strict);
}
return byteString
return byteString;
}
/*--------------------------------------------------------------------------*/
function readContinuationByte() {
if (byteIndex >= byteCount) {
throw Error('Invalid byte index')
throw Error('Invalid byte index');
}
var continuationByte = byteArray[byteIndex] & 0xFF
byteIndex++
var continuationByte = byteArray[byteIndex] & 0xFF;
byteIndex++;
if ((continuationByte & 0xC0) == 0x80) {
return continuationByte & 0x3F
return continuationByte & 0x3F;
}
// If we end up here, its not a continuation byte
throw Error('Invalid continuation byte')
throw Error('Invalid continuation byte');
}
function decodeSymbol(strict) {
var byte1
var byte2
var byte3
var byte4
var codePoint
var byte1;
var byte2;
var byte3;
var byte4;
var codePoint;
if (byteIndex > byteCount) {
throw Error('Invalid byte index')
throw Error('Invalid byte index');
}
if (byteIndex == byteCount) {
return false
return false;
}
// Read first byte
byte1 = byteArray[byteIndex] & 0xFF
byteIndex++
byte1 = byteArray[byteIndex] & 0xFF;
byteIndex++;
// 1-byte sequence (no continuation bytes)
if ((byte1 & 0x80) == 0) {
return byte1
return byte1;
}
// 2-byte sequence
if ((byte1 & 0xE0) == 0xC0) {
byte2 = readContinuationByte()
codePoint = ((byte1 & 0x1F) << 6) | byte2
byte2 = readContinuationByte();
codePoint = ((byte1 & 0x1F) << 6) | byte2;
if (codePoint >= 0x80) {
return codePoint
return codePoint;
} else {
throw Error('Invalid continuation byte')
throw Error('Invalid continuation byte');
}
}
// 3-byte sequence (may include unpaired surrogates)
if ((byte1 & 0xF0) == 0xE0) {
byte2 = readContinuationByte()
byte3 = readContinuationByte()
codePoint = ((byte1 & 0x0F) << 12) | (byte2 << 6) | byte3
byte2 = readContinuationByte();
byte3 = readContinuationByte();
codePoint = ((byte1 & 0x0F) << 12) | (byte2 << 6) | byte3;
if (codePoint >= 0x0800) {
return checkScalarValue(codePoint, strict) ? codePoint : 0xFFFD
return checkScalarValue(codePoint, strict) ? codePoint : 0xFFFD;
} else {
throw Error('Invalid continuation byte')
throw Error('Invalid continuation byte');
}
}
// 4-byte sequence
if ((byte1 & 0xF8) == 0xF0) {
byte2 = readContinuationByte()
byte3 = readContinuationByte()
byte4 = readContinuationByte()
byte2 = readContinuationByte();
byte3 = readContinuationByte();
byte4 = readContinuationByte();
codePoint = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0C) |
(byte3 << 0x06) | byte4
(byte3 << 0x06) | byte4;
if (codePoint >= 0x010000 && codePoint <= 0x10FFFF) {
return codePoint
return codePoint;
}
}
throw Error('Invalid UTF-8 detected')
throw Error('Invalid UTF-8 detected');
}
var byteArray
var byteCount
var byteIndex
var byteArray;
var byteCount;
var byteIndex;
function utf8decode(byteString, opts) {
opts = opts || {}
var strict = false !== opts.strict
opts = opts || {};
var strict = false !== opts.strict;
byteArray = ucs2decode(byteString)
byteCount = byteArray.length
byteIndex = 0
var codePoints = []
var tmp
byteArray = ucs2decode(byteString);
byteCount = byteArray.length;
byteIndex = 0;
var codePoints = [];
var tmp;
while ((tmp = decodeSymbol(strict)) !== false) {
codePoints.push(tmp)
codePoints.push(tmp);
}
return ucs2encode(codePoints)
return ucs2encode(codePoints);
}
module.exports = {
version: '2.1.2',
encode: utf8encode,
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 { IncomingMessage } from "http"
import { Transport } from "./transport"
import { Server } from "./server"
import { Transport } from "./transport";
import { Server } from "./server";
// import { setTimeout, clearTimeout } from "timers"
// import { Packet, PacketType, RawData } from "engine.io-parser"
import { Packet, PacketType, RawData } from "../engine.io-parser"
import { Packet, PacketType, RawData } from "../engine.io-parser";
// const debug = debugModule("engine:socket")
const debug = require('../debug')("engine:socket")
const debug = require("../debug")("engine:socket");
export class Socket extends EventEmitter {
public readonly protocol: number
public readonly protocol: number;
// public readonly request: IncomingMessage
public readonly request: any
public readonly remoteAddress: string
public readonly request: any;
public readonly remoteAddress: string;
public _readyState: string
public transport: Transport
public _readyState: string;
public transport: Transport;
private server: Server
private upgrading: boolean
private upgraded: boolean
private writeBuffer: Packet[]
private packetsFn: any[]
private sentCallbackFn: any[]
private cleanupFn: any[]
private checkIntervalTimer
private upgradeTimeoutTimer
private pingTimeoutTimer
private pingIntervalTimer
private server: Server;
private upgrading: boolean;
private upgraded: boolean;
private writeBuffer: Packet[];
private packetsFn: any[];
private sentCallbackFn: any[];
private cleanupFn: any[];
private checkIntervalTimer;
private upgradeTimeoutTimer;
private pingTimeoutTimer;
private pingIntervalTimer;
private readonly id: string
private readonly id: string;
get readyState() {
return this._readyState
return this._readyState;
}
set readyState(state) {
debug("readyState updated from %s to %s", this._readyState, state)
this._readyState = state
debug("readyState updated from %s to %s", this._readyState, state);
this._readyState = state;
}
/**
@ -48,33 +48,33 @@ export class Socket extends EventEmitter {
* @api private
*/
constructor(id, server, transport, req, protocol) {
super()
this.id = id
this.server = server
this.upgrading = false
this.upgraded = false
this.readyState = "opening"
this.writeBuffer = []
this.packetsFn = []
this.sentCallbackFn = []
this.cleanupFn = []
this.request = req
this.protocol = protocol
super();
this.id = id;
this.server = server;
this.upgrading = false;
this.upgraded = false;
this.readyState = "opening";
this.writeBuffer = [];
this.packetsFn = [];
this.sentCallbackFn = [];
this.cleanupFn = [];
this.request = req;
this.protocol = protocol;
// Cache IP since it might not be in the req later
if (req.websocket && req.websocket._socket) {
this.remoteAddress = req.websocket._socket.remoteAddress
this.remoteAddress = req.websocket._socket.remoteAddress;
} else {
this.remoteAddress = req.connection.remoteAddress
this.remoteAddress = req.connection.remoteAddress;
}
this.checkIntervalTimer = null
this.upgradeTimeoutTimer = null
this.pingTimeoutTimer = null
this.pingIntervalTimer = null
this.checkIntervalTimer = null;
this.upgradeTimeoutTimer = null;
this.pingTimeoutTimer = null;
this.pingIntervalTimer = null;
this.setTransport(transport)
this.onOpen()
this.setTransport(transport);
this.onOpen();
}
/**
@ -83,10 +83,10 @@ export class Socket extends EventEmitter {
* @api private
*/
private onOpen() {
this.readyState = "open"
this.readyState = "open";
// sends an `open` packet
this.transport.sid = this.id
this.transport.sid = this.id;
this.sendPacket(
"open",
JSON.stringify({
@ -94,24 +94,24 @@ export class Socket extends EventEmitter {
upgrades: this.getAvailableUpgrades(),
pingInterval: this.server.opts.pingInterval,
pingTimeout: this.server.opts.pingTimeout,
maxPayload: this.server.opts.maxHttpBufferSize
maxPayload: this.server.opts.maxHttpBufferSize,
})
)
);
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) {
// in protocol v3, the client sends a ping, and the server answers with a pong
this.resetPingTimeout(
this.server.opts.pingInterval + this.server.opts.pingTimeout
)
);
} else {
// 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) {
if ("open" !== this.readyState) {
return debug("packet received with closed socket")
return debug("packet received with closed socket");
}
// export packet event
debug(`received packet ${packet.type}`)
this.emit("packet", packet)
debug(`received packet ${packet.type}`);
this.emit("packet", packet);
// Reset ping timeout on any packet, incoming data is a good sign of
// other side's liveness
this.resetPingTimeout(
this.server.opts.pingInterval + this.server.opts.pingTimeout
)
);
switch (packet.type) {
case "ping":
if (this.transport.protocol !== 3) {
this.onError("invalid heartbeat direction")
return
this.onError("invalid heartbeat direction");
return;
}
debug("got ping")
this.sendPacket("pong")
this.emit("heartbeat")
break
debug("got ping");
this.sendPacket("pong");
this.emit("heartbeat");
break;
case "pong":
if (this.transport.protocol === 3) {
this.onError("invalid heartbeat direction")
return
this.onError("invalid heartbeat direction");
return;
}
debug("got pong")
debug("got pong");
// this.pingIntervalTimer.refresh()
this.schedulePing()
this.emit("heartbeat")
break
this.schedulePing();
this.emit("heartbeat");
break;
case "error":
this.onClose("parse error")
break
this.onClose("parse error");
break;
case "message":
this.emit("data", packet.data)
this.emit("message", packet.data)
break
this.emit("data", packet.data);
this.emit("message", packet.data);
break;
}
}
@ -175,8 +175,8 @@ export class Socket extends EventEmitter {
* @api private
*/
private onError(err) {
debug("transport error")
this.onClose("transport error", err)
debug("transport error");
this.onClose("transport error", err);
}
/**
@ -190,10 +190,10 @@ export class Socket extends EventEmitter {
debug(
"writing ping packet - expecting pong within %sms",
this.server.opts.pingTimeout
)
this.sendPacket("ping")
this.resetPingTimeout(this.server.opts.pingTimeout)
}, this.server.opts.pingInterval)
);
this.sendPacket("ping");
this.resetPingTimeout(this.server.opts.pingTimeout);
}, this.server.opts.pingInterval);
}
/**
@ -202,11 +202,11 @@ export class Socket extends EventEmitter {
* @api private
*/
private resetPingTimeout(timeout) {
clearTimeout(this.pingTimeoutTimer)
clearTimeout(this.pingTimeoutTimer);
this.pingTimeoutTimer = setTimeout(() => {
if (this.readyState === "closed") return
this.onClose("ping timeout")
}, timeout)
if (this.readyState === "closed") return;
this.onClose("ping timeout");
}, timeout);
}
/**
@ -216,25 +216,25 @@ export class Socket extends EventEmitter {
* @api private
*/
private setTransport(transport) {
const onError = this.onError.bind(this)
const onPacket = this.onPacket.bind(this)
const flush = this.flush.bind(this)
const onClose = this.onClose.bind(this, "transport close")
const onError = this.onError.bind(this);
const onPacket = this.onPacket.bind(this);
const flush = this.flush.bind(this);
const onClose = this.onClose.bind(this, "transport close");
this.transport = transport
this.transport.once("error", onError)
this.transport.on("packet", onPacket)
this.transport.on("drain", flush)
this.transport.once("close", onClose)
this.transport = transport;
this.transport.once("error", onError);
this.transport.on("packet", onPacket);
this.transport.on("drain", flush);
this.transport.once("close", onClose);
// this function will manage packet events (also message callbacks)
this.setupSendCallback()
this.setupSendCallback();
this.cleanupFn.push(function () {
transport.removeListener("error", onError)
transport.removeListener("packet", onPacket)
transport.removeListener("drain", flush)
transport.removeListener("close", onClose)
})
transport.removeListener("error", onError);
transport.removeListener("packet", onPacket);
transport.removeListener("drain", flush);
transport.removeListener("close", onClose);
});
}
/**
@ -248,89 +248,89 @@ export class Socket extends EventEmitter {
'might upgrade socket transport from "%s" to "%s"',
this.transport.name,
transport.name
)
);
this.upgrading = true
this.upgrading = true;
// set transport upgrade timer
this.upgradeTimeoutTimer = setTimeout(() => {
debug("client did not complete upgrade - closing transport")
cleanup()
debug("client did not complete upgrade - closing transport");
cleanup();
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) {
debug("got probe ping packet, sending pong")
transport.send([{ type: "pong", data: "probe" }])
this.emit("upgrading", transport)
clearInterval(this.checkIntervalTimer)
this.checkIntervalTimer = setInterval(check, 100)
debug("got probe ping packet, sending pong");
transport.send([{ type: "pong", data: "probe" }]);
this.emit("upgrading", transport);
clearInterval(this.checkIntervalTimer);
this.checkIntervalTimer = setInterval(check, 100);
} else if ("upgrade" === packet.type && this.readyState !== "closed") {
debug("got upgrade packet - upgrading")
cleanup()
this.transport.discard()
this.upgraded = true
this.clearTransport()
this.setTransport(transport)
this.emit("upgrade", transport)
this.flush()
debug("got upgrade packet - upgrading");
cleanup();
this.transport.discard();
this.upgraded = true;
this.clearTransport();
this.setTransport(transport);
this.emit("upgrade", transport);
this.flush();
if (this.readyState === "closing") {
transport.close(() => {
this.onClose("forced close")
})
this.onClose("forced close");
});
}
} else {
cleanup()
transport.close()
}
cleanup();
transport.close();
}
};
// we force a polling cycle to ensure a fast upgrade
const check = () => {
if ("polling" === this.transport.name && this.transport.writable) {
debug("writing a noop packet to polling for fast upgrade")
this.transport.send([{ type: "noop" }])
}
debug("writing a noop packet to polling for fast upgrade");
this.transport.send([{ type: "noop" }]);
}
};
const cleanup = () => {
this.upgrading = false
this.upgrading = false;
clearInterval(this.checkIntervalTimer)
this.checkIntervalTimer = null
clearInterval(this.checkIntervalTimer);
this.checkIntervalTimer = null;
clearTimeout(this.upgradeTimeoutTimer)
this.upgradeTimeoutTimer = null
clearTimeout(this.upgradeTimeoutTimer);
this.upgradeTimeoutTimer = null;
transport.removeListener("packet", onPacket)
transport.removeListener("close", onTransportClose)
transport.removeListener("error", onError)
this.removeListener("close", onClose)
}
transport.removeListener("packet", onPacket);
transport.removeListener("close", onTransportClose);
transport.removeListener("error", onError);
this.removeListener("close", onClose);
};
const onError = err => {
debug("client did not complete upgrade - %s", err)
cleanup()
transport.close()
transport = null
}
const onError = (err) => {
debug("client did not complete upgrade - %s", err);
cleanup();
transport.close();
transport = null;
};
const onTransportClose = () => {
onError("transport closed")
}
onError("transport closed");
};
const onClose = () => {
onError("socket closed")
}
onError("socket closed");
};
transport.on("packet", onPacket)
transport.once("close", onTransportClose)
transport.once("error", onError)
transport.on("packet", onPacket);
transport.once("close", onTransportClose);
transport.once("error", onError);
this.once("close", onClose)
this.once("close", onClose);
}
/**
@ -339,24 +339,24 @@ export class Socket extends EventEmitter {
* @api private
*/
private clearTransport() {
let cleanup
let cleanup;
const toCleanUp = this.cleanupFn.length
const toCleanUp = this.cleanupFn.length;
for (let i = 0; i < toCleanUp; i++) {
cleanup = this.cleanupFn.shift()
cleanup()
cleanup = this.cleanupFn.shift();
cleanup();
}
// silence further transport errors and prevent uncaught exceptions
this.transport.on("error", function () {
debug("error triggered by discarded transport")
})
debug("error triggered by discarded transport");
});
// ensure transport won't stay open
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?) {
if ("closed" !== this.readyState) {
this.readyState = "closed"
this.readyState = "closed";
// clear timers
clearTimeout(this.pingIntervalTimer)
clearTimeout(this.pingTimeoutTimer)
clearTimeout(this.pingIntervalTimer);
clearTimeout(this.pingTimeoutTimer);
clearInterval(this.checkIntervalTimer)
this.checkIntervalTimer = null
clearTimeout(this.upgradeTimeoutTimer)
clearInterval(this.checkIntervalTimer);
this.checkIntervalTimer = null;
clearTimeout(this.upgradeTimeoutTimer);
// clean writeBuffer in next tick, so developers can still
// grab the writeBuffer on 'close' event
process.nextTick(() => {
this.writeBuffer = []
})
this.packetsFn = []
this.sentCallbackFn = []
this.clearTransport()
this.emit("close", reason, description)
this.writeBuffer = [];
});
this.packetsFn = [];
this.sentCallbackFn = [];
this.clearTransport();
this.emit("close", reason, description);
}
}
@ -396,28 +396,28 @@ export class Socket extends EventEmitter {
// the message was sent successfully, execute the callback
const onDrain = () => {
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) {
debug("executing send callback")
seqFn(this.transport)
debug("executing send callback");
seqFn(this.transport);
} else if (Array.isArray(seqFn)) {
debug("executing batch send callback")
const l = seqFn.length
let i = 0
debug("executing batch send callback");
const l = seqFn.length;
let i = 0;
for (; i < l; 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.transport.removeListener("drain", onDrain)
})
this.transport.removeListener("drain", onDrain);
});
}
/**
@ -430,13 +430,13 @@ export class Socket extends EventEmitter {
* @api public
*/
public send(data, options, callback?) {
this.sendPacket("message", data, options, callback)
return this
this.sendPacket("message", data, options, callback);
return this;
}
public write(data, options, callback?) {
this.sendPacket("message", data, options, callback)
return this
this.sendPacket("message", data, options, callback);
return this;
}
/**
@ -451,32 +451,32 @@ export class Socket extends EventEmitter {
*/
private sendPacket(type: PacketType, data?: RawData, options?, callback?) {
if ("function" === typeof options) {
callback = options
options = null
callback = options;
options = null;
}
options = options || {}
options.compress = false !== options.compress
options = options || {};
options.compress = false !== options.compress;
if ("closing" !== this.readyState && "closed" !== this.readyState) {
debug('sending packet "%s" (%s)', type, data)
debug('sending packet "%s" (%s)', type, data);
const packet: Packet = {
type,
options
}
options,
};
if (data) packet.data = data
if (data) packet.data = data;
// 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
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.writeBuffer.length
) {
debug("flushing buffer to transport")
this.emit("flush", this.writeBuffer)
this.server.emit("flush", this, this.writeBuffer)
const wbuf = this.writeBuffer
this.writeBuffer = []
debug("flushing buffer to transport");
this.emit("flush", this.writeBuffer);
this.server.emit("flush", this, this.writeBuffer);
const wbuf = this.writeBuffer;
this.writeBuffer = [];
if (!this.transport.supportsFraming) {
this.sentCallbackFn.push(this.packetsFn)
this.sentCallbackFn.push(this.packetsFn);
} else {
this.sentCallbackFn.push.apply(this.sentCallbackFn, this.packetsFn)
this.sentCallbackFn.push.apply(this.sentCallbackFn, this.packetsFn);
}
this.packetsFn = []
this.transport.send(wbuf)
this.emit("drain")
this.server.emit("drain", this)
this.packetsFn = [];
this.transport.send(wbuf);
this.emit("drain");
this.server.emit("drain", this);
}
}
@ -514,17 +514,17 @@ export class Socket extends EventEmitter {
* @api private
*/
private getAvailableUpgrades() {
const availableUpgrades = []
const allUpgrades = this.server.upgrades(this.transport.name)
let i = 0
const l = allUpgrades.length
const availableUpgrades = [];
const allUpgrades = this.server.upgrades(this.transport.name);
let i = 0;
const l = allUpgrades.length;
for (; i < l; ++i) {
const upg = allUpgrades[i]
const upg = allUpgrades[i];
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
*/
public close(discard?: boolean) {
if ("open" !== this.readyState) return
if ("open" !== this.readyState) return;
this.readyState = "closing"
this.readyState = "closing";
if (this.writeBuffer.length) {
this.once("drain", this.closeTransport.bind(this, discard))
return
this.once("drain", this.closeTransport.bind(this, discard));
return;
}
this.closeTransport(discard)
this.closeTransport(discard);
}
/**
@ -554,7 +554,7 @@ export class Socket extends EventEmitter {
* @api private
*/
private closeTransport(discard) {
if (discard) this.transport.discard()
this.transport.close(this.onClose.bind(this, "forced close"))
if (discard) this.transport.discard();
this.transport.close(this.onClose.bind(this, "forced close"));
}
}

View File

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

View File

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

View File

@ -1,11 +1,11 @@
import { Transport } from "../transport"
import { Transport } from "../transport";
// import debugModule from "debug";
const debug = require('../../debug')("engine:ws")
const debug = require("../../debug")("engine:ws");
export class WebSocket extends Transport {
protected perMessageDeflate: any
private socket: any
protected perMessageDeflate: any;
private socket: any;
/**
* WebSocket transport
@ -14,17 +14,17 @@ export class WebSocket extends Transport {
* @api public
*/
constructor(req) {
super(req)
this.socket = req.websocket
super(req);
this.socket = req.websocket;
this.socket.on("message", (data, isBinary) => {
const message = isBinary ? data : data.toString()
debug('received "%s"', message)
super.onData(message)
})
this.socket.once("close", this.onClose.bind(this))
this.socket.on("error", this.onError.bind(this))
this.writable = true
this.perMessageDeflate = null
const message = isBinary ? data : data.toString();
debug('received "%s"', message);
super.onData(message);
});
this.socket.once("close", this.onClose.bind(this));
this.socket.on("error", this.onError.bind(this));
this.writable = true;
this.perMessageDeflate = null;
}
/**
@ -33,7 +33,7 @@ export class WebSocket extends Transport {
* @api public
*/
get name() {
return "websocket"
return "websocket";
}
/**
@ -42,7 +42,7 @@ export class WebSocket extends Transport {
* @api public
*/
get handlesUpgrades() {
return true
return true;
}
/**
@ -51,7 +51,7 @@ export class WebSocket extends Transport {
* @api public
*/
get supportsFraming() {
return true
return true;
}
/**
@ -61,40 +61,40 @@ export class WebSocket extends Transport {
* @api private
*/
send(packets) {
const packet = packets.shift()
const packet = packets.shift();
if (typeof packet === "undefined") {
this.writable = true
this.emit("drain")
return
this.writable = true;
this.emit("drain");
return;
}
// always creates a new object since ws modifies it
const opts: { compress?: boolean } = {}
const opts: { compress?: boolean } = {};
if (packet.options) {
opts.compress = packet.options.compress
opts.compress = packet.options.compress;
}
const send = data => {
if (this.perMessageDeflate) {
const len =
"string" === typeof data ? Buffer.byteLength(data) : data.length
"string" === typeof data ? Buffer.byteLength(data) : data.length;
if (len < this.perMessageDeflate.threshold) {
opts.compress = false
opts.compress = false;
}
}
debug('writing "%s"', data)
this.writable = false
debug('writing "%s"', data);
this.writable = false;
this.socket.send(data, opts, err => {
if (err) return this.onError("write error", err.stack)
this.send(packets)
})
}
if (err) return this.onError("write error", err.stack);
this.send(packets);
});
};
if (packet.options && typeof packet.options.wsPreEncoded === "string") {
send(packet.options.wsPreEncoded)
send(packet.options.wsPreEncoded);
} 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
*/
doClose(fn) {
debug("closing")
this.socket.close()
fn && fn()
debug("closing");
this.socket.close();
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
// related: https://github.com/socketio/socket.io-redis-adapter/issues/418
export type Room = string
export type Room = string;
export interface BroadcastFlags {
volatile?: boolean
compress?: boolean
local?: boolean
broadcast?: boolean
binary?: boolean
timeout?: number
volatile?: boolean;
compress?: boolean;
local?: boolean;
broadcast?: boolean;
binary?: boolean;
timeout?: number;
}
export interface BroadcastOptions {
rooms: Set<Room>
except?: Set<Room>
flags?: BroadcastFlags
rooms: Set<Room>;
except?: Set<Room>;
flags?: BroadcastFlags;
}
interface SessionToPersist {
sid: SocketId;
pid: PrivateSessionId;
rooms: Room[];
data: unknown;
}
export type Session = SessionToPersist & { missedPackets: unknown[][] };
export class Adapter extends EventEmitter {
public rooms: Map<Room, Set<SocketId>> = new Map();
public sids: Map<SocketId, Set<Room>> = new Map();
private readonly encoder
private readonly encoder;
/**
* In-memory adapter constructor.
@ -31,19 +53,19 @@ export class Adapter extends EventEmitter {
* @param {Namespace} nsp
*/
constructor(readonly nsp: any) {
super()
this.encoder = nsp.server.encoder
super();
this.encoder = nsp.server.encoder;
}
/**
* To be overridden
*/
public init(): Promise<void> | void { }
public init(): Promise<void> | void {}
/**
* To be overridden
*/
public close(): Promise<void> | void { }
public close(): Promise<void> | void {}
/**
* Returns the number of Socket.IO servers in the cluster
@ -51,7 +73,7 @@ export class Adapter extends EventEmitter {
* @public
*/
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 {
if (!this.sids.has(id)) {
this.sids.set(id, new Set())
this.sids.set(id, new Set());
}
for (const room of rooms) {
this.sids.get(id).add(room)
this.sids.get(id).add(room);
if (!this.rooms.has(room)) {
this.rooms.set(room, new Set())
this.emit("create-room", room)
this.rooms.set(room, new Set());
this.emit("create-room", room);
}
if (!this.rooms.get(room).has(id)) {
this.rooms.get(room).add(id)
this.emit("join-room", room, id)
this.rooms.get(room).add(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 {
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) {
const _room = this.rooms.get(room)
const _room = this.rooms.get(room);
if (_room != null) {
const deleted = _room.delete(id)
const deleted = _room.delete(id);
if (deleted) {
this.emit("leave-room", room, id)
this.emit("leave-room", room, id);
}
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 {
if (!this.sids.has(id)) {
return
return;
}
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 broadcast(packet: any, opts: BroadcastOptions): void {
const flags = opts.flags || {}
const flags = opts.flags || {};
const packetOpts = {
preEncoded: true,
volatile: flags.volatile,
compress: flags.compress
}
compress: flags.compress,
};
packet.nsp = this.nsp.name
const encodedPackets = this.encoder.encode(packet)
packet.nsp = this.nsp.name;
const encodedPackets = this._encode(packet, packetOpts);
this.apply(opts, socket => {
this.apply(opts, (socket) => {
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,
ack: (...args: any[]) => void
) {
const flags = opts.flags || {}
const flags = opts.flags || {};
const packetOpts = {
preEncoded: true,
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)
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
clientCount++
clientCount++;
// 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") {
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.
*/
public sockets(rooms: Set<Room>): Promise<Set<SocketId>> {
const sids = new Set<SocketId>()
const sids = new Set<SocketId>();
this.apply({ rooms }, socket => {
sids.add(socket.id)
})
this.apply({ rooms }, (socket) => {
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
*/
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
*/
public fetchSockets(opts: BroadcastOptions): Promise<any[]> {
const sockets = []
const sockets = [];
this.apply(opts, socket => {
sockets.push(socket)
})
this.apply(opts, (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
*/
public addSockets(opts: BroadcastOptions, rooms: Room[]): void {
this.apply(opts, socket => {
socket.join(rooms)
})
this.apply(opts, (socket) => {
socket.join(rooms);
});
}
/**
@ -266,9 +311,9 @@ export class Adapter extends EventEmitter {
* @param rooms - the rooms to leave
*/
public delSockets(opts: BroadcastOptions, rooms: Room[]): void {
this.apply(opts, socket => {
rooms.forEach(room => socket.leave(room))
})
this.apply(opts, (socket) => {
rooms.forEach((room) => socket.leave(room));
});
}
/**
@ -278,48 +323,48 @@ export class Adapter extends EventEmitter {
* @param close - whether to close the underlying connection
*/
public disconnectSockets(opts: BroadcastOptions, close: boolean): void {
this.apply(opts, socket => {
socket.disconnect(close)
})
this.apply(opts, (socket) => {
socket.disconnect(close);
});
}
private apply(opts: BroadcastOptions, callback: (socket) => void): void {
const rooms = opts.rooms
const except = this.computeExceptSids(opts.except)
const rooms = opts.rooms;
const except = this.computeExceptSids(opts.except);
if (rooms.size) {
const ids = new Set()
const ids = new Set();
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)) {
if (ids.has(id) || except.has(id)) continue
const socket = this.nsp.sockets.get(id)
if (ids.has(id) || except.has(id)) continue;
const socket = this.nsp.sockets.get(id);
if (socket) {
callback(socket)
ids.add(id)
callback(socket);
ids.add(id);
}
}
}
} else {
for (const [id] of this.sids) {
if (except.has(id)) continue
const socket = this.nsp.sockets.get(id)
if (socket) callback(socket)
if (except.has(id)) continue;
const socket = this.nsp.sockets.get(id);
if (socket) callback(socket);
}
}
}
private computeExceptSids(exceptRooms?: Set<Room>) {
const exceptSids = new Set()
const exceptSids = new Set();
if (exceptRooms && exceptRooms.size > 0) {
for (const room of exceptRooms) {
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 {
console.warn(
"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 { Manager, ManagerOptions } from "./manager"
import { Socket, SocketOptions } from "./socket"
// import debugModule from "debug"; // debug()
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 { on } from "./on.js"
import { Backoff } from "./contrib/backo2"
import { Backoff } from "./contrib/backo2.js"
import {
DefaultEventsMap,
EventsMap,
@ -470,6 +470,10 @@ export class Manager<
this.nsps[nsp] = socket
}
if (this._autoConnect) {
socket.connect()
}
return socket
}

View File

@ -14,11 +14,69 @@ import {
// const debug = debugModule("socket.io-client:socket") // debug()
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 {
/**
* 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
volatile?: boolean
timeout?: number
fromQueue?: boolean
}
export type DisconnectDescription =
| Error
| {
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 {
@ -101,6 +160,20 @@ export class Socket<
*/
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.
*
@ -116,7 +189,11 @@ export class Socket<
* });
*/
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.
*
@ -143,8 +220,16 @@ export class Socket<
* Buffer for packets that will be sent once the socket is connected
*/
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 _opts: SocketOptions
private ids: number = 0;
private acks: object = {};
@ -163,6 +248,7 @@ export class Socket<
if (opts && opts.auth) {
this.auth = opts.auth
}
this._opts = Object.assign({}, opts)
if (this.io._autoConnect) this.open()
}
@ -296,6 +382,12 @@ export class Socket<
}
args.unshift(ev)
if (this._opts.retries && !this.flags.fromQueue && !this.flags.volatile) {
this._addToQueue(args)
return this
}
const packet: any = {
type: PacketType.EVENT,
data: args,
@ -339,7 +431,7 @@ export class Socket<
* @private
*/
private _registerAckCallback(id: number, ack: Function) {
const timeout = this.flags.timeout
const timeout = this.flags.timeout ?? this._opts.ackTimeout
if (timeout === undefined) {
this.acks[id] = ack
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.
*
@ -385,13 +593,28 @@ export class Socket<
debug("transport is open - connecting")
if (typeof this.auth == "function") {
this.auth((data) => {
this.packet({ type: PacketType.CONNECT, data })
this._sendConnectPacket(data as Record<string, unknown>)
})
} 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`.
*
@ -435,8 +658,7 @@ export class Socket<
switch (packet.type) {
case PacketType.CONNECT:
if (packet.data && packet.data.sid) {
const id = packet.data.sid
this.onconnect(id)
this.onconnect(packet.data.sid, packet.data.pid)
} else {
this.emitReserved(
"connect_error",
@ -503,6 +725,9 @@ export class Socket<
}
// @ts-ignore
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 onconnect(id: string): void {
private onconnect(id: string, pid: string) {
debug("socket connected with id %s", 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.emitBuffered()
this.emitReserved("connect")
@ -682,7 +909,9 @@ export class Socket<
*
* @returns self
*/
public timeout(timeout: number): this {
public timeout(
timeout: number
): Socket<ListenEvents, DecorateAcknowledgements<EmitEvents>> {
this.flags.timeout = timeout
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.
@ -9,37 +9,37 @@ import { isBinary } from "./is-binary.js"
*/
export function deconstructPacket(packet) {
const buffers = []
const packetData = packet.data
const pack = packet
pack.data = _deconstructPacket(packetData, buffers)
pack.attachments = buffers.length // number of binary 'attachments'
return { packet: pack, buffers: buffers }
const buffers = [];
const packetData = packet.data;
const pack = packet;
pack.data = _deconstructPacket(packetData, buffers);
pack.attachments = buffers.length; // number of binary 'attachments'
return { packet: pack, buffers: buffers };
}
function _deconstructPacket(data, buffers) {
if (!data) return data
if (!data) return data;
if (isBinary(data)) {
const placeholder = { _placeholder: true, num: buffers.length }
buffers.push(data)
return placeholder
const placeholder = { _placeholder: true, num: buffers.length };
buffers.push(data);
return placeholder;
} 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++) {
newData[i] = _deconstructPacket(data[i], buffers)
newData[i] = _deconstructPacket(data[i], buffers);
}
return newData
return newData;
} else if (typeof data === "object" && !(data instanceof Date)) {
const newData = {}
const newData = {};
for (const key in data) {
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) {
packet.data = _reconstructPacket(packet.data, buffers)
packet.attachments = undefined // no longer useful
return packet
packet.data = _reconstructPacket(packet.data, buffers);
delete packet.attachments; // no longer useful
return packet;
}
function _reconstructPacket(data, buffers) {
if (!data) return data
if (!data) return data;
if (data && data._placeholder === true) {
const isIndexValid =
typeof data.num === "number" &&
data.num >= 0 &&
data.num < buffers.length
data.num < buffers.length;
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 {
throw new Error("illegal attachments")
throw new Error("illegal attachments");
}
} else if (Array.isArray(data)) {
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") {
for (const key in data) {
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 { deconstructPacket, reconstructPacket } from "./binary.js"
import { isBinary, hasBinary } from "./is-binary.js"
import { Emitter } from "@socket.io/component-emitter";
import { deconstructPacket, reconstructPacket } from "./binary.js";
import { isBinary, hasBinary } from "./is-binary.js";
// import debugModule from "debug" // debug()
// const debug = debugModule("socket.io-parser") // debug()
const debug = require("../debug")("socket.io-client")
const debug = require("../debug")("socket.io-parser");
/**
* Protocol version.
@ -12,7 +11,7 @@ const debug = require("../debug")("socket.io-client")
* @public
*/
export const protocol: number = 5
export const protocol: number = 5;
export enum PacketType {
CONNECT,
@ -25,11 +24,11 @@ export enum PacketType {
}
export interface Packet {
type: PacketType
nsp: string
data?: any
id?: number
attachments?: number
type: PacketType;
nsp: string;
data?: any;
id?: number;
attachments?: number;
}
/**
@ -42,7 +41,7 @@ export class Encoder {
*
* @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
* buffer sequence, depending on packet type.
@ -50,18 +49,22 @@ export class Encoder {
* @param {Object} obj - packet object
*/
public encode(obj: Packet) {
debug("encoding packet %j", obj)
debug("encoding packet %j", obj);
if (obj.type === PacketType.EVENT || obj.type === PacketType.ACK) {
if (hasBinary(obj)) {
obj.type =
return this.encodeAsBinary({
type:
obj.type === PacketType.EVENT
? PacketType.BINARY_EVENT
: PacketType.BINARY_ACK
return this.encodeAsBinary(obj)
: PacketType.BINARY_ACK,
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) {
// first is type
let str = "" + obj.type
let str = "" + obj.type;
// attachments if we have them
if (
obj.type === PacketType.BINARY_EVENT ||
obj.type === PacketType.BINARY_ACK
) {
str += obj.attachments + "-"
str += obj.attachments + "-";
}
// if we have a namespace other than `/`
// we append it followed by a comma `,`
if (obj.nsp && "/" !== obj.nsp) {
str += obj.nsp + ","
str += obj.nsp + ",";
}
// immediately followed by the id
if (null != obj.id) {
str += obj.id
str += obj.id;
}
// json 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)
return str
debug("encoded %j as %s", obj, str);
return str;
}
/**
@ -107,17 +110,17 @@ export class Encoder {
*/
private encodeAsBinary(obj: Packet) {
const deconstruction = deconstructPacket(obj)
const pack = this.encodeAsString(deconstruction.packet)
const buffers = deconstruction.buffers
const deconstruction = deconstructPacket(obj);
const pack = this.encodeAsString(deconstruction.packet);
const buffers = deconstruction.buffers;
buffers.unshift(pack) // add packet info to beginning of data list
return buffers // write all the buffers
buffers.unshift(pack); // add packet info to beginning of data list
return buffers; // write all the buffers
}
}
interface DecoderReservedEvents {
decoded: (packet: Packet) => void
decoded: (packet: Packet) => void;
}
/**
@ -126,7 +129,7 @@ interface DecoderReservedEvents {
* @return {Object} decoder
*/
export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
private reconstructor: BinaryReconstructor
private reconstructor: BinaryReconstructor;
/**
* Decoder constructor
@ -134,7 +137,7 @@ export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
* @param {function} reviver - custom reviver to pass down to JSON.stringify
*/
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) {
let packet
let packet;
if (typeof obj === "string") {
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)
if (
packet.type === PacketType.BINARY_EVENT ||
packet.type === PacketType.BINARY_ACK
) {
packet = this.decodeString(obj);
const isBinaryEvent = packet.type === PacketType.BINARY_EVENT;
if (isBinaryEvent || packet.type === PacketType.BINARY_ACK) {
packet.type = isBinaryEvent ? PacketType.EVENT : PacketType.ACK;
// binary packet's json
this.reconstructor = new BinaryReconstructor(packet)
this.reconstructor = new BinaryReconstructor(packet);
// no attachments, labeled binary but no binary data to follow
if (packet.attachments === 0) {
super.emitReserved("decoded", packet)
super.emitReserved("decoded", packet);
}
} else {
// non-binary full packet
super.emitReserved("decoded", packet)
super.emitReserved("decoded", packet);
}
} else if (isBinary(obj) || obj.base64) {
// raw binary data
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 {
packet = this.reconstructor.takeBinaryData(obj)
packet = this.reconstructor.takeBinaryData(obj);
if (packet) {
// received final buffer
this.reconstructor = null
super.emitReserved("decoded", packet)
this.reconstructor = null;
super.emitReserved("decoded", packet);
}
}
} 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
*/
private decodeString(str): Packet {
let i = 0
let i = 0;
// look up type
const p: any = {
type: Number(str.charAt(0)),
}
};
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
@ -204,79 +206,79 @@ export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
p.type === PacketType.BINARY_EVENT ||
p.type === PacketType.BINARY_ACK
) {
const start = i + 1
while (str.charAt(++i) !== "-" && i != str.length) { }
const buf = str.substring(start, i)
const start = i + 1;
while (str.charAt(++i) !== "-" && i != str.length) {}
const buf = str.substring(start, 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)
if ("/" === str.charAt(i + 1)) {
const start = i + 1
const start = i + 1;
while (++i) {
const c = str.charAt(i)
if ("," === c) break
if (i === str.length) break
const c = str.charAt(i);
if ("," === c) break;
if (i === str.length) break;
}
p.nsp = str.substring(start, i)
p.nsp = str.substring(start, i);
} else {
p.nsp = "/"
p.nsp = "/";
}
// look up id
const next = str.charAt(i + 1)
const next = str.charAt(i + 1);
if ("" !== next && Number(next) == next) {
const start = i + 1
const start = i + 1;
while (++i) {
const c = str.charAt(i)
const c = str.charAt(i);
if (null == c || Number(c) != c) {
--i
break
--i;
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
if (str.charAt(++i)) {
const payload = this.tryParse(str.substr(i))
const payload = this.tryParse(str.substr(i));
if (Decoder.isPayloadValid(p.type, payload)) {
p.data = payload
p.data = payload;
} else {
throw new Error("invalid payload")
throw new Error("invalid payload");
}
}
debug("decoded %s as %j", str, p)
return p
debug("decoded %s as %j", str, p);
return p;
}
private tryParse(str) {
try {
return JSON.parse(str, this.reviver)
return JSON.parse(str, this.reviver);
} catch (e) {
return false
return false;
}
}
private static isPayloadValid(type: PacketType, payload: any): boolean {
switch (type) {
case PacketType.CONNECT:
return typeof payload === "object"
return typeof payload === "object";
case PacketType.DISCONNECT:
return payload === undefined
return payload === undefined;
case PacketType.CONNECT_ERROR:
return typeof payload === "string" || typeof payload === "object"
return typeof payload === "string" || typeof payload === "object";
case PacketType.EVENT:
case PacketType.BINARY_EVENT:
return Array.isArray(payload) && payload.length > 0
return Array.isArray(payload) && payload.length > 0;
case PacketType.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() {
if (this.reconstructor) {
this.reconstructor.finishedReconstruction()
this.reconstructor.finishedReconstruction();
this.reconstructor = null;
}
}
}
@ -300,11 +303,11 @@ export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
*/
class BinaryReconstructor {
private reconPack
private reconPack;
private buffers: Array<Buffer | ArrayBuffer> = [];
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.
*/
public takeBinaryData(binData) {
this.buffers.push(binData)
this.buffers.push(binData);
if (this.buffers.length === this.reconPack.attachments) {
// done with buffer list
const packet = reconstructPacket(this.reconPack, this.buffers)
this.finishedReconstruction()
return packet
const packet = reconstructPacket(this.reconPack, this.buffers);
this.finishedReconstruction();
return packet;
}
return null
return null;
}
/**
* Cleans up binary packet reconstruction variables.
*/
public finishedReconstruction() {
this.reconPack = null
this.buffers = []
this.reconPack = null;
this.buffers = [];
}
}

View File

@ -1,20 +1,20 @@
const withNativeArrayBuffer: boolean = typeof ArrayBuffer === "function"
const withNativeArrayBuffer: boolean = typeof ArrayBuffer === "function";
const isView = (obj: any) => {
return typeof ArrayBuffer.isView === "function"
? ArrayBuffer.isView(obj)
: obj.buffer instanceof ArrayBuffer
}
: obj.buffer instanceof ArrayBuffer;
};
const toString = Object.prototype.toString
const toString = Object.prototype.toString;
const withNativeBlob =
typeof Blob === "function" ||
(typeof Blob !== "undefined" &&
toString.call(Blob) === "[object BlobConstructor]")
toString.call(Blob) === "[object BlobConstructor]");
const withNativeFile =
typeof File === "function" ||
(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.
@ -27,25 +27,25 @@ export function isBinary(obj: any) {
(withNativeArrayBuffer && (obj instanceof ArrayBuffer || isView(obj))) ||
(withNativeBlob && obj instanceof Blob) ||
(withNativeFile && obj instanceof File)
)
);
}
export function hasBinary(obj: any, toJSON?: boolean) {
if (!obj || typeof obj !== "object") {
return false
return false;
}
if (Array.isArray(obj)) {
for (let i = 0, l = obj.length; i < l; i++) {
if (hasBinary(obj[i])) {
return true
return true;
}
}
return false
return false;
}
if (isBinary(obj)) {
return true
return true;
}
if (
@ -53,14 +53,14 @@ export function hasBinary(obj: any, toJSON?: boolean) {
typeof obj.toJSON === "function" &&
arguments.length === 1
) {
return hasBinary(obj.toJSON(), true)
return hasBinary(obj.toJSON(), true);
}
for (const key in obj) {
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 { 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 type { Adapter } from "../socket.io-adapter"
import type { BroadcastFlags, Room, SocketId } from "../socket.io-adapter";
import { Handshake, RESERVED_EVENTS, Socket } from "./socket";
import { PacketType } from "../socket.io-parser";
import type { Adapter } from "../socket.io-adapter";
import type {
EventParams,
EventNames,
EventsMap,
TypedEventBroadcaster,
} from "./typed-events"
DecorateAcknowledgements,
DecorateAcknowledgementsWithTimeoutAndMultipleResponses,
AllButLast,
Last,
SecondArg,
} from "./typed-events";
export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
implements TypedEventBroadcaster<EmitEvents>
@ -19,8 +21,10 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
private readonly adapter: Adapter,
private readonly rooms: 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.
@ -39,18 +43,18 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
* @return a new {@link BroadcastOperator} instance for chaining
*/
public to(room: Room | Room[]) {
const rooms = new Set(this.rooms)
const rooms = new Set(this.rooms);
if (Array.isArray(room)) {
room.forEach((r) => rooms.add(r))
room.forEach((r) => rooms.add(r));
} else {
rooms.add(room)
rooms.add(room);
}
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
rooms,
this.exceptRooms,
this.flags
)
);
}
/**
@ -64,7 +68,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
* @return a new {@link BroadcastOperator} instance for chaining
*/
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
*/
public except(room: Room | Room[]) {
const exceptRooms = new Set(this.exceptRooms)
const exceptRooms = new Set(this.exceptRooms);
if (Array.isArray(room)) {
room.forEach((r) => exceptRooms.add(r))
room.forEach((r) => exceptRooms.add(r));
} else {
exceptRooms.add(room)
exceptRooms.add(room);
}
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
exceptRooms,
this.flags
)
);
}
/**
@ -108,13 +112,13 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
* @return a new BroadcastOperator instance
*/
public compress(compress: boolean) {
const flags = Object.assign({}, this.flags, { compress })
const flags = Object.assign({}, this.flags, { compress });
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
this.exceptRooms,
flags
)
);
}
/**
@ -128,13 +132,13 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
* @return a new BroadcastOperator instance
*/
public get volatile() {
const flags = Object.assign({}, this.flags, { volatile: true })
const flags = Object.assign({}, this.flags, { volatile: true });
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
this.exceptRooms,
flags
)
);
}
/**
@ -147,13 +151,13 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
* @return a new {@link BroadcastOperator} instance for chaining
*/
public get local() {
const flags = Object.assign({}, this.flags, { local: true })
const flags = Object.assign({}, this.flags, { local: true });
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
this.exceptRooms,
flags
)
);
}
/**
@ -171,13 +175,11 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
* @param timeout
*/
public timeout(timeout: number) {
const flags = Object.assign({}, this.flags, { timeout })
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
this.exceptRooms,
flags
)
const flags = Object.assign({}, this.flags, { timeout });
return new BroadcastOperator<
DecorateAcknowledgementsWithTimeoutAndMultipleResponses<EmitEvents>,
SocketData
>(this.adapter, this.rooms, this.exceptRooms, flags);
}
/**
@ -206,39 +208,42 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
...args: EventParams<EmitEvents, Ev>
): boolean {
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
const data = [ev, ...args]
const data = [ev, ...args];
const packet = {
type: PacketType.EVENT,
data: data,
}
};
const withAck = typeof data[data.length - 1] === "function"
const withAck = typeof data[data.length - 1] === "function";
if (!withAck) {
this.adapter.broadcast(packet, {
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
})
});
return true
return true;
}
const ack = data.pop() as (...args: any[]) => void
let timedOut = false
let responses: any[] = []
const ack = data.pop() as (...args: any[]) => void;
let timedOut = false;
let responses: any[] = [];
const timer = setTimeout(() => {
timedOut = true
ack.apply(this, [new Error("operation has timed out"), responses])
}, this.flags.timeout)
timedOut = true;
ack.apply(this, [
new Error("operation has timed out"),
this.flags.expectSingleResponse ? null : responses,
]);
}, this.flags.timeout);
let expectedServerCount = -1
let actualServerCount = 0
let expectedClientCount = 0
let expectedServerCount = -1;
let actualServerCount = 0;
let expectedClientCount = 0;
const checkCompleteness = () => {
if (
@ -246,10 +251,13 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
expectedServerCount === actualServerCount &&
responses.length === expectedClientCount
) {
clearTimeout(timer)
ack.apply(this, [null, responses])
}
clearTimeout(timer);
ack.apply(this, [
null,
this.flags.expectSingleResponse ? null : responses,
]);
}
};
this.adapter.broadcastWithAck(
packet,
@ -260,23 +268,53 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
},
(clientCount) => {
// each Socket.IO server in the cluster sends the number of clients that were notified
expectedClientCount += clientCount
actualServerCount++
checkCompleteness()
expectedClientCount += clientCount;
actualServerCount++;
checkCompleteness();
},
(clientResponse) => {
// each client sends an acknowledgement
responses.push(clientResponse)
checkCompleteness()
responses.push(clientResponse);
checkCompleteness();
}
)
);
this.adapter.serverCount().then((serverCount) => {
expectedServerCount = serverCount
checkCompleteness()
})
expectedServerCount = serverCount;
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) {
throw new Error(
"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) => {
if (socket instanceof Socket) {
// 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 {
return new RemoteSocket(
this.adapter,
socket as SocketDetails<SocketData>
)
);
}
})
})
});
});
}
/**
@ -363,7 +401,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
flags: this.flags,
},
Array.isArray(room) ? room : [room]
)
);
}
/**
@ -388,7 +426,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
flags: this.flags,
},
Array.isArray(room) ? room : [room]
)
);
}
/**
@ -413,7 +451,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
flags: this.flags,
},
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
*/
interface SocketDetails<SocketData> {
id: SocketId
handshake: Handshake
rooms: Room[]
data: SocketData
id: SocketId;
handshake: Handshake;
rooms: Room[];
data: SocketData;
}
/**
@ -433,29 +471,63 @@ interface SocketDetails<SocketData> {
export class RemoteSocket<EmitEvents extends EventsMap, SocketData>
implements TypedEventBroadcaster<EmitEvents>
{
public readonly id: SocketId
public readonly handshake: Handshake
public readonly rooms: Set<Room>
public readonly data: SocketData
public readonly id: SocketId;
public readonly handshake: Handshake;
public readonly rooms: Set<Room>;
public readonly data: SocketData;
private readonly operator: BroadcastOperator<EmitEvents, SocketData>
private readonly operator: BroadcastOperator<EmitEvents, SocketData>;
constructor(adapter: Adapter, details: SocketDetails<SocketData>) {
this.id = details.id
this.handshake = details.handshake
this.rooms = new Set(details.rooms)
this.data = details.data
this.id = details.id;
this.handshake = details.handshake;
this.rooms = new Set(details.rooms);
this.data = details.data;
this.operator = new BroadcastOperator<EmitEvents, SocketData>(
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>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): 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
*/
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
*/
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
*/
public disconnect(close = false): this {
this.operator.disconnectSockets(close)
return this
this.operator.disconnectSockets(close);
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 url = require("url")
import url = require("url");
// import type { IncomingMessage } from "http"
import type { Server } from "./index"
import type { Namespace } from "./namespace"
import type { EventsMap } from "./typed-events"
import type { Socket } from "./socket"
// import type { SocketId } from "socket.io-adapter"
import type { SocketId } from "../socket.io-adapter"
import type { Socket as RawSocket } from '../engine.io/socket'
import type { Server } from "./index";
import type { Namespace } from "./namespace";
import type { EventsMap } from "./typed-events";
import type { Socket } from "./socket";
import type { SocketId } from "../socket.io-adapter";
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 {
compress?: boolean
volatile?: boolean
preEncoded?: boolean
wsPreEncoded?: string
compress?: boolean;
volatile?: boolean;
preEncoded?: boolean;
wsPreEncoded?: string;
}
type CloseReason =
@ -26,7 +23,7 @@ type CloseReason =
| "transport close"
| "forced close"
| "ping timeout"
| "parse error"
| "parse error";
export class Client<
ListenEvents extends EventsMap,
@ -34,17 +31,17 @@ export class Client<
ServerSideEvents extends EventsMap,
SocketData = any
> {
public readonly conn: RawSocket
public readonly conn: RawSocket;
private readonly id: string
private readonly id: string;
private readonly server: Server<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>
private readonly encoder: Encoder
private readonly decoder: Decoder
>;
private readonly encoder: Encoder;
private readonly decoder: Decoder;
private sockets: Map<
SocketId,
Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
@ -53,7 +50,7 @@ export class Client<
string,
Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map();
private connectTimeout?: NodeJS.Timeout
private connectTimeout?: NodeJS.Timeout;
/**
* Client constructor.
@ -66,12 +63,12 @@ export class Client<
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
conn: any
) {
this.server = server
this.conn = conn
this.encoder = server.encoder
this.decoder = new server._parser.Decoder()
this.id = conn.id
this.setup()
this.server = server;
this.conn = conn;
this.encoder = server.encoder;
this.decoder = new server._parser.Decoder();
this.id = conn.id;
this.setup();
}
/**
@ -81,7 +78,7 @@ export class Client<
*/
// public get request(): IncomingMessage {
public get request(): any {
return this.conn.request
return this.conn.request;
}
/**
@ -90,25 +87,25 @@ export class Client<
* @private
*/
private setup() {
this.onclose = this.onclose.bind(this)
this.ondata = this.ondata.bind(this)
this.onerror = this.onerror.bind(this)
this.ondecoded = this.ondecoded.bind(this)
this.onclose = this.onclose.bind(this);
this.ondata = this.ondata.bind(this);
this.onerror = this.onerror.bind(this);
this.ondecoded = this.ondecoded.bind(this);
// @ts-ignore
this.decoder.on("decoded", this.ondecoded)
this.conn.on("data", this.ondata)
this.conn.on("error", this.onerror)
this.conn.on("close", this.onclose)
this.decoder.on("decoded", this.ondecoded);
this.conn.on("data", this.ondata);
this.conn.on("error", this.onerror);
this.conn.on("close", this.onclose);
this.connectTimeout = setTimeout(() => {
if (this.nsps.size === 0) {
debug("no namespace joined yet, close the client")
this.close()
debug("no namespace joined yet, close the client");
this.close();
} 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
* @private
*/
private connect(name: string, auth: object = {}): void {
private connect(name: string, auth: Record<string, unknown> = {}): void {
if (this.server._nsps.has(name)) {
debug("connecting to namespace %s", name)
return this.doConnect(name, auth)
debug("connecting to namespace %s", name);
return this.doConnect(name, auth);
}
this.server._checkNamespace(
@ -133,19 +130,19 @@ export class Client<
| false
) => {
if (dynamicNspName) {
this.doConnect(name, auth)
this.doConnect(name, auth);
} else {
debug("creation of namespace %s was denied", name)
debug("creation of namespace %s was denied", name);
this._packet({
type: PacketType.CONNECT_ERROR,
nsp: name,
data: {
message: "Invalid namespace",
},
})
});
}
}
)
);
}
/**
@ -156,19 +153,19 @@ export class Client<
*
* @private
*/
private doConnect(name: string, auth: object): void {
const nsp = this.server.of(name)
private doConnect(name: string, auth: Record<string, unknown>): void {
const nsp = this.server.of(name);
// @java-patch multi thread need direct callback socket
const socket = nsp._add(this, auth, (socket) => {
this.sockets.set(socket.id, socket)
this.nsps.set(nsp.name, socket)
this.sockets.set(socket.id, socket);
this.nsps.set(nsp.name, socket);
if (this.connectTimeout) {
clearTimeout(this.connectTimeout)
this.connectTimeout = undefined
clearTimeout(this.connectTimeout);
this.connectTimeout = undefined;
}
})
});
}
/**
@ -178,10 +175,10 @@ export class Client<
*/
_disconnect(): void {
for (const socket of this.sockets.values()) {
socket.disconnect()
socket.disconnect();
}
this.sockets.clear()
this.close()
this.sockets.clear();
this.close();
}
/**
@ -193,11 +190,11 @@ export class Client<
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
): void {
if (this.sockets.has(socket.id)) {
const nsp = this.sockets.get(socket.id)!.nsp.name
this.sockets.delete(socket.id)
this.nsps.delete(nsp)
const nsp = this.sockets.get(socket.id)!.nsp.name;
this.sockets.delete(socket.id);
this.nsps.delete(nsp);
} 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 {
if ("open" === this.conn.readyState) {
debug("forcing transport close")
this.conn.close()
this.onclose("forced server close")
debug("forcing transport close");
this.conn.close();
this.onclose("forced server close");
}
}
@ -223,30 +220,30 @@ export class Client<
*/
_packet(packet: Packet | any[], opts: WriteOptions = {}): void {
if (this.conn.readyState !== "open") {
debug("ignoring packet write %j", packet)
return
debug("ignoring packet write %j", packet);
return;
}
const encodedPackets = opts.preEncoded
? (packet as any[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine()
: this.encoder.encode(packet as Packet)
this.writeToEngine(encodedPackets, opts)
: this.encoder.encode(packet as Packet);
this.writeToEngine(encodedPackets, opts);
}
private writeToEngine(
encodedPackets: Array<String | Buffer>,
encodedPackets: Array<string | Buffer>,
opts: WriteOptions
): void {
if (opts.volatile && !this.conn.transport.writable) {
debug(
"volatile packet is discarded since the transport is not currently writable"
)
return
);
return;
}
const packets = Array.isArray(encodedPackets)
? encodedPackets
: [encodedPackets]
: [encodedPackets];
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 {
// try/catch is needed for protocol violations (GH-1880)
try {
this.decoder.add(data)
this.decoder.add(data);
} catch (e) {
debug("invalid packet format")
this.onerror(e)
debug("invalid packet format");
this.onerror(e);
}
}
@ -271,31 +268,31 @@ export class Client<
* @private
*/
private ondecoded(packet: Packet): void {
let namespace: string
let authPayload
let namespace: string;
let authPayload: Record<string, unknown>;
if (this.conn.protocol === 3) {
const parsed = url.parse(packet.nsp, true)
namespace = parsed.pathname!
authPayload = parsed.query
const parsed = url.parse(packet.nsp, true);
namespace = parsed.pathname!;
authPayload = parsed.query;
} else {
namespace = packet.nsp
authPayload = packet.data
namespace = packet.nsp;
authPayload = packet.data;
}
const socket = this.nsps.get(namespace)
const socket = this.nsps.get(namespace);
if (!socket && packet.type === PacketType.CONNECT) {
this.connect(namespace, authPayload)
this.connect(namespace, authPayload);
} else if (
socket &&
packet.type !== PacketType.CONNECT &&
packet.type !== PacketType.CONNECT_ERROR
) {
process.nextTick(function () {
socket._onpacket(packet)
})
socket._onpacket(packet);
});
} else {
debug("invalid state (packet type: %s)", packet.type)
this.close()
debug("invalid state (packet type: %s)", packet.type);
this.close();
}
}
@ -307,30 +304,34 @@ export class Client<
*/
private onerror(err): void {
for (const socket of this.sockets.values()) {
socket._onerror(err)
socket._onerror(err);
}
this.conn.close()
this.conn.close();
}
/**
* Called upon transport close.
*
* @param reason
* @param description
* @private
*/
private onclose(reason: CloseReason | "forced server close"): void {
debug("client close with reason %s", reason)
private onclose(
reason: CloseReason | "forced server close",
description?: any
): void {
debug("client close with reason %s", reason);
// ignore a potential subsequent `close` event
this.destroy()
this.destroy();
// `nsps` and `sockets` are cleaned up seamlessly
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 destroy(): void {
this.conn.removeListener("data", this.ondata)
this.conn.removeListener("error", this.onerror)
this.conn.removeListener("close", this.onclose)
this.conn.removeListener("data", this.ondata);
this.conn.removeListener("error", this.onerror);
this.conn.removeListener("close", this.onclose);
// @ts-ignore
this.decoder.removeListener("decoded", this.ondecoded)
this.decoder.removeListener("decoded", this.ondecoded);
if (this.connectTimeout) {
clearTimeout(this.connectTimeout)
this.connectTimeout = undefined
clearTimeout(this.connectTimeout);
this.connectTimeout = undefined;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,26 @@
import { Socket } from "./socket"
import type { Server } from "./index"
import { Socket } from "./socket";
import type { Server } from "./index";
import {
EventParams,
EventNames,
EventsMap,
StrictEventEmitter,
DefaultEventsMap,
} from "./typed-events"
import type { Client } from "./client"
DecorateAcknowledgementsWithTimeoutAndMultipleResponses,
AllButLast,
Last,
FirstArg,
SecondArg,
} from "./typed-events";
import type { Client } from "./client";
// import debugModule from "debug"
// import type { Adapter, Room, SocketId } from "socket.io-adapter"
import type { Adapter, Room, SocketId } from "../socket.io-adapter"
import { BroadcastOperator, RemoteSocket } from "./broadcast-operator"
import type { Adapter, Room, SocketId } from "../socket.io-adapter";
import { BroadcastOperator } 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 {
data?: any
data?: any;
}
export interface NamespaceReservedEventsMap<
@ -28,10 +31,10 @@ export interface NamespaceReservedEventsMap<
> {
connect: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void
) => void;
connection: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void
) => void;
}
export interface ServerReservedEventsMap<
@ -44,15 +47,15 @@ export interface ServerReservedEventsMap<
EmitEvents,
ServerSideEvents,
SocketData
> {
> {
new_namespace: (
namespace: Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void
) => void;
}
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
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
@ -122,13 +125,13 @@ export class Namespace<
SocketData
>
> {
public readonly name: string
public readonly name: string;
public readonly sockets: Map<
SocketId,
Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map();
public adapter: Adapter
public adapter: Adapter;
/** @private */
readonly server: Server<
@ -136,7 +139,7 @@ export class Namespace<
EmitEvents,
ServerSideEvents,
SocketData
>
>;
/** @private */
_fns: Array<
@ -159,10 +162,10 @@ export class Namespace<
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
name: string
) {
super()
this.server = server
this.name = name
this._initAdapter()
super();
this.server = server;
this.name = name;
this._initAdapter();
}
/**
@ -174,7 +177,7 @@ export class Namespace<
*/
_initAdapter(): void {
// @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
) => void
): this {
this._fns.push(fn)
return this
this._fns.push(fn);
return this;
}
/**
@ -211,23 +214,23 @@ export class Namespace<
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
fn: (err: ExtendedError | null) => void
) {
const fns = this._fns.slice(0)
if (!fns.length) return fn(null)
const fns = this._fns.slice(0);
if (!fns.length) return fn(null);
function run(i: number) {
fns[i](socket, function (err) {
// upon error, short-circuit
if (err) return fn(err)
if (err) return fn(err);
// if no middleware left, summon callback
if (!fns[i + 1]) return fn(null)
if (!fns[i + 1]) return fn(null);
// 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
*/
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
*/
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[]) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).except(
room
)
);
}
/**
@ -298,52 +301,95 @@ export class Namespace<
* @return {Socket}
* @private
*/
// @java-patch sync
_add(
client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
query,
fn?: (socket: Socket) => void
): Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
debug("adding socket to nsp %s", this.name)
const socket = new Socket(this, client, query)
auth: Record<string, unknown>,
fn: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void
) {
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) => {
process.nextTick(() => {
if ("open" !== client.conn.readyState) {
debug("next called after client was closed - ignoring socket")
socket._cleanup()
return
debug("next called after client was closed - ignoring socket");
socket._cleanup();
return;
}
if (err) {
debug("middleware error, sending CONNECT_ERROR packet to the client")
socket._cleanup()
debug("middleware error, sending CONNECT_ERROR packet to the client");
socket._cleanup();
if (client.conn.protocol === 3) {
return socket._error(err.data || err.message)
return socket._error(err.data || err.message);
} else {
return socket._error({
message: err.message,
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
this.sockets.set(socket.id, socket)
this.sockets.set(socket.id, socket);
// it's paramount that the internal `onconnect` logic
// fires before user-set events to prevent state order
// violations (such as a disconnection before the connection
// logic is complete)
socket._onconnect()
// if (fn) fn()
// @java-patch multi thread need direct callback socket
if (fn) fn(socket)
socket._onconnect();
if (fn) fn(socket);
// fire user-set events
this.emitReserved("connect", socket)
this.emitReserved("connection", socket)
})
})
return socket
this.emitReserved("connect", socket);
this.emitReserved("connection", socket);
}
/**
@ -355,9 +401,9 @@ export class Namespace<
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
): void {
if (this.sockets.has(socket.id)) {
this.sockets.delete(socket.id)
this.sockets.delete(socket.id);
} 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(
ev,
...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
*/
public send(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args)
return this
this.emit("message", ...args);
return this;
}
/**
@ -421,8 +491,8 @@ export class Namespace<
* @return self
*/
public write(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args)
return this
this.emit("message", ...args);
return this;
}
/**
@ -440,9 +510,9 @@ export class Namespace<
* // acknowledgements (without binary content) are supported too:
* myNamespace.serverSideEmit("ping", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* // some servers did not acknowledge the event in the given delay
* } 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>>(
ev: Ev,
...args: EventParams<ServerSideEvents, Ev>
...args: EventParams<
DecorateAcknowledgementsWithTimeoutAndMultipleResponses<ServerSideEvents>,
Ev
>
): boolean {
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)
this.adapter.serverSideEmit(args)
return true
args.unshift(ev);
this.adapter.serverSideEmit(args);
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
*/
_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>> {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).allSockets()
).allSockets();
}
/**
@ -502,7 +613,7 @@ export class Namespace<
public compress(compress: boolean) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).compress(
compress
)
);
}
/**
@ -518,7 +629,7 @@ export class Namespace<
* @return self
*/
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
*/
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) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).timeout(
timeout
)
);
}
/**
@ -587,7 +698,7 @@ export class Namespace<
public fetchSockets() {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).fetchSockets()
).fetchSockets();
}
/**
@ -609,7 +720,7 @@ export class Namespace<
public socketsJoin(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).socketsJoin(room)
).socketsJoin(room);
}
/**
@ -631,7 +742,7 @@ export class Namespace<
public socketsLeave(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).socketsLeave(room)
).socketsLeave(room);
}
/**
@ -653,14 +764,16 @@ export class Namespace<
public disconnectSockets(close: boolean = false) {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).disconnectSockets(close)
).disconnectSockets(close);
}
// @java-patch
public close() {
RESERVED_EVENTS.forEach(event => this.removeAllListeners(event as any))
this.server._nsps.delete(this.name)
RESERVED_EVENTS.forEach((event) => this.removeAllListeners(event as any));
this.server._nsps.delete(this.name);
// @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 type { Server, RemoteSocket } from "./index"
import { Namespace } from "./namespace";
import type { Server, RemoteSocket } from "./index";
import type {
EventParams,
EventNames,
EventsMap,
DefaultEventsMap,
} from "./typed-events"
// import type { BroadcastOptions } from "socket.io-adapter"
import type { BroadcastOptions } from "../socket.io-adapter"
} from "./typed-events";
import type { BroadcastOptions } from "../socket.io-adapter";
// import debugModule from "debug";
const debug = require("../debug")("socket.io:parent-namespace");
export class ParentNamespace<
ListenEvents extends EventsMap = DefaultEventsMap,
@ -23,7 +25,7 @@ export class ParentNamespace<
constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) {
super(server, "/_" + ParentNamespace.count++)
super(server, "/_" + ParentNamespace.count++);
}
/**
@ -32,11 +34,11 @@ export class ParentNamespace<
_initAdapter(): void {
const broadcast = (packet: any, opts: BroadcastOptions) => {
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?
this.adapter = { broadcast }
this.adapter = { broadcast };
}
public emit<Ev extends EventNames<EmitEvents>>(
@ -44,26 +46,42 @@ export class ParentNamespace<
...args: EventParams<EmitEvents, Ev>
): boolean {
this.children.forEach((nsp) => {
nsp.emit(ev, ...args)
})
nsp.emit(ev, ...args);
});
return true
return true;
}
createChild(
name: string
): Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
const namespace = new Namespace(this.server, name)
namespace._fns = this._fns.slice(0)
debug("creating child namespace %s", name);
const namespace = new Namespace(this.server, name);
namespace._fns = this._fns.slice(0);
this.listeners("connect").forEach((listener) =>
namespace.on("connect", listener)
)
);
this.listeners("connection").forEach((listener) =>
namespace.on("connection", listener)
)
this.children.add(namespace)
this.server._nsps.set(name, namespace)
return namespace
);
this.children.add(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>[]> {
@ -72,6 +90,6 @@ export class ParentNamespace<
// the behavior for namespaces created with a function is less clear
// note²: we cannot loop over each children namespace, because with multiple Socket.IO servers, a given namespace
// may exist on one node but not exist on another (since it is created upon client connection)
throw new Error("fetchSockets() is not supported on parent namespaces")
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
* represents the type of the `on` listener.
*/
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.
*/
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.
*/
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 */
export type EventParams<
Map extends EventsMap,
Ev extends EventNames<Map>
> = Parameters<Map[Ev]>
> = Parameters<Map[Ev]>;
/**
* The event names that are either in ReservedEvents or in UserEvents
@ -33,7 +33,7 @@ export type EventParams<
export type ReservedOrUserEventNames<
ReservedEventsMap 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
@ -43,13 +43,13 @@ export type ReservedOrUserListener<
ReservedEvents extends EventsMap,
UserEvents extends EventsMap,
Ev extends ReservedOrUserEventNames<ReservedEvents, UserEvents>
> = FallbackToUntypedListener<
> = FallbackToUntypedListener<
Ev extends EventNames<ReservedEvents>
? ReservedEvents[Ev]
: Ev extends EventNames<UserEvents>
? UserEvents[Ev]
: never
>
>;
/**
* Returns an untyped listener type if `T` is `never`; otherwise, returns `T`.
@ -59,7 +59,7 @@ export type ReservedOrUserListener<
*/
type FallbackToUntypedListener<T> = [T] extends [never]
? (...args: any[]) => void
: T
: T;
/**
* 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>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean
): boolean;
}
/**
@ -103,7 +103,7 @@ export abstract class StrictEventEmitter<
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this {
return super.on(ev, listener)
return super.on(ev, listener);
}
/**
@ -116,7 +116,7 @@ export abstract class StrictEventEmitter<
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this {
return super.once(ev, listener)
return super.once(ev, listener);
}
/**
@ -129,7 +129,7 @@ export abstract class StrictEventEmitter<
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
return super.emit(ev, ...args)
return super.emit(ev, ...args);
}
/**
@ -145,7 +145,7 @@ export abstract class StrictEventEmitter<
ev: Ev,
...args: EventParams<ReservedEvents, Ev>
): 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
*/
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,
ListenEvents,
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];
};