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 {
|
||||
public static CONNECTING = 0
|
||||
@ -31,6 +31,7 @@ export class WebSocket extends EventEmitter {
|
||||
public static CLOSING = 2
|
||||
public static CLOSED = 3
|
||||
public binaryType: 'blob' | 'arraybuffer'
|
||||
protected manager: WebSocketManager
|
||||
|
||||
protected _url: string
|
||||
protected _headers: WebSocketHeader = {}
|
||||
@ -39,6 +40,7 @@ export class WebSocket extends EventEmitter {
|
||||
|
||||
constructor(url: string, subProtocol: string = '', headers: WebSocketHeader = {}) {
|
||||
super()
|
||||
this.manager = manager
|
||||
this._url = url
|
||||
this._headers = headers
|
||||
try {
|
||||
@ -51,12 +53,12 @@ export class WebSocket extends EventEmitter {
|
||||
}
|
||||
this.client.on('open', (event) => {
|
||||
this.onopen?.(event)
|
||||
managers.add(this)
|
||||
manager.add(this)
|
||||
})
|
||||
this.client.on('message', (event) => this.onmessage?.(event))
|
||||
this.client.on('close', (event) => {
|
||||
this.onclose?.(event)
|
||||
managers.del(this)
|
||||
manager.del(this)
|
||||
})
|
||||
this.client.on('error', (event) => this.onerror?.(event))
|
||||
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="@javatypes/tomcat-websocket-api" />
|
||||
|
||||
import { Server, ServerOptions } from './socket-io'
|
||||
import { Server, ServerOptions } from './socket.io'
|
||||
|
||||
interface SocketIOStatic {
|
||||
/**
|
||||
@ -44,7 +44,7 @@ let io: SocketStatic = function (pipeline: any, options: Partial<ServerOptions>)
|
||||
}
|
||||
io.Instance = Symbol("@ccms/websocket")
|
||||
export default io
|
||||
export * from './socket-io'
|
||||
export * from './socket.io'
|
||||
export * from './client'
|
||||
export * from './server'
|
||||
export * from './transport'
|
||||
export * from './engine.io/transport'
|
||||
|
@ -1,24 +1,26 @@
|
||||
import { Transport } from '../transport'
|
||||
import { AttributeKeys } from './constants'
|
||||
import { WebSocketClient } from '../server/client'
|
||||
|
||||
const TextWebSocketFrame = Java.type('io.netty.handler.codec.http.websocketx.TextWebSocketFrame')
|
||||
|
||||
export class NettyClient extends Transport {
|
||||
export class NettyClient extends WebSocketClient {
|
||||
private channel: any
|
||||
|
||||
constructor(server: any, channel: any) {
|
||||
super(server)
|
||||
this.remoteAddress = channel.remoteAddress() + ''
|
||||
this.request = channel.attr(AttributeKeys.Request).get()
|
||||
|
||||
this._id = channel.id() + ''
|
||||
constructor(channel: any) {
|
||||
super()
|
||||
this.id = channel.id() + ''
|
||||
this.channel = channel
|
||||
}
|
||||
|
||||
doSend(text: string) {
|
||||
this.channel.writeAndFlush(new TextWebSocketFrame(text))
|
||||
send(text: string, opts?: any, callback?: (err?: Error) => void) {
|
||||
try {
|
||||
this.channel.writeAndFlush(new TextWebSocketFrame(text))
|
||||
callback?.()
|
||||
} catch (error) {
|
||||
callback?.(error)
|
||||
}
|
||||
}
|
||||
doClose() {
|
||||
|
||||
close() {
|
||||
this.channel.close()
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { JavaServerOptions } from '../server'
|
||||
|
||||
import { HttpRequestHandlerAdapter } from './adapter'
|
||||
import { AttributeKeys } from './constants'
|
||||
import { ServerOptions } from 'socket-io'
|
||||
|
||||
const DefaultHttpResponse = Java.type('io.netty.handler.codec.http.DefaultHttpResponse')
|
||||
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 {
|
||||
private ws: string
|
||||
private root: string
|
||||
constructor(options: ServerOptions) {
|
||||
constructor(options: JavaServerOptions) {
|
||||
super()
|
||||
this.root = options.root
|
||||
this.ws = options.path
|
||||
|
@ -1,65 +1,70 @@
|
||||
import { EventEmitter } from 'events'
|
||||
|
||||
import { ServerOptions } from '../socket-io'
|
||||
import { ServerEvent } from '../socket-io/constants'
|
||||
import { JavaServerOptions, ServerEvent, WebSocketServer } from '../server'
|
||||
import { Request } from '../server/request'
|
||||
|
||||
import { NettyClient } from './client'
|
||||
import { Keys } from './constants'
|
||||
import { AttributeKeys, Keys } from './constants'
|
||||
import { WebSocketDetect } from './websocket_detect'
|
||||
import { WebSocketHandler } from './websocket_handler'
|
||||
|
||||
class NettyWebSocketServer extends EventEmitter {
|
||||
private pipeline: any
|
||||
private clients: Map<string, NettyClient>
|
||||
class NettyWebSocketServer extends WebSocketServer {
|
||||
constructor(pipeline: any, options: JavaServerOptions) {
|
||||
super(pipeline, options)
|
||||
}
|
||||
|
||||
constructor(pipeline: any, options: ServerOptions) {
|
||||
super()
|
||||
this.clients = new Map()
|
||||
this.pipeline = pipeline
|
||||
let connectEvent = options.event
|
||||
try { this.pipeline.remove(Keys.Detect) } catch (error) { }
|
||||
this.pipeline.addFirst(Keys.Detect, new WebSocketDetect(connectEvent).getHandler())
|
||||
protected initialize() {
|
||||
let connectEvent = this.options.event
|
||||
try { this.instance.remove(Keys.Detect) } catch (error) { }
|
||||
this.instance.addFirst(Keys.Detect, new WebSocketDetect(connectEvent).getHandler())
|
||||
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)
|
||||
})
|
||||
connectEvent.on(ServerEvent.connect, (ctx) => {
|
||||
let cid = ctx?.channel().id() + ''
|
||||
let nettyClient = new NettyClient(this, ctx.channel())
|
||||
this.clients.set(cid, nettyClient)
|
||||
this.emit(ServerEvent.connect, nettyClient)
|
||||
this.onconnect(ctx)
|
||||
})
|
||||
connectEvent.on(ServerEvent.message, (ctx, msg) => {
|
||||
let cid = ctx?.channel().id() + ''
|
||||
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()}`)
|
||||
}
|
||||
this.onmessage(ctx, msg.text())
|
||||
})
|
||||
connectEvent.on(ServerEvent.disconnect, (ctx, cause) => {
|
||||
let cid = ctx?.channel().id() + ''
|
||||
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}`)
|
||||
}
|
||||
this.ondisconnect(ctx, cause)
|
||||
})
|
||||
connectEvent.on(ServerEvent.error, (ctx, cause) => {
|
||||
let cid = ctx?.channel().id() + ''
|
||||
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)
|
||||
}
|
||||
connectEvent.on(ServerEvent.error, (ctx, error) => {
|
||||
this.onerror(ctx, error)
|
||||
})
|
||||
}
|
||||
close() {
|
||||
if (this.pipeline.names().contains(Keys.Detect)) {
|
||||
this.pipeline.remove(Keys.Detect)
|
||||
|
||||
protected getId(ctx: any) {
|
||||
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 { ServerOptions } from '../socket-io'
|
||||
import { ServerEvent } from '../socket-io/constants'
|
||||
import { JavaServerOptions, ServerEvent } from '../server'
|
||||
|
||||
import { TextWebSocketFrameHandlerAdapter } from './adapter'
|
||||
|
||||
export class TextWebSocketFrameHandler extends TextWebSocketFrameHandlerAdapter {
|
||||
private event: EventEmitter
|
||||
constructor(options: ServerOptions) {
|
||||
constructor(options: JavaServerOptions) {
|
||||
super()
|
||||
this.event = options.event
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { EventEmitter } from 'events'
|
||||
import { WebSocketHandlerAdapter } from "./adapter"
|
||||
import { ServerEvent } from '../socket-io/constants'
|
||||
|
||||
import { ServerEvent } from '../server'
|
||||
|
||||
export class WebSocketDetect extends WebSocketHandlerAdapter {
|
||||
private event: EventEmitter
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { ServerOptions } from '../socket-io'
|
||||
import { ServerEvent } from '../socket-io/constants'
|
||||
import { JavaServerOptions, ServerEvent } from '../server'
|
||||
|
||||
import { Keys } from './constants'
|
||||
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')
|
||||
|
||||
export class WebSocketHandler extends WebSocketHandlerAdapter {
|
||||
private options: ServerOptions
|
||||
constructor(options: ServerOptions) {
|
||||
private options: JavaServerOptions
|
||||
constructor(options: JavaServerOptions) {
|
||||
super()
|
||||
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 { 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
|
||||
root?: string
|
||||
/**
|
||||
* name of the path to capture
|
||||
* @default "/socket.io"
|
||||
*/
|
||||
path: string
|
||||
}
|
||||
|
||||
interface WebSocketServerImpl extends EventEmitter {
|
||||
close(): void
|
||||
}
|
||||
|
||||
export class WebSocketServer extends EventEmitter {
|
||||
options: Partial<ServerOptions>
|
||||
private websocketServer: WebSocketServerImpl
|
||||
|
||||
constructor(instance: any, options: Partial<ServerOptions>) {
|
||||
export abstract class WebSocketServer extends EventEmitter {
|
||||
protected instance: any
|
||||
protected options: JavaServerOptions
|
||||
private clients: Map<string, WebSocketClient>
|
||||
constructor(instance: any, options: JavaServerOptions) {
|
||||
super()
|
||||
if (!instance) { throw new Error('instance can\'t be undefiend!') }
|
||||
this.options = Object.assign({
|
||||
event: new EventEmitter(),
|
||||
path: '/ws',
|
||||
root: root + '/wwwroot',
|
||||
}, options)
|
||||
this.selectServerImpl(instance)
|
||||
this.instance = instance
|
||||
this.options = options
|
||||
this.clients = new Map()
|
||||
console.debug('create websocket server from ' + this.constructor.name)
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
on(event: "connect", cb: (transport: Transport) => void): this
|
||||
on(event: "message", cb: (transport: Transport, text: string) => void): this
|
||||
on(event: "disconnect", cb: (transport: Transport, reason: string) => void): this
|
||||
on(event: "error", cb: (transport: Transport, cause: Error) => void): this
|
||||
on(event: string, cb: (transport: Transport, extra?: any) => void): this {
|
||||
this.websocketServer.on(event, cb)
|
||||
return this
|
||||
protected onconnect(handler: any) {
|
||||
let id = this.getId(handler)
|
||||
console.log('client', id, 'connect')
|
||||
let request = this.getRequest(handler)
|
||||
request.id = id
|
||||
let websocket = this.getSocket(handler)
|
||||
this.clients.set(this.getId(handler), websocket)
|
||||
this.emit(ServerEvent.connect, request, websocket)
|
||||
}
|
||||
|
||||
private selectServerImpl(instance: any) {
|
||||
let WebSocketServerImpl = undefined
|
||||
if (instance.class.name.startsWith('io.netty.channel')) {
|
||||
WebSocketServerImpl = require("../netty").NettyWebSocketServer
|
||||
} else {
|
||||
WebSocketServerImpl = require("../tomcat").TomcatWebSocketServer
|
||||
protected onmessage(handler: any, message: string) {
|
||||
this.execute(handler, (websocket) => websocket.emit(ServerEvent.message, message))
|
||||
}
|
||||
protected ondisconnect(handler: any, cause: string) {
|
||||
this.execute(handler, (websocket) => websocket.emit(ServerEvent.disconnect, cause))
|
||||
}
|
||||
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
|
||||
|
||||
constructor(server: any, session: javax.websocket.Session) {
|
||||
super(server)
|
||||
this.remoteAddress = session + ''
|
||||
this.request = {
|
||||
uri: () => `${session.getRequestURI()}`,
|
||||
headers: () => []
|
||||
}
|
||||
this._id = session.getId() + ''
|
||||
constructor(session: javax.websocket.Session) {
|
||||
super()
|
||||
this.id = session.getId() + ''
|
||||
this.session = session
|
||||
}
|
||||
doSend(text: string) {
|
||||
Java.synchronized(() => this.session.getBasicRemote().sendText(text), this.session)()
|
||||
send(text: string, opts?: any, callback?: (err?: Error) => void) {
|
||||
Java.synchronized(() => {
|
||||
try {
|
||||
this.session.getBasicRemote().sendText(text)
|
||||
callback?.()
|
||||
} catch (error) {
|
||||
callback?.(error)
|
||||
}
|
||||
}, this.session)()
|
||||
}
|
||||
doClose() {
|
||||
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 { ProxyBeanName } from './constants'
|
||||
|
||||
const ThreadPoolExecutor = Java.type('java.util.concurrent.ThreadPoolExecutor')
|
||||
|
||||
type TomcatWebSocketSession = javax.websocket.Session
|
||||
|
||||
class TomcatWebSocketServer extends EventEmitter {
|
||||
private beanFactory: any
|
||||
class TomcatWebSocketServer extends WebSocketServer {
|
||||
private executor: any
|
||||
private clients: Map<string, any>
|
||||
|
||||
constructor(beanFactory: any, options: ServerOptions) {
|
||||
super()
|
||||
this.clients = new Map()
|
||||
this.beanFactory = beanFactory
|
||||
constructor(beanFactory: any, options: JavaServerOptions) {
|
||||
super(beanFactory, options)
|
||||
}
|
||||
|
||||
protected initialize(): void {
|
||||
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"), {
|
||||
onOpen: (session: TomcatWebSocketSession) => {
|
||||
let cid = `${session?.getId()}`
|
||||
let tomcatClient = new TomcatClient(this, session)
|
||||
this.clients.set(cid, tomcatClient)
|
||||
this.emit(ServerEvent.connect, tomcatClient)
|
||||
this.onconnect(session)
|
||||
},
|
||||
onMessage: (session: TomcatWebSocketSession, message: string) => {
|
||||
let cid = `${session?.getId()}`
|
||||
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}`)
|
||||
}
|
||||
this.onmessage(session, message)
|
||||
},
|
||||
onClose: (session: TomcatWebSocketSession, reason: any) => {
|
||||
let cid = `${session?.getId()}`
|
||||
if (this.clients.has(cid)) {
|
||||
this.emit(ServerEvent.disconnect, this.clients.get(cid), reason)
|
||||
} else {
|
||||
console.error(`unknow client ${session} disconnect cause ${reason}`)
|
||||
}
|
||||
this.ondisconnect(session, reason)
|
||||
},
|
||||
onError: (session: TomcatWebSocketSession, error: Error) => {
|
||||
let cid = `${session?.getId()}`
|
||||
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.onerror(session, 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() {
|
||||
const ThreadPoolTaskExecutor = Java.type('org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor')
|
||||
this.executor = new ThreadPoolTaskExecutor()
|
||||
@ -66,11 +67,6 @@ class TomcatWebSocketServer extends EventEmitter {
|
||||
this.executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy())
|
||||
this.executor.initialize()
|
||||
}
|
||||
close() {
|
||||
this.clients.forEach(client => client.close())
|
||||
this.beanFactory.destroySingleton(ProxyBeanName)
|
||||
this.executor.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
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