/
githubmirror
/
socket.io
Обзор
Документация
Войти
/
githubmirror
/
socket.io
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/socket.io-cluster-engine/lib/engine.ts
693 строки
17 KB
Damien Arrachequesne
chore: upgrade to TypeScript 6
17 июн 2026, 14:24
Не верифицирован
17 июн 2026, 14:24
8bead0f
Код
Авторство
О чём код?
import { Server, type ServerOptions, type ErrorCallback, Socket, type Transport, } from "engine.io"; import { randomBytes } from "node:crypto"; import { setTimeout, clearTimeout } from "node:timers"; import { type IncomingMessage } from "node:http"; import { type Packet } from "engine.io-parser"; import debugModule from "debug"; const debug = debugModule("engine"); const kDelayed = Symbol("delayed"); const kDelayedTimer = Symbol("delayedTimer"); const kBuffer = Symbol("buffer"); const kPacketListener = Symbol("packetListener"); const kNoopTimer = Symbol("noopTimer"); const kSenderId = Symbol("senderId"); type Brand<K, T> = K & { __brand: T }; type NodeId = Brand<string, "NodeId">; type SessionId = Brand<string, "SessionId">; type RequestId = Brand<number, "RequestId">; function randomId() { return randomBytes(3).toString("hex"); } enum MessageType { ACQUIRE_LOCK = 0, ACQUIRE_LOCK_RESPONSE, DRAIN, PACKET, UPGRADE, UPGRADE_RESPONSE, CLOSE, } export type Message = { senderId: NodeId; } & ( | { requestId: RequestId; type: MessageType.ACQUIRE_LOCK; data: { sid: SessionId; transportName: string; type: "read" | "write"; }; } | { recipientId: NodeId; requestId: RequestId; type: MessageType.ACQUIRE_LOCK_RESPONSE; data: { success: boolean; }; } | { recipientId: NodeId; type: MessageType.DRAIN; data: { sid: SessionId; packets: Packet[]; }; } | { recipientId: NodeId; type: MessageType.PACKET; data: { sid: SessionId; packet: Packet; }; } | { requestId: RequestId; recipientId: NodeId; type: MessageType.UPGRADE; data: { sid: SessionId; success: boolean; }; } | { requestId: RequestId; recipientId: NodeId; type: MessageType.UPGRADE_RESPONSE; data: { takeOver: boolean; packets: Packet[]; }; } | { recipientId: NodeId; type: MessageType.CLOSE; data: { sid: SessionId; reason: string; }; } ); type ClusterRequest = { timer: NodeJS.Timeout; onSuccess: (...args: any[]) => void; onError: () => void; }; function isClientLockable( client: Socket, transportName: string, lockType: "read" | "write", ) { switch (transportName) { case "polling": return ( client.transport.name === "polling" && (lockType === "write" || !client.transport.writable) ); case "websocket": case "webtransport": return ( client.transport.name === "polling" && !client.upgrading && !client.upgraded ); } } function isValidSessionId(str: string) { return typeof str === "string" && str.length === 20; } interface ClusterEngineOptions { /** * The maximum waiting time for responses from other nodes, in ms. * * @default 1000 */ responseTimeout?: number; /** * The delay between two "noop" packets when the client upgrades, in ms. * * @default 200 */ noopUpgradeInterval?: number; /** * The maximum waiting time for a successful upgrade, in ms. * * @default 300 */ delayedConnectionTimeout?: number; } // @ts-expect-error onWebSocket() method is private in parent class export abstract class ClusterEngine extends Server { private readonly _opts: Required<ClusterEngineOptions>; protected readonly _nodeId = randomId() as NodeId; private readonly _requests = new Map<RequestId, ClusterRequest>(); private readonly _remoteTransports = new Map<SessionId, Transport>(); private _requestCount = 0; constructor(opts?: ServerOptions & ClusterEngineOptions) { super(opts); this._opts = Object.assign( { responseTimeout: 1000, noopUpgradeInterval: 200, delayedConnectionTimeout: 300, }, opts, ); } protected onMessage(message: Message) { if (message.senderId === this._nodeId) { return; } debug("received: %j", message); switch (message.type) { case MessageType.ACQUIRE_LOCK: { const sid = message.data.sid; const client = this.clients[sid]; if (!client) { return; } const transportName = message.data.transportName; const success = isClientLockable( client, transportName, message.data.type, ); this.publishMessage({ requestId: message.requestId, senderId: this._nodeId, recipientId: message.senderId, type: MessageType.ACQUIRE_LOCK_RESPONSE, data: { success, }, }); switch (transportName) { case "polling": { if (message.data.type === "read") { this._forwardFlushWhenPolling(client, sid, message.senderId); } break; } case "websocket": case "webtransport": { client.upgrading = true; client[kNoopTimer] = setTimeout(() => { debug("writing a noop packet to polling for fast upgrade"); // @ts-expect-error sendPacket() is private client.sendPacket("noop"); }, this._opts.noopUpgradeInterval); } } break; } case MessageType.ACQUIRE_LOCK_RESPONSE: { const requestId = message.requestId; const request = this._requests.get(requestId); if (!request) { return; } this._requests.delete(requestId); clearTimeout(request.timer); if (message.data.success) { request.onSuccess(message.senderId); } else { request.onError(); } break; } case MessageType.DRAIN: { const transport = this._remoteTransports.get(message.data.sid); if (!transport) { return; } if (transport.name === "polling") { // HTTP long-polling can only be drained once this._remoteTransports.delete(message.data.sid); } transport.send(message.data.packets); break; } case MessageType.PACKET: { const client = this.clients[message.data.sid]; if (!client) { return; } if (client[kDelayed]) { client[kBuffer].push(message.data.packet); } else { // @ts-expect-error onPacket() is private client.onPacket(message.data.packet); } break; } case MessageType.UPGRADE: { const sid = message.data.sid; const client = this.clients[sid]; if (!client) { return; } clearInterval(client[kNoopTimer]); client.upgrading = false; if (message.data.success) { client.upgraded = true; client.emit("upgrade"); if (client[kDelayed]) { client[kDelayed] = false; clearTimeout(client[kDelayedTimer]); client.close(true); delete this.clients[sid]; this.publishMessage({ requestId: message.requestId, senderId: this._nodeId, recipientId: message.senderId, type: MessageType.UPGRADE_RESPONSE, data: { takeOver: true, packets: client[kBuffer], }, }); } else { this._forwardFlushWhenWebSocket(client, sid, message.senderId); this.publishMessage({ requestId: message.requestId, senderId: this._nodeId, recipientId: message.senderId, type: MessageType.UPGRADE_RESPONSE, data: { takeOver: false, packets: [], }, }); } } break; } case MessageType.UPGRADE_RESPONSE: { const requestId = message.requestId; const request = this._requests.get(requestId); if (!request) { return; } this._requests.delete(requestId); clearTimeout(request.timer); request.onSuccess(message.data.takeOver, message.data.packets); break; } case MessageType.CLOSE: { const client = this.clients[message.data.sid]; if (!client) { return; } this._doConnect(client); // @ts-expect-error onClose() is private client.onClose(message.data.reason); break; } } } private _forwardFlushWhenPolling( client: Socket, sid: SessionId, senderId: NodeId, ) { // @ts-expect-error req is private client.transport.req = true; client.transport.writable = true; const oldSend = client.transport.send; client.transport.send = (packets) => { this.publishMessage({ senderId: this._nodeId, recipientId: senderId, type: MessageType.DRAIN, data: { sid, packets, }, }); // @ts-expect-error req is private client.transport.req = null; client.transport.writable = false; client.transport.send = oldSend; }; // @ts-expect-error flush() is private client.flush(); } private _forwardFlushWhenWebSocket( client: Socket, sid: SessionId, senderId: NodeId, ) { client.transport.writable = true; client.transport.send = (packets) => { this.publishMessage({ senderId: this._nodeId, recipientId: senderId, type: MessageType.DRAIN, data: { sid, packets, }, }); }; // @ts-expect-error flush() is private client.flush(); } override verify( req: IncomingMessage & { _query: Record<string, string> }, upgrade: boolean, fn: ErrorCallback, ): void { super.verify(req, upgrade, (errorCode, errorContext) => { if (errorCode !== Server.errors.UNKNOWN_SID) { return fn(errorCode, errorContext); } const sid = req._query.sid as SessionId; if (!isValidSessionId(sid)) { return fn(errorCode, errorContext); } const transportName = req._query.transport; const lockType = req.method === "GET" ? "read" : "write"; const onSuccess = async (senderId: NodeId) => { if (upgrade) { req[kSenderId] = senderId; fn(); } else { const transport = this.createTransport(transportName as any, req); this._hookTransport(sid, transport, lockType, senderId); transport.onRequest(req); } }; this._acquireLock(sid, transportName, lockType, onSuccess, () => fn(errorCode, errorContext), ); }); } private _acquireLock( sid: SessionId, transportName: string, lockType: "read" | "write", onSuccess: (senderId: NodeId) => void, onError: () => void, ) { const requestId = ++this._requestCount as RequestId; const timer = setTimeout(() => { this._requests.delete(requestId); onError(); }, this._opts.responseTimeout); this._requests.set(requestId, { timer, onSuccess, onError, }); this.publishMessage({ requestId, senderId: this._nodeId, type: MessageType.ACQUIRE_LOCK, data: { sid, transportName, type: lockType, }, }); } private _hookTransport( sid: SessionId, transport: Transport, lockType: "read" | "write", senderId: NodeId, ) { if (lockType === "read") { this._remoteTransports.set(sid, transport); } transport.on("packet", async (packet: Packet) => this._onPacket(sid, senderId, packet), ); transport.once("error", () => this._onClose(sid, senderId, "transport error"), ); transport.once("close", () => this._onClose(sid, senderId, "transport close"), ); } private _tryUpgrade( transport: Transport, onSuccess: () => void, onError: () => void, ) { debug("starting upgrade process"); const upgradeTimeoutTimer = setTimeout(() => { debug("client did not complete upgrade - closing transport"); transport.close(); transport.removeAllListeners(); onError(); }, this.opts.upgradeTimeout); transport.on("packet", (packet) => { if (packet.type === "ping" && packet.data === "probe") { debug("got probe ping packet, sending pong"); transport.send([{ type: "pong", data: "probe" }]); } else if (packet.type === "upgrade") { clearTimeout(upgradeTimeoutTimer); transport.removeAllListeners(); onSuccess(); } else { transport.removeAllListeners(); transport.close(); onError(); } }); transport.on("error", () => { transport.removeAllListeners(); onError(); }); transport.on("close", () => { transport.removeAllListeners(); onError(); }); } private _onPacket(sid: SessionId, senderId: NodeId, packet: Packet) { this.publishMessage({ senderId: this._nodeId, recipientId: senderId, type: MessageType.PACKET, data: { sid, packet, }, }); } private _onClose( sid: SessionId, senderId: NodeId, reason: "transport error" | "transport close", ) { this.publishMessage({ senderId: this._nodeId, recipientId: senderId, type: MessageType.CLOSE, data: { sid, reason, }, }); } override onWebSocket(req: any, socket: any, websocket: any) { const sid = req._query.sid; if (!sid || this.clients[sid]) { // @ts-expect-error onWebSocket() is private return super.onWebSocket(req, socket, websocket); } websocket.on("error", () => {}); req.websocket = websocket; const transport = this.createTransport(req._query.transport, req); const senderId = req[kSenderId]; this._tryUpgrade( transport, () => this._onUpgradeSuccess(sid, transport, req, senderId), () => { debug("upgrade failure"); }, ); } private _onUpgradeSuccess( sid: SessionId, transport: Transport, req: any, senderId: NodeId, ) { debug("upgrade success"); this._hookTransport(sid, transport, "read", senderId); const requestId = ++this._requestCount as RequestId; const onSuccess = (takeOver: boolean, packets: Packet[]) => { if (takeOver) { this._remoteTransports.delete(sid); const send = transport.send; transport.send = () => {}; const socket = new Socket(sid, this, transport, req, 4); transport.send = send; this.clients[sid] = socket; this.clientsCount++; socket.once("close", () => { delete this.clients[sid]; this.clientsCount--; }); super.emit("connection", socket); socket.emit("upgrade"); for (const packet of packets) { // @ts-expect-error onPacket() is private socket.onPacket(packet); } } }; const onError = () => { transport.close(); }; const timer = setTimeout(() => { this._requests.delete(requestId); onError(); }, this._opts.responseTimeout); this._requests.set(requestId, { timer, onSuccess, onError, }); this.publishMessage({ requestId, senderId: this._nodeId, recipientId: senderId, type: MessageType.UPGRADE, data: { sid, success: true, }, }); } override emit(ev: string, ...args: any[]): boolean { if (ev !== "connection") { return super.emit(ev, ...args); } const socket = args[0] as Socket; if (socket.transport.name === "websocket") { return super.emit(ev, ...args); } debug("delaying connection"); socket[kDelayed] = true; socket[kBuffer] = []; socket[kPacketListener] = (packet: Packet) => { socket[kBuffer].push(packet); }; socket.on("packet", socket[kPacketListener]); socket[kDelayedTimer] = setTimeout( () => this._doConnect(socket), this._opts.delayedConnectionTimeout, ); } private _doConnect(socket: Socket) { if (!socket[kDelayed] || socket.readyState !== "open") { return; } debug( "the client has not upgraded yet, so the connection process is completed here", ); socket[kDelayed] = false; socket.off("packet", socket[kPacketListener]); clearTimeout(socket[kDelayedTimer]); super.emit("connection", socket); socket[kBuffer].forEach((packet: Packet) => { // @ts-expect-error onPacket() method is private socket.onPacket(packet); }); delete socket[kBuffer]; if (socket.upgraded) { socket.emit("upgrade"); } } abstract publishMessage(message: Message): void; }