feat: 更新WebSocket 优化逻辑
Signed-off-by: MiaoWoo <admin@yumc.pw>
This commit is contained in:
parent
7d02194ac7
commit
b36b63277f
@ -23,7 +23,7 @@ export class WebSocketManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const managers = new WebSocketManager()
|
export const manager = new WebSocketManager()
|
||||||
|
|
||||||
export class WebSocket extends EventEmitter {
|
export class WebSocket extends EventEmitter {
|
||||||
public static CONNECTING = 0
|
public static CONNECTING = 0
|
||||||
@ -31,6 +31,7 @@ export class WebSocket extends EventEmitter {
|
|||||||
public static CLOSING = 2
|
public static CLOSING = 2
|
||||||
public static CLOSED = 3
|
public static CLOSED = 3
|
||||||
public binaryType: 'blob' | 'arraybuffer'
|
public binaryType: 'blob' | 'arraybuffer'
|
||||||
|
protected manager: WebSocketManager
|
||||||
|
|
||||||
protected _url: string
|
protected _url: string
|
||||||
protected _headers: WebSocketHeader = {}
|
protected _headers: WebSocketHeader = {}
|
||||||
@ -39,6 +40,7 @@ export class WebSocket extends EventEmitter {
|
|||||||
|
|
||||||
constructor(url: string, subProtocol: string = '', headers: WebSocketHeader = {}) {
|
constructor(url: string, subProtocol: string = '', headers: WebSocketHeader = {}) {
|
||||||
super()
|
super()
|
||||||
|
this.manager = manager
|
||||||
this._url = url
|
this._url = url
|
||||||
this._headers = headers
|
this._headers = headers
|
||||||
try {
|
try {
|
||||||
@ -51,12 +53,12 @@ export class WebSocket extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this.client.on('open', (event) => {
|
this.client.on('open', (event) => {
|
||||||
this.onopen?.(event)
|
this.onopen?.(event)
|
||||||
managers.add(this)
|
manager.add(this)
|
||||||
})
|
})
|
||||||
this.client.on('message', (event) => this.onmessage?.(event))
|
this.client.on('message', (event) => this.onmessage?.(event))
|
||||||
this.client.on('close', (event) => {
|
this.client.on('close', (event) => {
|
||||||
this.onclose?.(event)
|
this.onclose?.(event)
|
||||||
managers.del(this)
|
manager.del(this)
|
||||||
})
|
})
|
||||||
this.client.on('error', (event) => this.onerror?.(event))
|
this.client.on('error', (event) => this.onerror?.(event))
|
||||||
setTimeout(() => this.client.connect(), 20)
|
setTimeout(() => this.client.connect(), 20)
|
||||||
|
21
packages/websocket/src/engine.io-parser/commons.ts
Normal file
21
packages/websocket/src/engine.io-parser/commons.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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)
|
||||||
|
Object.keys(PACKET_TYPES).forEach(key => {
|
||||||
|
PACKET_TYPES_REVERSE[PACKET_TYPES[key]] = key
|
||||||
|
})
|
||||||
|
|
||||||
|
const ERROR_PACKET = { type: "error", data: "parser error" }
|
||||||
|
|
||||||
|
export = {
|
||||||
|
PACKET_TYPES,
|
||||||
|
PACKET_TYPES_REVERSE,
|
||||||
|
ERROR_PACKET
|
||||||
|
}
|
48
packages/websocket/src/engine.io-parser/decodePacket.ts
Normal file
48
packages/websocket/src/engine.io-parser/decodePacket.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
const { PACKET_TYPES_REVERSE, ERROR_PACKET } = require("./commons")
|
||||||
|
|
||||||
|
export const decodePacket = (encodedPacket, binaryType) => {
|
||||||
|
if (typeof encodedPacket !== "string") {
|
||||||
|
return {
|
||||||
|
type: "message",
|
||||||
|
data: mapBinary(encodedPacket, binaryType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const type = encodedPacket.charAt(0)
|
||||||
|
if (type === "b") {
|
||||||
|
const buffer = Buffer.from(encodedPacket.substring(1), "base64")
|
||||||
|
return {
|
||||||
|
type: "message",
|
||||||
|
data: mapBinary(buffer, binaryType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!PACKET_TYPES_REVERSE[type]) {
|
||||||
|
return ERROR_PACKET
|
||||||
|
}
|
||||||
|
return encodedPacket.length > 1
|
||||||
|
? {
|
||||||
|
type: PACKET_TYPES_REVERSE[type],
|
||||||
|
data: encodedPacket.substring(1)
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: PACKET_TYPES_REVERSE[type]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapBinary = (data, binaryType) => {
|
||||||
|
switch (binaryType) {
|
||||||
|
case "arraybuffer":
|
||||||
|
return Buffer.isBuffer(data) ? toArrayBuffer(data) : data
|
||||||
|
case "nodebuffer":
|
||||||
|
default:
|
||||||
|
return data // assuming the data is already a Buffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toArrayBuffer = buffer => {
|
||||||
|
const arrayBuffer = new ArrayBuffer(buffer.length)
|
||||||
|
const view = new Uint8Array(arrayBuffer)
|
||||||
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
view[i] = buffer[i]
|
||||||
|
}
|
||||||
|
return arrayBuffer
|
||||||
|
}
|
26
packages/websocket/src/engine.io-parser/encodePacket.ts
Normal file
26
packages/websocket/src/engine.io-parser/encodePacket.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
const { PACKET_TYPES } = require("./commons")
|
||||||
|
|
||||||
|
export const encodePacket = ({ type, data }, supportsBinary, callback) => {
|
||||||
|
console.trace('encodePacket', type, JSON.stringify(data))
|
||||||
|
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
||||||
|
const buffer = toBuffer(data)
|
||||||
|
return callback(encodeBuffer(buffer, supportsBinary))
|
||||||
|
}
|
||||||
|
// plain string
|
||||||
|
return callback(PACKET_TYPES[type] + (data || ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
const toBuffer = data => {
|
||||||
|
if (Buffer.isBuffer(data)) {
|
||||||
|
return data
|
||||||
|
} else if (data instanceof ArrayBuffer) {
|
||||||
|
return Buffer.from(data)
|
||||||
|
} else {
|
||||||
|
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, supportsBinary) => {
|
||||||
|
return supportsBinary ? data : "b" + data.toString("base64")
|
||||||
|
}
|
42
packages/websocket/src/engine.io-parser/index.ts
Normal file
42
packages/websocket/src/engine.io-parser/index.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { encodePacket } from "./encodePacket"
|
||||||
|
import { decodePacket } from "./decodePacket"
|
||||||
|
|
||||||
|
const SEPARATOR = String.fromCharCode(30) // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
|
||||||
|
|
||||||
|
const encodePayload = (packets, callback) => {
|
||||||
|
// 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
|
||||||
|
|
||||||
|
packets.forEach((packet, i) => {
|
||||||
|
// force base64 encoding for binary packets
|
||||||
|
encodePacket(packet, false, encodedPacket => {
|
||||||
|
encodedPackets[i] = encodedPacket
|
||||||
|
if (++count === length) {
|
||||||
|
callback(encodedPackets.join(SEPARATOR))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodePayload = (encodedPayload, binaryType) => {
|
||||||
|
const encodedPackets = encodedPayload.split(SEPARATOR)
|
||||||
|
const packets = []
|
||||||
|
for (let i = 0; i < encodedPackets.length; i++) {
|
||||||
|
const decodedPacket = decodePacket(encodedPackets[i], binaryType)
|
||||||
|
packets.push(decodedPacket)
|
||||||
|
if (decodedPacket.type === "error") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return packets
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
protocol: 4,
|
||||||
|
encodePacket,
|
||||||
|
encodePayload,
|
||||||
|
decodePacket,
|
||||||
|
decodePayload
|
||||||
|
}
|
27
packages/websocket/src/engine.io/index.ts
Normal file
27
packages/websocket/src/engine.io/index.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as server from "../server"
|
||||||
|
// const http = require("http")
|
||||||
|
// const Server = require("./server")
|
||||||
|
import { Server } from './server'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures upgrade requests for a http.Server.
|
||||||
|
*
|
||||||
|
* @param {http.Server} server
|
||||||
|
* @param {Object} options
|
||||||
|
* @return {Server} engine server
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
|
||||||
|
function attach(srv, options) {
|
||||||
|
const engine = new Server(options)
|
||||||
|
engine.attach(server.attach(srv, options), options)
|
||||||
|
return engine
|
||||||
|
}
|
||||||
|
|
||||||
|
export = {
|
||||||
|
attach
|
||||||
|
}
|
690
packages/websocket/src/engine.io/server.ts
Normal file
690
packages/websocket/src/engine.io/server.ts
Normal file
@ -0,0 +1,690 @@
|
|||||||
|
const qs = require("querystring")
|
||||||
|
const parse = require("url").parse
|
||||||
|
// const base64id = require("base64id")
|
||||||
|
import transports from './transports'
|
||||||
|
import { EventEmitter } from 'events'
|
||||||
|
// const EventEmitter = require("events").EventEmitter
|
||||||
|
import { Socket } from './socket'
|
||||||
|
// const debug = require("debug")("engine")
|
||||||
|
const debug = function (...args) { }
|
||||||
|
// const cookieMod = require("cookie")
|
||||||
|
|
||||||
|
// const DEFAULT_WS_ENGINE = require("ws").Server;
|
||||||
|
import { WebSocketServer } from '../server'
|
||||||
|
import { Transport } from './transport'
|
||||||
|
const DEFAULT_WS_ENGINE = WebSocketServer
|
||||||
|
|
||||||
|
import { Request } from '../server/request'
|
||||||
|
import { WebSocketClient } from '../server/client'
|
||||||
|
|
||||||
|
export class Server extends EventEmitter {
|
||||||
|
public static errors = {
|
||||||
|
UNKNOWN_TRANSPORT: 0,
|
||||||
|
UNKNOWN_SID: 1,
|
||||||
|
BAD_HANDSHAKE_METHOD: 2,
|
||||||
|
BAD_REQUEST: 3,
|
||||||
|
FORBIDDEN: 4,
|
||||||
|
UNSUPPORTED_PROTOCOL_VERSION: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
public static errorMessages = {
|
||||||
|
0: "Transport unknown",
|
||||||
|
1: "Session ID unknown",
|
||||||
|
2: "Bad handshake method",
|
||||||
|
3: "Bad request",
|
||||||
|
4: "Forbidden",
|
||||||
|
5: "Unsupported protocol version"
|
||||||
|
}
|
||||||
|
|
||||||
|
private clients = {}
|
||||||
|
private clientsCount = 0
|
||||||
|
public opts: any
|
||||||
|
|
||||||
|
private corsMiddleware: any
|
||||||
|
|
||||||
|
private ws: any
|
||||||
|
private perMessageDeflate: any
|
||||||
|
|
||||||
|
constructor(opts: any = {}) {
|
||||||
|
super()
|
||||||
|
this.opts = Object.assign(
|
||||||
|
{
|
||||||
|
wsEngine: DEFAULT_WS_ENGINE,
|
||||||
|
pingTimeout: 20000,
|
||||||
|
pingInterval: 25000,
|
||||||
|
upgradeTimeout: 10000,
|
||||||
|
maxHttpBufferSize: 1e6,
|
||||||
|
transports: Object.keys(transports),
|
||||||
|
allowUpgrades: true,
|
||||||
|
httpCompression: {
|
||||||
|
threshold: 1024
|
||||||
|
},
|
||||||
|
cors: false,
|
||||||
|
allowEIO3: false
|
||||||
|
},
|
||||||
|
opts
|
||||||
|
)
|
||||||
|
|
||||||
|
// if (opts.cookie) {
|
||||||
|
// this.opts.cookie = Object.assign(
|
||||||
|
// {
|
||||||
|
// name: "io",
|
||||||
|
// path: "/",
|
||||||
|
// httpOnly: opts.cookie.path !== false,
|
||||||
|
// sameSite: "lax"
|
||||||
|
// },
|
||||||
|
// opts.cookie
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (this.opts.cors) {
|
||||||
|
// this.corsMiddleware = require("cors")(this.opts.cors)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (opts.perMessageDeflate) {
|
||||||
|
// this.opts.perMessageDeflate = Object.assign(
|
||||||
|
// {
|
||||||
|
// threshold: 1024
|
||||||
|
// },
|
||||||
|
// opts.perMessageDeflate
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Initialize websocket server
|
||||||
|
// *
|
||||||
|
// * @api private
|
||||||
|
// */
|
||||||
|
// init() {
|
||||||
|
// if (!~this.opts.transports.indexOf("websocket")) return
|
||||||
|
|
||||||
|
// if (this.ws) this.ws.close()
|
||||||
|
|
||||||
|
// this.ws = new this.opts.wsEngine({
|
||||||
|
// noServer: true,
|
||||||
|
// clientTracking: false,
|
||||||
|
// perMessageDeflate: this.opts.perMessageDeflate,
|
||||||
|
// maxPayload: this.opts.maxHttpBufferSize
|
||||||
|
// })
|
||||||
|
|
||||||
|
// if (typeof this.ws.on === "function") {
|
||||||
|
// this.ws.on("headers", (headersArray, req) => {
|
||||||
|
// // note: 'ws' uses an array of headers, while Engine.IO uses an object (response.writeHead() accepts both formats)
|
||||||
|
// // we could also try to parse the array and then sync the values, but that will be error-prone
|
||||||
|
// const additionalHeaders = {}
|
||||||
|
|
||||||
|
// const isInitialRequest = !req._query.sid
|
||||||
|
// if (isInitialRequest) {
|
||||||
|
// this.emit("initial_headers", additionalHeaders, req)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this.emit("headers", additionalHeaders, req)
|
||||||
|
|
||||||
|
// Object.keys(additionalHeaders).forEach(key => {
|
||||||
|
// headersArray.push(`${key}: ${additionalHeaders[key]}`)
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of available transports for upgrade given a certain transport.
|
||||||
|
*
|
||||||
|
* @return {Array}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
upgrades(transport): Array<any> {
|
||||||
|
if (!this.opts.allowUpgrades) return []
|
||||||
|
return transports[transport].upgradesTo || []
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Verifies a request.
|
||||||
|
// *
|
||||||
|
// * @param {http.IncomingMessage}
|
||||||
|
// * @return {Boolean} whether the request is valid
|
||||||
|
// * @api private
|
||||||
|
// */
|
||||||
|
// verify(req, upgrade, fn) {
|
||||||
|
// // transport check
|
||||||
|
// const transport = req._query.transport
|
||||||
|
// if (!~this.opts.transports.indexOf(transport)) {
|
||||||
|
// debug('unknown transport "%s"', transport)
|
||||||
|
// return fn(Server.errors.UNKNOWN_TRANSPORT, { transport })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 'Origin' header check
|
||||||
|
// const isOriginInvalid = checkInvalidHeaderChar(req.headers.origin)
|
||||||
|
// if (isOriginInvalid) {
|
||||||
|
// const origin = req.headers.origin
|
||||||
|
// req.headers.origin = null
|
||||||
|
// debug("origin header invalid")
|
||||||
|
// return fn(Server.errors.BAD_REQUEST, {
|
||||||
|
// name: "INVALID_ORIGIN",
|
||||||
|
// origin
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // sid check
|
||||||
|
// const sid = req._query.sid
|
||||||
|
// if (sid) {
|
||||||
|
// if (!this.clients.hasOwnProperty(sid)) {
|
||||||
|
// debug('unknown sid "%s"', sid)
|
||||||
|
// return fn(Server.errors.UNKNOWN_SID, {
|
||||||
|
// sid
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// const previousTransport = this.clients[sid].transport.name
|
||||||
|
// if (!upgrade && previousTransport !== transport) {
|
||||||
|
// debug("bad request: unexpected transport without upgrade")
|
||||||
|
// return fn(Server.errors.BAD_REQUEST, {
|
||||||
|
// name: "TRANSPORT_MISMATCH",
|
||||||
|
// transport,
|
||||||
|
// previousTransport
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// // handshake is GET only
|
||||||
|
// if ("GET" !== req.method) {
|
||||||
|
// return fn(Server.errors.BAD_HANDSHAKE_METHOD, {
|
||||||
|
// method: req.method
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!this.opts.allowRequest) return fn()
|
||||||
|
|
||||||
|
// return this.opts.allowRequest(req, (message, success) => {
|
||||||
|
// if (!success) {
|
||||||
|
// return fn(Server.errors.FORBIDDEN, {
|
||||||
|
// message
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// fn()
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn()
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares a request by processing the query string.
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
prepare(req) {
|
||||||
|
// try to leverage pre-existing `req._query` (e.g: from connect)
|
||||||
|
if (!req._query) {
|
||||||
|
req._query = ~req.url.indexOf("?") ? qs.parse(parse(req.url).query) : {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes all clients.
|
||||||
|
*
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
debug("closing all open clients")
|
||||||
|
for (let i in this.clients) {
|
||||||
|
if (this.clients.hasOwnProperty(i)) {
|
||||||
|
this.clients[i].close(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.ws) {
|
||||||
|
debug("closing webSocketServer")
|
||||||
|
this.ws.close()
|
||||||
|
// don't delete this.ws because it can be used again if the http server starts listening again
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Handles an Engine.IO HTTP request.
|
||||||
|
// *
|
||||||
|
// * @param {http.IncomingMessage} request
|
||||||
|
// * @param {http.ServerResponse|http.OutgoingMessage} response
|
||||||
|
// * @api public
|
||||||
|
// */
|
||||||
|
// handleRequest(req, res) {
|
||||||
|
// debug('handling "%s" http request "%s"', req.method, req.url)
|
||||||
|
// this.prepare(req)
|
||||||
|
// req.res = res
|
||||||
|
|
||||||
|
// const callback = (errorCode, errorContext) => {
|
||||||
|
// if (errorCode !== undefined) {
|
||||||
|
// this.emit("connection_error", {
|
||||||
|
// req,
|
||||||
|
// code: errorCode,
|
||||||
|
// message: Server.errorMessages[errorCode],
|
||||||
|
// context: errorContext
|
||||||
|
// })
|
||||||
|
// abortRequest(res, errorCode, errorContext)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (req._query.sid) {
|
||||||
|
// debug("setting new request for existing client")
|
||||||
|
// this.clients[req._query.sid].transport.onRequest(req)
|
||||||
|
// } else {
|
||||||
|
// const closeConnection = (errorCode, errorContext) =>
|
||||||
|
// abortRequest(res, errorCode, errorContext)
|
||||||
|
// this.handshake(req._query.transport, req, closeConnection)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (this.corsMiddleware) {
|
||||||
|
// this.corsMiddleware.call(null, req, res, () => {
|
||||||
|
// this.verify(req, false, callback)
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// this.verify(req, false, callback)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generate a socket id.
|
||||||
|
* Overwrite this method to generate your custom socket id
|
||||||
|
*
|
||||||
|
* @param {Object} request object
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
generateId(req) {
|
||||||
|
return req.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handshakes a new client.
|
||||||
|
*
|
||||||
|
* @param {String} transport name
|
||||||
|
* @param {Object} request object
|
||||||
|
* @param {Function} closeConnection
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
// @java-patch sync handshake
|
||||||
|
handshake(transportName, req, closeConnection: (code: number) => void) {
|
||||||
|
console.debug('engine.io server handshake transport', transportName, 'from', req.url)
|
||||||
|
const protocol = req._query.EIO === "4" ? 4 : 3 // 3rd revision by default
|
||||||
|
if (protocol === 3 && !this.opts.allowEIO3) {
|
||||||
|
debug("unsupported protocol version")
|
||||||
|
this.emit("connection_error", {
|
||||||
|
req,
|
||||||
|
code: Server.errors.UNSUPPORTED_PROTOCOL_VERSION,
|
||||||
|
message:
|
||||||
|
Server.errorMessages[Server.errors.UNSUPPORTED_PROTOCOL_VERSION],
|
||||||
|
context: {
|
||||||
|
protocol
|
||||||
|
}
|
||||||
|
})
|
||||||
|
closeConnection(Server.errors.UNSUPPORTED_PROTOCOL_VERSION)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let id
|
||||||
|
try {
|
||||||
|
id = this.generateId(req)
|
||||||
|
} catch (e) {
|
||||||
|
console.debug("error while generating an id")
|
||||||
|
this.emit("connection_error", {
|
||||||
|
req,
|
||||||
|
code: Server.errors.BAD_REQUEST,
|
||||||
|
message: Server.errorMessages[Server.errors.BAD_REQUEST],
|
||||||
|
context: {
|
||||||
|
name: "ID_GENERATION_ERROR",
|
||||||
|
error: e
|
||||||
|
}
|
||||||
|
})
|
||||||
|
closeConnection(Server.errors.BAD_REQUEST)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug('engine.io server handshaking client "' + id + '"')
|
||||||
|
|
||||||
|
try {
|
||||||
|
var transport: Transport = new transports[transportName](req)
|
||||||
|
if ("websocket" !== transportName) {
|
||||||
|
throw new Error('Unsupport polling at MiaoScript!')
|
||||||
|
}
|
||||||
|
// if ("polling" === transportName) {
|
||||||
|
// transport.maxHttpBufferSize = this.opts.maxHttpBufferSize
|
||||||
|
// transport.httpCompression = this.opts.httpCompression
|
||||||
|
// } else if ("websocket" === transportName) {
|
||||||
|
transport.perMessageDeflate = this.opts.perMessageDeflate
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (req._query && req._query.b64) {
|
||||||
|
transport.supportsBinary = false
|
||||||
|
} else {
|
||||||
|
transport.supportsBinary = true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.ex(e)
|
||||||
|
this.emit("connection_error", {
|
||||||
|
req,
|
||||||
|
code: Server.errors.BAD_REQUEST,
|
||||||
|
message: Server.errorMessages[Server.errors.BAD_REQUEST],
|
||||||
|
context: {
|
||||||
|
name: "TRANSPORT_HANDSHAKE_ERROR",
|
||||||
|
error: e
|
||||||
|
}
|
||||||
|
})
|
||||||
|
closeConnection(Server.errors.BAD_REQUEST)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.debug(`engine.io server create socket ${id} from transport ${transport.name} protocol ${protocol}`)
|
||||||
|
const socket = new Socket(id, this, transport, req, protocol)
|
||||||
|
|
||||||
|
transport.on("headers", (headers, req) => {
|
||||||
|
const isInitialRequest = !req._query.sid
|
||||||
|
|
||||||
|
if (isInitialRequest) {
|
||||||
|
if (this.opts.cookie) {
|
||||||
|
headers["Set-Cookie"] = [
|
||||||
|
// cookieMod.serialize(this.opts.cookie.name, id, this.opts.cookie)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
this.emit("initial_headers", headers, req)
|
||||||
|
}
|
||||||
|
this.emit("headers", headers, req)
|
||||||
|
})
|
||||||
|
|
||||||
|
transport.onRequest(req)
|
||||||
|
|
||||||
|
this.clients[id] = socket
|
||||||
|
this.clientsCount++
|
||||||
|
|
||||||
|
socket.once("close", () => {
|
||||||
|
delete this.clients[id]
|
||||||
|
this.clientsCount--
|
||||||
|
})
|
||||||
|
this.emit("connection", socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Handles an Engine.IO HTTP Upgrade.
|
||||||
|
// *
|
||||||
|
// * @api public
|
||||||
|
// */
|
||||||
|
// handleUpgrade(req, socket, upgradeHead) {
|
||||||
|
// this.prepare(req)
|
||||||
|
|
||||||
|
// this.verify(req, true, (errorCode, errorContext) => {
|
||||||
|
// if (errorCode) {
|
||||||
|
// this.emit("connection_error", {
|
||||||
|
// req,
|
||||||
|
// code: errorCode,
|
||||||
|
// message: Server.errorMessages[errorCode],
|
||||||
|
// context: errorContext
|
||||||
|
// })
|
||||||
|
// abortUpgrade(socket, errorCode, errorContext)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const head = Buffer.from(upgradeHead) // eslint-disable-line node/no-deprecated-api
|
||||||
|
// upgradeHead = null
|
||||||
|
|
||||||
|
// // delegate to ws
|
||||||
|
// this.ws.handleUpgrade(req, socket, head, websocket => {
|
||||||
|
// this.onWebSocket(req, socket, websocket)
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called upon a ws.io connection.
|
||||||
|
*
|
||||||
|
* @param {ws.Socket} websocket
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
onWebSocket(req: Request, socket, websocket: WebSocketClient) {
|
||||||
|
websocket.on("error", onUpgradeError)
|
||||||
|
|
||||||
|
if (
|
||||||
|
transports[req._query.transport] !== undefined &&
|
||||||
|
!transports[req._query.transport].prototype.handlesUpgrades
|
||||||
|
) {
|
||||||
|
console.debug("transport doesnt handle upgraded requests")
|
||||||
|
websocket.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get client id
|
||||||
|
const id = req._query.sid
|
||||||
|
|
||||||
|
// keep a reference to the ws.Socket
|
||||||
|
req.websocket = websocket
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
const client = this.clients[id]
|
||||||
|
if (!client) {
|
||||||
|
console.debug("upgrade attempt for closed client")
|
||||||
|
websocket.close()
|
||||||
|
} else if (client.upgrading) {
|
||||||
|
console.debug("transport has already been trying to upgrade")
|
||||||
|
websocket.close()
|
||||||
|
} else if (client.upgraded) {
|
||||||
|
console.debug("transport had already been upgraded")
|
||||||
|
websocket.close()
|
||||||
|
} else {
|
||||||
|
console.debug("upgrading existing transport")
|
||||||
|
|
||||||
|
// transport error handling takes over
|
||||||
|
websocket.removeListener("error", onUpgradeError)
|
||||||
|
|
||||||
|
const transport = new transports[req._query.transport](req)
|
||||||
|
if (req._query && req._query.b64) {
|
||||||
|
transport.supportsBinary = false
|
||||||
|
} else {
|
||||||
|
transport.supportsBinary = true
|
||||||
|
}
|
||||||
|
transport.perMessageDeflate = this.perMessageDeflate
|
||||||
|
client.maybeUpgrade(transport)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// transport error handling takes over
|
||||||
|
websocket.removeListener("error", onUpgradeError)
|
||||||
|
|
||||||
|
// const closeConnection = (errorCode, errorContext) =>
|
||||||
|
// abortUpgrade(socket, errorCode, errorContext)
|
||||||
|
this.handshake(req._query.transport, req, () => { })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpgradeError() {
|
||||||
|
console.debug("websocket error before upgrade")
|
||||||
|
// websocket.close() not needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures upgrade requests for a http.Server.
|
||||||
|
*
|
||||||
|
* @param {http.Server} server
|
||||||
|
* @param {Object} options
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
attach(server, options: any = {}) {
|
||||||
|
// let path = (options.path || "/engine.io").replace(/\/$/, "")
|
||||||
|
|
||||||
|
// const destroyUpgradeTimeout = options.destroyUpgradeTimeout || 1000
|
||||||
|
|
||||||
|
// // normalize path
|
||||||
|
// path += "/"
|
||||||
|
|
||||||
|
// function check(req) {
|
||||||
|
// return path === req.url.substr(0, path.length)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// cache and clean up listeners
|
||||||
|
// const listeners = server.listeners("request").slice(0)
|
||||||
|
// server.removeAllListeners("request")
|
||||||
|
server.on("close", this.close.bind(this))
|
||||||
|
// server.on("listening", this.init.bind(this))
|
||||||
|
// @java-patch transfer to Netty Server
|
||||||
|
server.on("connect", (request: Request, websocket: WebSocketClient) => {
|
||||||
|
console.debug('Engine.IO connect client from', request.url)
|
||||||
|
this.prepare(request)
|
||||||
|
this.onWebSocket(request, undefined, websocket)
|
||||||
|
})
|
||||||
|
// set server as ws server
|
||||||
|
this.ws = server
|
||||||
|
|
||||||
|
// // add request handler
|
||||||
|
// server.on("request", (req, res) => {
|
||||||
|
// if (check(req)) {
|
||||||
|
// debug('intercepting request for path "%s"', path)
|
||||||
|
// this.handleRequest(req, res)
|
||||||
|
// } else {
|
||||||
|
// let i = 0
|
||||||
|
// const l = listeners.length
|
||||||
|
// for (; i < l; i++) {
|
||||||
|
// listeners[i].call(server, req, res)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// if (~this.opts.transports.indexOf("websocket")) {
|
||||||
|
// server.on("upgrade", (req, socket, head) => {
|
||||||
|
// if (check(req)) {
|
||||||
|
// this.handleUpgrade(req, socket, head)
|
||||||
|
// } else if (false !== options.destroyUpgrade) {
|
||||||
|
// // default node behavior is to disconnect when no handlers
|
||||||
|
// // but by adding a handler, we prevent that
|
||||||
|
// // and if no eio thing handles the upgrade
|
||||||
|
// // then the socket needs to die!
|
||||||
|
// setTimeout(function () {
|
||||||
|
// if (socket.writable && socket.bytesWritten <= 0) {
|
||||||
|
// return socket.end()
|
||||||
|
// }
|
||||||
|
// }, destroyUpgradeTimeout)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Close the HTTP long-polling request
|
||||||
|
// *
|
||||||
|
// * @param res - the response object
|
||||||
|
// * @param errorCode - the error code
|
||||||
|
// * @param errorContext - additional error context
|
||||||
|
// *
|
||||||
|
// * @api private
|
||||||
|
// */
|
||||||
|
|
||||||
|
// function abortRequest(res, errorCode, errorContext) {
|
||||||
|
// const statusCode = errorCode === Server.errors.FORBIDDEN ? 403 : 400
|
||||||
|
// const message =
|
||||||
|
// errorContext && errorContext.message
|
||||||
|
// ? errorContext.message
|
||||||
|
// : Server.errorMessages[errorCode]
|
||||||
|
|
||||||
|
// res.writeHead(statusCode, { "Content-Type": "application/json" })
|
||||||
|
// res.end(
|
||||||
|
// JSON.stringify({
|
||||||
|
// code: errorCode,
|
||||||
|
// message
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Close the WebSocket connection
|
||||||
|
// *
|
||||||
|
// * @param {net.Socket} socket
|
||||||
|
// * @param {string} errorCode - the error code
|
||||||
|
// * @param {object} errorContext - additional error context
|
||||||
|
// *
|
||||||
|
// * @api private
|
||||||
|
// */
|
||||||
|
|
||||||
|
// function abortUpgrade(socket, errorCode, errorContext: any = {}) {
|
||||||
|
// socket.on("error", () => {
|
||||||
|
// debug("ignoring error from closed connection")
|
||||||
|
// })
|
||||||
|
// if (socket.writable) {
|
||||||
|
// const message = errorContext.message || Server.errorMessages[errorCode]
|
||||||
|
// const length = Buffer.byteLength(message)
|
||||||
|
// socket.write(
|
||||||
|
// "HTTP/1.1 400 Bad Request\r\n" +
|
||||||
|
// "Connection: close\r\n" +
|
||||||
|
// "Content-type: text/html\r\n" +
|
||||||
|
// "Content-Length: " +
|
||||||
|
// length +
|
||||||
|
// "\r\n" +
|
||||||
|
// "\r\n" +
|
||||||
|
// message
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// socket.destroy()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// module.exports = Server
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * From https://github.com/nodejs/node/blob/v8.4.0/lib/_http_common.js#L303-L354
|
||||||
|
// *
|
||||||
|
// * True if val contains an invalid field-vchar
|
||||||
|
// * field-value = *( field-content / obs-fold )
|
||||||
|
// * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
|
||||||
|
// * field-vchar = VCHAR / obs-text
|
||||||
|
// *
|
||||||
|
// * checkInvalidHeaderChar() is currently designed to be inlinable by v8,
|
||||||
|
// * so take care when making changes to the implementation so that the source
|
||||||
|
// * code size does not exceed v8's default max_inlined_source_size setting.
|
||||||
|
// **/
|
||||||
|
// // prettier-ignore
|
||||||
|
// const validHdrChars = [
|
||||||
|
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, // 0 - 15
|
||||||
|
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
|
||||||
|
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 32 - 47
|
||||||
|
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 48 - 63
|
||||||
|
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79
|
||||||
|
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 80 - 95
|
||||||
|
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111
|
||||||
|
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 112 - 127
|
||||||
|
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 128 ...
|
||||||
|
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||||
|
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // ... 255
|
||||||
|
// ]
|
||||||
|
|
||||||
|
// function checkInvalidHeaderChar(val) {
|
||||||
|
// val += ""
|
||||||
|
// if (val.length < 1) return false
|
||||||
|
// if (!validHdrChars[val.charCodeAt(0)]) {
|
||||||
|
// debug('invalid header, index 0, char "%s"', val.charCodeAt(0))
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// if (val.length < 2) return false
|
||||||
|
// if (!validHdrChars[val.charCodeAt(1)]) {
|
||||||
|
// debug('invalid header, index 1, char "%s"', val.charCodeAt(1))
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// if (val.length < 3) return false
|
||||||
|
// if (!validHdrChars[val.charCodeAt(2)]) {
|
||||||
|
// debug('invalid header, index 2, char "%s"', val.charCodeAt(2))
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// if (val.length < 4) return false
|
||||||
|
// if (!validHdrChars[val.charCodeAt(3)]) {
|
||||||
|
// debug('invalid header, index 3, char "%s"', val.charCodeAt(3))
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// for (let i = 4; i < val.length; ++i) {
|
||||||
|
// if (!validHdrChars[val.charCodeAt(i)]) {
|
||||||
|
// debug('invalid header, index "%i", char "%s"', i, val.charCodeAt(i))
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return false
|
||||||
|
// }
|
530
packages/websocket/src/engine.io/socket.ts
Normal file
530
packages/websocket/src/engine.io/socket.ts
Normal file
@ -0,0 +1,530 @@
|
|||||||
|
import { EventEmitter } from "events"
|
||||||
|
import { Server } from "./server"
|
||||||
|
import { Transport } from "./transport"
|
||||||
|
import type { Request } from "../server/request"
|
||||||
|
// const debug = require("debug")("engine:socket")
|
||||||
|
|
||||||
|
export class Socket extends EventEmitter {
|
||||||
|
public id: string
|
||||||
|
private server: Server
|
||||||
|
private upgrading = false
|
||||||
|
private upgraded = false
|
||||||
|
public readyState = "opening"
|
||||||
|
private writeBuffer = []
|
||||||
|
private packetsFn = []
|
||||||
|
private sentCallbackFn = []
|
||||||
|
private cleanupFn = []
|
||||||
|
public request: Request
|
||||||
|
public protocol: number
|
||||||
|
public remoteAddress: any
|
||||||
|
public transport: Transport
|
||||||
|
|
||||||
|
private checkIntervalTimer: NodeJS.Timeout
|
||||||
|
private upgradeTimeoutTimer: NodeJS.Timeout
|
||||||
|
private pingTimeoutTimer: NodeJS.Timeout
|
||||||
|
private pingIntervalTimer: NodeJS.Timeout
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client class (abstract).
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
constructor(id: string, server: Server, transport: Transport, req: Request, protocol: number) {
|
||||||
|
super()
|
||||||
|
this.id = id
|
||||||
|
this.server = server
|
||||||
|
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
|
||||||
|
} else {
|
||||||
|
this.remoteAddress = req.connection.remoteAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkIntervalTimer = null
|
||||||
|
this.upgradeTimeoutTimer = null
|
||||||
|
this.pingTimeoutTimer = null
|
||||||
|
this.pingIntervalTimer = null
|
||||||
|
|
||||||
|
this.setTransport(transport)
|
||||||
|
this.onOpen()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called upon transport considered open.
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
onOpen() {
|
||||||
|
this.readyState = "open"
|
||||||
|
|
||||||
|
// sends an `open` packet
|
||||||
|
this.transport.sid = this.id
|
||||||
|
this.sendPacket(
|
||||||
|
"open",
|
||||||
|
JSON.stringify({
|
||||||
|
sid: this.id,
|
||||||
|
upgrades: this.getAvailableUpgrades(),
|
||||||
|
pingInterval: this.server.opts.pingInterval,
|
||||||
|
pingTimeout: this.server.opts.pingTimeout
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
if (this.server.opts.initialPacket) {
|
||||||
|
this.sendPacket("message", this.server.opts.initialPacket)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called upon transport packet.
|
||||||
|
*
|
||||||
|
* @param {Object} packet
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
onPacket(packet: { type: any; data: any }) {
|
||||||
|
if ("open" !== this.readyState) {
|
||||||
|
console.debug("packet received with closed socket")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// export packet event
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
// debug("got ping")
|
||||||
|
this.sendPacket("pong")
|
||||||
|
this.emit("heartbeat")
|
||||||
|
break
|
||||||
|
|
||||||
|
case "pong":
|
||||||
|
if (this.transport.protocol === 3) {
|
||||||
|
this.onError("invalid heartbeat direction")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// debug("got pong")
|
||||||
|
this.schedulePing()
|
||||||
|
this.emit("heartbeat")
|
||||||
|
break
|
||||||
|
|
||||||
|
case "error":
|
||||||
|
this.onClose("parse error")
|
||||||
|
break
|
||||||
|
|
||||||
|
case "message":
|
||||||
|
this.emit("data", packet.data)
|
||||||
|
this.emit("message", packet.data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called upon transport error.
|
||||||
|
*
|
||||||
|
* @param {Error} error object
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
onError(err: string) {
|
||||||
|
// debug("transport error")
|
||||||
|
this.onClose("transport error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pings client every `this.pingInterval` and expects response
|
||||||
|
* within `this.pingTimeout` or closes connection.
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
schedulePing() {
|
||||||
|
clearTimeout(this.pingIntervalTimer)
|
||||||
|
this.pingIntervalTimer = setTimeout(() => {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets ping timeout.
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
resetPingTimeout(timeout: number) {
|
||||||
|
clearTimeout(this.pingTimeoutTimer)
|
||||||
|
this.pingTimeoutTimer = setTimeout(() => {
|
||||||
|
if (this.readyState === "closed") return
|
||||||
|
this.onClose("ping timeout")
|
||||||
|
}, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches handlers for the given transport.
|
||||||
|
*
|
||||||
|
* @param {Transport} transport
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
setTransport(transport: Transport) {
|
||||||
|
console.debug(`engine.io socket ${this.id} set transport ${transport.name}`)
|
||||||
|
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 function will manage packet events (also message callbacks)
|
||||||
|
this.setupSendCallback()
|
||||||
|
|
||||||
|
this.cleanupFn.push(function () {
|
||||||
|
transport.removeListener("error", onError)
|
||||||
|
transport.removeListener("packet", onPacket)
|
||||||
|
transport.removeListener("drain", flush)
|
||||||
|
transport.removeListener("close", onClose)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrades socket to the given transport
|
||||||
|
*
|
||||||
|
* @param {Transport} transport
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
maybeUpgrade(transport: Transport) {
|
||||||
|
console.debug(
|
||||||
|
'might upgrade socket transport from "', this.transport.name, '" to "', transport.name, '"'
|
||||||
|
)
|
||||||
|
|
||||||
|
this.upgrading = true
|
||||||
|
|
||||||
|
// set transport upgrade timer
|
||||||
|
this.upgradeTimeoutTimer = setTimeout(() => {
|
||||||
|
console.debug("client did not complete upgrade - closing transport")
|
||||||
|
cleanup()
|
||||||
|
if ("open" === transport.readyState) {
|
||||||
|
transport.close()
|
||||||
|
}
|
||||||
|
}, this.server.opts.upgradeTimeout)
|
||||||
|
|
||||||
|
const onPacket = (packet: { type: string; data: string }) => {
|
||||||
|
if ("ping" === packet.type && "probe" === packet.data) {
|
||||||
|
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()
|
||||||
|
if (this.readyState === "closing") {
|
||||||
|
transport.close(() => {
|
||||||
|
this.onClose("forced close")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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" }])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
this.upgrading = false
|
||||||
|
|
||||||
|
clearInterval(this.checkIntervalTimer)
|
||||||
|
this.checkIntervalTimer = null
|
||||||
|
|
||||||
|
clearTimeout(this.upgradeTimeoutTimer)
|
||||||
|
this.upgradeTimeoutTimer = null
|
||||||
|
|
||||||
|
transport.removeListener("packet", onPacket)
|
||||||
|
transport.removeListener("close", onTransportClose)
|
||||||
|
transport.removeListener("error", onError)
|
||||||
|
this.removeListener("close", onClose)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onError = (err: string) => {
|
||||||
|
// debug("client did not complete upgrade - %s", err)
|
||||||
|
cleanup()
|
||||||
|
transport.close()
|
||||||
|
transport = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTransportClose = () => {
|
||||||
|
onError("transport closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
onError("socket closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
transport.on("packet", onPacket)
|
||||||
|
transport.once("close", onTransportClose)
|
||||||
|
transport.once("error", onError)
|
||||||
|
|
||||||
|
this.once("close", onClose)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears listeners and timers associated with current transport.
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
clearTransport() {
|
||||||
|
let cleanup: () => void
|
||||||
|
|
||||||
|
const toCleanUp = this.cleanupFn.length
|
||||||
|
|
||||||
|
for (let i = 0; i < toCleanUp; i++) {
|
||||||
|
cleanup = this.cleanupFn.shift()
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
// silence further transport errors and prevent uncaught exceptions
|
||||||
|
this.transport.on("error", function () {
|
||||||
|
// debug("error triggered by discarded transport")
|
||||||
|
})
|
||||||
|
|
||||||
|
// ensure transport won't stay open
|
||||||
|
this.transport.close()
|
||||||
|
|
||||||
|
clearTimeout(this.pingTimeoutTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called upon transport considered closed.
|
||||||
|
* Possible reasons: `ping timeout`, `client error`, `parse error`,
|
||||||
|
* `transport error`, `server close`, `transport close`
|
||||||
|
*/
|
||||||
|
onClose(reason: string, description?: string) {
|
||||||
|
if ("closed" !== this.readyState) {
|
||||||
|
this.readyState = "closed"
|
||||||
|
|
||||||
|
// clear timers
|
||||||
|
clearTimeout(this.pingIntervalTimer)
|
||||||
|
clearTimeout(this.pingTimeoutTimer)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup and manage send callback
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
setupSendCallback() {
|
||||||
|
// the message was sent successfully, execute the callback
|
||||||
|
const onDrain = () => {
|
||||||
|
if (this.sentCallbackFn.length > 0) {
|
||||||
|
const seqFn = this.sentCallbackFn.splice(0, 1)[0]
|
||||||
|
if ("function" === typeof seqFn) {
|
||||||
|
// debug("executing send callback")
|
||||||
|
seqFn(this.transport)
|
||||||
|
} else if (Array.isArray(seqFn)) {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.transport.on("drain", onDrain)
|
||||||
|
|
||||||
|
this.cleanupFn.push(() => {
|
||||||
|
this.transport.removeListener("drain", onDrain)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message packet.
|
||||||
|
*
|
||||||
|
* @param {String} message
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {Function} callback
|
||||||
|
* @return {Socket} for chaining
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
send(data: any, options: any, callback: any) {
|
||||||
|
this.sendPacket("message", data, options, callback)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
write(data: any, options: any, callback?: any) {
|
||||||
|
this.sendPacket("message", data, options, callback)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a packet.
|
||||||
|
*
|
||||||
|
* @param {String} packet type
|
||||||
|
* @param {String} optional, data
|
||||||
|
* @param {Object} options
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
sendPacket(type: string, data?: string, options?: { compress?: any }, callback?: undefined) {
|
||||||
|
if ("function" === typeof options) {
|
||||||
|
callback = options
|
||||||
|
options = null
|
||||||
|
}
|
||||||
|
|
||||||
|
options = options || {}
|
||||||
|
options.compress = false !== options.compress
|
||||||
|
|
||||||
|
if ("closing" !== this.readyState && "closed" !== this.readyState) {
|
||||||
|
// console.debug('sending packet "%s" (%s)', type, data)
|
||||||
|
|
||||||
|
const packet: any = {
|
||||||
|
type: type,
|
||||||
|
options: options
|
||||||
|
}
|
||||||
|
if (data) packet.data = data
|
||||||
|
|
||||||
|
// exports packetCreate event
|
||||||
|
this.emit("packetCreate", packet)
|
||||||
|
|
||||||
|
this.writeBuffer.push(packet)
|
||||||
|
|
||||||
|
// add send callback to object, if defined
|
||||||
|
if (callback) this.packetsFn.push(callback)
|
||||||
|
|
||||||
|
this.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to flush the packets buffer.
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
flush() {
|
||||||
|
if (
|
||||||
|
"closed" !== this.readyState &&
|
||||||
|
this.transport.writable &&
|
||||||
|
this.writeBuffer.length
|
||||||
|
) {
|
||||||
|
console.trace("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)
|
||||||
|
} else {
|
||||||
|
this.sentCallbackFn.push.apply(this.sentCallbackFn, this.packetsFn)
|
||||||
|
}
|
||||||
|
this.packetsFn = []
|
||||||
|
this.transport.send(wbuf)
|
||||||
|
this.emit("drain")
|
||||||
|
this.server.emit("drain", this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available upgrades for this socket.
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
getAvailableUpgrades() {
|
||||||
|
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]
|
||||||
|
if (this.server.opts.transports.indexOf(upg) !== -1) {
|
||||||
|
availableUpgrades.push(upg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return availableUpgrades
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the socket and underlying transport.
|
||||||
|
*
|
||||||
|
* @param {Boolean} optional, discard
|
||||||
|
* @return {Socket} for chaining
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
close(discard?: any) {
|
||||||
|
if ("open" !== this.readyState) return
|
||||||
|
|
||||||
|
this.readyState = "closing"
|
||||||
|
|
||||||
|
if (this.writeBuffer.length) {
|
||||||
|
this.once("drain", this.closeTransport.bind(this, discard))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeTransport(discard)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the underlying transport.
|
||||||
|
*
|
||||||
|
* @param {Boolean} discard
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
closeTransport(discard: any) {
|
||||||
|
if (discard) this.transport.discard()
|
||||||
|
this.transport.close(this.onClose.bind(this, "forced close"))
|
||||||
|
}
|
||||||
|
}
|
121
packages/websocket/src/engine.io/transport.ts
Normal file
121
packages/websocket/src/engine.io/transport.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { EventEmitter } from 'events'
|
||||||
|
import parser_v4 from "../engine.io-parser"
|
||||||
|
import type { WebSocketClient } from '../server/client'
|
||||||
|
/**
|
||||||
|
* Noop function.
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function noop() { }
|
||||||
|
|
||||||
|
export abstract class Transport extends EventEmitter {
|
||||||
|
public sid: string
|
||||||
|
public req /**http.IncomingMessage */
|
||||||
|
public socket: WebSocketClient
|
||||||
|
public writable: boolean
|
||||||
|
public readyState: string
|
||||||
|
public discarded: boolean
|
||||||
|
public protocol: Number
|
||||||
|
public parser: any
|
||||||
|
public perMessageDeflate: any
|
||||||
|
public supportsBinary: boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transport constructor.
|
||||||
|
*
|
||||||
|
* @param {http.IncomingMessage} request
|
||||||
|
* @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 = parser_v4//= this.protocol === 4 ? parser_v4 : parser_v3
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flags the transport as discarded.
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
discard() {
|
||||||
|
this.discarded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called with an incoming HTTP request.
|
||||||
|
*
|
||||||
|
* @param {http.IncomingMessage} request
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
onRequest(req) {
|
||||||
|
console.debug(`engine.io transport ${this.socket.id} setting request`, JSON.stringify(req))
|
||||||
|
this.req = req
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the transport.
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
close(fn?) {
|
||||||
|
if ("closed" === this.readyState || "closing" === this.readyState) return
|
||||||
|
|
||||||
|
this.readyState = "closing"
|
||||||
|
this.doClose(fn || noop)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called with a transport error.
|
||||||
|
*
|
||||||
|
* @param {String} message error
|
||||||
|
* @param {Object} error description
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
onError(msg: string, desc?: string) {
|
||||||
|
if (this.listeners("error").length) {
|
||||||
|
const err: any = new Error(msg)
|
||||||
|
err.type = "TransportError"
|
||||||
|
err.description = desc
|
||||||
|
this.emit("error", err)
|
||||||
|
} else {
|
||||||
|
console.debug(`ignored transport error ${msg} (${desc})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called with parsed out a packets from the data stream.
|
||||||
|
*
|
||||||
|
* @param {Object} packet
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
onPacket(packet) {
|
||||||
|
this.emit("packet", packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called with the encoded packet data.
|
||||||
|
*
|
||||||
|
* @param {String} data
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
onData(data) {
|
||||||
|
this.onPacket(this.parser.decodePacket(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called upon transport close.
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
onClose() {
|
||||||
|
this.readyState = "closed"
|
||||||
|
this.emit("close")
|
||||||
|
}
|
||||||
|
abstract get supportsFraming()
|
||||||
|
abstract get name()
|
||||||
|
abstract send(...args: any[])
|
||||||
|
abstract doClose(d: Function)
|
||||||
|
}
|
3
packages/websocket/src/engine.io/transports/index.ts
Normal file
3
packages/websocket/src/engine.io/transports/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
websocket: require("./websocket").WebSocket
|
||||||
|
}
|
116
packages/websocket/src/engine.io/transports/websocket.ts
Normal file
116
packages/websocket/src/engine.io/transports/websocket.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { Transport } from '../transport'
|
||||||
|
// const debug = require("debug")("engine:ws")
|
||||||
|
|
||||||
|
export class WebSocket extends Transport {
|
||||||
|
public perMessageDeflate: any
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket transport
|
||||||
|
*
|
||||||
|
* @param {http.IncomingMessage}
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
constructor(req) {
|
||||||
|
super(req)
|
||||||
|
this.socket = req.websocket
|
||||||
|
this.socket.on("message", this.onData.bind(this))
|
||||||
|
this.socket.once("close", this.onClose.bind(this))
|
||||||
|
this.socket.on("error", this.onError.bind(this))
|
||||||
|
this.writable = true
|
||||||
|
this.perMessageDeflate = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transport name
|
||||||
|
*
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
get name() {
|
||||||
|
return "websocket"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advertise upgrade support.
|
||||||
|
*
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
get handlesUpgrades() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advertise framing support.
|
||||||
|
*
|
||||||
|
* @api public
|
||||||
|
*/
|
||||||
|
get supportsFraming() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the incoming data.
|
||||||
|
*
|
||||||
|
* @param {String} encoded packet
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
onData(data) {
|
||||||
|
// debug('received "%s"', data)
|
||||||
|
super.onData(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a packet payload.
|
||||||
|
*
|
||||||
|
* @param {Array} packets
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
send(packets) {
|
||||||
|
// console.log('WebSocket send packets', JSON.stringify(packets))
|
||||||
|
const packet = packets.shift()
|
||||||
|
if (typeof packet === "undefined") {
|
||||||
|
this.writable = true
|
||||||
|
this.emit("drain")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// always creates a new object since ws modifies it
|
||||||
|
const opts: any = {}
|
||||||
|
if (packet.options) {
|
||||||
|
opts.compress = packet.options.compress
|
||||||
|
}
|
||||||
|
|
||||||
|
const send = data => {
|
||||||
|
if (this.perMessageDeflate) {
|
||||||
|
const len =
|
||||||
|
"string" === typeof data ? Buffer.byteLength(data) : data.length
|
||||||
|
if (len < this.perMessageDeflate.threshold) {
|
||||||
|
opts.compress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.trace('writing', data)
|
||||||
|
this.writable = false
|
||||||
|
|
||||||
|
this.socket.send(data, opts, err => {
|
||||||
|
if (err) return this.onError("write error", err.stack)
|
||||||
|
this.send(packets)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet.options && typeof packet.options.wsPreEncoded === "string") {
|
||||||
|
send(packet.options.wsPreEncoded)
|
||||||
|
} else {
|
||||||
|
this.parser.encodePacket(packet, this.supportsBinary, send)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the transport.
|
||||||
|
*
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
doClose(fn) {
|
||||||
|
// debug("closing")
|
||||||
|
this.socket.close()
|
||||||
|
fn && fn()
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
/// <reference types="@ccms/nashorn" />
|
/// <reference types="@ccms/nashorn" />
|
||||||
/// <reference types="@javatypes/tomcat-websocket-api" />
|
/// <reference types="@javatypes/tomcat-websocket-api" />
|
||||||
|
|
||||||
import { Server, ServerOptions } from './socket-io'
|
import { Server, ServerOptions } from './socket.io'
|
||||||
|
|
||||||
interface SocketIOStatic {
|
interface SocketIOStatic {
|
||||||
/**
|
/**
|
||||||
@ -44,7 +44,7 @@ let io: SocketStatic = function (pipeline: any, options: Partial<ServerOptions>)
|
|||||||
}
|
}
|
||||||
io.Instance = Symbol("@ccms/websocket")
|
io.Instance = Symbol("@ccms/websocket")
|
||||||
export default io
|
export default io
|
||||||
export * from './socket-io'
|
export * from './socket.io'
|
||||||
export * from './client'
|
export * from './client'
|
||||||
export * from './server'
|
export * from './server'
|
||||||
export * from './transport'
|
export * from './engine.io/transport'
|
||||||
|
@ -1,24 +1,26 @@
|
|||||||
import { Transport } from '../transport'
|
import { WebSocketClient } from '../server/client'
|
||||||
import { AttributeKeys } from './constants'
|
|
||||||
|
|
||||||
const TextWebSocketFrame = Java.type('io.netty.handler.codec.http.websocketx.TextWebSocketFrame')
|
const TextWebSocketFrame = Java.type('io.netty.handler.codec.http.websocketx.TextWebSocketFrame')
|
||||||
|
|
||||||
export class NettyClient extends Transport {
|
export class NettyClient extends WebSocketClient {
|
||||||
private channel: any
|
private channel: any
|
||||||
|
|
||||||
constructor(server: any, channel: any) {
|
constructor(channel: any) {
|
||||||
super(server)
|
super()
|
||||||
this.remoteAddress = channel.remoteAddress() + ''
|
this.id = channel.id() + ''
|
||||||
this.request = channel.attr(AttributeKeys.Request).get()
|
|
||||||
|
|
||||||
this._id = channel.id() + ''
|
|
||||||
this.channel = channel
|
this.channel = channel
|
||||||
}
|
}
|
||||||
|
|
||||||
doSend(text: string) {
|
send(text: string, opts?: any, callback?: (err?: Error) => void) {
|
||||||
this.channel.writeAndFlush(new TextWebSocketFrame(text))
|
try {
|
||||||
|
this.channel.writeAndFlush(new TextWebSocketFrame(text))
|
||||||
|
callback?.()
|
||||||
|
} catch (error) {
|
||||||
|
callback?.(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
doClose() {
|
|
||||||
|
close() {
|
||||||
this.channel.close()
|
this.channel.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import { JavaServerOptions } from '../server'
|
||||||
|
|
||||||
import { HttpRequestHandlerAdapter } from './adapter'
|
import { HttpRequestHandlerAdapter } from './adapter'
|
||||||
import { AttributeKeys } from './constants'
|
import { AttributeKeys } from './constants'
|
||||||
import { ServerOptions } from 'socket-io'
|
|
||||||
|
|
||||||
const DefaultHttpResponse = Java.type('io.netty.handler.codec.http.DefaultHttpResponse')
|
const DefaultHttpResponse = Java.type('io.netty.handler.codec.http.DefaultHttpResponse')
|
||||||
const DefaultFullHttpResponse = Java.type('io.netty.handler.codec.http.DefaultFullHttpResponse')
|
const DefaultFullHttpResponse = Java.type('io.netty.handler.codec.http.DefaultFullHttpResponse')
|
||||||
@ -18,7 +19,7 @@ const ChannelFutureListener = Java.type('io.netty.channel.ChannelFutureListener'
|
|||||||
export class HttpRequestHandler extends HttpRequestHandlerAdapter {
|
export class HttpRequestHandler extends HttpRequestHandlerAdapter {
|
||||||
private ws: string
|
private ws: string
|
||||||
private root: string
|
private root: string
|
||||||
constructor(options: ServerOptions) {
|
constructor(options: JavaServerOptions) {
|
||||||
super()
|
super()
|
||||||
this.root = options.root
|
this.root = options.root
|
||||||
this.ws = options.path
|
this.ws = options.path
|
||||||
|
@ -1,65 +1,70 @@
|
|||||||
import { EventEmitter } from 'events'
|
import { JavaServerOptions, ServerEvent, WebSocketServer } from '../server'
|
||||||
|
import { Request } from '../server/request'
|
||||||
import { ServerOptions } from '../socket-io'
|
|
||||||
import { ServerEvent } from '../socket-io/constants'
|
|
||||||
|
|
||||||
import { NettyClient } from './client'
|
import { NettyClient } from './client'
|
||||||
import { Keys } from './constants'
|
import { AttributeKeys, Keys } from './constants'
|
||||||
import { WebSocketDetect } from './websocket_detect'
|
import { WebSocketDetect } from './websocket_detect'
|
||||||
import { WebSocketHandler } from './websocket_handler'
|
import { WebSocketHandler } from './websocket_handler'
|
||||||
|
|
||||||
class NettyWebSocketServer extends EventEmitter {
|
class NettyWebSocketServer extends WebSocketServer {
|
||||||
private pipeline: any
|
constructor(pipeline: any, options: JavaServerOptions) {
|
||||||
private clients: Map<string, NettyClient>
|
super(pipeline, options)
|
||||||
|
}
|
||||||
|
|
||||||
constructor(pipeline: any, options: ServerOptions) {
|
protected initialize() {
|
||||||
super()
|
let connectEvent = this.options.event
|
||||||
this.clients = new Map()
|
try { this.instance.remove(Keys.Detect) } catch (error) { }
|
||||||
this.pipeline = pipeline
|
this.instance.addFirst(Keys.Detect, new WebSocketDetect(connectEvent).getHandler())
|
||||||
let connectEvent = options.event
|
|
||||||
try { this.pipeline.remove(Keys.Detect) } catch (error) { }
|
|
||||||
this.pipeline.addFirst(Keys.Detect, new WebSocketDetect(connectEvent).getHandler())
|
|
||||||
connectEvent.on(ServerEvent.detect, (ctx, channel) => {
|
connectEvent.on(ServerEvent.detect, (ctx, channel) => {
|
||||||
channel.pipeline().addFirst(Keys.Handler, new WebSocketHandler(options).getHandler())
|
channel.pipeline().addFirst(Keys.Handler, new WebSocketHandler(this.options).getHandler())
|
||||||
ctx.fireChannelRead(channel)
|
ctx.fireChannelRead(channel)
|
||||||
})
|
})
|
||||||
connectEvent.on(ServerEvent.connect, (ctx) => {
|
connectEvent.on(ServerEvent.connect, (ctx) => {
|
||||||
let cid = ctx?.channel().id() + ''
|
this.onconnect(ctx)
|
||||||
let nettyClient = new NettyClient(this, ctx.channel())
|
|
||||||
this.clients.set(cid, nettyClient)
|
|
||||||
this.emit(ServerEvent.connect, nettyClient)
|
|
||||||
})
|
})
|
||||||
connectEvent.on(ServerEvent.message, (ctx, msg) => {
|
connectEvent.on(ServerEvent.message, (ctx, msg) => {
|
||||||
let cid = ctx?.channel().id() + ''
|
this.onmessage(ctx, msg.text())
|
||||||
if (this.clients.has(cid)) {
|
|
||||||
this.emit(ServerEvent.message, this.clients.get(cid), msg.text())
|
|
||||||
} else if (global.debug) {
|
|
||||||
console.error(`unknow client ${ctx} reciver message ${msg.text()}`)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
connectEvent.on(ServerEvent.disconnect, (ctx, cause) => {
|
connectEvent.on(ServerEvent.disconnect, (ctx, cause) => {
|
||||||
let cid = ctx?.channel().id() + ''
|
this.ondisconnect(ctx, cause)
|
||||||
if (this.clients.has(cid)) {
|
|
||||||
this.emit(ServerEvent.disconnect, this.clients.get(cid), cause)
|
|
||||||
} else if (global.debug) {
|
|
||||||
console.error(`unknow client ${ctx} disconnect cause ${cause}`)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
connectEvent.on(ServerEvent.error, (ctx, cause) => {
|
connectEvent.on(ServerEvent.error, (ctx, error) => {
|
||||||
let cid = ctx?.channel().id() + ''
|
this.onerror(ctx, error)
|
||||||
if (this.clients.has(cid)) {
|
|
||||||
this.emit(ServerEvent.error, this.clients.get(cid), cause)
|
|
||||||
} else if (global.debug) {
|
|
||||||
console.error(`unknow client ${ctx} cause error ${cause}`)
|
|
||||||
console.ex(cause)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
close() {
|
|
||||||
if (this.pipeline.names().contains(Keys.Detect)) {
|
protected getId(ctx: any) {
|
||||||
this.pipeline.remove(Keys.Detect)
|
try {
|
||||||
|
return ctx.channel().id() + ''
|
||||||
|
} catch (error) {
|
||||||
|
console.log(Object.toString.apply(ctx))
|
||||||
|
console.ex(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getRequest(ctx) {
|
||||||
|
let channel = ctx.channel()
|
||||||
|
let req = channel.attr(AttributeKeys.Request).get()
|
||||||
|
let headers = {}
|
||||||
|
let nativeHeaders = req.headers()
|
||||||
|
nativeHeaders.forEach(function (header) {
|
||||||
|
headers[header.getKey()] = header.getValue()
|
||||||
|
})
|
||||||
|
let request = new Request(req.uri(), req.method().name(), headers)
|
||||||
|
request.connection = {
|
||||||
|
remoteAddress: channel.remoteAddress() + ''
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getSocket(ctx) {
|
||||||
|
return new NettyClient(ctx.channel())
|
||||||
|
}
|
||||||
|
|
||||||
|
protected doClose() {
|
||||||
|
if (this.instance.names().contains(Keys.Detect)) {
|
||||||
|
this.instance.remove(Keys.Detect)
|
||||||
}
|
}
|
||||||
this.clients.forEach(client => client.close())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
import { ServerOptions } from '../socket-io'
|
import { JavaServerOptions, ServerEvent } from '../server'
|
||||||
import { ServerEvent } from '../socket-io/constants'
|
|
||||||
import { TextWebSocketFrameHandlerAdapter } from './adapter'
|
import { TextWebSocketFrameHandlerAdapter } from './adapter'
|
||||||
|
|
||||||
export class TextWebSocketFrameHandler extends TextWebSocketFrameHandlerAdapter {
|
export class TextWebSocketFrameHandler extends TextWebSocketFrameHandlerAdapter {
|
||||||
private event: EventEmitter
|
private event: EventEmitter
|
||||||
constructor(options: ServerOptions) {
|
constructor(options: JavaServerOptions) {
|
||||||
super()
|
super()
|
||||||
this.event = options.event
|
this.event = options.event
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
import { WebSocketHandlerAdapter } from "./adapter"
|
import { WebSocketHandlerAdapter } from "./adapter"
|
||||||
import { ServerEvent } from '../socket-io/constants'
|
|
||||||
|
import { ServerEvent } from '../server'
|
||||||
|
|
||||||
export class WebSocketDetect extends WebSocketHandlerAdapter {
|
export class WebSocketDetect extends WebSocketHandlerAdapter {
|
||||||
private event: EventEmitter
|
private event: EventEmitter
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { ServerOptions } from '../socket-io'
|
import { JavaServerOptions, ServerEvent } from '../server'
|
||||||
import { ServerEvent } from '../socket-io/constants'
|
|
||||||
|
|
||||||
import { Keys } from './constants'
|
import { Keys } from './constants'
|
||||||
import { HttpRequestHandler } from './httprequest'
|
import { HttpRequestHandler } from './httprequest'
|
||||||
@ -13,8 +12,8 @@ const HttpObjectAggregator = Java.type('io.netty.handler.codec.http.HttpObjectAg
|
|||||||
const WebSocketServerProtocolHandler = Java.type('io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler')
|
const WebSocketServerProtocolHandler = Java.type('io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler')
|
||||||
|
|
||||||
export class WebSocketHandler extends WebSocketHandlerAdapter {
|
export class WebSocketHandler extends WebSocketHandlerAdapter {
|
||||||
private options: ServerOptions
|
private options: JavaServerOptions
|
||||||
constructor(options: ServerOptions) {
|
constructor(options: JavaServerOptions) {
|
||||||
super()
|
super()
|
||||||
this.options = options
|
this.options = options
|
||||||
}
|
}
|
||||||
|
7
packages/websocket/src/server/client.ts
Normal file
7
packages/websocket/src/server/client.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { EventEmitter } from 'events'
|
||||||
|
export abstract class WebSocketClient extends EventEmitter {
|
||||||
|
public id: string
|
||||||
|
public _socket: any
|
||||||
|
abstract send(text: string, opts?: any, callback?: (err?: Error) => void)
|
||||||
|
abstract close()
|
||||||
|
}
|
@ -1,52 +1,90 @@
|
|||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
|
import { ServerOptions } from '../socket.io'
|
||||||
|
import { WebSocketClient } from './client'
|
||||||
|
|
||||||
import { Transport } from '../transport'
|
import type { Request } from './request'
|
||||||
|
|
||||||
interface ServerOptions {
|
export enum ServerEvent {
|
||||||
|
detect = 'detect',
|
||||||
|
request = 'request',
|
||||||
|
upgrade = 'upgrade',
|
||||||
|
connect = 'connect',
|
||||||
|
connection = 'connection',
|
||||||
|
message = 'message',
|
||||||
|
error = 'error',
|
||||||
|
disconnecting = 'disconnecting',
|
||||||
|
disconnect = 'disconnect',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JavaServerOptions extends ServerOptions {
|
||||||
event?: EventEmitter
|
event?: EventEmitter
|
||||||
root?: string
|
root?: string
|
||||||
/**
|
|
||||||
* name of the path to capture
|
|
||||||
* @default "/socket.io"
|
|
||||||
*/
|
|
||||||
path: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WebSocketServerImpl extends EventEmitter {
|
export abstract class WebSocketServer extends EventEmitter {
|
||||||
close(): void
|
protected instance: any
|
||||||
}
|
protected options: JavaServerOptions
|
||||||
|
private clients: Map<string, WebSocketClient>
|
||||||
export class WebSocketServer extends EventEmitter {
|
constructor(instance: any, options: JavaServerOptions) {
|
||||||
options: Partial<ServerOptions>
|
|
||||||
private websocketServer: WebSocketServerImpl
|
|
||||||
|
|
||||||
constructor(instance: any, options: Partial<ServerOptions>) {
|
|
||||||
super()
|
super()
|
||||||
if (!instance) { throw new Error('instance can\'t be undefiend!') }
|
this.instance = instance
|
||||||
this.options = Object.assign({
|
this.options = options
|
||||||
event: new EventEmitter(),
|
this.clients = new Map()
|
||||||
path: '/ws',
|
console.debug('create websocket server from ' + this.constructor.name)
|
||||||
root: root + '/wwwroot',
|
this.initialize()
|
||||||
}, options)
|
|
||||||
this.selectServerImpl(instance)
|
|
||||||
}
|
}
|
||||||
|
protected onconnect(handler: any) {
|
||||||
on(event: "connect", cb: (transport: Transport) => void): this
|
let id = this.getId(handler)
|
||||||
on(event: "message", cb: (transport: Transport, text: string) => void): this
|
console.log('client', id, 'connect')
|
||||||
on(event: "disconnect", cb: (transport: Transport, reason: string) => void): this
|
let request = this.getRequest(handler)
|
||||||
on(event: "error", cb: (transport: Transport, cause: Error) => void): this
|
request.id = id
|
||||||
on(event: string, cb: (transport: Transport, extra?: any) => void): this {
|
let websocket = this.getSocket(handler)
|
||||||
this.websocketServer.on(event, cb)
|
this.clients.set(this.getId(handler), websocket)
|
||||||
return this
|
this.emit(ServerEvent.connect, request, websocket)
|
||||||
}
|
}
|
||||||
|
protected onmessage(handler: any, message: string) {
|
||||||
private selectServerImpl(instance: any) {
|
this.execute(handler, (websocket) => websocket.emit(ServerEvent.message, message))
|
||||||
let WebSocketServerImpl = undefined
|
}
|
||||||
if (instance.class.name.startsWith('io.netty.channel')) {
|
protected ondisconnect(handler: any, cause: string) {
|
||||||
WebSocketServerImpl = require("../netty").NettyWebSocketServer
|
this.execute(handler, (websocket) => websocket.emit(ServerEvent.disconnect, cause))
|
||||||
} else {
|
}
|
||||||
WebSocketServerImpl = require("../tomcat").TomcatWebSocketServer
|
protected onerror(handler: any, error: Error) {
|
||||||
|
if (global.debug) {
|
||||||
|
console.ex(error)
|
||||||
}
|
}
|
||||||
this.websocketServer = new WebSocketServerImpl(instance, this.options)
|
this.execute(handler, (websocket) => websocket.emit(ServerEvent.error, error))
|
||||||
}
|
}
|
||||||
|
protected execute(handler: any, callback: (websocket: WebSocketClient) => void) {
|
||||||
|
let id = this.getId(handler)
|
||||||
|
if (this.clients.has(id)) {
|
||||||
|
this.clients.has(id) && callback(this.clients.get(id))
|
||||||
|
} else {
|
||||||
|
console.debug('ignore execute', handler, 'callback', callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public close() {
|
||||||
|
this.clients.forEach(websocket => websocket.close())
|
||||||
|
this.doClose()
|
||||||
|
}
|
||||||
|
protected abstract initialize(): void
|
||||||
|
protected abstract getId(handler: any): string
|
||||||
|
protected abstract getRequest(handler: any): Request
|
||||||
|
protected abstract getSocket(handler: any): WebSocketClient
|
||||||
|
protected abstract doClose(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const attach = (instance, options) => {
|
||||||
|
if (!instance) { throw new Error('instance can\'t be undefiend!') }
|
||||||
|
options = Object.assign({
|
||||||
|
event: new EventEmitter(),
|
||||||
|
path: '/ws',
|
||||||
|
root: root + '/wwwroot',
|
||||||
|
}, options)
|
||||||
|
let WebSocketServerImpl = undefined
|
||||||
|
if (instance.class.name.startsWith('io.netty.channel')) {
|
||||||
|
WebSocketServerImpl = require("../netty").NettyWebSocketServer
|
||||||
|
} else {
|
||||||
|
WebSocketServerImpl = require("../tomcat").TomcatWebSocketServer
|
||||||
|
}
|
||||||
|
return new WebSocketServerImpl(instance, options)
|
||||||
}
|
}
|
||||||
|
23
packages/websocket/src/server/request.ts
Normal file
23
packages/websocket/src/server/request.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { WebSocketClient } from "./client"
|
||||||
|
interface HttpHeaders {
|
||||||
|
[name: string]: string
|
||||||
|
}
|
||||||
|
interface Connection {
|
||||||
|
remoteAddress: string
|
||||||
|
}
|
||||||
|
export class Request {
|
||||||
|
public id: string
|
||||||
|
public url: string
|
||||||
|
public method: string
|
||||||
|
public headers: HttpHeaders
|
||||||
|
public connection: Connection
|
||||||
|
public websocket: WebSocketClient
|
||||||
|
|
||||||
|
public _query: any
|
||||||
|
|
||||||
|
constructor(url: string, method = "GET", headers = {}) {
|
||||||
|
this.url = url
|
||||||
|
this.method = method
|
||||||
|
this.headers = headers
|
||||||
|
}
|
||||||
|
}
|
@ -1,164 +0,0 @@
|
|||||||
import { EventEmitter } from 'events'
|
|
||||||
import { Namespace } from './namespace'
|
|
||||||
import { Parser } from './parser'
|
|
||||||
import { Socket } from './socket'
|
|
||||||
|
|
||||||
export type SocketId = string
|
|
||||||
export type Room = string
|
|
||||||
|
|
||||||
export interface BroadcastFlags {
|
|
||||||
volatile?: boolean
|
|
||||||
compress?: boolean
|
|
||||||
local?: boolean
|
|
||||||
broadcast?: boolean
|
|
||||||
binary?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BroadcastOptions {
|
|
||||||
rooms: Set<Room>
|
|
||||||
except?: Set<SocketId>
|
|
||||||
flags?: BroadcastFlags
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Adapter extends EventEmitter implements Adapter {
|
|
||||||
rooms: Map<Room, Set<SocketId>>
|
|
||||||
sids: Map<SocketId, Set<Room>>
|
|
||||||
private readonly encoder: Parser
|
|
||||||
parser: Parser
|
|
||||||
|
|
||||||
constructor(readonly nsp: Namespace) {
|
|
||||||
super()
|
|
||||||
this.rooms = new Map()
|
|
||||||
this.sids = new Map()
|
|
||||||
this.parser = nsp.server._parser
|
|
||||||
this.encoder = this.parser
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a socket to a list of room.
|
|
||||||
*
|
|
||||||
* @param {String} socket id
|
|
||||||
* @param {String} rooms
|
|
||||||
* @param {Function} callback
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
addAll(id: SocketId, rooms: Set<Room>): Promise<void> | void {
|
|
||||||
for (const room of rooms) {
|
|
||||||
if (!this.sids.has(id)) {
|
|
||||||
this.sids.set(id, new Set())
|
|
||||||
}
|
|
||||||
this.sids.get(id).add(room)
|
|
||||||
|
|
||||||
if (!this.rooms.has(room)) {
|
|
||||||
this.rooms.set(room, new Set())
|
|
||||||
}
|
|
||||||
this.rooms.get(room).add(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
del(id: string, room: string, callback?: (err?: any) => void): void {
|
|
||||||
if (this.sids.has(id)) {
|
|
||||||
this.sids.get(id).delete(room)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.rooms.has(room)) {
|
|
||||||
this.rooms.get(room).delete(id)
|
|
||||||
if (this.rooms.get(room).size === 0) this.rooms.delete(room)
|
|
||||||
}
|
|
||||||
callback && callback.bind(null, null)
|
|
||||||
}
|
|
||||||
delAll(id: string): void {
|
|
||||||
if (!this.sids.has(id)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const room of this.sids.get(id)) {
|
|
||||||
if (this.rooms.has(room)) {
|
|
||||||
this.rooms.get(room).delete(id)
|
|
||||||
if (this.rooms.get(room).size === 0) this.rooms.delete(room)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sids.delete(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcasts a packet.
|
|
||||||
*
|
|
||||||
* Options:
|
|
||||||
* - `flags` {Object} flags for this packet
|
|
||||||
* - `except` {Array} sids that should be excluded
|
|
||||||
* - `rooms` {Array} list of rooms to broadcast to
|
|
||||||
*
|
|
||||||
* @param {Object} packet the packet object
|
|
||||||
* @param {Object} opts the options
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public broadcast(packet: any, opts: BroadcastOptions): void {
|
|
||||||
const rooms = opts.rooms
|
|
||||||
const except = opts.except || new Set()
|
|
||||||
const flags = opts.flags || {}
|
|
||||||
const packetOpts = {
|
|
||||||
preEncoded: true,
|
|
||||||
volatile: flags.volatile,
|
|
||||||
compress: flags.compress
|
|
||||||
}
|
|
||||||
const ids = new Set()
|
|
||||||
|
|
||||||
packet.nsp = this.nsp.name
|
|
||||||
const encodedPackets = this.encoder.encode(packet)
|
|
||||||
|
|
||||||
if (rooms.size) {
|
|
||||||
for (const room of rooms) {
|
|
||||||
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 (socket) {
|
|
||||||
socket.packet(encodedPackets as any, packetOpts)
|
|
||||||
ids.add(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const [id] of this.sids) {
|
|
||||||
if (except.has(id)) continue
|
|
||||||
const socket = this.nsp.sockets.get(id)
|
|
||||||
if (socket) socket.packet(encodedPackets as any, packetOpts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a list of sockets by sid.
|
|
||||||
*
|
|
||||||
* @param {Set<Room>} rooms the explicit set of rooms to check.
|
|
||||||
*/
|
|
||||||
public sockets(rooms: Set<Room>): Promise<Set<SocketId>> {
|
|
||||||
const sids = new Set<SocketId>()
|
|
||||||
if (rooms.size) {
|
|
||||||
for (const room of rooms) {
|
|
||||||
if (!this.rooms.has(room)) continue
|
|
||||||
for (const id of this.rooms.get(room)) {
|
|
||||||
if (this.nsp.sockets.has(id)) {
|
|
||||||
sids.add(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const [id] of this.sids) {
|
|
||||||
if (this.nsp.sockets.has(id)) sids.add(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.resolve(sids)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the list of rooms a given socket has joined.
|
|
||||||
*
|
|
||||||
* @param {SocketId} id the socket id
|
|
||||||
*/
|
|
||||||
public socketRooms(id: SocketId): Set<Room> | undefined {
|
|
||||||
return this.sids.get(id)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,360 +0,0 @@
|
|||||||
import { EventEmitter } from 'events'
|
|
||||||
import { Parser } from './parser'
|
|
||||||
import { Packet } from './packet'
|
|
||||||
import { Namespace, Server, Socket } from './index'
|
|
||||||
import { PacketTypes, SubPacketTypes } from './types'
|
|
||||||
import { ServerEvent } from './constants'
|
|
||||||
import { SocketId } from './adapter'
|
|
||||||
import { Transport } from '../transport'
|
|
||||||
|
|
||||||
const parser = new Parser()
|
|
||||||
|
|
||||||
export class Client extends EventEmitter {
|
|
||||||
public readonly conn: Transport
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
readonly id: string
|
|
||||||
private readonly server: Server
|
|
||||||
// private readonly encoder: Encoder
|
|
||||||
private readonly decoder: any
|
|
||||||
private sockets: Map<SocketId, Socket>
|
|
||||||
private nsps: Map<string, Socket>
|
|
||||||
private connectTimeout: NodeJS.Timeout
|
|
||||||
|
|
||||||
private checkIntervalTimer: NodeJS.Timeout
|
|
||||||
private upgradeTimeoutTimer: NodeJS.Timeout
|
|
||||||
private pingTimeoutTimer: NodeJS.Timeout
|
|
||||||
private pingIntervalTimer: NodeJS.Timeout
|
|
||||||
|
|
||||||
constructor(server: Server, conn) {
|
|
||||||
super()
|
|
||||||
this.server = server
|
|
||||||
this.conn = conn
|
|
||||||
// this.encoder = server.encoder
|
|
||||||
this.decoder = server._parser
|
|
||||||
this.id = this.conn.id + ''
|
|
||||||
this.setup()
|
|
||||||
// =============================
|
|
||||||
this.sockets = new Map()
|
|
||||||
this.nsps = new Map()
|
|
||||||
// ================== engine.io
|
|
||||||
this.onOpen()
|
|
||||||
// ================== Transport
|
|
||||||
this.conn.on(ServerEvent.disconnect, (reason) => {
|
|
||||||
this.onclose(reason)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @return the reference to the request that originated the Engine.IO connection
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public get request(): any /**IncomingMessage */ {
|
|
||||||
return this.conn.request
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Sets up event listeners.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private setup() {
|
|
||||||
// @ts-ignore
|
|
||||||
// this.decoder.on("decoded", this.ondecoded)
|
|
||||||
this.conn.on("data", this.ondata.bind(this))
|
|
||||||
this.conn.on("error", this.onerror.bind(this))
|
|
||||||
this.conn.on("close", this.onclose.bind(this))
|
|
||||||
console.debug(`setup client ${this.id}`)
|
|
||||||
this.connectTimeout = setTimeout(() => {
|
|
||||||
if (this.nsps.size === 0) {
|
|
||||||
console.debug("no namespace joined yet, close the client")
|
|
||||||
this.close()
|
|
||||||
} else {
|
|
||||||
console.debug("the client has already joined a namespace, nothing to do")
|
|
||||||
}
|
|
||||||
}, this.server._connectTimeout)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Connects a client to a namespace.
|
|
||||||
*
|
|
||||||
* @param {String} name - the namespace
|
|
||||||
* @param {Object} auth - the auth parameters
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private connect(name: string, auth: object = {}) {
|
|
||||||
console.debug(`client ${this.id} connecting to namespace ${name} has: ${this.server._nsps.has(name)}`)
|
|
||||||
if (this.server._nsps.has(name)) {
|
|
||||||
return this.doConnect(name, auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.server._checkNamespace(name, auth, (dynamicNsp: Namespace) => {
|
|
||||||
if (dynamicNsp) {
|
|
||||||
console.debug(`dynamic namespace ${dynamicNsp.name} was created`)
|
|
||||||
this.doConnect(name, auth)
|
|
||||||
} else {
|
|
||||||
console.debug(`creation of namespace ${name} was denied`)
|
|
||||||
this._packet({
|
|
||||||
type: PacketTypes.MESSAGE,
|
|
||||||
sub_type: SubPacketTypes.ERROR,
|
|
||||||
nsp: name,
|
|
||||||
data: {
|
|
||||||
message: "Invalid namespace"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
doConnect(name, auth: object) {
|
|
||||||
if (this.connectTimeout) {
|
|
||||||
clearTimeout(this.connectTimeout)
|
|
||||||
this.connectTimeout = null
|
|
||||||
}
|
|
||||||
const nsp = this.server.of(name)
|
|
||||||
|
|
||||||
nsp._add(this, auth, (socket: Socket) => {
|
|
||||||
this.sockets.set(socket.id, socket)
|
|
||||||
this.nsps.set(nsp.name, socket)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Disconnects from all namespaces and closes transport.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_disconnect() {
|
|
||||||
for (const socket of this.sockets.values()) {
|
|
||||||
socket.disconnect()
|
|
||||||
}
|
|
||||||
this.sockets.clear()
|
|
||||||
this.close()
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Removes a socket. Called by each `Socket`.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_remove(socket: Socket) {
|
|
||||||
if (this.sockets.has(socket.id)) {
|
|
||||||
this.sockets.delete(socket.id)
|
|
||||||
this.nsps.delete(socket.nsp.name)
|
|
||||||
} else {
|
|
||||||
console.debug(`ignoring remove for ${socket.id}`,)
|
|
||||||
}
|
|
||||||
process.nextTick(() => {
|
|
||||||
if (this.sockets.size == 0) {
|
|
||||||
this.onclose('no live socket')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Closes the underlying connection.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private close() {
|
|
||||||
console.debug(`client ${this.id} close`)
|
|
||||||
if ("open" == this.conn.readyState) {
|
|
||||||
console.debug("forcing transport close")
|
|
||||||
this.onclose("forced server close")
|
|
||||||
this.conn.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Writes a packet to the transport.
|
|
||||||
*
|
|
||||||
* @param {Object} packet object
|
|
||||||
* @param {Object} opts
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_packet(packet: Packet, opts = { preEncoded: false }) {
|
|
||||||
// opts = opts || {}
|
|
||||||
// const self = this
|
|
||||||
|
|
||||||
// // this writes to the actual connection
|
|
||||||
// function writeToEngine(encodedPackets) {
|
|
||||||
// if (opts.volatile && !self.conn.transport.writable) return
|
|
||||||
// for (let i = 0; i < encodedPackets.length; i++) {
|
|
||||||
// self.conn.write(encodedPackets[i], { compress: opts.compress })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if ("open" == this.conn.readyState) {
|
|
||||||
// debug("writing packet %j", packet)
|
|
||||||
// if (!opts.preEncoded) {
|
|
||||||
// // not broadcasting, need to encode
|
|
||||||
// writeToEngine(this.encoder.encode(packet)) // encode, then write results to engine
|
|
||||||
// } else {
|
|
||||||
// // a broadcast pre-encodes a packet
|
|
||||||
// writeToEngine(packet)
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// debug("ignoring packet write %j", packet)
|
|
||||||
// }
|
|
||||||
if ("open" == this.conn.readyState) {
|
|
||||||
this.conn.send(opts.preEncoded ? packet as unknown as string : parser.encode(packet))
|
|
||||||
} else {
|
|
||||||
console.debug(`ignoring write packet ${JSON.stringify(packet)} to client ${this.id} is already close!`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Called with incoming transport data.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private ondata(data) {
|
|
||||||
// try/catch is needed for protocol violations (GH-1880)
|
|
||||||
try {
|
|
||||||
this.decoder.add(data)
|
|
||||||
} catch (e) {
|
|
||||||
this.onerror(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Called when parser fully decodes a packet.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
ondecoded(packet: Packet) {
|
|
||||||
if (SubPacketTypes.CONNECT == packet.sub_type) {
|
|
||||||
this.connect(packet.nsp, packet.data)
|
|
||||||
} else {
|
|
||||||
process.nextTick(() => {
|
|
||||||
const socket = this.nsps.get(packet.nsp)
|
|
||||||
if (socket) {
|
|
||||||
socket._onpacket(packet)
|
|
||||||
} else {
|
|
||||||
console.debug(`client ${this.id} no socket for namespace ${packet.nsp}.`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Handles an error.
|
|
||||||
*
|
|
||||||
* @param {Object} err object
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private onerror(err) {
|
|
||||||
for (const socket of this.sockets.values()) {
|
|
||||||
socket._onerror(err)
|
|
||||||
}
|
|
||||||
this.conn.close()
|
|
||||||
}
|
|
||||||
onclose(reason?: string) {
|
|
||||||
this.conn.readyState = "closing"
|
|
||||||
// ======= engine.io
|
|
||||||
this.onClose(reason)
|
|
||||||
// cleanup connectTimeout
|
|
||||||
if (this.connectTimeout) {
|
|
||||||
clearTimeout(this.connectTimeout)
|
|
||||||
this.connectTimeout = null
|
|
||||||
}
|
|
||||||
console.debug(`client ${this.id} close with reason ${reason}`)
|
|
||||||
// ignore a potential subsequent `close` event
|
|
||||||
// `nsps` and `sockets` are cleaned up seamlessly
|
|
||||||
for (const socket of this.sockets.values()) {
|
|
||||||
socket._onclose(reason)
|
|
||||||
}
|
|
||||||
this.sockets.clear()
|
|
||||||
// this.decoder.destroy(); // clean up decoder
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
// this.conn.removeListener('data', this.ondata);
|
|
||||||
// this.conn.removeListener('error', this.onerror);
|
|
||||||
// this.conn.removeListener('close', this.onclose);
|
|
||||||
// this.decoder.removeListener('decoded', this.ondecoded);
|
|
||||||
}
|
|
||||||
|
|
||||||
//================== engine.io
|
|
||||||
onOpen() {
|
|
||||||
this.conn.readyState = "open"
|
|
||||||
this._packet({
|
|
||||||
type: PacketTypes.OPEN,
|
|
||||||
data: {
|
|
||||||
sid: this.id,
|
|
||||||
upgrades: [],
|
|
||||||
pingInterval: this.server.options.pingInterval,
|
|
||||||
pingTimeout: this.server.options.pingTimeout
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.schedulePing()
|
|
||||||
}
|
|
||||||
|
|
||||||
onPacket(packet: Packet) {
|
|
||||||
if ("open" === this.conn.readyState) {
|
|
||||||
// export packet event
|
|
||||||
// debug("packet")
|
|
||||||
// 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.options.pingInterval + this.server.options.pingTimeout * 2)
|
|
||||||
switch (packet.type) {
|
|
||||||
case PacketTypes.PING:
|
|
||||||
this._packet({
|
|
||||||
type: PacketTypes.PONG,
|
|
||||||
data: packet.data
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case PacketTypes.PONG:
|
|
||||||
this.schedulePing()
|
|
||||||
break
|
|
||||||
case PacketTypes.UPGRADE:
|
|
||||||
break
|
|
||||||
case PacketTypes.MESSAGE:
|
|
||||||
this.ondecoded(packet)
|
|
||||||
break
|
|
||||||
case PacketTypes.CLOSE:
|
|
||||||
this.onclose()
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
console.log(`client ${this.id} reciver unknow packet type: ${packet.type}`)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.debug(`packet received with closed client ${this.id}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Called upon transport considered closed.
|
|
||||||
* Possible reasons: `ping timeout`, `client error`, `parse error`,
|
|
||||||
* `transport error`, `server close`, `transport close`
|
|
||||||
*/
|
|
||||||
onClose(reason, description?: string) {
|
|
||||||
// if ("closed" !== this.conn.readyState) {
|
|
||||||
clearTimeout(this.pingIntervalTimer)
|
|
||||||
clearTimeout(this.pingTimeoutTimer)
|
|
||||||
|
|
||||||
clearInterval(this.checkIntervalTimer)
|
|
||||||
this.checkIntervalTimer = null
|
|
||||||
clearTimeout(this.upgradeTimeoutTimer)
|
|
||||||
// this.emit("close", reason, description)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Pings client every `this.pingInterval` and expects response
|
|
||||||
* within `this.pingTimeout` or closes connection.
|
|
||||||
*
|
|
||||||
* @api private
|
|
||||||
*/
|
|
||||||
schedulePing() {
|
|
||||||
clearTimeout(this.pingIntervalTimer)
|
|
||||||
this.pingIntervalTimer = setTimeout(() => {
|
|
||||||
this.resetPingTimeout(this.server.options.pingTimeout)
|
|
||||||
process.nextTick(() => this._packet({ type: PacketTypes.PING }))
|
|
||||||
}, this.server.options.pingInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets ping timeout.
|
|
||||||
*
|
|
||||||
* @api private
|
|
||||||
*/
|
|
||||||
resetPingTimeout(timeout: number) {
|
|
||||||
clearTimeout(this.pingTimeoutTimer)
|
|
||||||
this.pingTimeoutTimer = setTimeout(() => {
|
|
||||||
if (this.conn.readyState === "closed") return
|
|
||||||
this.onclose("ping timeout")
|
|
||||||
}, timeout)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
export enum ServerEvent {
|
|
||||||
detect = 'detect',
|
|
||||||
connect = 'connect',
|
|
||||||
connection = 'connection',
|
|
||||||
message = 'message',
|
|
||||||
error = 'error',
|
|
||||||
disconnecting = 'disconnecting',
|
|
||||||
disconnect = 'disconnect',
|
|
||||||
}
|
|
@ -1,677 +0,0 @@
|
|||||||
import { EventEmitter } from 'events'
|
|
||||||
|
|
||||||
import { ServerEvent } from './constants'
|
|
||||||
import { Namespace } from './namespace'
|
|
||||||
import { Client } from './client'
|
|
||||||
import { Parser } from './parser'
|
|
||||||
import { Socket } from './socket'
|
|
||||||
import { Adapter } from './adapter'
|
|
||||||
import { Transport } from '../transport'
|
|
||||||
import { ParentNamespace } from './parent-namespace'
|
|
||||||
|
|
||||||
interface EngineOptions {
|
|
||||||
/**
|
|
||||||
* how many ms without a pong packet to consider the connection closed
|
|
||||||
* @default 5000
|
|
||||||
*/
|
|
||||||
pingTimeout: number
|
|
||||||
/**
|
|
||||||
* how many ms before sending a new ping packet
|
|
||||||
* @default 25000
|
|
||||||
*/
|
|
||||||
pingInterval: number
|
|
||||||
/**
|
|
||||||
* how many ms before an uncompleted transport upgrade is cancelled
|
|
||||||
* @default 10000
|
|
||||||
*/
|
|
||||||
upgradeTimeout: number
|
|
||||||
/**
|
|
||||||
* how many bytes or characters a message can be, before closing the session (to avoid DoS).
|
|
||||||
* @default 1e5 (100 KB)
|
|
||||||
*/
|
|
||||||
maxHttpBufferSize: number
|
|
||||||
/**
|
|
||||||
* A function that receives a given handshake or upgrade request as its first parameter,
|
|
||||||
* and can decide whether to continue or not. The second argument is a function that needs
|
|
||||||
* to be called with the decided information: fn(err, success), where success is a boolean
|
|
||||||
* value where false means that the request is rejected, and err is an error code.
|
|
||||||
*/
|
|
||||||
// allowRequest: (
|
|
||||||
// req: http.IncomingMessage,
|
|
||||||
// fn: (err: string | null | undefined, success: boolean) => void
|
|
||||||
// ) => void
|
|
||||||
/**
|
|
||||||
* the low-level transports that are enabled
|
|
||||||
* @default ["polling", "websocket"]
|
|
||||||
*/
|
|
||||||
// transports: Transport[]
|
|
||||||
/**
|
|
||||||
* whether to allow transport upgrades
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
allowUpgrades: boolean
|
|
||||||
/**
|
|
||||||
* parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable.
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
perMessageDeflate: boolean | object
|
|
||||||
/**
|
|
||||||
* parameters of the http compression for the polling transports (see zlib api docs). Set to false to disable.
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
httpCompression: boolean | object
|
|
||||||
/**
|
|
||||||
* what WebSocket server implementation to use. Specified module must
|
|
||||||
* conform to the ws interface (see ws module api docs). Default value is ws.
|
|
||||||
* An alternative c++ addon is also available by installing uws module.
|
|
||||||
*/
|
|
||||||
wsEngine: string
|
|
||||||
/**
|
|
||||||
* an optional packet which will be concatenated to the handshake packet emitted by Engine.IO.
|
|
||||||
*/
|
|
||||||
initialPacket: any
|
|
||||||
/**
|
|
||||||
* configuration of the cookie that contains the client sid to send as part of handshake response headers. This cookie
|
|
||||||
* might be used for sticky-session. Defaults to not sending any cookie.
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
// cookie: CookieSerializeOptions | boolean
|
|
||||||
/**
|
|
||||||
* the options that will be forwarded to the cors module
|
|
||||||
*/
|
|
||||||
// cors: CorsOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AttachOptions {
|
|
||||||
/**
|
|
||||||
* name of the path to capture
|
|
||||||
* @default "/engine.io"
|
|
||||||
*/
|
|
||||||
path: string
|
|
||||||
/**
|
|
||||||
* destroy unhandled upgrade requests
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
destroyUpgrade: boolean
|
|
||||||
/**
|
|
||||||
* milliseconds after which unhandled requests are ended
|
|
||||||
* @default 1000
|
|
||||||
*/
|
|
||||||
destroyUpgradeTimeout: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EngineAttachOptions extends EngineOptions, AttachOptions { }
|
|
||||||
|
|
||||||
interface ServerOptions extends EngineAttachOptions {
|
|
||||||
event?: EventEmitter
|
|
||||||
root?: string
|
|
||||||
/**
|
|
||||||
* name of the path to capture
|
|
||||||
* @default "/socket.io"
|
|
||||||
*/
|
|
||||||
path: string
|
|
||||||
/**
|
|
||||||
* whether to serve the client files
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
serveClient: boolean
|
|
||||||
/**
|
|
||||||
* the adapter to use
|
|
||||||
* @default the in-memory adapter (https://github.com/socketio/socket.io-adapter)
|
|
||||||
*/
|
|
||||||
adapter: any
|
|
||||||
/**
|
|
||||||
* the parser to use
|
|
||||||
* @default the default parser (https://github.com/socketio/socket.io-parser)
|
|
||||||
*/
|
|
||||||
parser: any
|
|
||||||
/**
|
|
||||||
* how many ms before a client without namespace is closed
|
|
||||||
* @default 45000
|
|
||||||
*/
|
|
||||||
connectTimeout: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WebSocketServer extends EventEmitter {
|
|
||||||
close(): void
|
|
||||||
}
|
|
||||||
|
|
||||||
class Server {
|
|
||||||
public readonly sockets: Namespace
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_parser: Parser
|
|
||||||
private readonly encoder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_nsps: Map<string, Namespace>
|
|
||||||
private parentNsps: Map<
|
|
||||||
| string
|
|
||||||
| RegExp
|
|
||||||
| ((
|
|
||||||
name: string,
|
|
||||||
query: object,
|
|
||||||
fn: (err: Error, success: boolean) => void
|
|
||||||
) => void),
|
|
||||||
ParentNamespace
|
|
||||||
> = new Map();
|
|
||||||
private _adapter: Adapter
|
|
||||||
// private _serveClient: boolean;
|
|
||||||
private eio
|
|
||||||
private engine: { ws: any }
|
|
||||||
private _path: string
|
|
||||||
private clientPathRegex: RegExp
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_connectTimeout: number
|
|
||||||
|
|
||||||
options: Partial<ServerOptions>
|
|
||||||
private websocketServer: WebSocketServer
|
|
||||||
private allClients: Map<string, Client>
|
|
||||||
|
|
||||||
constructor(instance: any, options: Partial<ServerOptions>) {
|
|
||||||
if (!instance) { throw new Error('instance can\'t be undefiend!') }
|
|
||||||
this.options = Object.assign({
|
|
||||||
event: new EventEmitter(),
|
|
||||||
path: '/socket.io',
|
|
||||||
root: root + '/wwwroot',
|
|
||||||
serveClient: false,
|
|
||||||
connectTimeout: 45000,
|
|
||||||
wsEngine: process.env.EIO_WS_ENGINE || "ws",
|
|
||||||
pingTimeout: 5000,
|
|
||||||
pingInterval: 25000,
|
|
||||||
upgradeTimeout: 10000,
|
|
||||||
maxHttpBufferSize: 1e6,
|
|
||||||
transports: 'websocket',
|
|
||||||
allowUpgrades: true,
|
|
||||||
httpCompression: {
|
|
||||||
threshold: 1024
|
|
||||||
},
|
|
||||||
cors: false
|
|
||||||
}, options)
|
|
||||||
this.initServerConfig()
|
|
||||||
this.sockets = this.of('/')
|
|
||||||
this.selectServerImpl(instance)
|
|
||||||
this.initServer()
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Sets/gets whether client code is being served.
|
|
||||||
*
|
|
||||||
* @param {Boolean} v - whether to serve client code
|
|
||||||
* @return {Server|Boolean} self when setting or value when getting
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public serveClient(v: boolean): Server
|
|
||||||
public serveClient(): boolean
|
|
||||||
public serveClient(v?: boolean): Server | boolean {
|
|
||||||
throw new Error("Method not implemented.")
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Executes the middleware for an incoming namespace not already created on the server.
|
|
||||||
*
|
|
||||||
* @param {String} name - name of incoming namespace
|
|
||||||
* @param {Object} auth - the auth parameters
|
|
||||||
* @param {Function} fn - callback
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_checkNamespace(
|
|
||||||
name: string,
|
|
||||||
auth: object,
|
|
||||||
fn: (nsp: Namespace) => void
|
|
||||||
) {
|
|
||||||
// if (this.parentNsps.size === 0) return fn(false)
|
|
||||||
|
|
||||||
// const keysIterator = this.parentNsps.keys()
|
|
||||||
|
|
||||||
// const run = () => {
|
|
||||||
// let nextFn = keysIterator.next()
|
|
||||||
// if (nextFn.done) {
|
|
||||||
// return fn(false)
|
|
||||||
// }
|
|
||||||
// nextFn.value(name, auth, (err, allow) => {
|
|
||||||
// if (err || !allow) {
|
|
||||||
// run()
|
|
||||||
// } else {
|
|
||||||
// fn(this.parentNsps.get(nextFn.value).createChild(name))
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
fn(undefined)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Sets the client serving path.
|
|
||||||
*
|
|
||||||
* @param {String} v pathname
|
|
||||||
* @return {Server|String} self when setting or value when getting
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
path(): string
|
|
||||||
path(v: string): Server
|
|
||||||
path(v?: any): string | Server {
|
|
||||||
if (!arguments.length) return this._path
|
|
||||||
|
|
||||||
this._path = v.replace(/\/$/, "")
|
|
||||||
|
|
||||||
const escapedPath = this._path.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")
|
|
||||||
this.clientPathRegex = new RegExp(
|
|
||||||
"^" +
|
|
||||||
escapedPath +
|
|
||||||
"/socket\\.io(\\.min|\\.msgpack\\.min)?\\.js(\\.map)?$"
|
|
||||||
)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Set the delay after which a client without namespace is closed
|
|
||||||
* @param v
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public connectTimeout(v: number): Server
|
|
||||||
public connectTimeout(): number
|
|
||||||
public connectTimeout(v?: number): Server | number {
|
|
||||||
if (v === undefined) return this._connectTimeout
|
|
||||||
this._connectTimeout = v
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Sets the adapter for rooms.
|
|
||||||
*
|
|
||||||
* @param {Adapter} v pathname
|
|
||||||
* @return {Server|Adapter} self when setting or value when getting
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public adapter(): any
|
|
||||||
public adapter(v: any)
|
|
||||||
public adapter(v?): Server | any {
|
|
||||||
if (!arguments.length) return this._adapter
|
|
||||||
this._adapter = v
|
|
||||||
for (const nsp of this._nsps.values()) {
|
|
||||||
nsp._initAdapter()
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
// /**
|
|
||||||
// * Attaches socket.io to a server or port.
|
|
||||||
// *
|
|
||||||
// * @param {http.Server|Number} srv - server or port
|
|
||||||
// * @param {Object} opts - options passed to engine.io
|
|
||||||
// * @return {Server} self
|
|
||||||
// * @public
|
|
||||||
// */
|
|
||||||
// public listen(srv: http.Server, opts?: Partial<ServerOptions>): Server
|
|
||||||
// public listen(srv: number, opts?: Partial<ServerOptions>): Server
|
|
||||||
// public listen(srv: any, opts: Partial<ServerOptions> = {}): Server {
|
|
||||||
// return this.attach(srv, opts)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Attaches socket.io to a server or port.
|
|
||||||
// *
|
|
||||||
// * @param {http.Server|Number} srv - server or port
|
|
||||||
// * @param {Object} opts - options passed to engine.io
|
|
||||||
// * @return {Server} self
|
|
||||||
// * @public
|
|
||||||
// */
|
|
||||||
// public attach(srv: http.Server, opts?: Partial<ServerOptions>): Server
|
|
||||||
// public attach(port: number, opts?: Partial<ServerOptions>): Server
|
|
||||||
// public attach(srv: any, opts: Partial<ServerOptions> = {}): Server {
|
|
||||||
// if ("function" == typeof srv) {
|
|
||||||
// const msg =
|
|
||||||
// "You are trying to attach socket.io to an express " +
|
|
||||||
// "request handler function. Please pass a http.Server instance."
|
|
||||||
// throw new Error(msg)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // handle a port as a string
|
|
||||||
// if (Number(srv) == srv) {
|
|
||||||
// srv = Number(srv)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if ("number" == typeof srv) {
|
|
||||||
// debug("creating http server and binding to %d", srv)
|
|
||||||
// const port = srv
|
|
||||||
// srv = http.createServer((req, res) => {
|
|
||||||
// res.writeHead(404)
|
|
||||||
// res.end()
|
|
||||||
// })
|
|
||||||
// srv.listen(port)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // set engine.io path to `/socket.io`
|
|
||||||
// opts.path = opts.path || this._path
|
|
||||||
|
|
||||||
// this.initEngine(srv, opts)
|
|
||||||
|
|
||||||
// return this
|
|
||||||
// }
|
|
||||||
// /**
|
|
||||||
// * Initialize engine
|
|
||||||
// *
|
|
||||||
// * @param srv - the server to attach to
|
|
||||||
// * @param opts - options passed to engine.io
|
|
||||||
// * @private
|
|
||||||
// */
|
|
||||||
// private initEngine(srv: http.Server, opts: Partial<EngineAttachOptions>) {
|
|
||||||
// // initialize engine
|
|
||||||
// debug("creating engine.io instance with opts %j", opts)
|
|
||||||
// this.eio = engine.attach(srv, opts)
|
|
||||||
|
|
||||||
// // attach static file serving
|
|
||||||
// if (this._serveClient) this.attachServe(srv)
|
|
||||||
|
|
||||||
// // Export http server
|
|
||||||
// this.httpServer = srv
|
|
||||||
|
|
||||||
// // bind to engine events
|
|
||||||
// this.bind(this.eio)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Attaches the static file serving.
|
|
||||||
// *
|
|
||||||
// * @param {Function|http.Server} srv http server
|
|
||||||
// * @private
|
|
||||||
// */
|
|
||||||
// private attachServe(srv) {
|
|
||||||
// debug("attaching client serving req handler")
|
|
||||||
|
|
||||||
// const evs = srv.listeners("request").slice(0)
|
|
||||||
// srv.removeAllListeners("request")
|
|
||||||
// srv.on("request", (req, res) => {
|
|
||||||
// if (this.clientPathRegex.test(req.url)) {
|
|
||||||
// this.serve(req, res)
|
|
||||||
// } else {
|
|
||||||
// for (let i = 0; i < evs.length; i++) {
|
|
||||||
// evs[i].call(srv, req, res)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// /**
|
|
||||||
// * Handles a request serving of client source and map
|
|
||||||
// *
|
|
||||||
// * @param {http.IncomingMessage} req
|
|
||||||
// * @param {http.ServerResponse} res
|
|
||||||
// * @private
|
|
||||||
// */
|
|
||||||
// private serve(req: http.IncomingMessage, res: http.ServerResponse) {
|
|
||||||
// const filename = req.url.replace(this._path, "")
|
|
||||||
// const isMap = dotMapRegex.test(filename)
|
|
||||||
// const type = isMap ? "map" : "source"
|
|
||||||
|
|
||||||
// // Per the standard, ETags must be quoted:
|
|
||||||
// // https://tools.ietf.org/html/rfc7232#section-2.3
|
|
||||||
// const expectedEtag = '"' + clientVersion + '"'
|
|
||||||
|
|
||||||
// const etag = req.headers["if-none-match"]
|
|
||||||
// if (etag) {
|
|
||||||
// if (expectedEtag == etag) {
|
|
||||||
// debug("serve client %s 304", type)
|
|
||||||
// res.writeHead(304)
|
|
||||||
// res.end()
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// debug("serve client %s", type)
|
|
||||||
|
|
||||||
// res.setHeader("Cache-Control", "public, max-age=0")
|
|
||||||
// res.setHeader(
|
|
||||||
// "Content-Type",
|
|
||||||
// "application/" + (isMap ? "json" : "javascript")
|
|
||||||
// )
|
|
||||||
// res.setHeader("ETag", expectedEtag)
|
|
||||||
|
|
||||||
// if (!isMap) {
|
|
||||||
// res.setHeader("X-SourceMap", filename.substring(1) + ".map")
|
|
||||||
// }
|
|
||||||
// Server.sendFile(filename, req, res)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * @param filename
|
|
||||||
// * @param req
|
|
||||||
// * @param res
|
|
||||||
// * @private
|
|
||||||
// */
|
|
||||||
// private static sendFile(
|
|
||||||
// filename: string,
|
|
||||||
// req: http.IncomingMessage,
|
|
||||||
// res: http.ServerResponse
|
|
||||||
// ) {
|
|
||||||
// const readStream = createReadStream(
|
|
||||||
// path.join(__dirname, "../client-dist/", filename)
|
|
||||||
// )
|
|
||||||
// const encoding = accepts(req).encodings(["br", "gzip", "deflate"])
|
|
||||||
|
|
||||||
// const onError = err => {
|
|
||||||
// if (err) {
|
|
||||||
// res.end()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// switch (encoding) {
|
|
||||||
// case "br":
|
|
||||||
// res.writeHead(200, { "content-encoding": "br" })
|
|
||||||
// readStream.pipe(createBrotliCompress()).pipe(res)
|
|
||||||
// pipeline(readStream, createBrotliCompress(), res, onError)
|
|
||||||
// break
|
|
||||||
// case "gzip":
|
|
||||||
// res.writeHead(200, { "content-encoding": "gzip" })
|
|
||||||
// pipeline(readStream, createGzip(), res, onError)
|
|
||||||
// break
|
|
||||||
// case "deflate":
|
|
||||||
// res.writeHead(200, { "content-encoding": "deflate" })
|
|
||||||
// pipeline(readStream, createDeflate(), res, onError)
|
|
||||||
// break
|
|
||||||
// default:
|
|
||||||
// res.writeHead(200)
|
|
||||||
// pipeline(readStream, res, onError)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Binds socket.io to an engine.io instance.
|
|
||||||
// *
|
|
||||||
// * @param {engine.Server} engine engine.io (or compatible) server
|
|
||||||
// * @return {Server} self
|
|
||||||
// * @public
|
|
||||||
// */
|
|
||||||
// public bind(engine): Server {
|
|
||||||
// this.engine = engine
|
|
||||||
// this.engine.on("connection", this.onconnection.bind(this))
|
|
||||||
// return this
|
|
||||||
// }
|
|
||||||
/**
|
|
||||||
* Called with each incoming transport connection.
|
|
||||||
*
|
|
||||||
* @param {engine.Socket} conn
|
|
||||||
* @return {Server} self
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private onconnection(conn): Server {
|
|
||||||
console.debug(`incoming connection with id ${conn.id}`)
|
|
||||||
let client = new Client(this, conn)
|
|
||||||
this.allClients.set(conn.id, client)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
// of(nsp: string): Namespace {
|
|
||||||
// if (!this._nsps.has(nsp)) {
|
|
||||||
// console.debug(`create Namespace ${nsp}`)
|
|
||||||
// this._nsps.set(nsp, new Namespace(this, nsp))
|
|
||||||
// }
|
|
||||||
// return this._nsps.get(nsp)
|
|
||||||
// }
|
|
||||||
/**
|
|
||||||
* Looks up a namespace.
|
|
||||||
*
|
|
||||||
* @param {String|RegExp|Function} name nsp name
|
|
||||||
* @param {Function} [fn] optional, nsp `connection` ev handler
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public of(
|
|
||||||
name:
|
|
||||||
| string
|
|
||||||
| RegExp
|
|
||||||
| ((
|
|
||||||
name: string,
|
|
||||||
query: object,
|
|
||||||
fn: (err: Error, success: boolean) => void
|
|
||||||
) => void),
|
|
||||||
fn?: (socket: Socket) => void
|
|
||||||
) {
|
|
||||||
if (typeof name === "function" || name instanceof RegExp) {
|
|
||||||
const parentNsp = new ParentNamespace(this)
|
|
||||||
console.debug(`initializing parent namespace ${parentNsp.name}`)
|
|
||||||
if (typeof name === "function") {
|
|
||||||
this.parentNsps.set(name, parentNsp)
|
|
||||||
} else {
|
|
||||||
this.parentNsps.set(
|
|
||||||
(nsp, conn, next) => next(null, (name as RegExp).test(nsp)),
|
|
||||||
parentNsp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (fn) {
|
|
||||||
// @ts-ignore
|
|
||||||
parentNsp.on("connect", fn)
|
|
||||||
}
|
|
||||||
return parentNsp
|
|
||||||
}
|
|
||||||
if (String(name)[0] !== "/") name = "/" + name
|
|
||||||
let nsp = this._nsps.get(name)
|
|
||||||
if (!nsp) {
|
|
||||||
console.debug(`initializing namespace ${name}`)
|
|
||||||
nsp = new Namespace(this, name)
|
|
||||||
this._nsps.set(name, nsp)
|
|
||||||
}
|
|
||||||
if (fn) nsp.on("connect", fn)
|
|
||||||
return nsp
|
|
||||||
}
|
|
||||||
close(fn?: () => void): void {
|
|
||||||
this.clients.length
|
|
||||||
for (const client of this.allClients.values()) {
|
|
||||||
client._disconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
// this.engine.close()
|
|
||||||
this.websocketServer.close()
|
|
||||||
|
|
||||||
// if (this.httpServer) {
|
|
||||||
// this.httpServer.close(fn)
|
|
||||||
// } else {
|
|
||||||
fn && fn()
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
on(event: "connection", listener: (socket: Socket) => void): Namespace
|
|
||||||
on(event: "connect", listener: (socket: Socket) => void): Namespace
|
|
||||||
on(event: string, listener: Function): Namespace
|
|
||||||
on(event: any, listener: any): Namespace {
|
|
||||||
return this.sockets.on(event, listener)
|
|
||||||
}
|
|
||||||
to(room: string): Namespace {
|
|
||||||
return this.sockets.to(room)
|
|
||||||
}
|
|
||||||
in(room: string): Namespace {
|
|
||||||
return this.sockets.in(room)
|
|
||||||
}
|
|
||||||
use(fn: (socket: Socket, fn: (err?: any) => void) => void): Namespace {
|
|
||||||
return this.sockets.use(fn)
|
|
||||||
}
|
|
||||||
emit(event: string, ...args: any[]): Namespace {
|
|
||||||
// @ts-ignore
|
|
||||||
return this.sockets.emit(event, ...args)
|
|
||||||
}
|
|
||||||
send(...args: any[]): Namespace {
|
|
||||||
return this.sockets.send(...args)
|
|
||||||
}
|
|
||||||
write(...args: any[]): Namespace {
|
|
||||||
return this.sockets.write(...args)
|
|
||||||
}
|
|
||||||
clients(...args: any[]): Namespace {
|
|
||||||
return this.sockets.clients(args[0])
|
|
||||||
}
|
|
||||||
compress(...args: any[]): Namespace {
|
|
||||||
return this.sockets.compress(args[0])
|
|
||||||
}
|
|
||||||
// ===============================
|
|
||||||
private initServerConfig() {
|
|
||||||
this.allClients = new Map()
|
|
||||||
this._nsps = new Map()
|
|
||||||
this.connectTimeout(this.options.connectTimeout || 45000)
|
|
||||||
this._parser = this.options.parser || new Parser()
|
|
||||||
this.adapter(this.options.adapter || Adapter)
|
|
||||||
}
|
|
||||||
private selectServerImpl(instance: any) {
|
|
||||||
let WebSocketServerImpl = undefined
|
|
||||||
if (instance.class.name.startsWith('io.netty.channel')) {
|
|
||||||
WebSocketServerImpl = require("../netty").NettyWebSocketServer
|
|
||||||
} else {
|
|
||||||
WebSocketServerImpl = require("../tomcat").TomcatWebSocketServer
|
|
||||||
}
|
|
||||||
this.websocketServer = new WebSocketServerImpl(instance, this.options)
|
|
||||||
}
|
|
||||||
private initServer() {
|
|
||||||
this.websocketServer.on(ServerEvent.connect, (transport: Transport) => {
|
|
||||||
this.onconnection(transport)
|
|
||||||
})
|
|
||||||
this.websocketServer.on(ServerEvent.message, (transport: Transport, text) => {
|
|
||||||
if (this.allClients.has(transport.id)) {
|
|
||||||
let client = this.allClients.get(transport.id)
|
|
||||||
client.onPacket(this._parser.decode(text))
|
|
||||||
} else {
|
|
||||||
console.error(`unknow transport ${transport.id} reciver message ${text}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.websocketServer.on(ServerEvent.disconnect, (transport: Transport, reason) => {
|
|
||||||
if (this.allClients.has(transport.id)) {
|
|
||||||
this.allClients.get(transport.id).onclose(reason)
|
|
||||||
this.allClients.delete(transport.id)
|
|
||||||
} else {
|
|
||||||
console.error(`unknow transport ${transport?.id} disconnect cause ${reason}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.websocketServer.on(ServerEvent.error, (transport: Transport, cause) => {
|
|
||||||
if (this.allClients.has(transport?.id)) {
|
|
||||||
let client = this.allClients.get(transport?.id)
|
|
||||||
if (client.listeners(ServerEvent.error).length) {
|
|
||||||
client.emit(ServerEvent.error, cause)
|
|
||||||
} else {
|
|
||||||
console.error(`client ${client.id} cause error: ${cause}`)
|
|
||||||
console.ex(cause)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error(`unknow transport ${transport?.id} cause error: ${cause}`)
|
|
||||||
console.ex(cause)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expose main namespace (/).
|
|
||||||
*/
|
|
||||||
|
|
||||||
const emitterMethods = Object.keys(EventEmitter.prototype).filter(function (
|
|
||||||
key
|
|
||||||
) {
|
|
||||||
return typeof EventEmitter.prototype[key] === "function"
|
|
||||||
})
|
|
||||||
|
|
||||||
emitterMethods.forEach(function (fn) {
|
|
||||||
Server.prototype[fn] = function () {
|
|
||||||
return this.sockets[fn].apply(this.sockets, arguments)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export {
|
|
||||||
Server,
|
|
||||||
Socket,
|
|
||||||
Client,
|
|
||||||
Namespace,
|
|
||||||
ServerOptions
|
|
||||||
}
|
|
@ -1,242 +0,0 @@
|
|||||||
import { EventEmitter } from 'events'
|
|
||||||
|
|
||||||
import { Client } from './client'
|
|
||||||
import { ServerEvent } from './constants'
|
|
||||||
import { RESERVED_EVENTS, Socket } from './socket'
|
|
||||||
import { Adapter, Room, SocketId } from './adapter'
|
|
||||||
import { Server } from './index'
|
|
||||||
import { Packet } from './packet'
|
|
||||||
import { PacketTypes, SubPacketTypes } from './types'
|
|
||||||
|
|
||||||
export interface ExtendedError extends Error {
|
|
||||||
data?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Namespace extends EventEmitter {
|
|
||||||
public readonly name: string
|
|
||||||
public readonly sockets: Map<SocketId, Socket>
|
|
||||||
|
|
||||||
public adapter: Adapter
|
|
||||||
|
|
||||||
/** @private */
|
|
||||||
readonly server: Server
|
|
||||||
json: Namespace
|
|
||||||
|
|
||||||
/** @private */
|
|
||||||
_fns: Array<
|
|
||||||
(socket: Socket, next: (err: ExtendedError) => void) => void
|
|
||||||
> = [];
|
|
||||||
|
|
||||||
/** @private */
|
|
||||||
_rooms: Set<Room>
|
|
||||||
|
|
||||||
/** @private */
|
|
||||||
_flags: any = {}
|
|
||||||
|
|
||||||
/** @private */
|
|
||||||
_ids: number = 0
|
|
||||||
|
|
||||||
constructor(server: Server, name: string) {
|
|
||||||
super()
|
|
||||||
this.server = server
|
|
||||||
this.name = name + ''
|
|
||||||
this._initAdapter()
|
|
||||||
// =======================
|
|
||||||
this.sockets = new Map()
|
|
||||||
this._rooms = new Set()
|
|
||||||
}
|
|
||||||
_initAdapter() {
|
|
||||||
// @ts-ignore
|
|
||||||
this.adapter = new (this.server.adapter())(this)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Sets up namespace middleware.
|
|
||||||
*
|
|
||||||
* @return {Namespace} self
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public use(
|
|
||||||
fn: (socket: Socket, next: (err?: ExtendedError) => void) => void
|
|
||||||
): Namespace {
|
|
||||||
this._fns.push(fn)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Executes the middleware for an incoming client.
|
|
||||||
*
|
|
||||||
* @param {Socket} socket - the socket that will get added
|
|
||||||
* @param {Function} fn - last fn call in the middleware
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private run(socket: Socket, fn: (err: ExtendedError) => void) {
|
|
||||||
const fns = this._fns.slice(0)
|
|
||||||
if (!fns.length) return fn(null)
|
|
||||||
|
|
||||||
function run(i) {
|
|
||||||
fns[i](socket, function (err) {
|
|
||||||
// upon error, short-circuit
|
|
||||||
if (err) return fn(err)
|
|
||||||
|
|
||||||
// if no middleware left, summon callback
|
|
||||||
if (!fns[i + 1]) return fn(null)
|
|
||||||
|
|
||||||
// go on to next
|
|
||||||
run(i + 1)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
run(0)
|
|
||||||
}
|
|
||||||
to(name: string): Namespace {
|
|
||||||
this._rooms.add(name)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
in(name: string): Namespace {
|
|
||||||
return this.to(name)
|
|
||||||
}
|
|
||||||
_add(client: Client, query?: any, fn?: (socket: Socket) => void) {
|
|
||||||
const socket = new Socket(this, client, query || {})
|
|
||||||
console.debug(`client ${client.id} adding socket ${socket.id} to nsp ${this.name}`)
|
|
||||||
this.run(socket, err => {
|
|
||||||
process.nextTick(() => {
|
|
||||||
if ("open" == client.conn.readyState) {
|
|
||||||
if (err)
|
|
||||||
return socket._error({
|
|
||||||
message: err.message,
|
|
||||||
data: err.data
|
|
||||||
})
|
|
||||||
|
|
||||||
// track socket
|
|
||||||
this.sockets.set(socket.id, socket)
|
|
||||||
|
|
||||||
// it's paramount that the internal `onconnect` logic
|
|
||||||
// fires before user-set events to prevent state order
|
|
||||||
// violations (such as a disconnection before the connection
|
|
||||||
// logic is complete)
|
|
||||||
socket._onconnect()
|
|
||||||
// !!! at java multi thread need direct callback socket
|
|
||||||
if (fn) fn(socket)
|
|
||||||
|
|
||||||
// fire user-set events
|
|
||||||
super.emit(ServerEvent.connect, socket)
|
|
||||||
super.emit(ServerEvent.connection, socket)
|
|
||||||
} else {
|
|
||||||
console.debug(`next called after client ${client.id} was closed - ignoring socket`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return socket
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Removes a client. Called by each `Socket`.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_remove(socket: Socket): void {
|
|
||||||
if (this.sockets.has(socket.id)) {
|
|
||||||
console.debug(`namespace ${this.name} remove socket ${socket.id}`)
|
|
||||||
this.sockets.delete(socket.id)
|
|
||||||
} else {
|
|
||||||
console.debug(`namespace ${this.name} ignoring remove for ${socket.id}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emit(event: string, ...args: any[]): boolean {
|
|
||||||
if (RESERVED_EVENTS.has(event)) {
|
|
||||||
throw new Error(`"${event}" is a reserved event name`)
|
|
||||||
}
|
|
||||||
// set up packet object
|
|
||||||
var packet = {
|
|
||||||
type: PacketTypes.MESSAGE,
|
|
||||||
sub_type: (this._flags.binary !== undefined ? this._flags.binary : this.hasBin(args)) ? SubPacketTypes.BINARY_EVENT : SubPacketTypes.EVENT,
|
|
||||||
name: event,
|
|
||||||
data: args
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('function' == typeof args[args.length - 1]) {
|
|
||||||
throw new Error('Callbacks are not supported when broadcasting')
|
|
||||||
}
|
|
||||||
|
|
||||||
var rooms = new Set(this._rooms)
|
|
||||||
var flags = Object.assign({}, this._flags)
|
|
||||||
|
|
||||||
// reset flags
|
|
||||||
this._rooms.clear()
|
|
||||||
this._flags = {}
|
|
||||||
|
|
||||||
this.adapter.broadcast(packet, {
|
|
||||||
rooms: new Set(rooms),
|
|
||||||
flags: flags
|
|
||||||
})
|
|
||||||
// @ts-ignore
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
send(...args: any[]): Namespace {
|
|
||||||
this.emit('message', ...args)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
write(...args: any[]): Namespace {
|
|
||||||
return this.send(...args)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Gets a list of clients.
|
|
||||||
*
|
|
||||||
* @return {Namespace} self
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public allSockets(): Promise<Set<SocketId>> {
|
|
||||||
if (!this.adapter) {
|
|
||||||
throw new Error("No adapter for this namespace, are you trying to get the list of clients of a dynamic namespace?")
|
|
||||||
}
|
|
||||||
const rooms = new Set(this._rooms)
|
|
||||||
this._rooms.clear()
|
|
||||||
return this.adapter.sockets(rooms)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the compress flag.
|
|
||||||
*
|
|
||||||
* @param {Boolean} compress - if `true`, compresses the sending data
|
|
||||||
* @return {Namespace} self
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public compress(compress: boolean): Namespace {
|
|
||||||
this._flags.compress = compress
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
|
|
||||||
* receive messages (because of network slowness or other issues, or because they’re connected through long polling
|
|
||||||
* and is in the middle of a request-response cycle).
|
|
||||||
*
|
|
||||||
* @return {Namespace} self
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public get volatile(): Namespace {
|
|
||||||
this._flags.volatile = true
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
|
|
||||||
*
|
|
||||||
* @return {Namespace} self
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public get local(): Namespace {
|
|
||||||
this._flags.local = true
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
hasBin(args: any[]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
clients(fn: (sockets: Socket[]) => Namespace): Namespace {
|
|
||||||
return fn(Object.values(this.sockets))
|
|
||||||
}
|
|
||||||
close() {
|
|
||||||
this.removeAllListeners(ServerEvent.connect)
|
|
||||||
this.removeAllListeners(ServerEvent.connection)
|
|
||||||
Object.values(this.sockets).forEach(socket => socket.disconnect(false))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
import { PacketTypes, SubPacketTypes } from './types'
|
|
||||||
|
|
||||||
export interface Packet {
|
|
||||||
type: PacketTypes;
|
|
||||||
sub_type?: SubPacketTypes;
|
|
||||||
nsp?: string;
|
|
||||||
id?: number;
|
|
||||||
name?: string;
|
|
||||||
data?: any;
|
|
||||||
attachments?: any;
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
import { Namespace } from "./namespace"
|
|
||||||
|
|
||||||
export class ParentNamespace extends Namespace {
|
|
||||||
private static count: number = 0;
|
|
||||||
private children: Set<Namespace> = new Set();
|
|
||||||
|
|
||||||
constructor(server) {
|
|
||||||
super(server, "/_" + ParentNamespace.count++)
|
|
||||||
}
|
|
||||||
|
|
||||||
_initAdapter() { }
|
|
||||||
|
|
||||||
public emit(...args: any[]): boolean {
|
|
||||||
this.children.forEach(nsp => {
|
|
||||||
nsp._rooms = this._rooms
|
|
||||||
nsp._flags = this._flags
|
|
||||||
nsp.emit.apply(nsp, args as any)
|
|
||||||
})
|
|
||||||
this._rooms.clear()
|
|
||||||
this._flags = {}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
createChild(name) {
|
|
||||||
const namespace = new Namespace(this.server, name)
|
|
||||||
namespace._fns = this._fns.slice(0)
|
|
||||||
this.listeners("connect").forEach(listener =>
|
|
||||||
// @ts-ignore
|
|
||||||
namespace.on("connect", listener)
|
|
||||||
)
|
|
||||||
this.listeners("connection").forEach(listener =>
|
|
||||||
// @ts-ignore
|
|
||||||
namespace.on("connection", listener)
|
|
||||||
)
|
|
||||||
this.children.add(namespace)
|
|
||||||
this.server._nsps.set(name, namespace)
|
|
||||||
return namespace
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,164 +0,0 @@
|
|||||||
import { EventEmitter } from 'events'
|
|
||||||
import { Packet } from "./packet"
|
|
||||||
import { PacketTypes, SubPacketTypes } from "./types"
|
|
||||||
|
|
||||||
export class Parser extends EventEmitter {
|
|
||||||
encode(packet: Packet): string {
|
|
||||||
let origin = JSON.stringify(packet)
|
|
||||||
// first is type
|
|
||||||
let str = '' + packet.type
|
|
||||||
if (packet.type == PacketTypes.PONG) {
|
|
||||||
if (packet.data) { str += packet.data };
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
if (packet.sub_type != undefined) {
|
|
||||||
str += packet.sub_type
|
|
||||||
}
|
|
||||||
// attachments if we have them
|
|
||||||
if ([SubPacketTypes.BINARY_EVENT, SubPacketTypes.BINARY_ACK].includes(packet.sub_type)) {
|
|
||||||
str += packet.attachments + '-'
|
|
||||||
}
|
|
||||||
// if we have a namespace other than `/`
|
|
||||||
// we append it followed by a comma `,`
|
|
||||||
if (packet.nsp && '/' !== packet.nsp) {
|
|
||||||
str += packet.nsp + ','
|
|
||||||
}
|
|
||||||
// immediately followed by the id
|
|
||||||
if (null != packet.id) {
|
|
||||||
str += packet.id
|
|
||||||
}
|
|
||||||
if (packet.sub_type == SubPacketTypes.EVENT) {
|
|
||||||
if (packet.name == undefined) { throw new Error(`SubPacketTypes.EVENT name can't be empty!`) }
|
|
||||||
packet.data = [packet.name, ...packet.data]
|
|
||||||
}
|
|
||||||
// json data
|
|
||||||
if (null != packet.data) {
|
|
||||||
let payload = this.tryStringify(packet.data)
|
|
||||||
if (payload !== false) {
|
|
||||||
str += payload
|
|
||||||
} else {
|
|
||||||
return '4"encode error"'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.trace(`encoded ${origin} as ${str}`)
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
tryStringify(str: any) {
|
|
||||||
try {
|
|
||||||
return JSON.stringify(str)
|
|
||||||
} catch (e) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
decode(str: string): Packet {
|
|
||||||
let i = 0
|
|
||||||
// ignore parse binary
|
|
||||||
// if ((frame.getByte(0) == 'b' && frame.getByte(1) == '4')
|
|
||||||
// || frame.getByte(0) == 4 || frame.getByte(0) == 1) {
|
|
||||||
// return parseBinary(head, frame);
|
|
||||||
// }
|
|
||||||
// look up type
|
|
||||||
let p: Packet = {
|
|
||||||
type: Number(str.charAt(i))
|
|
||||||
}
|
|
||||||
if (null == PacketTypes[p.type]) {
|
|
||||||
return this.error('unknown packet type ' + p.type)
|
|
||||||
}
|
|
||||||
// if str empty return
|
|
||||||
if (str.length == i + 1) {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
// if is ping packet read data and return
|
|
||||||
if (PacketTypes.PING == p.type) {
|
|
||||||
p.data = str.substr(++i)
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
// look up sub type
|
|
||||||
p.sub_type = Number(str.charAt(++i))
|
|
||||||
if (null == PacketTypes[p.sub_type]) {
|
|
||||||
return this.error('unknown sub packet type ' + p.type)
|
|
||||||
}
|
|
||||||
// look up attachments if type binary
|
|
||||||
if ([SubPacketTypes.BINARY_ACK, SubPacketTypes.BINARY_EVENT].includes(p.sub_type)) {
|
|
||||||
let buf = ''
|
|
||||||
while (str.charAt(++i) !== '-') {
|
|
||||||
buf += str.charAt(i)
|
|
||||||
if (i == str.length) break
|
|
||||||
}
|
|
||||||
if (buf != `${Number(buf)}` || str.charAt(i) !== '-') {
|
|
||||||
return this.error('Illegal attachments')
|
|
||||||
}
|
|
||||||
p.attachments = Number(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
// look up namespace (if any)
|
|
||||||
if ('/' === str.charAt(i + 1)) {
|
|
||||||
p.nsp = ''
|
|
||||||
while (++i) {
|
|
||||||
let c = str.charAt(i)
|
|
||||||
if (',' === c) break
|
|
||||||
p.nsp += c
|
|
||||||
if (i === str.length) break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
p.nsp = '/'
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle namespace query
|
|
||||||
if (p.nsp.indexOf('?') !== -1) {
|
|
||||||
p.nsp = p.nsp.split('?')[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// look up id
|
|
||||||
let next = str.charAt(i + 1)
|
|
||||||
if ('' !== next && !isNaN(Number(next))) {
|
|
||||||
let id = ''
|
|
||||||
while (++i) {
|
|
||||||
let c = str.charAt(i)
|
|
||||||
if (null == c || isNaN(Number(c))) {
|
|
||||||
--i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
id += str.charAt(i)
|
|
||||||
if (i === str.length) break
|
|
||||||
}
|
|
||||||
p.id = Number(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore binary packet
|
|
||||||
if (p.sub_type == SubPacketTypes.BINARY_EVENT) {
|
|
||||||
return this.error('not support binary parse...')
|
|
||||||
}
|
|
||||||
|
|
||||||
// look up json data
|
|
||||||
if (str.charAt(++i)) {
|
|
||||||
let payload = this.tryParse(str.substr(i))
|
|
||||||
let isPayloadValid = payload !== false && (p.sub_type == SubPacketTypes.ERROR || Array.isArray(payload))
|
|
||||||
if (isPayloadValid) {
|
|
||||||
p.name = payload[0]
|
|
||||||
p.data = payload.slice(1)
|
|
||||||
} else {
|
|
||||||
return this.error('invalid payload ' + str.substr(i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.trace(`decoded ${str} as ${JSON.stringify(p)}`)
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
tryParse(str: string) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(str)
|
|
||||||
} catch (e) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
error(error: string): Packet {
|
|
||||||
return {
|
|
||||||
type: PacketTypes.MESSAGE,
|
|
||||||
sub_type: SubPacketTypes.ERROR,
|
|
||||||
data: 'parser error: ' + error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,491 +0,0 @@
|
|||||||
import { EventEmitter } from 'events'
|
|
||||||
|
|
||||||
import { Packet } from './packet'
|
|
||||||
import { PacketTypes, SubPacketTypes } from './types'
|
|
||||||
import { Client } from './client'
|
|
||||||
import { Namespace } from './namespace'
|
|
||||||
import * as querystring from 'querystring'
|
|
||||||
import { ServerEvent } from './constants'
|
|
||||||
import { Adapter, BroadcastFlags, Room, SocketId } from './adapter'
|
|
||||||
import { Server } from 'index'
|
|
||||||
|
|
||||||
export const RESERVED_EVENTS = new Set([
|
|
||||||
"connect",
|
|
||||||
"connect_error",
|
|
||||||
"disconnect",
|
|
||||||
"disconnecting",
|
|
||||||
// EventEmitter reserved events: https://nodejs.org/api/events.html#events_event_newlistener
|
|
||||||
"newListener",
|
|
||||||
"removeListener"
|
|
||||||
])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The handshake details
|
|
||||||
*/
|
|
||||||
export interface Handshake {
|
|
||||||
/**
|
|
||||||
* The headers sent as part of the handshake
|
|
||||||
*/
|
|
||||||
headers: object
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The date of creation (as string)
|
|
||||||
*/
|
|
||||||
time: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ip of the client
|
|
||||||
*/
|
|
||||||
address: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the connection is cross-domain
|
|
||||||
*/
|
|
||||||
xdomain: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the connection is secure
|
|
||||||
*/
|
|
||||||
secure: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The date of creation (as unix timestamp)
|
|
||||||
*/
|
|
||||||
issued: number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The request URL string
|
|
||||||
*/
|
|
||||||
url: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The query object
|
|
||||||
*/
|
|
||||||
query: any
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The auth object
|
|
||||||
*/
|
|
||||||
auth: any
|
|
||||||
}
|
|
||||||
export class Socket extends EventEmitter {
|
|
||||||
nsp: Namespace
|
|
||||||
|
|
||||||
public readonly id: SocketId
|
|
||||||
public readonly handshake: Handshake
|
|
||||||
|
|
||||||
public connected: boolean
|
|
||||||
public disconnected: boolean
|
|
||||||
|
|
||||||
private readonly server: Server
|
|
||||||
private readonly adapter: Adapter
|
|
||||||
|
|
||||||
client: Client
|
|
||||||
private acks: Map<number, () => void>
|
|
||||||
|
|
||||||
fns: any[]
|
|
||||||
private flags: BroadcastFlags = {};
|
|
||||||
private _rooms: Set<Room> = new Set();
|
|
||||||
private _anyListeners: Array<(...args: any[]) => void>
|
|
||||||
|
|
||||||
constructor(nsp: Namespace, client: Client, auth = {}) {
|
|
||||||
super()
|
|
||||||
this.nsp = nsp
|
|
||||||
this.server = nsp.server
|
|
||||||
this.adapter = this.nsp.adapter
|
|
||||||
this.id = nsp.name !== '/' ? nsp.name + '#' + client.id : client.id
|
|
||||||
this.client = client
|
|
||||||
this.acks = new Map()
|
|
||||||
this.connected = true
|
|
||||||
this.disconnected = false
|
|
||||||
this.handshake = this.buildHandshake(auth)
|
|
||||||
|
|
||||||
this.fns = []
|
|
||||||
this.flags = {}
|
|
||||||
this._rooms = new Set()
|
|
||||||
}
|
|
||||||
emit(event: string, ...args: any[]): boolean {
|
|
||||||
let packet: Packet = {
|
|
||||||
type: PacketTypes.MESSAGE,
|
|
||||||
sub_type: (this.flags.binary !== undefined ? this.flags.binary : this.hasBin(args)) ? SubPacketTypes.BINARY_EVENT : SubPacketTypes.EVENT,
|
|
||||||
name: event,
|
|
||||||
data: args
|
|
||||||
}
|
|
||||||
|
|
||||||
// access last argument to see if it's an ACK callback
|
|
||||||
if (typeof args[args.length - 1] === "function") {
|
|
||||||
if (this._rooms.size || this.flags.broadcast) {
|
|
||||||
throw new Error("Callbacks are not supported when broadcasting")
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.debug("emitting packet with ack id %d", this.nsp._ids)
|
|
||||||
this.acks.set(this.nsp._ids, args.pop())
|
|
||||||
packet.id = this.nsp._ids++
|
|
||||||
}
|
|
||||||
|
|
||||||
const rooms = new Set(this._rooms)
|
|
||||||
const flags = Object.assign({}, this.flags)
|
|
||||||
|
|
||||||
// reset flags
|
|
||||||
this._rooms.clear()
|
|
||||||
this.flags = {}
|
|
||||||
|
|
||||||
if (rooms.size || flags.broadcast) {
|
|
||||||
this.adapter.broadcast(packet, {
|
|
||||||
except: new Set([this.id]),
|
|
||||||
rooms: rooms,
|
|
||||||
flags: flags
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// dispatch packet
|
|
||||||
this.packet(packet, flags)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
to(name: Room): Socket {
|
|
||||||
this._rooms.add(name)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
in(room: string): Socket {
|
|
||||||
return this.to(room)
|
|
||||||
}
|
|
||||||
use(fn: (packet: Packet, next: (err?: any) => void) => void): Socket {
|
|
||||||
throw new Error("Method not implemented.")
|
|
||||||
}
|
|
||||||
send(...args: any[]): Socket {
|
|
||||||
this.emit("message", ...args)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
write(...args: any[]): Socket {
|
|
||||||
return this.send(...args)
|
|
||||||
}
|
|
||||||
public join(rooms: Room | Array<Room>): Promise<void> | void {
|
|
||||||
console.debug(`join room ${rooms}`)
|
|
||||||
|
|
||||||
return this.adapter.addAll(
|
|
||||||
this.id,
|
|
||||||
new Set(Array.isArray(rooms) ? rooms : [rooms])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Leaves a room.
|
|
||||||
*
|
|
||||||
* @param {String} room
|
|
||||||
* @return a Promise or nothing, depending on the adapter
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public leave(room: string): Promise<void> | void {
|
|
||||||
console.debug(`leave room ${room}`)
|
|
||||||
|
|
||||||
return this.adapter.del(this.id, room)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Leave all rooms.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private leaveAll(): void {
|
|
||||||
this.adapter.delAll(this.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by `Namespace` upon successful
|
|
||||||
* middleware execution (ie: authorization).
|
|
||||||
* Socket is added to namespace array before
|
|
||||||
* call to join, so adapters can access it.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_onconnect(): void {
|
|
||||||
console.debug(`socket ${this.id} connected - writing packet`)
|
|
||||||
this.join(this.id)
|
|
||||||
this.packet({ type: PacketTypes.MESSAGE, sub_type: SubPacketTypes.CONNECT, data: { sid: this.id } })
|
|
||||||
}
|
|
||||||
_onpacket(packet: Packet) {
|
|
||||||
switch (packet.sub_type) {
|
|
||||||
// 2
|
|
||||||
case SubPacketTypes.EVENT:
|
|
||||||
this.onevent(packet)
|
|
||||||
break
|
|
||||||
// 5
|
|
||||||
case SubPacketTypes.BINARY_EVENT:
|
|
||||||
this.onevent(packet)
|
|
||||||
break
|
|
||||||
// 3
|
|
||||||
case SubPacketTypes.ACK:
|
|
||||||
this.onack(packet)
|
|
||||||
break
|
|
||||||
// 6
|
|
||||||
case SubPacketTypes.BINARY_ACK:
|
|
||||||
this.onack(packet)
|
|
||||||
break
|
|
||||||
// 1
|
|
||||||
case SubPacketTypes.DISCONNECT:
|
|
||||||
this.ondisconnect()
|
|
||||||
break
|
|
||||||
// 4
|
|
||||||
case SubPacketTypes.ERROR:
|
|
||||||
this._onerror(new Error(packet.data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onevent(packet: Packet) {
|
|
||||||
if (null != packet.id) {
|
|
||||||
console.trace(`attaching ack ${packet.id} callback to client ${this.id} event`)
|
|
||||||
this.dispatch(packet, this.ack(packet.id))
|
|
||||||
} else {
|
|
||||||
this.dispatch(packet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ack(id: number) {
|
|
||||||
let sent = false
|
|
||||||
return (...args: any[]) => {
|
|
||||||
if (sent) return
|
|
||||||
this.packet({
|
|
||||||
id: id,
|
|
||||||
type: PacketTypes.MESSAGE,
|
|
||||||
sub_type: this.hasBin(args) ? SubPacketTypes.BINARY_ACK : SubPacketTypes.ACK,
|
|
||||||
data: args
|
|
||||||
})
|
|
||||||
sent = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onack(packet: Packet) {
|
|
||||||
let ack = this.acks.get(packet.id)
|
|
||||||
if ('function' == typeof ack) {
|
|
||||||
console.trace(`calling ack ${packet.id} on socket ${this.id} with ${packet.data}`)
|
|
||||||
ack.apply(this, packet.data)
|
|
||||||
this.acks.delete(packet.id)
|
|
||||||
} else {
|
|
||||||
console.trace(`bad ack ${packet.id} on socket ${this.id}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Called upon client disconnect packet.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private ondisconnect(): void {
|
|
||||||
console.debug(`socket ${this.id} got disconnect packet`)
|
|
||||||
this._onclose("client namespace disconnect")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a client error.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_onerror(err): void {
|
|
||||||
if (this.listeners("error").length) {
|
|
||||||
super.emit("error", err)
|
|
||||||
} else {
|
|
||||||
console.error(`Missing error handler on 'socket(${this.id})'.`)
|
|
||||||
console.error(err.stack)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called upon closing. Called by `Client`.
|
|
||||||
*
|
|
||||||
* @param {String} reason
|
|
||||||
* @throw {Error} optional error object
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_onclose(reason: string) {
|
|
||||||
if (!this.connected) return this
|
|
||||||
console.debug(`closing socket ${this.id} - reason: ${reason} connected: ${this.connected}`)
|
|
||||||
super.emit(ServerEvent.disconnecting, reason)
|
|
||||||
this.leaveAll()
|
|
||||||
this.nsp._remove(this)
|
|
||||||
this.client._remove(this)
|
|
||||||
this.connected = false
|
|
||||||
this.disconnected = true
|
|
||||||
super.emit(ServerEvent.disconnect, reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Produces an `error` packet.
|
|
||||||
*
|
|
||||||
* @param {Object} err - error object
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_error(err) {
|
|
||||||
this.packet({ type: PacketTypes.MESSAGE, sub_type: SubPacketTypes.ERROR, data: err })
|
|
||||||
}
|
|
||||||
disconnect(close?: boolean): Socket {
|
|
||||||
if (!this.connected) return this
|
|
||||||
if (close) {
|
|
||||||
this.client._disconnect()
|
|
||||||
} else {
|
|
||||||
this.packet({ type: PacketTypes.MESSAGE, sub_type: SubPacketTypes.DISCONNECT })
|
|
||||||
this._onclose('server namespace disconnect')
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
compress(compress: boolean): Socket {
|
|
||||||
throw new Error("Method not implemented.")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
|
|
||||||
* receive messages (because of network slowness or other issues, or because they’re connected through long polling
|
|
||||||
* and is in the middle of a request-response cycle).
|
|
||||||
*
|
|
||||||
* @return {Socket} self
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public get volatile(): Socket {
|
|
||||||
this.flags.volatile = true
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to every sockets but the
|
|
||||||
* sender.
|
|
||||||
*
|
|
||||||
* @return {Socket} self
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public get broadcast(): Socket {
|
|
||||||
this.flags.broadcast = true
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
|
|
||||||
*
|
|
||||||
* @return {Socket} self
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public get local(): Socket {
|
|
||||||
this.flags.local = true
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A reference to the request that originated the underlying Engine.IO Socket.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public get request(): any {
|
|
||||||
return this.client.request
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A reference to the underlying Client transport connection (Engine.IO Socket object).
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public get conn() {
|
|
||||||
return this.client.conn
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public get rooms(): Set<Room> {
|
|
||||||
return this.adapter.socketRooms(this.id) || new Set()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
|
|
||||||
* callback.
|
|
||||||
*
|
|
||||||
* @param listener
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public onAny(listener: (...args: any[]) => void): Socket {
|
|
||||||
this._anyListeners = this._anyListeners || []
|
|
||||||
this._anyListeners.push(listener)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
|
|
||||||
* callback. The listener is added to the beginning of the listeners array.
|
|
||||||
*
|
|
||||||
* @param listener
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public prependAny(listener: (...args: any[]) => void): Socket {
|
|
||||||
this._anyListeners = this._anyListeners || []
|
|
||||||
this._anyListeners.unshift(listener)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the listener that will be fired when any event is emitted.
|
|
||||||
*
|
|
||||||
* @param listener
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public offAny(listener?: (...args: any[]) => void): Socket {
|
|
||||||
if (!this._anyListeners) {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
if (listener) {
|
|
||||||
const listeners = this._anyListeners
|
|
||||||
for (let i = 0; i < listeners.length; i++) {
|
|
||||||
if (listener === listeners[i]) {
|
|
||||||
listeners.splice(i, 1)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._anyListeners = []
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
|
|
||||||
* e.g. to remove listeners.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
public listenersAny() {
|
|
||||||
return this._anyListeners || []
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
buildHandshake(auth): Handshake {
|
|
||||||
let requestUri = this.request.uri()
|
|
||||||
let headers = {}
|
|
||||||
let nativeHeaders = this.request.headers()
|
|
||||||
nativeHeaders.forEach(function (header) {
|
|
||||||
headers[header.getKey()] = header.getValue()
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
headers: headers,
|
|
||||||
time: new Date() + '',
|
|
||||||
address: this.conn.remoteAddress + '',
|
|
||||||
xdomain: !!headers['origin'],
|
|
||||||
secure: false,
|
|
||||||
issued: +new Date(),
|
|
||||||
url: requestUri,
|
|
||||||
query: querystring.parse(requestUri.indexOf('?') != -1 ? requestUri.split('?')[1] : ''),
|
|
||||||
auth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
packet(packet: Packet, opts: any = { preEncoded: false }) {
|
|
||||||
if (!opts.preEncoded) {
|
|
||||||
packet.nsp = this.nsp.name
|
|
||||||
opts.compress = false !== opts.compress
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this.client._packet(packet, opts)
|
|
||||||
} catch (error) {
|
|
||||||
this._onerror(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dispatch(packet: Packet, ack?: () => void) {
|
|
||||||
if (ack) { this.acks.set(packet.id, ack) }
|
|
||||||
super.emit(packet.name, ...packet.data, ack)
|
|
||||||
}
|
|
||||||
private hasBin(obj: any) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
export enum PacketTypes {
|
|
||||||
OPEN,
|
|
||||||
CLOSE,
|
|
||||||
PING,
|
|
||||||
PONG,
|
|
||||||
MESSAGE,
|
|
||||||
UPGRADE,
|
|
||||||
NOOP,
|
|
||||||
}
|
|
||||||
export enum SubPacketTypes {
|
|
||||||
CONNECT,
|
|
||||||
DISCONNECT,
|
|
||||||
EVENT,
|
|
||||||
ACK,
|
|
||||||
ERROR,
|
|
||||||
BINARY_EVENT,
|
|
||||||
BINARY_ACK
|
|
||||||
}
|
|
279
packages/websocket/src/socket.io-adapter/index.ts
Normal file
279
packages/websocket/src/socket.io-adapter/index.ts
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
import { EventEmitter } from "events"
|
||||||
|
|
||||||
|
export type SocketId = string
|
||||||
|
export type Room = string
|
||||||
|
|
||||||
|
export interface BroadcastFlags {
|
||||||
|
volatile?: boolean
|
||||||
|
compress?: boolean
|
||||||
|
local?: boolean
|
||||||
|
broadcast?: boolean
|
||||||
|
binary?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BroadcastOptions {
|
||||||
|
rooms: Set<Room>
|
||||||
|
except?: Set<SocketId>
|
||||||
|
flags?: BroadcastFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Adapter extends EventEmitter {
|
||||||
|
public rooms: Map<Room, Set<SocketId>> = new Map();
|
||||||
|
public sids: Map<SocketId, Set<Room>> = new Map();
|
||||||
|
private readonly encoder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory adapter constructor.
|
||||||
|
*
|
||||||
|
* @param {Namespace} nsp
|
||||||
|
*/
|
||||||
|
constructor(readonly nsp: any) {
|
||||||
|
super()
|
||||||
|
this.encoder = nsp.server.encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To be overridden
|
||||||
|
*/
|
||||||
|
public init(): Promise<void> | void { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To be overridden
|
||||||
|
*/
|
||||||
|
public close(): Promise<void> | void { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a socket to a list of room.
|
||||||
|
*
|
||||||
|
* @param {SocketId} id the socket id
|
||||||
|
* @param {Set<Room>} rooms a set of rooms
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public addAll(id: SocketId, rooms: Set<Room>): Promise<void> | void {
|
||||||
|
if (!this.sids.has(id)) {
|
||||||
|
this.sids.set(id, new Set())
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const room of rooms) {
|
||||||
|
this.sids.get(id).add(room)
|
||||||
|
|
||||||
|
if (!this.rooms.has(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a socket from a room.
|
||||||
|
*
|
||||||
|
* @param {SocketId} id the socket id
|
||||||
|
* @param {Room} room the room name
|
||||||
|
*/
|
||||||
|
public del(id: SocketId, room: Room): Promise<void> | void {
|
||||||
|
if (this.sids.has(id)) {
|
||||||
|
this.sids.get(id).delete(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
this._del(room, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private _del(room, id) {
|
||||||
|
if (this.rooms.has(room)) {
|
||||||
|
const deleted = this.rooms.get(room).delete(id)
|
||||||
|
if (deleted) {
|
||||||
|
this.emit("leave-room", room, id)
|
||||||
|
}
|
||||||
|
if (this.rooms.get(room).size === 0) {
|
||||||
|
this.rooms.delete(room)
|
||||||
|
this.emit("delete-room", room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a socket from all rooms it's joined.
|
||||||
|
*
|
||||||
|
* @param {SocketId} id the socket id
|
||||||
|
*/
|
||||||
|
public delAll(id: SocketId): void {
|
||||||
|
if (!this.sids.has(id)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const room of this.sids.get(id)) {
|
||||||
|
this._del(room, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sids.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts a packet.
|
||||||
|
*
|
||||||
|
* Options:
|
||||||
|
* - `flags` {Object} flags for this packet
|
||||||
|
* - `except` {Array} sids that should be excluded
|
||||||
|
* - `rooms` {Array} list of rooms to broadcast to
|
||||||
|
*
|
||||||
|
* @param {Object} packet the packet object
|
||||||
|
* @param {Object} opts the options
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public broadcast(packet: any, opts: BroadcastOptions): void {
|
||||||
|
const flags = opts.flags || {}
|
||||||
|
const basePacketOpts = {
|
||||||
|
preEncoded: true,
|
||||||
|
volatile: flags.volatile,
|
||||||
|
compress: flags.compress
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.nsp = this.nsp.name
|
||||||
|
const encodedPackets = this.encoder.encode(packet)
|
||||||
|
|
||||||
|
const packetOpts = encodedPackets.map(encodedPacket => {
|
||||||
|
if (typeof encodedPacket === "string") {
|
||||||
|
return {
|
||||||
|
...basePacketOpts,
|
||||||
|
wsPreEncoded: "4" + encodedPacket // "4" being the "message" packet type in Engine.IO
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return basePacketOpts
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.apply(opts, socket => {
|
||||||
|
for (let i = 0; i < encodedPackets.length; i++) {
|
||||||
|
socket.client.writeToEngine(encodedPackets[i], packetOpts[i])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of sockets by sid.
|
||||||
|
*
|
||||||
|
* @param {Set<Room>} rooms the explicit set of rooms to check.
|
||||||
|
*/
|
||||||
|
public sockets(rooms: Set<Room>): Promise<Set<SocketId>> {
|
||||||
|
const sids = new Set<SocketId>()
|
||||||
|
|
||||||
|
this.apply({ rooms }, socket => {
|
||||||
|
sids.add(socket.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
return Promise.resolve(sids)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the list of rooms a given socket has joined.
|
||||||
|
*
|
||||||
|
* @param {SocketId} id the socket id
|
||||||
|
*/
|
||||||
|
public socketRooms(id: SocketId): Set<Room> | undefined {
|
||||||
|
return this.sids.get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the matching socket instances
|
||||||
|
*
|
||||||
|
* @param opts - the filters to apply
|
||||||
|
*/
|
||||||
|
public fetchSockets(opts: BroadcastOptions): Promise<any[]> {
|
||||||
|
const sockets = []
|
||||||
|
|
||||||
|
this.apply(opts, socket => {
|
||||||
|
sockets.push(socket)
|
||||||
|
})
|
||||||
|
|
||||||
|
return Promise.resolve(sockets)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the matching socket instances join the specified rooms
|
||||||
|
*
|
||||||
|
* @param opts - the filters to apply
|
||||||
|
* @param rooms - the rooms to join
|
||||||
|
*/
|
||||||
|
public addSockets(opts: BroadcastOptions, rooms: Room[]): void {
|
||||||
|
this.apply(opts, socket => {
|
||||||
|
socket.join(rooms)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the matching socket instances leave the specified rooms
|
||||||
|
*
|
||||||
|
* @param opts - the filters to apply
|
||||||
|
* @param rooms - the rooms to leave
|
||||||
|
*/
|
||||||
|
public delSockets(opts: BroadcastOptions, rooms: Room[]): void {
|
||||||
|
this.apply(opts, socket => {
|
||||||
|
rooms.forEach(room => socket.leave(room))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the matching socket instances disconnect
|
||||||
|
*
|
||||||
|
* @param opts - the filters to apply
|
||||||
|
* @param close - whether to close the underlying connection
|
||||||
|
*/
|
||||||
|
public disconnectSockets(opts: BroadcastOptions, close: boolean): void {
|
||||||
|
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)
|
||||||
|
|
||||||
|
if (rooms.size) {
|
||||||
|
const ids = new Set()
|
||||||
|
for (const room of rooms) {
|
||||||
|
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 (socket) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeExceptSids(exceptRooms?: Set<Room>) {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return exceptSids
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a packet to the other Socket.IO servers in the cluster
|
||||||
|
* @param packet - an array of arguments, which may include an acknowledgement callback at the end
|
||||||
|
*/
|
||||||
|
public serverSideEmit(packet: any[]): void {
|
||||||
|
throw new Error(
|
||||||
|
"this adapter does not support the serverSideEmit() functionality"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
78
packages/websocket/src/socket.io-parser/binary.ts
Normal file
78
packages/websocket/src/socket.io-parser/binary.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { isBinary } from "./is-binary"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces every Buffer | ArrayBuffer | Blob | File in packet with a numbered placeholder.
|
||||||
|
*
|
||||||
|
* @param {Object} packet - socket.io event packet
|
||||||
|
* @return {Object} with deconstructed packet and list of buffers
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function _deconstructPacket(data, buffers) {
|
||||||
|
if (!data) return data
|
||||||
|
|
||||||
|
if (isBinary(data)) {
|
||||||
|
const placeholder = { _placeholder: true, num: buffers.length }
|
||||||
|
buffers.push(data)
|
||||||
|
return placeholder
|
||||||
|
} else if (Array.isArray(data)) {
|
||||||
|
const newData = new Array(data.length)
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
newData[i] = _deconstructPacket(data[i], buffers)
|
||||||
|
}
|
||||||
|
return newData
|
||||||
|
} else if (typeof data === "object" && !(data instanceof Date)) {
|
||||||
|
const newData = {}
|
||||||
|
for (const key in data) {
|
||||||
|
if (data.hasOwnProperty(key)) {
|
||||||
|
newData[key] = _deconstructPacket(data[key], buffers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newData
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconstructs a binary packet from its placeholder packet and buffers
|
||||||
|
*
|
||||||
|
* @param {Object} packet - event packet with placeholders
|
||||||
|
* @param {Array} buffers - binary buffers to put in placeholder positions
|
||||||
|
* @return {Object} reconstructed packet
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function reconstructPacket(packet, buffers) {
|
||||||
|
packet.data = _reconstructPacket(packet.data, buffers)
|
||||||
|
packet.attachments = undefined // no longer useful
|
||||||
|
return packet
|
||||||
|
}
|
||||||
|
|
||||||
|
function _reconstructPacket(data, buffers) {
|
||||||
|
if (!data) return data
|
||||||
|
|
||||||
|
if (data && data._placeholder) {
|
||||||
|
return buffers[data.num] // appropriate buffer (should be natural order anyway)
|
||||||
|
} else if (Array.isArray(data)) {
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
data[i] = _reconstructPacket(data[i], buffers)
|
||||||
|
}
|
||||||
|
} else if (typeof data === "object") {
|
||||||
|
for (const key in data) {
|
||||||
|
if (data.hasOwnProperty(key)) {
|
||||||
|
data[key] = _reconstructPacket(data[key], buffers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
316
packages/websocket/src/socket.io-parser/index.ts
Normal file
316
packages/websocket/src/socket.io-parser/index.ts
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
import EventEmitter = require("events")
|
||||||
|
import { deconstructPacket, reconstructPacket } from "./binary"
|
||||||
|
import { isBinary, hasBinary } from "./is-binary"
|
||||||
|
|
||||||
|
// const debug = require("debug")("socket.io-parser")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protocol version.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const protocol: number = 5
|
||||||
|
|
||||||
|
export enum PacketType {
|
||||||
|
CONNECT,
|
||||||
|
DISCONNECT,
|
||||||
|
EVENT,
|
||||||
|
ACK,
|
||||||
|
CONNECT_ERROR,
|
||||||
|
BINARY_EVENT,
|
||||||
|
BINARY_ACK,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Packet {
|
||||||
|
type: PacketType
|
||||||
|
nsp: string
|
||||||
|
data?: any
|
||||||
|
id?: number
|
||||||
|
attachments?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A socket.io Encoder instance
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class Encoder {
|
||||||
|
/**
|
||||||
|
* Encode a packet as a single string if non-binary, or as a
|
||||||
|
* buffer sequence, depending on packet type.
|
||||||
|
*
|
||||||
|
* @param {Object} obj - packet object
|
||||||
|
*/
|
||||||
|
public encode(obj: Packet) {
|
||||||
|
console.trace("encoding packet", JSON.stringify(obj))
|
||||||
|
|
||||||
|
if (obj.type === PacketType.EVENT || obj.type === PacketType.ACK) {
|
||||||
|
if (hasBinary(obj)) {
|
||||||
|
obj.type =
|
||||||
|
obj.type === PacketType.EVENT
|
||||||
|
? PacketType.BINARY_EVENT
|
||||||
|
: PacketType.BINARY_ACK
|
||||||
|
return this.encodeAsBinary(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [this.encodeAsString(obj)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode packet as string.
|
||||||
|
*/
|
||||||
|
|
||||||
|
private encodeAsString(obj: Packet) {
|
||||||
|
// first is type
|
||||||
|
let str = "" + obj.type
|
||||||
|
|
||||||
|
// attachments if we have them
|
||||||
|
if (
|
||||||
|
obj.type === PacketType.BINARY_EVENT ||
|
||||||
|
obj.type === PacketType.BINARY_ACK
|
||||||
|
) {
|
||||||
|
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 + ","
|
||||||
|
}
|
||||||
|
|
||||||
|
// immediately followed by the id
|
||||||
|
if (null != obj.id) {
|
||||||
|
str += obj.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// json data
|
||||||
|
if (null != obj.data) {
|
||||||
|
str += JSON.stringify(obj.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.trace("encoded", JSON.stringify(obj), "as", str)
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode packet as 'buffer sequence' by removing blobs, and
|
||||||
|
* deconstructing packet into object with placeholders and
|
||||||
|
* a list of buffers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
private encodeAsBinary(obj: Packet) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A socket.io Decoder instance
|
||||||
|
*
|
||||||
|
* @return {Object} decoder
|
||||||
|
*/
|
||||||
|
export class Decoder extends EventEmitter {
|
||||||
|
private reconstructor: BinaryReconstructor
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes an encoded packet string into packet JSON.
|
||||||
|
*
|
||||||
|
* @param {String} obj - encoded packet
|
||||||
|
*/
|
||||||
|
|
||||||
|
public add(obj: any) {
|
||||||
|
let packet
|
||||||
|
if (typeof obj === "string") {
|
||||||
|
packet = this.decodeString(obj)
|
||||||
|
if (
|
||||||
|
packet.type === PacketType.BINARY_EVENT ||
|
||||||
|
packet.type === PacketType.BINARY_ACK
|
||||||
|
) {
|
||||||
|
// binary packet's json
|
||||||
|
this.reconstructor = new BinaryReconstructor(packet)
|
||||||
|
|
||||||
|
// no attachments, labeled binary but no binary data to follow
|
||||||
|
if (packet.attachments === 0) {
|
||||||
|
super.emit("decoded", packet)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// non-binary full packet
|
||||||
|
super.emit("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")
|
||||||
|
} else {
|
||||||
|
packet = this.reconstructor.takeBinaryData(obj)
|
||||||
|
if (packet) {
|
||||||
|
// received final buffer
|
||||||
|
this.reconstructor = null
|
||||||
|
super.emit("decoded", packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("Unknown type: " + obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a packet String (JSON data)
|
||||||
|
*
|
||||||
|
* @param {String} str
|
||||||
|
* @return {Object} packet
|
||||||
|
*/
|
||||||
|
private decodeString(str): Packet {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// look up attachments if type binary
|
||||||
|
if (
|
||||||
|
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)
|
||||||
|
if (buf != Number(buf) || str.charAt(i) !== "-") {
|
||||||
|
throw new Error("Illegal attachments")
|
||||||
|
}
|
||||||
|
p.attachments = Number(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// look up namespace (if any)
|
||||||
|
if ("/" === str.charAt(i + 1)) {
|
||||||
|
const start = i + 1
|
||||||
|
while (++i) {
|
||||||
|
const c = str.charAt(i)
|
||||||
|
if ("," === c) break
|
||||||
|
if (i === str.length) break
|
||||||
|
}
|
||||||
|
p.nsp = str.substring(start, i)
|
||||||
|
} else {
|
||||||
|
p.nsp = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// look up id
|
||||||
|
const next = str.charAt(i + 1)
|
||||||
|
if ("" !== next && Number(next) == next) {
|
||||||
|
const start = i + 1
|
||||||
|
while (++i) {
|
||||||
|
const c = str.charAt(i)
|
||||||
|
if (null == c || Number(c) != c) {
|
||||||
|
--i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (i === str.length) break
|
||||||
|
}
|
||||||
|
p.id = Number(str.substring(start, i + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// look up json data
|
||||||
|
if (str.charAt(++i)) {
|
||||||
|
const payload = tryParse(str.substr(i))
|
||||||
|
if (Decoder.isPayloadValid(p.type, payload)) {
|
||||||
|
p.data = payload
|
||||||
|
} else {
|
||||||
|
throw new Error("invalid payload")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.trace("decoded", str, "as", p)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
private static isPayloadValid(type: PacketType, payload: any): boolean {
|
||||||
|
switch (type) {
|
||||||
|
case PacketType.CONNECT:
|
||||||
|
return typeof payload === "object"
|
||||||
|
case PacketType.DISCONNECT:
|
||||||
|
return payload === undefined
|
||||||
|
case PacketType.CONNECT_ERROR:
|
||||||
|
return typeof payload === "string" || typeof payload === "object"
|
||||||
|
case PacketType.EVENT:
|
||||||
|
case PacketType.BINARY_EVENT:
|
||||||
|
return Array.isArray(payload) && payload.length > 0
|
||||||
|
case PacketType.ACK:
|
||||||
|
case PacketType.BINARY_ACK:
|
||||||
|
return Array.isArray(payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deallocates a parser's resources
|
||||||
|
*/
|
||||||
|
public destroy() {
|
||||||
|
if (this.reconstructor) {
|
||||||
|
this.reconstructor.finishedReconstruction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryParse(str) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(str)
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A manager of a binary event's 'buffer sequence'. Should
|
||||||
|
* be constructed whenever a packet of type BINARY_EVENT is
|
||||||
|
* decoded.
|
||||||
|
*
|
||||||
|
* @param {Object} packet
|
||||||
|
* @return {BinaryReconstructor} initialized reconstructor
|
||||||
|
*/
|
||||||
|
|
||||||
|
class BinaryReconstructor {
|
||||||
|
private reconPack
|
||||||
|
private buffers: Array<Buffer | ArrayBuffer> = [];
|
||||||
|
|
||||||
|
constructor(readonly packet: Packet) {
|
||||||
|
this.reconPack = packet
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to be called when binary data received from connection
|
||||||
|
* after a BINARY_EVENT packet.
|
||||||
|
*
|
||||||
|
* @param {Buffer | ArrayBuffer} binData - the raw binary data received
|
||||||
|
* @return {null | Object} returns null if more binary data is expected or
|
||||||
|
* a reconstructed packet object if all buffers have been received.
|
||||||
|
*/
|
||||||
|
public takeBinaryData(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
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up binary packet reconstruction variables.
|
||||||
|
*/
|
||||||
|
public finishedReconstruction() {
|
||||||
|
this.reconPack = null
|
||||||
|
this.buffers = []
|
||||||
|
}
|
||||||
|
}
|
65
packages/websocket/src/socket.io-parser/is-binary.ts
Normal file
65
packages/websocket/src/socket.io-parser/is-binary.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
const withNativeArrayBuffer: boolean = typeof ArrayBuffer === "function"
|
||||||
|
|
||||||
|
const isView = (obj: any) => {
|
||||||
|
return typeof ArrayBuffer.isView === "function"
|
||||||
|
? ArrayBuffer.isView(obj)
|
||||||
|
: obj.buffer instanceof ArrayBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
const toString = Object.prototype.toString
|
||||||
|
const withNativeBlob = false
|
||||||
|
// typeof Blob === "function" ||
|
||||||
|
// (typeof Blob !== "undefined" &&
|
||||||
|
// toString.call(Blob) === "[object BlobConstructor]")
|
||||||
|
const withNativeFile = false
|
||||||
|
// typeof File === "function" ||
|
||||||
|
// (typeof File !== "undefined" &&
|
||||||
|
// toString.call(File) === "[object FileConstructor]")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if obj is a Buffer, an ArrayBuffer, a Blob or a File.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function isBinary(obj: any) {
|
||||||
|
return (
|
||||||
|
(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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
for (let i = 0, l = obj.length; i < l; i++) {
|
||||||
|
if (hasBinary(obj[i])) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBinary(obj)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
obj.toJSON &&
|
||||||
|
typeof obj.toJSON === "function" &&
|
||||||
|
arguments.length === 1
|
||||||
|
) {
|
||||||
|
return hasBinary(obj.toJSON(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in obj) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, key) && hasBinary(obj[key])) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
320
packages/websocket/src/socket.io/broadcast-operator.ts
Normal file
320
packages/websocket/src/socket.io/broadcast-operator.ts
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
// 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 {
|
||||||
|
EventParams,
|
||||||
|
EventNames,
|
||||||
|
EventsMap,
|
||||||
|
TypedEventBroadcaster,
|
||||||
|
} from "./typed-events"
|
||||||
|
|
||||||
|
export class BroadcastOperator<EmitEvents extends EventsMap>
|
||||||
|
implements TypedEventBroadcaster<EmitEvents>
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private readonly adapter: Adapter,
|
||||||
|
private readonly rooms: Set<Room> = new Set<Room>(),
|
||||||
|
private readonly exceptRooms: Set<Room> = new Set<Room>(),
|
||||||
|
private readonly flags: BroadcastFlags = {}
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Targets a room when emitting.
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @return a new BroadcastOperator instance
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
|
||||||
|
const rooms = new Set(this.rooms)
|
||||||
|
if (Array.isArray(room)) {
|
||||||
|
room.forEach((r) => rooms.add(r))
|
||||||
|
} else {
|
||||||
|
rooms.add(room)
|
||||||
|
}
|
||||||
|
return new BroadcastOperator(
|
||||||
|
this.adapter,
|
||||||
|
rooms,
|
||||||
|
this.exceptRooms,
|
||||||
|
this.flags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Targets a room when emitting.
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @return a new BroadcastOperator instance
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
|
||||||
|
return this.to(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Excludes a room when emitting.
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @return a new BroadcastOperator instance
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> {
|
||||||
|
const exceptRooms = new Set(this.exceptRooms)
|
||||||
|
if (Array.isArray(room)) {
|
||||||
|
room.forEach((r) => exceptRooms.add(r))
|
||||||
|
} else {
|
||||||
|
exceptRooms.add(room)
|
||||||
|
}
|
||||||
|
return new BroadcastOperator(
|
||||||
|
this.adapter,
|
||||||
|
this.rooms,
|
||||||
|
exceptRooms,
|
||||||
|
this.flags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the compress flag.
|
||||||
|
*
|
||||||
|
* @param compress - if `true`, compresses the sending data
|
||||||
|
* @return a new BroadcastOperator instance
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
|
||||||
|
const flags = Object.assign({}, this.flags, { compress })
|
||||||
|
return new BroadcastOperator(
|
||||||
|
this.adapter,
|
||||||
|
this.rooms,
|
||||||
|
this.exceptRooms,
|
||||||
|
flags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
|
||||||
|
* receive messages (because of network slowness or other issues, or because they’re connected through long polling
|
||||||
|
* and is in the middle of a request-response cycle).
|
||||||
|
*
|
||||||
|
* @return a new BroadcastOperator instance
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public get volatile(): BroadcastOperator<EmitEvents> {
|
||||||
|
const flags = Object.assign({}, this.flags, { volatile: true })
|
||||||
|
return new BroadcastOperator(
|
||||||
|
this.adapter,
|
||||||
|
this.rooms,
|
||||||
|
this.exceptRooms,
|
||||||
|
flags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
|
||||||
|
*
|
||||||
|
* @return a new BroadcastOperator instance
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public get local(): BroadcastOperator<EmitEvents> {
|
||||||
|
const flags = Object.assign({}, this.flags, { local: true })
|
||||||
|
return new BroadcastOperator(
|
||||||
|
this.adapter,
|
||||||
|
this.rooms,
|
||||||
|
this.exceptRooms,
|
||||||
|
flags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits to all clients.
|
||||||
|
*
|
||||||
|
* @return Always true
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public emit<Ev extends EventNames<EmitEvents>>(
|
||||||
|
ev: Ev,
|
||||||
|
...args: EventParams<EmitEvents, Ev>
|
||||||
|
): boolean {
|
||||||
|
if (RESERVED_EVENTS.has(ev)) {
|
||||||
|
throw new Error(`"${ev}" is a reserved event name`)
|
||||||
|
}
|
||||||
|
// set up packet object
|
||||||
|
const data = [ev, ...args]
|
||||||
|
const packet = {
|
||||||
|
type: PacketType.EVENT,
|
||||||
|
data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("function" == typeof data[data.length - 1]) {
|
||||||
|
throw new Error("Callbacks are not supported when broadcasting")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.adapter.broadcast(packet, {
|
||||||
|
rooms: this.rooms,
|
||||||
|
except: this.exceptRooms,
|
||||||
|
flags: this.flags,
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of clients.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public allSockets(): Promise<Set<SocketId>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the matching socket instances
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
|
||||||
|
return this.adapter
|
||||||
|
.fetchSockets({
|
||||||
|
rooms: this.rooms,
|
||||||
|
except: this.exceptRooms,
|
||||||
|
})
|
||||||
|
.then((sockets) => {
|
||||||
|
return sockets.map((socket) => {
|
||||||
|
if (socket instanceof Socket) {
|
||||||
|
// FIXME the TypeScript compiler complains about missing private properties
|
||||||
|
return socket as unknown as RemoteSocket<EmitEvents>
|
||||||
|
} else {
|
||||||
|
return new RemoteSocket(this.adapter, socket as SocketDetails)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the matching socket instances join the specified rooms
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public socketsJoin(room: Room | Room[]): void {
|
||||||
|
this.adapter.addSockets(
|
||||||
|
{
|
||||||
|
rooms: this.rooms,
|
||||||
|
except: this.exceptRooms,
|
||||||
|
},
|
||||||
|
Array.isArray(room) ? room : [room]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the matching socket instances leave the specified rooms
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public socketsLeave(room: Room | Room[]): void {
|
||||||
|
this.adapter.delSockets(
|
||||||
|
{
|
||||||
|
rooms: this.rooms,
|
||||||
|
except: this.exceptRooms,
|
||||||
|
},
|
||||||
|
Array.isArray(room) ? room : [room]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the matching socket instances disconnect
|
||||||
|
*
|
||||||
|
* @param close - whether to close the underlying connection
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public disconnectSockets(close: boolean = false): void {
|
||||||
|
this.adapter.disconnectSockets(
|
||||||
|
{
|
||||||
|
rooms: this.rooms,
|
||||||
|
except: this.exceptRooms,
|
||||||
|
},
|
||||||
|
close
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format of the data when the Socket instance exists on another Socket.IO server
|
||||||
|
*/
|
||||||
|
interface SocketDetails {
|
||||||
|
id: SocketId
|
||||||
|
handshake: Handshake
|
||||||
|
rooms: Room[]
|
||||||
|
data: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expose of subset of the attributes and methods of the Socket class
|
||||||
|
*/
|
||||||
|
export class RemoteSocket<EmitEvents extends EventsMap>
|
||||||
|
implements TypedEventBroadcaster<EmitEvents>
|
||||||
|
{
|
||||||
|
public readonly id: SocketId
|
||||||
|
public readonly handshake: Handshake
|
||||||
|
public readonly rooms: Set<Room>
|
||||||
|
public readonly data: any
|
||||||
|
|
||||||
|
private readonly operator: BroadcastOperator<EmitEvents>
|
||||||
|
|
||||||
|
constructor(adapter: Adapter, details: SocketDetails) {
|
||||||
|
this.id = details.id
|
||||||
|
this.handshake = details.handshake
|
||||||
|
this.rooms = new Set(details.rooms)
|
||||||
|
this.data = details.data
|
||||||
|
this.operator = new BroadcastOperator(adapter, new Set([this.id]))
|
||||||
|
}
|
||||||
|
|
||||||
|
public emit<Ev extends EventNames<EmitEvents>>(
|
||||||
|
ev: Ev,
|
||||||
|
...args: EventParams<EmitEvents, Ev>
|
||||||
|
): boolean {
|
||||||
|
return this.operator.emit(ev, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joins a room.
|
||||||
|
*
|
||||||
|
* @param {String|Array} room - room or array of rooms
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public join(room: Room | Room[]): void {
|
||||||
|
return this.operator.socketsJoin(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leaves a room.
|
||||||
|
*
|
||||||
|
* @param {String} room
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public leave(room: Room): void {
|
||||||
|
return this.operator.socketsLeave(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnects this client.
|
||||||
|
*
|
||||||
|
* @param {Boolean} close - if `true`, closes the underlying connection
|
||||||
|
* @return {Socket} self
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public disconnect(close = false): this {
|
||||||
|
this.operator.disconnectSockets(close)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
331
packages/websocket/src/socket.io/client.ts
Normal file
331
packages/websocket/src/socket.io/client.ts
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
// 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 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 EngineIOSocket } from '../engine.io/socket'
|
||||||
|
|
||||||
|
// const debug = debugModule("socket.io:client");
|
||||||
|
|
||||||
|
interface WriteOptions {
|
||||||
|
compress?: boolean
|
||||||
|
volatile?: boolean
|
||||||
|
preEncoded?: boolean
|
||||||
|
wsPreEncoded?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Client<
|
||||||
|
ListenEvents extends EventsMap,
|
||||||
|
EmitEvents extends EventsMap,
|
||||||
|
ServerSideEvents extends EventsMap
|
||||||
|
> {
|
||||||
|
public readonly conn: EngineIOSocket
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
readonly id: string
|
||||||
|
private readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents>
|
||||||
|
private readonly encoder: Encoder
|
||||||
|
private readonly decoder: any
|
||||||
|
private sockets: Map<
|
||||||
|
SocketId,
|
||||||
|
Socket<ListenEvents, EmitEvents, ServerSideEvents>
|
||||||
|
> = new Map()
|
||||||
|
private nsps: Map<
|
||||||
|
string,
|
||||||
|
Socket<ListenEvents, EmitEvents, ServerSideEvents>
|
||||||
|
> = new Map()
|
||||||
|
private connectTimeout: NodeJS.Timeout
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client constructor.
|
||||||
|
*
|
||||||
|
* @param server instance
|
||||||
|
* @param conn
|
||||||
|
* @package
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
server: Server<ListenEvents, EmitEvents, ServerSideEvents>,
|
||||||
|
conn: EngineIOSocket
|
||||||
|
) {
|
||||||
|
this.server = server
|
||||||
|
this.conn = conn
|
||||||
|
this.encoder = server.encoder
|
||||||
|
this.decoder = new server._parser.Decoder()
|
||||||
|
this.id = conn.id
|
||||||
|
this.setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the reference to the request that originated the Engine.IO connection
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public get request(): any/** IncomingMessage */ {
|
||||||
|
return this.conn.request
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up event listeners.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private setup() {
|
||||||
|
console.debug(`socket.io client setup conn ${this.conn.id}`)
|
||||||
|
this.onclose = this.onclose.bind(this)
|
||||||
|
this.ondata = this.ondata.bind(this)
|
||||||
|
this.onerror = this.onerror.bind(this)
|
||||||
|
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.connectTimeout = setTimeout(() => {
|
||||||
|
if (this.nsps.size === 0) {
|
||||||
|
console.debug(`no namespace joined yet, close the client ${this.id}`)
|
||||||
|
this.close()
|
||||||
|
} else {
|
||||||
|
console.debug(`the client ${this.id} has already joined a namespace, nothing to do`)
|
||||||
|
}
|
||||||
|
}, this.server._connectTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects a client to a namespace.
|
||||||
|
*
|
||||||
|
* @param {String} name - the namespace
|
||||||
|
* @param {Object} auth - the auth parameters
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private connect(name: string, auth: object = {}): void {
|
||||||
|
if (this.server._nsps.has(name)) {
|
||||||
|
console.debug(`socket.io client ${this.id} connecting to namespace ${name}`)
|
||||||
|
return this.doConnect(name, auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.server._checkNamespace(
|
||||||
|
name,
|
||||||
|
auth,
|
||||||
|
(
|
||||||
|
dynamicNspName:
|
||||||
|
| Namespace<ListenEvents, EmitEvents, ServerSideEvents>
|
||||||
|
| false
|
||||||
|
) => {
|
||||||
|
if (dynamicNspName) {
|
||||||
|
console.debug(`dynamic namespace ${dynamicNspName} was created`)
|
||||||
|
this.doConnect(name, auth)
|
||||||
|
} else {
|
||||||
|
console.debug(`creation of namespace ${name} was denied`)
|
||||||
|
this._packet({
|
||||||
|
type: PacketType.CONNECT_ERROR,
|
||||||
|
nsp: name,
|
||||||
|
data: {
|
||||||
|
message: "Invalid namespace",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects a client to a namespace.
|
||||||
|
*
|
||||||
|
* @param name - the namespace
|
||||||
|
* @param {Object} auth - the auth parameters
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private doConnect(name: string, auth: object): 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)
|
||||||
|
|
||||||
|
if (this.connectTimeout) {
|
||||||
|
clearTimeout(this.connectTimeout)
|
||||||
|
this.connectTimeout = undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnects from all namespaces and closes transport.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_disconnect(): void {
|
||||||
|
for (const socket of this.sockets.values()) {
|
||||||
|
socket.disconnect()
|
||||||
|
}
|
||||||
|
this.sockets.clear()
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a socket. Called by each `Socket`.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_remove(socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>): void {
|
||||||
|
if (this.sockets.has(socket.id)) {
|
||||||
|
const nsp = this.sockets.get(socket.id)!.nsp.name
|
||||||
|
this.sockets.delete(socket.id)
|
||||||
|
this.nsps.delete(nsp)
|
||||||
|
} else {
|
||||||
|
console.debug("ignoring remove for", socket.id)
|
||||||
|
}
|
||||||
|
// @java-patch disconnect client when no live socket
|
||||||
|
process.nextTick(() => {
|
||||||
|
if (this.sockets.size == 0) {
|
||||||
|
this.onclose('no live socket')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the underlying connection.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private close(): void {
|
||||||
|
console.debug(`client ${this.id} clise - reason: forcing transport close`)
|
||||||
|
if ("open" === this.conn.readyState) {
|
||||||
|
console.debug("forcing transport close")
|
||||||
|
this.conn.close()
|
||||||
|
this.onclose("forced server close")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a packet to the transport.
|
||||||
|
*
|
||||||
|
* @param {Object} packet object
|
||||||
|
* @param {Object} opts
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_packet(packet: Packet | any[], opts: WriteOptions = {}): void {
|
||||||
|
if (this.conn.readyState !== "open") {
|
||||||
|
console.debug(`client ${this.id} ignoring packet write ${JSON.stringify(packet)}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const encodedPackets = opts.preEncoded
|
||||||
|
? (packet as any[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine()
|
||||||
|
: this.encoder.encode(packet as Packet)
|
||||||
|
for (const encodedPacket of encodedPackets) {
|
||||||
|
this.writeToEngine(encodedPacket, opts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private writeToEngine(
|
||||||
|
encodedPacket: String | Buffer,
|
||||||
|
opts: WriteOptions
|
||||||
|
): void {
|
||||||
|
if (opts.volatile && !this.conn.transport.writable) {
|
||||||
|
console.debug(`client ${this.id} volatile packet is discarded since the transport is not currently writable`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.conn.write(encodedPacket, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called with incoming transport data.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private ondata(data): void {
|
||||||
|
// try/catch is needed for protocol violations (GH-1880)
|
||||||
|
try {
|
||||||
|
this.decoder.add(data)
|
||||||
|
} catch (e) {
|
||||||
|
this.onerror(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when parser fully decodes a packet.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private ondecoded(packet: Packet): void {
|
||||||
|
if (PacketType.CONNECT === packet.type) {
|
||||||
|
if (this.conn.protocol === 3) {
|
||||||
|
const parsed = url.parse(packet.nsp, true)
|
||||||
|
this.connect(parsed.pathname!, parsed.query)
|
||||||
|
} else {
|
||||||
|
this.connect(packet.nsp, packet.data)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const socket = this.nsps.get(packet.nsp)
|
||||||
|
if (socket) {
|
||||||
|
process.nextTick(function () {
|
||||||
|
socket._onpacket(packet)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.debug(`client ${this.id} no socket for namespace ${packet.nsp}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles an error.
|
||||||
|
*
|
||||||
|
* @param {Object} err object
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private onerror(err): void {
|
||||||
|
for (const socket of this.sockets.values()) {
|
||||||
|
socket._onerror(err)
|
||||||
|
}
|
||||||
|
this.conn.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called upon transport close.
|
||||||
|
*
|
||||||
|
* @param reason
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private onclose(reason: string): void {
|
||||||
|
console.debug(`client ${this.id} close with reason ${reason}`)
|
||||||
|
|
||||||
|
// ignore a potential subsequent `close` event
|
||||||
|
this.destroy()
|
||||||
|
|
||||||
|
// `nsps` and `sockets` are cleaned up seamlessly
|
||||||
|
for (const socket of this.sockets.values()) {
|
||||||
|
socket._onclose(reason)
|
||||||
|
}
|
||||||
|
this.sockets.clear()
|
||||||
|
|
||||||
|
this.decoder.destroy() // clean up decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up event listeners.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private destroy(): void {
|
||||||
|
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)
|
||||||
|
|
||||||
|
if (this.connectTimeout) {
|
||||||
|
clearTimeout(this.connectTimeout)
|
||||||
|
this.connectTimeout = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
825
packages/websocket/src/socket.io/index.ts
Normal file
825
packages/websocket/src/socket.io/index.ts
Normal file
@ -0,0 +1,825 @@
|
|||||||
|
// import http = require("http");
|
||||||
|
// import { createReadStream } from "fs";
|
||||||
|
// import { createDeflate, createGzip, createBrotliCompress } from "zlib";
|
||||||
|
// import accepts = require("accepts");
|
||||||
|
// import { pipeline } from "stream";
|
||||||
|
// import path = require("path");
|
||||||
|
import engine = require("../engine.io")
|
||||||
|
import { Client } from './client'
|
||||||
|
import { EventEmitter } from 'events'
|
||||||
|
import { ExtendedError, Namespace, ServerReservedEventsMap } from "./namespace"
|
||||||
|
import { ParentNamespace } from './parent-namespace'
|
||||||
|
// import { Adapter, Room, SocketId } from "socket.io-adapter"
|
||||||
|
import { Adapter, Room, SocketId } from "../socket.io-adapter"
|
||||||
|
// import * as parser from "socket.io-parser";
|
||||||
|
import * as parser from "../socket.io-parser"
|
||||||
|
// import type { Encoder } from "socket.io-parser";
|
||||||
|
import type { Encoder } from "../socket.io-parser"
|
||||||
|
// import debugModule from "debug";
|
||||||
|
import { Socket } from './socket'
|
||||||
|
// import type { CookieSerializeOptions } from "cookie";
|
||||||
|
// import type { CorsOptions } from "cors";
|
||||||
|
import type { BroadcastOperator, RemoteSocket } from "./broadcast-operator"
|
||||||
|
import {
|
||||||
|
EventsMap,
|
||||||
|
DefaultEventsMap,
|
||||||
|
EventParams,
|
||||||
|
StrictEventEmitter,
|
||||||
|
EventNames,
|
||||||
|
} from "./typed-events"
|
||||||
|
|
||||||
|
import type { Socket as EngineIOSocket } from '../engine.io/socket'
|
||||||
|
|
||||||
|
// const clientVersion = require("../package.json").version
|
||||||
|
// const dotMapRegex = /\.map/
|
||||||
|
|
||||||
|
// type Transport = "polling" | "websocket";
|
||||||
|
type ParentNspNameMatchFn = (
|
||||||
|
name: string,
|
||||||
|
auth: { [key: string]: any },
|
||||||
|
fn: (err: Error | null, success: boolean) => void
|
||||||
|
) => void
|
||||||
|
|
||||||
|
type AdapterConstructor = typeof Adapter | ((nsp: Namespace) => Adapter)
|
||||||
|
|
||||||
|
interface EngineOptions {
|
||||||
|
/**
|
||||||
|
* how many ms without a pong packet to consider the connection closed
|
||||||
|
* @default 5000
|
||||||
|
*/
|
||||||
|
pingTimeout: number
|
||||||
|
/**
|
||||||
|
* how many ms before sending a new ping packet
|
||||||
|
* @default 25000
|
||||||
|
*/
|
||||||
|
pingInterval: number
|
||||||
|
/**
|
||||||
|
* how many ms before an uncompleted transport upgrade is cancelled
|
||||||
|
* @default 10000
|
||||||
|
*/
|
||||||
|
upgradeTimeout: number
|
||||||
|
/**
|
||||||
|
* how many bytes or characters a message can be, before closing the session (to avoid DoS).
|
||||||
|
* @default 1e5 (100 KB)
|
||||||
|
*/
|
||||||
|
maxHttpBufferSize: number
|
||||||
|
/**
|
||||||
|
* A function that receives a given handshake or upgrade request as its first parameter,
|
||||||
|
* and can decide whether to continue or not. The second argument is a function that needs
|
||||||
|
* to be called with the decided information: fn(err, success), where success is a boolean
|
||||||
|
* value where false means that the request is rejected, and err is an error code.
|
||||||
|
*/
|
||||||
|
// allowRequest: (
|
||||||
|
// req: http.IncomingMessage,
|
||||||
|
// fn: (err: string | null | undefined, success: boolean) => void
|
||||||
|
// ) => void
|
||||||
|
/**
|
||||||
|
* the low-level transports that are enabled
|
||||||
|
* @default ["polling", "websocket"]
|
||||||
|
*/
|
||||||
|
// transports: Transport[]
|
||||||
|
/**
|
||||||
|
* whether to allow transport upgrades
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
allowUpgrades: boolean
|
||||||
|
/**
|
||||||
|
* parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
perMessageDeflate: boolean | object
|
||||||
|
/**
|
||||||
|
* parameters of the http compression for the polling transports (see zlib api docs). Set to false to disable.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
httpCompression: boolean | object
|
||||||
|
/**
|
||||||
|
* what WebSocket server implementation to use. Specified module must
|
||||||
|
* conform to the ws interface (see ws module api docs). Default value is ws.
|
||||||
|
* An alternative c++ addon is also available by installing uws module.
|
||||||
|
*/
|
||||||
|
wsEngine: string
|
||||||
|
/**
|
||||||
|
* an optional packet which will be concatenated to the handshake packet emitted by Engine.IO.
|
||||||
|
*/
|
||||||
|
initialPacket: any
|
||||||
|
/**
|
||||||
|
* configuration of the cookie that contains the client sid to send as part of handshake response headers. This cookie
|
||||||
|
* might be used for sticky-session. Defaults to not sending any cookie.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
// cookie: CookieSerializeOptions | boolean
|
||||||
|
/**
|
||||||
|
* the options that will be forwarded to the cors module
|
||||||
|
*/
|
||||||
|
// cors: CorsOptions
|
||||||
|
/**
|
||||||
|
* whether to enable compatibility with Socket.IO v2 clients
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
allowEIO3: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttachOptions {
|
||||||
|
/**
|
||||||
|
* name of the path to capture
|
||||||
|
* @default "/engine.io"
|
||||||
|
*/
|
||||||
|
path: string
|
||||||
|
/**
|
||||||
|
* destroy unhandled upgrade requests
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
destroyUpgrade: boolean
|
||||||
|
/**
|
||||||
|
* milliseconds after which unhandled requests are ended
|
||||||
|
* @default 1000
|
||||||
|
*/
|
||||||
|
destroyUpgradeTimeout: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EngineAttachOptions extends EngineOptions, AttachOptions { }
|
||||||
|
|
||||||
|
interface ServerOptions extends EngineAttachOptions {
|
||||||
|
/**
|
||||||
|
* name of the path to capture
|
||||||
|
* @default "/socket.io"
|
||||||
|
*/
|
||||||
|
path: string
|
||||||
|
/**
|
||||||
|
* whether to serve the client files
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
serveClient: boolean
|
||||||
|
/**
|
||||||
|
* the adapter to use
|
||||||
|
* @default the in-memory adapter (https://github.com/socketio/socket.io-adapter)
|
||||||
|
*/
|
||||||
|
adapter: any
|
||||||
|
/**
|
||||||
|
* the parser to use
|
||||||
|
* @default the default parser (https://github.com/socketio/socket.io-parser)
|
||||||
|
*/
|
||||||
|
parser: any
|
||||||
|
/**
|
||||||
|
* how many ms before a client without namespace is closed
|
||||||
|
* @default 45000
|
||||||
|
*/
|
||||||
|
connectTimeout: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Server<
|
||||||
|
ListenEvents extends EventsMap = DefaultEventsMap,
|
||||||
|
EmitEvents extends EventsMap = ListenEvents,
|
||||||
|
ServerSideEvents extends EventsMap = DefaultEventsMap
|
||||||
|
> extends StrictEventEmitter<
|
||||||
|
ServerSideEvents,
|
||||||
|
EmitEvents,
|
||||||
|
ServerReservedEventsMap<ListenEvents, EmitEvents, ServerSideEvents>
|
||||||
|
> {
|
||||||
|
public readonly sockets: Namespace<
|
||||||
|
ListenEvents,
|
||||||
|
EmitEvents,
|
||||||
|
ServerSideEvents
|
||||||
|
>
|
||||||
|
/**
|
||||||
|
* A reference to the underlying Engine.IO server.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* <code>
|
||||||
|
* const clientsCount = io.engine.clientsCount;
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public engine: any
|
||||||
|
/** @private */
|
||||||
|
readonly _parser: typeof parser
|
||||||
|
/** @private */
|
||||||
|
readonly encoder: Encoder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_nsps: Map<string, Namespace<ListenEvents, EmitEvents, ServerSideEvents>> =
|
||||||
|
new Map();
|
||||||
|
private parentNsps: Map<
|
||||||
|
ParentNspNameMatchFn,
|
||||||
|
ParentNamespace<ListenEvents, EmitEvents, ServerSideEvents>
|
||||||
|
> = new Map();
|
||||||
|
private _adapter?: AdapterConstructor
|
||||||
|
private _serveClient: boolean
|
||||||
|
private opts: Partial<EngineOptions>
|
||||||
|
private eio
|
||||||
|
private _path: string
|
||||||
|
private clientPathRegex: RegExp
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_connectTimeout: number
|
||||||
|
|
||||||
|
// private httpServer: http.Server
|
||||||
|
|
||||||
|
constructor(srv: any, opts: Partial<ServerOptions> = {}) {
|
||||||
|
super()
|
||||||
|
if (!srv) { throw new Error('srv can\'t be undefiend!') }
|
||||||
|
// if (
|
||||||
|
// "object" === typeof srv &&
|
||||||
|
// srv instanceof Object &&
|
||||||
|
// !(srv as Partial<http.Server>).listen
|
||||||
|
// ) {
|
||||||
|
// opts = srv as Partial<ServerOptions>
|
||||||
|
// srv = undefined
|
||||||
|
// }
|
||||||
|
this.path(opts.path || "/socket.io")
|
||||||
|
this.connectTimeout(opts.connectTimeout || 45000)
|
||||||
|
this.serveClient(false !== opts.serveClient)
|
||||||
|
this._parser = opts.parser || parser
|
||||||
|
this.encoder = new this._parser.Encoder()
|
||||||
|
this.adapter(opts.adapter || Adapter)
|
||||||
|
this.sockets = this.of('/')
|
||||||
|
// if (srv) this.attach(srv as http.Server);
|
||||||
|
this.attach(srv, this.opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets/gets whether client code is being served.
|
||||||
|
*
|
||||||
|
* @param v - whether to serve client code
|
||||||
|
* @return self when setting or value when getting
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public serveClient(v: boolean): this
|
||||||
|
public serveClient(): boolean
|
||||||
|
public serveClient(v?: boolean): this | boolean
|
||||||
|
public serveClient(v?: boolean): this | boolean {
|
||||||
|
if (!arguments.length) return this._serveClient
|
||||||
|
this._serveClient = v!
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the middleware for an incoming namespace not already created on the server.
|
||||||
|
*
|
||||||
|
* @param name - name of incoming namespace
|
||||||
|
* @param auth - the auth parameters
|
||||||
|
* @param fn - callback
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_checkNamespace(
|
||||||
|
name: string,
|
||||||
|
auth: { [key: string]: any },
|
||||||
|
fn: (
|
||||||
|
nsp: Namespace<ListenEvents, EmitEvents, ServerSideEvents> | false
|
||||||
|
) => void
|
||||||
|
): void {
|
||||||
|
if (this.parentNsps.size === 0) return fn(false)
|
||||||
|
|
||||||
|
const keysIterator = this.parentNsps.keys()
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
const nextFn = keysIterator.next()
|
||||||
|
if (nextFn.done) {
|
||||||
|
return fn(false)
|
||||||
|
}
|
||||||
|
nextFn.value(name, auth, (err, allow) => {
|
||||||
|
if (err || !allow) {
|
||||||
|
run()
|
||||||
|
} else {
|
||||||
|
const namespace = this.parentNsps
|
||||||
|
.get(nextFn.value)!
|
||||||
|
.createChild(name)
|
||||||
|
// @ts-ignore
|
||||||
|
this.sockets.emitReserved("new_namespace", namespace)
|
||||||
|
fn(namespace)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the client serving path.
|
||||||
|
*
|
||||||
|
* @param {String} v pathname
|
||||||
|
* @return {Server|String} self when setting or value when getting
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public path(v: string): this
|
||||||
|
public path(): string
|
||||||
|
public path(v?: string): this | string
|
||||||
|
public path(v?: string): this | string {
|
||||||
|
if (!arguments.length) return this._path
|
||||||
|
|
||||||
|
this._path = v!.replace(/\/$/, "")
|
||||||
|
|
||||||
|
const escapedPath = this._path.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")
|
||||||
|
this.clientPathRegex = new RegExp(
|
||||||
|
"^" +
|
||||||
|
escapedPath +
|
||||||
|
"/socket\\.io(\\.min|\\.msgpack\\.min)?\\.js(\\.map)?$"
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the delay after which a client without namespace is closed
|
||||||
|
* @param v
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public connectTimeout(v: number): this
|
||||||
|
public connectTimeout(): number
|
||||||
|
public connectTimeout(v?: number): this | number
|
||||||
|
public connectTimeout(v?: number): this | number {
|
||||||
|
if (v === undefined) return this._connectTimeout
|
||||||
|
this._connectTimeout = v
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the adapter for rooms.
|
||||||
|
*
|
||||||
|
* @param v pathname
|
||||||
|
* @return self when setting or value when getting
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public adapter(): AdapterConstructor | undefined
|
||||||
|
public adapter(v: AdapterConstructor): this
|
||||||
|
public adapter(
|
||||||
|
v?: AdapterConstructor
|
||||||
|
): AdapterConstructor | undefined | this {
|
||||||
|
if (!arguments.length) return this._adapter
|
||||||
|
this._adapter = v
|
||||||
|
for (const nsp of this._nsps.values()) {
|
||||||
|
nsp._initAdapter()
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches socket.io to a server or port.
|
||||||
|
*
|
||||||
|
* @param srv - server or port
|
||||||
|
* @param opts - options passed to engine.io
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public listen(
|
||||||
|
srv: any,//http.Server | number,
|
||||||
|
opts: Partial<ServerOptions> = {}
|
||||||
|
): this {
|
||||||
|
throw Error('Unsupport listen at MiaoScript Engine!')
|
||||||
|
//return this.attach(srv, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches socket.io to a server or port.
|
||||||
|
*
|
||||||
|
* @param srv - server or port
|
||||||
|
* @param opts - options passed to engine.io
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public attach(
|
||||||
|
srv: any,//http.Server | number,
|
||||||
|
opts: Partial<ServerOptions> = {}
|
||||||
|
): this {
|
||||||
|
// if ("function" == typeof srv) {
|
||||||
|
// const msg =
|
||||||
|
// "You are trying to attach socket.io to an express " +
|
||||||
|
// "request handler function. Please pass a http.Server instance."
|
||||||
|
// throw new Error(msg)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // handle a port as a string
|
||||||
|
// if (Number(srv) == srv) {
|
||||||
|
// srv = Number(srv)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if ("number" == typeof srv) {
|
||||||
|
// debug("creating http server and binding to %d", srv)
|
||||||
|
// const port = srv
|
||||||
|
// srv = http.createServer((req, res) => {
|
||||||
|
// res.writeHead(404)
|
||||||
|
// res.end()
|
||||||
|
// })
|
||||||
|
// srv.listen(port)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// merge the options passed to the Socket.IO server
|
||||||
|
Object.assign(opts, this.opts)
|
||||||
|
// set engine.io path to `/socket.io`
|
||||||
|
opts.path = opts.path || this._path
|
||||||
|
|
||||||
|
this.initEngine(srv, opts)
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize engine
|
||||||
|
*
|
||||||
|
* @param srv - the server to attach to
|
||||||
|
* @param opts - options passed to engine.io
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private initEngine(srv: any, opts: Partial<EngineAttachOptions>) {
|
||||||
|
// // initialize engine
|
||||||
|
console.debug("creating engine.io instance with opts", JSON.stringify(opts))
|
||||||
|
this.eio = engine.attach(srv, opts)
|
||||||
|
|
||||||
|
// // attach static file serving
|
||||||
|
// if (this._serveClient) this.attachServe(srv)
|
||||||
|
|
||||||
|
// // Export http server
|
||||||
|
// this.httpServer = srv
|
||||||
|
|
||||||
|
// bind to engine events
|
||||||
|
this.bind(this.eio)
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Attaches the static file serving.
|
||||||
|
// *
|
||||||
|
// * @param srv http server
|
||||||
|
// * @private
|
||||||
|
// */
|
||||||
|
// private attachServe(srv: http.Server): void {
|
||||||
|
// debug("attaching client serving req handler")
|
||||||
|
|
||||||
|
// const evs = srv.listeners("request").slice(0)
|
||||||
|
// srv.removeAllListeners("request")
|
||||||
|
// srv.on("request", (req, res) => {
|
||||||
|
// if (this.clientPathRegex.test(req.url)) {
|
||||||
|
// this.serve(req, res)
|
||||||
|
// } else {
|
||||||
|
// for (let i = 0; i < evs.length; i++) {
|
||||||
|
// evs[i].call(srv, req, res)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Handles a request serving of client source and map
|
||||||
|
// *
|
||||||
|
// * @param req
|
||||||
|
// * @param res
|
||||||
|
// * @private
|
||||||
|
// */
|
||||||
|
// private serve(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||||
|
// const filename = req.url!.replace(this._path, "")
|
||||||
|
// const isMap = dotMapRegex.test(filename)
|
||||||
|
// const type = isMap ? "map" : "source"
|
||||||
|
|
||||||
|
// // Per the standard, ETags must be quoted:
|
||||||
|
// // https://tools.ietf.org/html/rfc7232#section-2.3
|
||||||
|
// const expectedEtag = '"' + clientVersion + '"'
|
||||||
|
// const weakEtag = "W/" + expectedEtag
|
||||||
|
|
||||||
|
// const etag = req.headers["if-none-match"]
|
||||||
|
// if (etag) {
|
||||||
|
// if (expectedEtag === etag || weakEtag === etag) {
|
||||||
|
// debug("serve client %s 304", type)
|
||||||
|
// res.writeHead(304)
|
||||||
|
// res.end()
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// debug("serve client %s", type)
|
||||||
|
|
||||||
|
// res.setHeader("Cache-Control", "public, max-age=0")
|
||||||
|
// res.setHeader(
|
||||||
|
// "Content-Type",
|
||||||
|
// "application/" + (isMap ? "json" : "javascript")
|
||||||
|
// )
|
||||||
|
// res.setHeader("ETag", expectedEtag)
|
||||||
|
|
||||||
|
// Server.sendFile(filename, req, res)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * @param filename
|
||||||
|
// * @param req
|
||||||
|
// * @param res
|
||||||
|
// * @private
|
||||||
|
// */
|
||||||
|
// private static sendFile(
|
||||||
|
// filename: string,
|
||||||
|
// req: http.IncomingMessage,
|
||||||
|
// res: http.ServerResponse
|
||||||
|
// ): void {
|
||||||
|
// const readStream = createReadStream(
|
||||||
|
// path.join(__dirname, "../client-dist/", filename)
|
||||||
|
// )
|
||||||
|
// const encoding = accepts(req).encodings(["br", "gzip", "deflate"])
|
||||||
|
|
||||||
|
// const onError = (err: NodeJS.ErrnoException | null) => {
|
||||||
|
// if (err) {
|
||||||
|
// res.end()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// switch (encoding) {
|
||||||
|
// case "br":
|
||||||
|
// res.writeHead(200, { "content-encoding": "br" })
|
||||||
|
// readStream.pipe(createBrotliCompress()).pipe(res)
|
||||||
|
// pipeline(readStream, createBrotliCompress(), res, onError)
|
||||||
|
// break
|
||||||
|
// case "gzip":
|
||||||
|
// res.writeHead(200, { "content-encoding": "gzip" })
|
||||||
|
// pipeline(readStream, createGzip(), res, onError)
|
||||||
|
// break
|
||||||
|
// case "deflate":
|
||||||
|
// res.writeHead(200, { "content-encoding": "deflate" })
|
||||||
|
// pipeline(readStream, createDeflate(), res, onError)
|
||||||
|
// break
|
||||||
|
// default:
|
||||||
|
// res.writeHead(200)
|
||||||
|
// pipeline(readStream, res, onError)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds socket.io to an engine.io instance.
|
||||||
|
*
|
||||||
|
* @param {engine.Server} engine engine.io (or compatible) server
|
||||||
|
* @return {Server} self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public bind(engine): Server {
|
||||||
|
console.debug('engine.io', engine.constructor.name, 'bind to socket.io')
|
||||||
|
this.engine = engine
|
||||||
|
this.engine.on("connection", this.onconnection.bind(this))
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called with each incoming transport connection.
|
||||||
|
*
|
||||||
|
* @param {engine.Socket} conn
|
||||||
|
* @return {Server} self
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private onconnection(conn: EngineIOSocket): Server {
|
||||||
|
console.debug(`socket.io index incoming connection with id ${conn.id}`)
|
||||||
|
let client = new Client(this, conn)
|
||||||
|
if (conn.protocol === 3) {
|
||||||
|
// @ts-ignore
|
||||||
|
client.connect("/")
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up a namespace.
|
||||||
|
*
|
||||||
|
* @param {String|RegExp|Function} name nsp name
|
||||||
|
* @param fn optional, nsp `connection` ev handler
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public of(
|
||||||
|
name: string | RegExp | ParentNspNameMatchFn,
|
||||||
|
fn?: (socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>) => void
|
||||||
|
): Namespace<ListenEvents, EmitEvents, ServerSideEvents> {
|
||||||
|
if (typeof name === "function" || name instanceof RegExp) {
|
||||||
|
const parentNsp = new ParentNamespace(this)
|
||||||
|
console.debug(`initializing parent namespace ${parentNsp.name}`)
|
||||||
|
if (typeof name === "function") {
|
||||||
|
this.parentNsps.set(name, parentNsp)
|
||||||
|
} else {
|
||||||
|
this.parentNsps.set(
|
||||||
|
(nsp, conn, next) => next(null, (name as RegExp).test(nsp)),
|
||||||
|
parentNsp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (fn) {
|
||||||
|
// @ts-ignore
|
||||||
|
parentNsp.on("connect", fn)
|
||||||
|
}
|
||||||
|
return parentNsp
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(name)[0] !== "/") name = "/" + name
|
||||||
|
|
||||||
|
let nsp = this._nsps.get(name)
|
||||||
|
if (!nsp) {
|
||||||
|
console.debug("initializing namespace", name)
|
||||||
|
nsp = new Namespace(this, name)
|
||||||
|
this._nsps.set(name, nsp)
|
||||||
|
if (name !== "/") {
|
||||||
|
// @ts-ignore
|
||||||
|
this.sockets.emitReserved("new_namespace", nsp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fn) nsp.on("connect", fn)
|
||||||
|
return nsp
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes server connection
|
||||||
|
*
|
||||||
|
* @param [fn] optional, called as `fn([err])` on error OR all conns closed
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public close(fn?: (err?: Error) => void): void {
|
||||||
|
for (const socket of this.sockets.sockets.values()) {
|
||||||
|
socket._onclose("server shutting down")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.engine.close()
|
||||||
|
|
||||||
|
// if (this.httpServer) {
|
||||||
|
// this.httpServer.close(fn)
|
||||||
|
// } else {
|
||||||
|
fn && fn()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up namespace middleware.
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public use(
|
||||||
|
fn: (
|
||||||
|
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>,
|
||||||
|
next: (err?: ExtendedError) => void
|
||||||
|
) => void
|
||||||
|
): this {
|
||||||
|
this.sockets.use(fn)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Targets a room when emitting.
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
|
||||||
|
return this.sockets.to(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Targets a room when emitting.
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
|
||||||
|
return this.sockets.in(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Excludes a room when emitting.
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public except(name: Room | Room[]): BroadcastOperator<EmitEvents> {
|
||||||
|
return this.sockets.except(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a `message` event to all clients.
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public send(...args: EventParams<EmitEvents, "message">): this {
|
||||||
|
this.sockets.emit("message", ...args)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a `message` event to all clients.
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public write(...args: EventParams<EmitEvents, "message">): this {
|
||||||
|
this.sockets.emit("message", ...args)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit a packet to other Socket.IO servers
|
||||||
|
*
|
||||||
|
* @param ev - the event name
|
||||||
|
* @param args - an array of arguments, which may include an acknowledgement callback at the end
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
|
||||||
|
ev: Ev,
|
||||||
|
...args: EventParams<ServerSideEvents, Ev>
|
||||||
|
): boolean {
|
||||||
|
return this.sockets.serverSideEmit(ev, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of socket ids.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public allSockets(): Promise<Set<SocketId>> {
|
||||||
|
return this.sockets.allSockets()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the compress flag.
|
||||||
|
*
|
||||||
|
* @param compress - if `true`, compresses the sending data
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
|
||||||
|
return this.sockets.compress(compress)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
|
||||||
|
* receive messages (because of network slowness or other issues, or because they’re connected through long polling
|
||||||
|
* and is in the middle of a request-response cycle).
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public get volatile(): BroadcastOperator<EmitEvents> {
|
||||||
|
return this.sockets.volatile
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public get local(): BroadcastOperator<EmitEvents> {
|
||||||
|
return this.sockets.local
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the matching socket instances
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
|
||||||
|
return this.sockets.fetchSockets()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the matching socket instances join the specified rooms
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public socketsJoin(room: Room | Room[]): void {
|
||||||
|
return this.sockets.socketsJoin(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the matching socket instances leave the specified rooms
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public socketsLeave(room: Room | Room[]): void {
|
||||||
|
return this.sockets.socketsLeave(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the matching socket instances disconnect
|
||||||
|
*
|
||||||
|
* @param close - whether to close the underlying connection
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public disconnectSockets(close: boolean = false): void {
|
||||||
|
return this.sockets.disconnectSockets(close)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expose main namespace (/).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const emitterMethods = Object.keys(EventEmitter.prototype).filter(function (
|
||||||
|
key
|
||||||
|
) {
|
||||||
|
return typeof EventEmitter.prototype[key] === "function"
|
||||||
|
})
|
||||||
|
|
||||||
|
emitterMethods.forEach(function (fn) {
|
||||||
|
Server.prototype[fn] = function () {
|
||||||
|
return this.sockets[fn].apply(this.sockets, arguments)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export { Socket, ServerOptions, Namespace, BroadcastOperator, RemoteSocket }
|
407
packages/websocket/src/socket.io/namespace.ts
Normal file
407
packages/websocket/src/socket.io/namespace.ts
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
|
||||||
|
import { Socket } from "./socket"
|
||||||
|
import type { Server } from "./index"
|
||||||
|
import {
|
||||||
|
EventParams,
|
||||||
|
EventNames,
|
||||||
|
EventsMap,
|
||||||
|
StrictEventEmitter,
|
||||||
|
DefaultEventsMap,
|
||||||
|
} 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"
|
||||||
|
|
||||||
|
// const debug = debugModule("socket.io:namespace");
|
||||||
|
|
||||||
|
export interface ExtendedError extends Error {
|
||||||
|
data?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NamespaceReservedEventsMap<
|
||||||
|
ListenEvents extends EventsMap,
|
||||||
|
EmitEvents extends EventsMap,
|
||||||
|
ServerSideEvents extends EventsMap
|
||||||
|
> {
|
||||||
|
connect: (socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>) => void
|
||||||
|
connection: (
|
||||||
|
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerReservedEventsMap<
|
||||||
|
ListenEvents,
|
||||||
|
EmitEvents,
|
||||||
|
ServerSideEvents
|
||||||
|
> extends NamespaceReservedEventsMap<
|
||||||
|
ListenEvents,
|
||||||
|
EmitEvents,
|
||||||
|
ServerSideEvents
|
||||||
|
> {
|
||||||
|
new_namespace: (
|
||||||
|
namespace: Namespace<ListenEvents, EmitEvents, ServerSideEvents>
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
|
||||||
|
keyof ServerReservedEventsMap<never, never, never>
|
||||||
|
>(<const>["connect", "connection", "new_namespace"])
|
||||||
|
|
||||||
|
export class Namespace<
|
||||||
|
ListenEvents extends EventsMap = DefaultEventsMap,
|
||||||
|
EmitEvents extends EventsMap = ListenEvents,
|
||||||
|
ServerSideEvents extends EventsMap = DefaultEventsMap
|
||||||
|
> extends StrictEventEmitter<
|
||||||
|
ServerSideEvents,
|
||||||
|
EmitEvents,
|
||||||
|
NamespaceReservedEventsMap<ListenEvents, EmitEvents, ServerSideEvents>
|
||||||
|
> {
|
||||||
|
public readonly name: string
|
||||||
|
public readonly sockets: Map<
|
||||||
|
SocketId,
|
||||||
|
Socket<ListenEvents, EmitEvents, ServerSideEvents>
|
||||||
|
> = new Map();
|
||||||
|
|
||||||
|
public adapter: Adapter
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents>
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
_fns: Array<
|
||||||
|
(
|
||||||
|
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>,
|
||||||
|
next: (err?: ExtendedError) => void
|
||||||
|
) => void
|
||||||
|
> = [];
|
||||||
|
|
||||||
|
/** @private */
|
||||||
|
_ids: number = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace constructor.
|
||||||
|
*
|
||||||
|
* @param server instance
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
server: Server<ListenEvents, EmitEvents, ServerSideEvents>,
|
||||||
|
name: string
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
this.server = server
|
||||||
|
this.name = name
|
||||||
|
this._initAdapter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the `Adapter` for this nsp.
|
||||||
|
* Run upon changing adapter by `Server#adapter`
|
||||||
|
* in addition to the constructor.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_initAdapter() {
|
||||||
|
// @ts-ignore
|
||||||
|
this.adapter = new (this.server.adapter()!)(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up namespace middleware.
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public use(
|
||||||
|
fn: (
|
||||||
|
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>,
|
||||||
|
next: (err?: ExtendedError) => void
|
||||||
|
) => void
|
||||||
|
): this {
|
||||||
|
this._fns.push(fn)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the middleware for an incoming client.
|
||||||
|
*
|
||||||
|
* @param socket - the socket that will get added
|
||||||
|
* @param fn - last fn call in the middleware
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private run(
|
||||||
|
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>,
|
||||||
|
fn: (err: ExtendedError | null) => void
|
||||||
|
) {
|
||||||
|
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 no middleware left, summon callback
|
||||||
|
if (!fns[i + 1]) return fn(null)
|
||||||
|
|
||||||
|
// go on to next
|
||||||
|
run(i + 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
run(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Targets a room when emitting.
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
|
||||||
|
return new BroadcastOperator(this.adapter).to(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Targets a room when emitting.
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
|
||||||
|
return new BroadcastOperator(this.adapter).in(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Excludes a room when emitting.
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> {
|
||||||
|
return new BroadcastOperator(this.adapter).except(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new client.
|
||||||
|
*
|
||||||
|
* @return {Socket}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_add(
|
||||||
|
client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
|
||||||
|
query,
|
||||||
|
fn?: (socket: Socket) => void
|
||||||
|
): Socket<ListenEvents, EmitEvents, ServerSideEvents> {
|
||||||
|
const socket = new Socket(this, client, query || {})
|
||||||
|
console.debug(`socket.io namespace client ${client.id} adding socket ${socket.id} to nsp ${this.name}`)
|
||||||
|
this.run(socket, err => {
|
||||||
|
process.nextTick(() => {
|
||||||
|
if ("open" == client.conn.readyState) {
|
||||||
|
if (err) {
|
||||||
|
if (client.conn.protocol === 3) {
|
||||||
|
return socket._error(err.data || err.message)
|
||||||
|
} else {
|
||||||
|
return socket._error({
|
||||||
|
message: err.message,
|
||||||
|
data: err.data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// track socket
|
||||||
|
this.sockets.set(socket.id, socket)
|
||||||
|
console.debug(`socket.io namespace ${this.name} track client ${client.id} socket ${socket.id}`)
|
||||||
|
|
||||||
|
// it's paramount that the internal `onconnect` logic
|
||||||
|
// fires before user-set events to prevent state order
|
||||||
|
// violations (such as a disconnection before the connection
|
||||||
|
// logic is complete)
|
||||||
|
socket._onconnect()
|
||||||
|
// @java-patch multi thread need direct callback socket
|
||||||
|
if (fn) fn(socket)
|
||||||
|
|
||||||
|
// fire user-set events
|
||||||
|
this.emitReserved("connect", socket)
|
||||||
|
this.emitReserved("connection", socket)
|
||||||
|
} else {
|
||||||
|
console.debug(`next called after client ${client.id} was closed - ignoring socket`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return socket
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a client. Called by each `Socket`.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_remove(socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>): void {
|
||||||
|
if (this.sockets.has(socket.id)) {
|
||||||
|
console.debug(`namespace ${this.name} remove socket ${socket.id}`)
|
||||||
|
this.sockets.delete(socket.id)
|
||||||
|
} else {
|
||||||
|
console.debug(`namespace ${this.name} ignoring remove for ${socket.id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits to all clients.
|
||||||
|
*
|
||||||
|
* @return Always true
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public emit<Ev extends EventNames<EmitEvents>>(
|
||||||
|
ev: Ev,
|
||||||
|
...args: EventParams<EmitEvents, Ev>
|
||||||
|
): boolean {
|
||||||
|
return new BroadcastOperator<EmitEvents>(this.adapter).emit(ev, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a `message` event to all clients.
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public send(...args: EventParams<EmitEvents, "message">): this {
|
||||||
|
this.emit("message", ...args)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a `message` event to all clients.
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public write(...args: EventParams<EmitEvents, "message">): this {
|
||||||
|
this.emit("message", ...args)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit a packet to other Socket.IO servers
|
||||||
|
*
|
||||||
|
* @param ev - the event name
|
||||||
|
* @param args - an array of arguments, which may include an acknowledgement callback at the end
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
|
||||||
|
ev: Ev,
|
||||||
|
...args: EventParams<ServerSideEvents, Ev>
|
||||||
|
): boolean {
|
||||||
|
if (RESERVED_EVENTS.has(ev)) {
|
||||||
|
throw new Error(`"${ev}" is a reserved event name`)
|
||||||
|
}
|
||||||
|
args.unshift(ev)
|
||||||
|
this.adapter.serverSideEmit(args)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a packet is received from another Socket.IO server
|
||||||
|
*
|
||||||
|
* @param args - an array of arguments, which may include an acknowledgement callback at the end
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onServerSideEmit(args: [string, ...any[]]) {
|
||||||
|
super.emitUntyped.apply(this, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of clients.
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public allSockets(): Promise<Set<SocketId>> {
|
||||||
|
return new BroadcastOperator(this.adapter).allSockets()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the compress flag.
|
||||||
|
*
|
||||||
|
* @param compress - if `true`, compresses the sending data
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
|
||||||
|
return new BroadcastOperator(this.adapter).compress(compress)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
|
||||||
|
* receive messages (because of network slowness or other issues, or because they’re connected through long polling
|
||||||
|
* and is in the middle of a request-response cycle).
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public get volatile(): BroadcastOperator<EmitEvents> {
|
||||||
|
return new BroadcastOperator(this.adapter).volatile
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public get local(): BroadcastOperator<EmitEvents> {
|
||||||
|
return new BroadcastOperator(this.adapter).local
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the matching socket instances
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
|
||||||
|
return new BroadcastOperator(this.adapter).fetchSockets()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the matching socket instances join the specified rooms
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public socketsJoin(room: Room | Room[]): void {
|
||||||
|
return new BroadcastOperator(this.adapter).socketsJoin(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the matching socket instances leave the specified rooms
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public socketsLeave(room: Room | Room[]): void {
|
||||||
|
return new BroadcastOperator(this.adapter).socketsLeave(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the matching socket instances disconnect
|
||||||
|
*
|
||||||
|
* @param close - whether to close the underlying connection
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public disconnectSockets(close: boolean = false): void {
|
||||||
|
return new BroadcastOperator(this.adapter).disconnectSockets(close)
|
||||||
|
}
|
||||||
|
|
||||||
|
public close() {
|
||||||
|
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`))
|
||||||
|
}
|
||||||
|
}
|
72
packages/websocket/src/socket.io/parent-namespace.ts
Normal file
72
packages/websocket/src/socket.io/parent-namespace.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { Namespace } from "./namespace"
|
||||||
|
import type { Server } 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"
|
||||||
|
|
||||||
|
export class ParentNamespace<
|
||||||
|
ListenEvents extends EventsMap = DefaultEventsMap,
|
||||||
|
EmitEvents extends EventsMap = ListenEvents,
|
||||||
|
ServerSideEvents extends EventsMap = DefaultEventsMap
|
||||||
|
> extends Namespace<ListenEvents, EmitEvents, ServerSideEvents> {
|
||||||
|
private static count: number = 0;
|
||||||
|
private children: Set<Namespace<ListenEvents, EmitEvents, ServerSideEvents>> = new Set();
|
||||||
|
|
||||||
|
constructor(server: Server<ListenEvents, EmitEvents, ServerSideEvents>) {
|
||||||
|
super(server, "/_" + ParentNamespace.count++)
|
||||||
|
}
|
||||||
|
|
||||||
|
_initAdapter() {
|
||||||
|
const broadcast = (packet: any, opts: BroadcastOptions) => {
|
||||||
|
this.children.forEach((nsp) => {
|
||||||
|
nsp.adapter.broadcast(packet, opts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// @ts-ignore FIXME is there a way to declare an inner class in TypeScript?
|
||||||
|
this.adapter = { broadcast }
|
||||||
|
}
|
||||||
|
|
||||||
|
public emit<Ev extends EventNames<EmitEvents>>(
|
||||||
|
ev: Ev,
|
||||||
|
...args: EventParams<EmitEvents, Ev>
|
||||||
|
): boolean {
|
||||||
|
this.children.forEach((nsp) => {
|
||||||
|
nsp.emit(ev, ...args)
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// public emit(...args: any[]): boolean {
|
||||||
|
// this.children.forEach(nsp => {
|
||||||
|
// nsp._rooms = this._rooms
|
||||||
|
// nsp._flags = this._flags
|
||||||
|
// nsp.emit.apply(nsp, args as any)
|
||||||
|
// })
|
||||||
|
// this._rooms.clear()
|
||||||
|
// this._flags = {}
|
||||||
|
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
|
||||||
|
createChild(
|
||||||
|
name: string
|
||||||
|
): Namespace<ListenEvents, EmitEvents, ServerSideEvents> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
727
packages/websocket/src/socket.io/socket.ts
Normal file
727
packages/websocket/src/socket.io/socket.ts
Normal file
@ -0,0 +1,727 @@
|
|||||||
|
|
||||||
|
import { Packet, PacketType } from "../socket.io-parser"
|
||||||
|
import url = require("url")
|
||||||
|
// import debugModule from "debug"
|
||||||
|
import type { Server } from "./index"
|
||||||
|
import {
|
||||||
|
EventParams,
|
||||||
|
EventNames,
|
||||||
|
EventsMap,
|
||||||
|
StrictEventEmitter,
|
||||||
|
DefaultEventsMap,
|
||||||
|
} from "./typed-events"
|
||||||
|
import type { Client } from "./client"
|
||||||
|
import type { Namespace, NamespaceReservedEventsMap } from "./namespace"
|
||||||
|
// import type { IncomingMessage, IncomingHttpHeaders } from "http"
|
||||||
|
import type {
|
||||||
|
Adapter,
|
||||||
|
BroadcastFlags,
|
||||||
|
Room,
|
||||||
|
SocketId,
|
||||||
|
} from "socket.io-adapter"
|
||||||
|
// import base64id from "base64id"
|
||||||
|
import type { ParsedUrlQuery } from "querystring"
|
||||||
|
import { BroadcastOperator } from "./broadcast-operator"
|
||||||
|
|
||||||
|
// const debug = debugModule("socket.io:socket");
|
||||||
|
|
||||||
|
type ClientReservedEvents = "connect_error"
|
||||||
|
|
||||||
|
export interface SocketReservedEventsMap {
|
||||||
|
disconnect: (reason: string) => void
|
||||||
|
disconnecting: (reason: string) => void
|
||||||
|
error: (err: Error) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventEmitter reserved events: https://nodejs.org/api/events.html#events_event_newlistener
|
||||||
|
export interface EventEmitterReservedEventsMap {
|
||||||
|
newListener: (
|
||||||
|
eventName: string | Symbol,
|
||||||
|
listener: (...args: any[]) => void
|
||||||
|
) => void
|
||||||
|
removeListener: (
|
||||||
|
eventName: string | Symbol,
|
||||||
|
listener: (...args: any[]) => void
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
|
||||||
|
| ClientReservedEvents
|
||||||
|
| keyof NamespaceReservedEventsMap<never, never, never>
|
||||||
|
| keyof SocketReservedEventsMap
|
||||||
|
| keyof EventEmitterReservedEventsMap
|
||||||
|
>(<const>[
|
||||||
|
"connect",
|
||||||
|
"connect_error",
|
||||||
|
"disconnect",
|
||||||
|
"disconnecting",
|
||||||
|
"newListener",
|
||||||
|
"removeListener",
|
||||||
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The handshake details
|
||||||
|
*/
|
||||||
|
export interface Handshake {
|
||||||
|
/**
|
||||||
|
* The headers sent as part of the handshake
|
||||||
|
*/
|
||||||
|
headers: any//IncomingHttpHeaders
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The date of creation (as string)
|
||||||
|
*/
|
||||||
|
time: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ip of the client
|
||||||
|
*/
|
||||||
|
address: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the connection is cross-domain
|
||||||
|
*/
|
||||||
|
xdomain: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the connection is secure
|
||||||
|
*/
|
||||||
|
secure: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The date of creation (as unix timestamp)
|
||||||
|
*/
|
||||||
|
issued: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The request URL string
|
||||||
|
*/
|
||||||
|
url: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The query object
|
||||||
|
*/
|
||||||
|
query: ParsedUrlQuery
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The auth object
|
||||||
|
*/
|
||||||
|
auth: { [key: string]: any }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Socket<
|
||||||
|
ListenEvents extends EventsMap = DefaultEventsMap,
|
||||||
|
EmitEvents extends EventsMap = ListenEvents,
|
||||||
|
ServerSideEvents extends EventsMap = DefaultEventsMap
|
||||||
|
> extends StrictEventEmitter<
|
||||||
|
ListenEvents,
|
||||||
|
EmitEvents,
|
||||||
|
SocketReservedEventsMap
|
||||||
|
> {
|
||||||
|
public readonly id: SocketId
|
||||||
|
public readonly handshake: Handshake
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional information that can be attached to the Socket instance and which will be used in the fetchSockets method
|
||||||
|
*/
|
||||||
|
public data: any = {};
|
||||||
|
|
||||||
|
public connected: boolean
|
||||||
|
public disconnected: boolean
|
||||||
|
|
||||||
|
private readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents>
|
||||||
|
private readonly adapter: Adapter
|
||||||
|
private acks: Map<number, () => void> = new Map();
|
||||||
|
private fns: Array<(event: Array<any>, next: (err?: Error) => void) => void> =
|
||||||
|
[];
|
||||||
|
private flags: BroadcastFlags = {};
|
||||||
|
private _anyListeners?: Array<(...args: any[]) => void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to a `Client` for a given `Namespace`.
|
||||||
|
*
|
||||||
|
* @param {Namespace} nsp
|
||||||
|
* @param {Client} client
|
||||||
|
* @param {Object} auth
|
||||||
|
* @package
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
readonly nsp: Namespace<ListenEvents, EmitEvents, ServerSideEvents>,
|
||||||
|
readonly client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
|
||||||
|
auth: object
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
this.nsp = nsp
|
||||||
|
this.server = nsp.server
|
||||||
|
this.adapter = this.nsp.adapter
|
||||||
|
// if (client.conn.protocol === 3) {
|
||||||
|
// // @ts-ignore
|
||||||
|
this.id = nsp.name !== "/" ? nsp.name + "#" + client.id : client.id
|
||||||
|
// } else {
|
||||||
|
// this.id = base64id.generateId() // don't reuse the Engine.IO id because it's sensitive information
|
||||||
|
// }
|
||||||
|
this.client = client
|
||||||
|
this.acks = new Map()
|
||||||
|
this.connected = true
|
||||||
|
this.disconnected = false
|
||||||
|
this.handshake = this.buildHandshake(auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildHandshake(auth): Handshake {
|
||||||
|
return {
|
||||||
|
headers: this.request.headers,
|
||||||
|
time: new Date() + "",
|
||||||
|
address: this.conn.remoteAddress,
|
||||||
|
xdomain: !!this.request.headers.origin,
|
||||||
|
// @ts-ignore
|
||||||
|
secure: !!this.request.connection.encrypted,
|
||||||
|
issued: +new Date(),
|
||||||
|
url: this.request.url!,
|
||||||
|
query: url.parse(this.request.url!, true).query,
|
||||||
|
auth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits to this client.
|
||||||
|
*
|
||||||
|
* @return Always returns `true`.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public emit<Ev extends EventNames<EmitEvents>>(
|
||||||
|
ev: Ev,
|
||||||
|
...args: EventParams<EmitEvents, Ev>
|
||||||
|
): boolean {
|
||||||
|
if (RESERVED_EVENTS.has(ev)) {
|
||||||
|
throw new Error(`"${ev}" is a reserved event name`)
|
||||||
|
}
|
||||||
|
const data: any[] = [ev, ...args]
|
||||||
|
const packet: any = {
|
||||||
|
type: PacketType.EVENT,
|
||||||
|
data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
// access last argument to see if it's an ACK callback
|
||||||
|
if (typeof data[data.length - 1] === "function") {
|
||||||
|
console.trace("emitting packet with ack id %d", this.nsp._ids)
|
||||||
|
this.acks.set(this.nsp._ids, data.pop())
|
||||||
|
packet.id = this.nsp._ids++
|
||||||
|
}
|
||||||
|
|
||||||
|
const flags = Object.assign({}, this.flags)
|
||||||
|
this.flags = {}
|
||||||
|
|
||||||
|
this.packet(packet, flags)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Targets a room when broadcasting.
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
|
||||||
|
return this.newBroadcastOperator().to(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Targets a room when broadcasting.
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
|
||||||
|
return this.newBroadcastOperator().in(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Excludes a room when broadcasting.
|
||||||
|
*
|
||||||
|
* @param room
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> {
|
||||||
|
return this.newBroadcastOperator().except(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a `message` event.
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public send(...args: EventParams<EmitEvents, "message">): this {
|
||||||
|
this.emit("message", ...args)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a `message` event.
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public write(...args: EventParams<EmitEvents, "message">): this {
|
||||||
|
this.emit("message", ...args)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a packet.
|
||||||
|
*
|
||||||
|
* @param {Object} packet - packet object
|
||||||
|
* @param {Object} opts - options
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private packet(
|
||||||
|
packet: Omit<Packet, "nsp"> & Partial<Pick<Packet, "nsp">>,
|
||||||
|
opts: any = {}
|
||||||
|
): void {
|
||||||
|
packet.nsp = this.nsp.name
|
||||||
|
opts.compress = false !== opts.compress
|
||||||
|
this.client._packet(packet as Packet, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joins a room.
|
||||||
|
*
|
||||||
|
* @param {String|Array} rooms - room or array of rooms
|
||||||
|
* @return a Promise or nothing, depending on the adapter
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public join(rooms: Room | Array<Room>): Promise<void> | void {
|
||||||
|
console.debug(`join room ${rooms}`)
|
||||||
|
|
||||||
|
return this.adapter.addAll(
|
||||||
|
this.id,
|
||||||
|
new Set(Array.isArray(rooms) ? rooms : [rooms])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leaves a room.
|
||||||
|
*
|
||||||
|
* @param {String} room
|
||||||
|
* @return a Promise or nothing, depending on the adapter
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public leave(room: string): Promise<void> | void {
|
||||||
|
console.debug(`leave room ${room}`)
|
||||||
|
|
||||||
|
return this.adapter.del(this.id, room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leave all rooms.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private leaveAll(): void {
|
||||||
|
this.adapter.delAll(this.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by `Namespace` upon successful
|
||||||
|
* middleware execution (ie: authorization).
|
||||||
|
* Socket is added to namespace array before
|
||||||
|
* call to join, so adapters can access it.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onconnect(): void {
|
||||||
|
console.debug(`socket ${this.id} connected - writing packet`)
|
||||||
|
this.join(this.id)
|
||||||
|
if (this.conn.protocol === 3) {
|
||||||
|
this.packet({ type: PacketType.CONNECT })
|
||||||
|
} else {
|
||||||
|
this.packet({ type: PacketType.CONNECT, data: { sid: this.id } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called with each packet. Called by `Client`.
|
||||||
|
*
|
||||||
|
* @param {Object} packet
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onpacket(packet: Packet): void {
|
||||||
|
console.trace("got packet", JSON.stringify(packet))
|
||||||
|
switch (packet.type) {
|
||||||
|
case PacketType.EVENT:
|
||||||
|
this.onevent(packet)
|
||||||
|
break
|
||||||
|
|
||||||
|
case PacketType.BINARY_EVENT:
|
||||||
|
this.onevent(packet)
|
||||||
|
break
|
||||||
|
|
||||||
|
case PacketType.ACK:
|
||||||
|
this.onack(packet)
|
||||||
|
break
|
||||||
|
|
||||||
|
case PacketType.BINARY_ACK:
|
||||||
|
this.onack(packet)
|
||||||
|
break
|
||||||
|
|
||||||
|
case PacketType.DISCONNECT:
|
||||||
|
this.ondisconnect()
|
||||||
|
break
|
||||||
|
|
||||||
|
case PacketType.CONNECT_ERROR:
|
||||||
|
this._onerror(new Error(packet.data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called upon event packet.
|
||||||
|
*
|
||||||
|
* @param {Packet} packet - packet object
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private onevent(packet: Packet): void {
|
||||||
|
const args = packet.data || []
|
||||||
|
console.trace("emitting event", JSON.stringify(args))
|
||||||
|
|
||||||
|
if (null != packet.id) {
|
||||||
|
console.trace("attaching ack callback to event")
|
||||||
|
args.push(this.ack(packet.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._anyListeners && this._anyListeners.length) {
|
||||||
|
const listeners = this._anyListeners.slice()
|
||||||
|
for (const listener of listeners) {
|
||||||
|
listener.apply(this, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.dispatch(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produces an ack callback to emit with an event.
|
||||||
|
*
|
||||||
|
* @param {Number} id - packet id
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private ack(id: number): () => void {
|
||||||
|
const self = this
|
||||||
|
let sent = false
|
||||||
|
return function () {
|
||||||
|
// prevent double callbacks
|
||||||
|
if (sent) return
|
||||||
|
const args = Array.prototype.slice.call(arguments)
|
||||||
|
console.trace("sending ack", JSON.stringify(args))
|
||||||
|
|
||||||
|
self.packet({
|
||||||
|
id: id,
|
||||||
|
type: PacketType.ACK,
|
||||||
|
data: args,
|
||||||
|
})
|
||||||
|
|
||||||
|
sent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called upon ack packet.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private onack(packet: Packet): void {
|
||||||
|
const ack = this.acks.get(packet.id!)
|
||||||
|
if ("function" == typeof ack) {
|
||||||
|
console.trace(`socket ${this.id} calling ack ${packet.id} with ${packet.data}`)
|
||||||
|
ack.apply(this, packet.data)
|
||||||
|
this.acks.delete(packet.id!)
|
||||||
|
} else {
|
||||||
|
console.debug(`socket ${this.id} bad ack`, packet.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called upon client disconnect packet.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private ondisconnect(): void {
|
||||||
|
console.debug(`socket ${this.id} got disconnect packet`)
|
||||||
|
this._onclose("client namespace disconnect")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a client error.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onerror(err: Error): void {
|
||||||
|
if (this.listeners("error").length) {
|
||||||
|
this.emitReserved("error", err)
|
||||||
|
} else {
|
||||||
|
console.error("Missing error handler on `socket`.")
|
||||||
|
console.error(err.stack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called upon closing. Called by `Client`.
|
||||||
|
*
|
||||||
|
* @param {String} reason
|
||||||
|
* @throw {Error} optional error object
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onclose(reason: string): this | undefined {
|
||||||
|
if (!this.connected) return this
|
||||||
|
console.debug(`closing socket ${this.id} - reason: ${reason}`)
|
||||||
|
this.emitReserved("disconnecting", reason)
|
||||||
|
this.leaveAll()
|
||||||
|
this.nsp._remove(this)
|
||||||
|
this.client._remove(this)
|
||||||
|
this.connected = false
|
||||||
|
this.disconnected = true
|
||||||
|
this.emitReserved("disconnect", reason)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produces an `error` packet.
|
||||||
|
*
|
||||||
|
* @param {Object} err - error object
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_error(err): void {
|
||||||
|
this.packet({ type: PacketType.CONNECT_ERROR, data: err })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnects this client.
|
||||||
|
*
|
||||||
|
* @param {Boolean} close - if `true`, closes the underlying connection
|
||||||
|
* @return {Socket} self
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public disconnect(close = false): this {
|
||||||
|
if (!this.connected) return this
|
||||||
|
if (close) {
|
||||||
|
this.client._disconnect()
|
||||||
|
} else {
|
||||||
|
this.packet({ type: PacketType.DISCONNECT })
|
||||||
|
this._onclose("server namespace disconnect")
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the compress flag.
|
||||||
|
*
|
||||||
|
* @param {Boolean} compress - if `true`, compresses the sending data
|
||||||
|
* @return {Socket} self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public compress(compress: boolean): this {
|
||||||
|
this.flags.compress = compress
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
|
||||||
|
* receive messages (because of network slowness or other issues, or because they’re connected through long polling
|
||||||
|
* and is in the middle of a request-response cycle).
|
||||||
|
*
|
||||||
|
* @return {Socket} self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public get volatile(): this {
|
||||||
|
this.flags.volatile = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to every sockets but the
|
||||||
|
* sender.
|
||||||
|
*
|
||||||
|
* @return {Socket} self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public get broadcast(): BroadcastOperator<EmitEvents> {
|
||||||
|
return this.newBroadcastOperator()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
|
||||||
|
*
|
||||||
|
* @return {Socket} self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public get local(): BroadcastOperator<EmitEvents> {
|
||||||
|
return this.newBroadcastOperator().local
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch incoming event to socket listeners.
|
||||||
|
*
|
||||||
|
* @param {Array} event - event that will get emitted
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private dispatch(event: [eventName: string, ...args: any[]]): void {
|
||||||
|
console.trace("dispatching an event", JSON.stringify(event))
|
||||||
|
this.run(event, (err) => {
|
||||||
|
process.nextTick(() => {
|
||||||
|
if (err) {
|
||||||
|
return this._onerror(err)
|
||||||
|
}
|
||||||
|
if (this.connected) {
|
||||||
|
super.emitUntyped.apply(this, event)
|
||||||
|
} else {
|
||||||
|
console.debug("ignore packet received after disconnection")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up socket middleware.
|
||||||
|
*
|
||||||
|
* @param {Function} fn - middleware function (event, next)
|
||||||
|
* @return {Socket} self
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public use(
|
||||||
|
fn: (event: Array<any>, next: (err?: Error) => void) => void
|
||||||
|
): this {
|
||||||
|
this.fns.push(fn)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the middleware for an incoming event.
|
||||||
|
*
|
||||||
|
* @param {Array} event - event that will get emitted
|
||||||
|
* @param {Function} fn - last fn call in the middleware
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private run(
|
||||||
|
event: [eventName: string, ...args: any[]],
|
||||||
|
fn: (err: Error | null) => void
|
||||||
|
): void {
|
||||||
|
const fns = this.fns.slice(0)
|
||||||
|
if (!fns.length) return fn(null)
|
||||||
|
|
||||||
|
function run(i: number) {
|
||||||
|
fns[i](event, function (err) {
|
||||||
|
// upon error, short-circuit
|
||||||
|
if (err) return fn(err)
|
||||||
|
|
||||||
|
// if no middleware left, summon callback
|
||||||
|
if (!fns[i + 1]) return fn(null)
|
||||||
|
|
||||||
|
// go on to next
|
||||||
|
run(i + 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
run(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reference to the request that originated the underlying Engine.IO Socket.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public get request(): any /** IncomingMessage */ {
|
||||||
|
return this.client.request
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reference to the underlying Client transport connection (Engine.IO Socket object).
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public get conn() {
|
||||||
|
return this.client.conn
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public get rooms(): Set<Room> {
|
||||||
|
return this.adapter.socketRooms(this.id) || new Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
|
||||||
|
* callback.
|
||||||
|
*
|
||||||
|
* @param listener
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public onAny(listener: (...args: any[]) => void): this {
|
||||||
|
this._anyListeners = this._anyListeners || []
|
||||||
|
this._anyListeners.push(listener)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
|
||||||
|
* callback. The listener is added to the beginning of the listeners array.
|
||||||
|
*
|
||||||
|
* @param listener
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public prependAny(listener: (...args: any[]) => void): this {
|
||||||
|
this._anyListeners = this._anyListeners || []
|
||||||
|
this._anyListeners.unshift(listener)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the listener that will be fired when any event is emitted.
|
||||||
|
*
|
||||||
|
* @param listener
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public offAny(listener?: (...args: any[]) => void): this {
|
||||||
|
if (!this._anyListeners) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
if (listener) {
|
||||||
|
const listeners = this._anyListeners
|
||||||
|
for (let i = 0; i < listeners.length; i++) {
|
||||||
|
if (listener === listeners[i]) {
|
||||||
|
listeners.splice(i, 1)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._anyListeners = []
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
|
||||||
|
* e.g. to remove listeners.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
public listenersAny() {
|
||||||
|
return this._anyListeners || []
|
||||||
|
}
|
||||||
|
|
||||||
|
private newBroadcastOperator(): BroadcastOperator<EmitEvents> {
|
||||||
|
const flags = Object.assign({}, this.flags)
|
||||||
|
this.flags = {}
|
||||||
|
return new BroadcastOperator(
|
||||||
|
this.adapter,
|
||||||
|
new Set<Room>(),
|
||||||
|
new Set<Room>([this.id]),
|
||||||
|
flags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
180
packages/websocket/src/socket.io/typed-events.ts
Normal file
180
packages/websocket/src/socket.io/typed-events.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import { EventEmitter } from "events"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An events map is an interface that maps event names to their value, which
|
||||||
|
* represents the type of the `on` listener.
|
||||||
|
*/
|
||||||
|
export interface EventsMap {
|
||||||
|
[event: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default events map, used if no EventsMap is given. Using this EventsMap
|
||||||
|
* is equivalent to accepting all event names, and any data.
|
||||||
|
*/
|
||||||
|
export interface DefaultEventsMap {
|
||||||
|
[event: string]: (...args: any[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a union type containing all the keys of an event map.
|
||||||
|
*/
|
||||||
|
export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol)
|
||||||
|
|
||||||
|
/** The tuple type representing the parameters of an event listener */
|
||||||
|
export type EventParams<
|
||||||
|
Map extends EventsMap,
|
||||||
|
Ev extends EventNames<Map>
|
||||||
|
> = Parameters<Map[Ev]>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event names that are either in ReservedEvents or in UserEvents
|
||||||
|
*/
|
||||||
|
export type ReservedOrUserEventNames<
|
||||||
|
ReservedEventsMap extends EventsMap,
|
||||||
|
UserEvents extends EventsMap
|
||||||
|
> = EventNames<ReservedEventsMap> | EventNames<UserEvents>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of a listener of a user event or a reserved event. If `Ev` is in
|
||||||
|
* `ReservedEvents`, the reserved event listener is returned.
|
||||||
|
*/
|
||||||
|
export type ReservedOrUserListener<
|
||||||
|
ReservedEvents extends EventsMap,
|
||||||
|
UserEvents extends EventsMap,
|
||||||
|
Ev extends ReservedOrUserEventNames<ReservedEvents, UserEvents>
|
||||||
|
> = FallbackToUntypedListener<
|
||||||
|
Ev extends EventNames<ReservedEvents>
|
||||||
|
? ReservedEvents[Ev]
|
||||||
|
: Ev extends EventNames<UserEvents>
|
||||||
|
? UserEvents[Ev]
|
||||||
|
: never
|
||||||
|
>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an untyped listener type if `T` is `never`; otherwise, returns `T`.
|
||||||
|
*
|
||||||
|
* This is a hack to mitigate https://github.com/socketio/socket.io/issues/3833.
|
||||||
|
* Needed because of https://github.com/microsoft/TypeScript/issues/41778
|
||||||
|
*/
|
||||||
|
type FallbackToUntypedListener<T> = [T] extends [never]
|
||||||
|
? (...args: any[]) => void
|
||||||
|
: T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for classes that aren't `EventEmitter`s, but still expose a
|
||||||
|
* strictly typed `emit` method.
|
||||||
|
*/
|
||||||
|
export interface TypedEventBroadcaster<EmitEvents extends EventsMap> {
|
||||||
|
emit<Ev extends EventNames<EmitEvents>>(
|
||||||
|
ev: Ev,
|
||||||
|
...args: EventParams<EmitEvents, Ev>
|
||||||
|
): boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strictly typed version of an `EventEmitter`. A `TypedEventEmitter` takes type
|
||||||
|
* parameters for mappings of event names to event data types, and strictly
|
||||||
|
* types method calls to the `EventEmitter` according to these event maps.
|
||||||
|
*
|
||||||
|
* @typeParam ListenEvents - `EventsMap` of user-defined events that can be
|
||||||
|
* listened to with `on` or `once`
|
||||||
|
* @typeParam EmitEvents - `EventsMap` of user-defined events that can be
|
||||||
|
* emitted with `emit`
|
||||||
|
* @typeParam ReservedEvents - `EventsMap` of reserved events, that can be
|
||||||
|
* emitted by socket.io with `emitReserved`, and can be listened to with
|
||||||
|
* `listen`.
|
||||||
|
*/
|
||||||
|
export abstract class StrictEventEmitter<
|
||||||
|
ListenEvents extends EventsMap,
|
||||||
|
EmitEvents extends EventsMap,
|
||||||
|
ReservedEvents extends EventsMap = {}
|
||||||
|
>
|
||||||
|
extends EventEmitter
|
||||||
|
implements TypedEventBroadcaster<EmitEvents>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Adds the `listener` function as an event listener for `ev`.
|
||||||
|
*
|
||||||
|
* @param ev Name of the event
|
||||||
|
* @param listener Callback function
|
||||||
|
*/
|
||||||
|
on<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
|
||||||
|
ev: Ev,
|
||||||
|
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
|
||||||
|
): this {
|
||||||
|
return super.on(ev, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a one-time `listener` function as an event listener for `ev`.
|
||||||
|
*
|
||||||
|
* @param ev Name of the event
|
||||||
|
* @param listener Callback function
|
||||||
|
*/
|
||||||
|
once<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
|
||||||
|
ev: Ev,
|
||||||
|
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
|
||||||
|
): this {
|
||||||
|
return super.once(ev, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an event.
|
||||||
|
*
|
||||||
|
* @param ev Name of the event
|
||||||
|
* @param args Values to send to listeners of this event
|
||||||
|
*/
|
||||||
|
emit<Ev extends EventNames<EmitEvents>>(
|
||||||
|
ev: Ev,
|
||||||
|
...args: EventParams<EmitEvents, Ev>
|
||||||
|
): boolean {
|
||||||
|
return super.emit(ev, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits a reserved event.
|
||||||
|
*
|
||||||
|
* This method is `protected`, so that only a class extending
|
||||||
|
* `StrictEventEmitter` can emit its own reserved events.
|
||||||
|
*
|
||||||
|
* @param ev Reserved event name
|
||||||
|
* @param args Arguments to emit along with the event
|
||||||
|
*/
|
||||||
|
protected emitReserved<Ev extends EventNames<ReservedEvents>>(
|
||||||
|
ev: Ev,
|
||||||
|
...args: EventParams<ReservedEvents, Ev>
|
||||||
|
): boolean {
|
||||||
|
return super.emit(ev, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an event.
|
||||||
|
*
|
||||||
|
* This method is `protected`, so that only a class extending
|
||||||
|
* `StrictEventEmitter` can get around the strict typing. This is useful for
|
||||||
|
* calling `emit.apply`, which can be called as `emitUntyped.apply`.
|
||||||
|
*
|
||||||
|
* @param ev Event name
|
||||||
|
* @param args Arguments to emit along with the event
|
||||||
|
*/
|
||||||
|
protected emitUntyped(ev: string, ...args: any[]): boolean {
|
||||||
|
return super.emit(ev, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the listeners listening to an event.
|
||||||
|
*
|
||||||
|
* @param event Event name
|
||||||
|
* @returns Array of listeners subscribed to `event`
|
||||||
|
*/
|
||||||
|
listeners<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
|
||||||
|
event: Ev
|
||||||
|
): ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>[] {
|
||||||
|
return super.listeners(event) as ReservedOrUserListener<
|
||||||
|
ReservedEvents,
|
||||||
|
ListenEvents,
|
||||||
|
Ev
|
||||||
|
>[]
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +1,24 @@
|
|||||||
import { Transport } from '../transport'
|
import { WebSocketClient } from '../server/client'
|
||||||
|
|
||||||
export class TomcatClient extends Transport {
|
export class TomcatClient extends WebSocketClient {
|
||||||
private session: javax.websocket.Session
|
private session: javax.websocket.Session
|
||||||
|
|
||||||
constructor(server: any, session: javax.websocket.Session) {
|
constructor(session: javax.websocket.Session) {
|
||||||
super(server)
|
super()
|
||||||
this.remoteAddress = session + ''
|
this.id = session.getId() + ''
|
||||||
this.request = {
|
|
||||||
uri: () => `${session.getRequestURI()}`,
|
|
||||||
headers: () => []
|
|
||||||
}
|
|
||||||
this._id = session.getId() + ''
|
|
||||||
this.session = session
|
this.session = session
|
||||||
}
|
}
|
||||||
doSend(text: string) {
|
send(text: string, opts?: any, callback?: (err?: Error) => void) {
|
||||||
Java.synchronized(() => this.session.getBasicRemote().sendText(text), this.session)()
|
Java.synchronized(() => {
|
||||||
|
try {
|
||||||
|
this.session.getBasicRemote().sendText(text)
|
||||||
|
callback?.()
|
||||||
|
} catch (error) {
|
||||||
|
callback?.(error)
|
||||||
|
}
|
||||||
|
}, this.session)()
|
||||||
}
|
}
|
||||||
doClose() {
|
close() {
|
||||||
this.session.close()
|
this.session.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,60 +1,61 @@
|
|||||||
import { EventEmitter } from 'events'
|
import { JavaServerOptions, WebSocketServer } from '../server'
|
||||||
|
import { Request } from '../server/request'
|
||||||
|
|
||||||
import { ServerOptions } from '../socket-io'
|
|
||||||
import { ServerEvent } from '../socket-io/constants'
|
|
||||||
import { ProxyBeanName } from './constants'
|
|
||||||
import { TomcatClient } from './client'
|
import { TomcatClient } from './client'
|
||||||
|
import { ProxyBeanName } from './constants'
|
||||||
|
|
||||||
const ThreadPoolExecutor = Java.type('java.util.concurrent.ThreadPoolExecutor')
|
const ThreadPoolExecutor = Java.type('java.util.concurrent.ThreadPoolExecutor')
|
||||||
|
|
||||||
type TomcatWebSocketSession = javax.websocket.Session
|
type TomcatWebSocketSession = javax.websocket.Session
|
||||||
|
|
||||||
class TomcatWebSocketServer extends EventEmitter {
|
class TomcatWebSocketServer extends WebSocketServer {
|
||||||
private beanFactory: any
|
|
||||||
private executor: any
|
private executor: any
|
||||||
private clients: Map<string, any>
|
|
||||||
|
|
||||||
constructor(beanFactory: any, options: ServerOptions) {
|
constructor(beanFactory: any, options: JavaServerOptions) {
|
||||||
super()
|
super(beanFactory, options)
|
||||||
this.clients = new Map()
|
}
|
||||||
this.beanFactory = beanFactory
|
|
||||||
|
protected initialize(): void {
|
||||||
this.initThreadPool()
|
this.initThreadPool()
|
||||||
try { this.beanFactory.destroySingleton(ProxyBeanName) } catch (error) { }
|
try { this.instance.destroySingleton(ProxyBeanName) } catch (error) { }
|
||||||
let NashornWebSocketServerProxy = Java.extend(Java.type("pw.yumc.MiaoScript.websocket.WebSocketProxy"), {
|
let NashornWebSocketServerProxy = Java.extend(Java.type("pw.yumc.MiaoScript.websocket.WebSocketProxy"), {
|
||||||
onOpen: (session: TomcatWebSocketSession) => {
|
onOpen: (session: TomcatWebSocketSession) => {
|
||||||
let cid = `${session?.getId()}`
|
this.onconnect(session)
|
||||||
let tomcatClient = new TomcatClient(this, session)
|
|
||||||
this.clients.set(cid, tomcatClient)
|
|
||||||
this.emit(ServerEvent.connect, tomcatClient)
|
|
||||||
},
|
},
|
||||||
onMessage: (session: TomcatWebSocketSession, message: string) => {
|
onMessage: (session: TomcatWebSocketSession, message: string) => {
|
||||||
let cid = `${session?.getId()}`
|
this.onmessage(session, message)
|
||||||
if (this.clients.has(cid)) {
|
|
||||||
this.executor.execute(() => this.emit(ServerEvent.message, this.clients.get(cid), message))
|
|
||||||
} else {
|
|
||||||
console.error(`unknow client ${session} reciver message ${message}`)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onClose: (session: TomcatWebSocketSession, reason: any) => {
|
onClose: (session: TomcatWebSocketSession, reason: any) => {
|
||||||
let cid = `${session?.getId()}`
|
this.ondisconnect(session, reason)
|
||||||
if (this.clients.has(cid)) {
|
|
||||||
this.emit(ServerEvent.disconnect, this.clients.get(cid), reason)
|
|
||||||
} else {
|
|
||||||
console.error(`unknow client ${session} disconnect cause ${reason}`)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onError: (session: TomcatWebSocketSession, error: Error) => {
|
onError: (session: TomcatWebSocketSession, error: Error) => {
|
||||||
let cid = `${session?.getId()}`
|
this.onerror(session, error)
|
||||||
if (this.clients.has(cid)) {
|
|
||||||
this.emit(ServerEvent.error, this.clients.get(cid), error)
|
|
||||||
} else {
|
|
||||||
console.error(`unknow client ${session} cause error ${error}`)
|
|
||||||
console.ex(error)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.beanFactory.registerSingleton(ProxyBeanName, new NashornWebSocketServerProxy())
|
this.instance.registerSingleton(ProxyBeanName, new NashornWebSocketServerProxy())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getId(session) {
|
||||||
|
return session?.getId() + ''
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getRequest(session) {
|
||||||
|
let request = new Request(session.getRequestURI(), "GET")
|
||||||
|
request.connection = {
|
||||||
|
remoteAddress: ''
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getSocket(session) {
|
||||||
|
return new TomcatClient(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected doClose() {
|
||||||
|
this.instance.destroySingleton(ProxyBeanName)
|
||||||
|
this.executor.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
private initThreadPool() {
|
private initThreadPool() {
|
||||||
const ThreadPoolTaskExecutor = Java.type('org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor')
|
const ThreadPoolTaskExecutor = Java.type('org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor')
|
||||||
this.executor = new ThreadPoolTaskExecutor()
|
this.executor = new ThreadPoolTaskExecutor()
|
||||||
@ -66,11 +67,6 @@ class TomcatWebSocketServer extends EventEmitter {
|
|||||||
this.executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy())
|
this.executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy())
|
||||||
this.executor.initialize()
|
this.executor.initialize()
|
||||||
}
|
}
|
||||||
close() {
|
|
||||||
this.clients.forEach(client => client.close())
|
|
||||||
this.beanFactory.destroySingleton(ProxyBeanName)
|
|
||||||
this.executor.shutdown()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
import { EventEmitter } from 'events'
|
|
||||||
|
|
||||||
export abstract class Transport extends EventEmitter {
|
|
||||||
protected _id: string
|
|
||||||
|
|
||||||
server: any
|
|
||||||
readyState: 'opening' | 'open' | 'closing' | 'closed'
|
|
||||||
remoteAddress: string
|
|
||||||
upgraded: boolean
|
|
||||||
request: any
|
|
||||||
|
|
||||||
constructor(server: any) {
|
|
||||||
super()
|
|
||||||
this.server = server
|
|
||||||
this.readyState = 'open'
|
|
||||||
this.upgraded = true
|
|
||||||
}
|
|
||||||
get id() {
|
|
||||||
return this._id
|
|
||||||
}
|
|
||||||
send(text: string) {
|
|
||||||
if (this.readyState == 'open') {
|
|
||||||
this.doSend(text)
|
|
||||||
} else {
|
|
||||||
console.debug(`send message ${text} to close client ${this._id}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close() {
|
|
||||||
if ("closed" === this.readyState || "closing" === this.readyState) { return }
|
|
||||||
this.doClose()
|
|
||||||
this.readyState = 'closed'
|
|
||||||
}
|
|
||||||
abstract doSend(text: string)
|
|
||||||
abstract doClose()
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user