UNPKG

@qsocket/core

Version:

Powerful inter-process communication library for browser and Node.js environments, with flexible transport support including WebSocket, TCP, Unix Sockets, and Windows Pipes. Delivers high-performance data exchange through byte-stream buffers and automatic

1,113 lines (1,103 loc) 42.1 kB
import { EQSocketProtocolContentType, EQSocketProtocolMessageType, from, to } from '@qsocket/protocol'; // src/core/QSocketConnection.ts var contentTypeMap = /* @__PURE__ */ new Map([ [EQSocketProtocolContentType.UNDEFINED, "undefined"], [EQSocketProtocolContentType.NULL, "null"], [EQSocketProtocolContentType.BOOLEAN, "boolean"], [EQSocketProtocolContentType.NUMBER, "number"], [EQSocketProtocolContentType.STRING, "string"], [EQSocketProtocolContentType.JSON, "json"], [EQSocketProtocolContentType.BUFFER, "buffer"] ]); var reverseContentTypeMap = /* @__PURE__ */ new Map([ ["undefined", EQSocketProtocolContentType.UNDEFINED], ["null", EQSocketProtocolContentType.NULL], ["boolean", EQSocketProtocolContentType.BOOLEAN], ["number", EQSocketProtocolContentType.NUMBER], ["string", EQSocketProtocolContentType.STRING], ["json", EQSocketProtocolContentType.JSON], ["buffer", EQSocketProtocolContentType.BUFFER] ]); function determineContentType(data, contentType) { if (contentType) { const type = reverseContentTypeMap.get(contentType); if (type !== void 0) return type; } switch (typeof data) { case "undefined": return EQSocketProtocolContentType.UNDEFINED; case "boolean": return EQSocketProtocolContentType.BOOLEAN; case "number": return EQSocketProtocolContentType.NUMBER; case "string": return EQSocketProtocolContentType.STRING; case "symbol": return EQSocketProtocolContentType.UNDEFINED; case "object": if (data === null) return EQSocketProtocolContentType.NULL; if (Buffer.isBuffer(data)) return EQSocketProtocolContentType.BUFFER; return EQSocketProtocolContentType.JSON; default: return EQSocketProtocolContentType.UNDEFINED; } } function getContentTypeString(contentType) { var _a; if (contentType === void 0) return "undefined"; return (_a = contentTypeMap.get(contentType)) != null ? _a : "undefined"; } function createConfirmAckChunk(chunk, result) { return { meta: { type: EQSocketProtocolMessageType.ACK, uuid: chunk.meta.uuid }, payload: { data: result, "Content-Type": EQSocketProtocolContentType.BOOLEAN } }; } function createHandshakeAckChunk(chunk, result) { return { meta: { type: EQSocketProtocolMessageType.ACK, uuid: chunk.meta.uuid }, payload: { data: result, "Content-Type": EQSocketProtocolContentType.STRING } }; } function createErrorAckChunk(sourceChunk, error) { return { meta: { type: EQSocketProtocolMessageType.ACK, uuid: sourceChunk.meta.uuid }, payload: { data: { type: "error", value: error.message }, "Content-Type": EQSocketProtocolContentType.STRING } }; } function createDataAckChunk(chunk, data, contentType) { return { meta: { type: EQSocketProtocolMessageType.ACK, uuid: chunk.meta.uuid }, payload: { data, "Content-Type": determineContentType(data, contentType) } }; } function createDataChunk(uuid, event, namespace, data, contentType) { return { meta: { type: EQSocketProtocolMessageType.DATA, uuid, event, namespace }, payload: { data, "Content-Type": determineContentType(data, contentType) } }; } function uint8ArrayToBase64(uint8Array) { if (typeof window === "undefined") { return Buffer.from(uint8Array).toString("base64"); } else { let binaryString = ""; for (let i = 0; i < uint8Array.length; i++) { binaryString += String.fromCharCode(uint8Array[i]); } return btoa(binaryString); } } function base64ToUint8Array(base64String) { if (typeof window === "undefined") { const buffer = Buffer.from(base64String, "base64"); return new Uint8Array(buffer); } else { const binaryString = atob(base64String); const len = binaryString.length; const uint8Array = new Uint8Array(len); for (let i = 0; i < len; i++) { uint8Array[i] = binaryString.charCodeAt(i); } return uint8Array; } } // src/core/QSocketEventEmetter.ts var QSocketEventEmetterBase = class { constructor() { /** * Map of all event listeners, supporting multiple listeners for each event type. * @private * @type {Map<string, IQSocketListener<any, any>[]>} */ this.listeners = /* @__PURE__ */ new Map(); /** * Listeners for the "connection" event, triggered upon establishing a new connection. * @private * @type {((connection: QSocketConnection) => void)[]} */ this.connectionListeners = []; /** * Listeners for the "disconnection" event, triggered when a connection is terminated. * @private * @type {(() => void)[]} */ this.disconnectionListeners = []; } addEventListener(event, listener, type, contentType) { let listeners = this.listeners.get(event); if (!listeners) { listeners = []; this.listeners.set(event, listeners); } listeners.push({ type, listener, contentType }); } removeEventListener(event, listener) { const listeners = this.listeners.get(event); if (!listeners) return; const index = listeners.findIndex((item) => item.listener === listener); if (index !== -1) listeners.splice(index, 1); } async executor(chunk) { const event = chunk.meta.event; const listeners = this.listeners.get(event); if (!listeners) return []; const payload = chunk.payload; this.listeners.set( event, listeners.filter(({ type }) => type === 0 /* ON */) ); const results = await Promise.allSettled( listeners.map(async (eventInstance) => { const data = await Promise.resolve(eventInstance.listener(payload.data, getContentTypeString(payload["Content-Type"]))); return createDataAckChunk(chunk, data, eventInstance.contentType); }) ); return results.reduce((acc, cur) => { if (cur.status === "fulfilled" && cur.value) acc.push(cur.value); return acc; }, []); } }; var QSocketConnectionEventEmitter = class extends QSocketEventEmetterBase { /** * Main implementation of the `on` method, determining which handler to add. * @param {string} event - Event name. * @param {Function} listener - Callback function for the event. * @param {TQSocketContentType} [contentType] - Optional content type. */ on(event, listener, contentType) { if (event === "disconnection") { this.disconnectionListeners.push(listener); } else { this.addEventListener(event, listener, 0 /* ON */, contentType); } } /** * Main implementation of the `once` method, determining the addition of a one-time handler. * @param {string} event - Event name. * @param {Function} listener - Callback function for the event. * @param {TQSocketContentType} [contentType] - Optional content type. */ once(event, listener, contentType) { if (event === "disconnection") { this.disconnectionListeners.push(listener); } else { this.addEventListener(event, listener, 0 /* ON */, contentType); } } /** * Removes a listener for a custom event. * @example * ```typescript * emitter.off('customEvent', customEventHandler); * ``` * @param {string} event - Custom event name. * @param {Function} listener - Callback function registered for the event. */ off(event, listener) { if (event === "disconnection") { const index = this.disconnectionListeners.lastIndexOf(listener); if (index !== -1) this.disconnectionListeners.splice(index, 1); } else { this.removeEventListener(event, listener); } } }; var QSocketNamespaceEventEmitter = class extends QSocketEventEmetterBase { /** * Main implementation of the `on` method, determining which handler to add. * @param {string} event - Event name. * @param {Function} listener - Callback function for the event. * @param {TQSocketContentType} [contentType] - Optional content type. */ on(event, listener, contentType) { if (event === "connection") { this.connectionListeners.push(listener); this.addConnectionListennerHandle(listener); } else if (event === "disconnection") { this.disconnectionListeners.push(listener); } else { this.addEventListener(event, listener, 0 /* ON */, contentType); } } /** * Main implementation of the `once` method, determining the addition of a one-time handler. * @param {string} event - Event name. * @param {Function} listener - Callback function for the event. * @param {TQSocketContentType} [contentType] - Optional content type. */ once(event, listener, contentType) { if (event === "connection") { this.connectionListeners.push(listener); this.addConnectionListennerHandle(listener); } else if (event === "disconnection") { this.disconnectionListeners.push(listener); } else { this.addEventListener(event, listener, 0 /* ON */, contentType); } } /** * Removes a listener for a custom event. * @example * ```typescript * emitter.off('customEvent', customEventHandler); * ``` * @param {string} event - Custom event name. * @param {Function} listener - Callback function registered for the event. */ off(event, listener) { if (event === "connection") { const index = this.connectionListeners.lastIndexOf(listener); if (index !== -1) this.connectionListeners.splice(index, 1); } else if (event === "disconnection") { const index = this.disconnectionListeners.lastIndexOf(listener); if (index !== -1) this.disconnectionListeners.splice(index, 1); } else { this.removeEventListener(event, listener); } } }; // src/core/QSocketConnection.ts var QSocketConnection = class extends QSocketConnectionEventEmitter { //#endregion //#region Конструктор constructor(interaction, namespace) { super(); this.interaction = interaction; this.namespace = namespace; } //#endregion //#region Методы отправки и передачи данных /** * @description Отправка данных на связанный клиент */ async emit(event, data, options) { const message = [ { payload: { data, "Content-Type": determineContentType(data, options == null ? void 0 : options.contentType) }, meta: { type: EQSocketProtocolMessageType.DATA, uuid: this.interaction.uuid.next(), namespace: this.namespace.name, event } } ]; const returns = await this.interaction.sendData(message, options == null ? void 0 : options.timeout); return returns === void 0 ? [] : returns[0]; } async broadcast(event, data, options) { const chunk = createDataChunk(this.interaction.uuid.next(), event, this.namespace.name, data, options == null ? void 0 : options.contentType); const interactionsResults = await this.interaction.broadcast([chunk], options == null ? void 0 : options.timeout); return interactionsResults.map((interactionResult) => interactionResult[0]); } static async pipe(connection, chunk) { return await connection.executor(chunk); } //#endregion //#region Методы управления соединением static close(connection) { connection.disconnectionListeners.forEach((listener) => listener()); connection.listeners.clear(); } //#endregion }; // src/core/QSocketNamespace.ts var QSocketNamespace = class extends QSocketNamespaceEventEmitter { constructor(name, isActivated = true, debuger) { super(); this.connections = /* @__PURE__ */ new Map(); this.waiterWaited = () => void 0; this._name = name; if (!isActivated) { this.waiter = new Promise((resolve) => { this.waiterWaited = resolve; }); } this.debuger = debuger; } get name() { return this._name; } //#region Методы событий async emit(event, data, options) { if (this.waiter) { this.debuger.log(`The namespace "${this.name}" is not activated. Waiting for activation before sending...`); await this.waiter; this.debuger.log(`The waiting for sending in the namespace "${this.name}" is complete. Continuing with the event ${event}.`); } const promises = []; this.connections.forEach((connection) => { promises.push(connection.emit(event, data, options)); }); return (await Promise.allSettled(promises)).filter((res) => res.status === "fulfilled").map(({ value }) => value); } //#endregion //#region Методы управления потоком данных static async pipe(interaction, namespace, chunk) { if (namespace.waiter) await namespace.waiter; const connection = namespace.connections.get(interaction); if (!connection) return []; const namespaceResult = await namespace.executor(chunk); const connectionResult = await QSocketConnection.pipe(connection, chunk); const acks = [...namespaceResult, ...connectionResult]; if (acks.length > 0) return acks; else return [createDataAckChunk(chunk, void 0, "undefined")]; } //#endregion //#region Методы управления клиентами static async addClient(namespace, interaction) { const connection = new QSocketConnection(interaction, namespace); namespace.connections.set(interaction, connection); await Promise.allSettled( namespace.connectionListeners.map(async (listener) => { try { return await Promise.resolve(listener(connection)); } catch (error) { return namespace.debuger.error("Connection event error:", error); } }) ); namespace.debuger.info(`Interaction "${interaction.id}" join namespace "${namespace.name}"`); } static async deleteClient(namespace, interaction) { const connection = namespace.connections.get(interaction); namespace.connections.delete(interaction); await Promise.allSettled( namespace.disconnectionListeners.map(async (listener) => { try { return await Promise.resolve(listener()); } catch (error) { return namespace.debuger.error("Disconnection event error:", error); } }) ); namespace.debuger.info(`Interaction "${interaction.id}" leave namespace "${namespace.name}"`); if (connection !== void 0) QSocketConnection.close(connection); } static destroy(namespace) { namespace.connections.forEach((_, interaction) => this.deleteClient(namespace, interaction)); } //#endregion addConnectionListennerHandle(listenner) { this.connections.forEach((connection) => listenner(connection)); } static activate(namespace) { if (namespace.waiter !== void 0 && namespace.waiterWaited !== void 0) { namespace.debuger.log(`The namespace "${namespace.name}" has been activated!`); namespace.waiterWaited(); namespace.waiter = void 0; namespace.waiterWaited = void 0; } } static diactivate(namespace) { if (namespace.waiter === void 0 && namespace.waiterWaited === void 0) { namespace.waiter = new Promise((resolve) => { namespace.waiterWaited = resolve; }); namespace.debuger.log(`The namespace "${namespace.name}" has been deactivated!`); } } }; // src/core/QSocketDebuger.ts var colors = { log: "\x1B[32m", // Зеленый error: "\x1B[31m", // Красный warn: "\x1B[33m", // Желтый info: "\x1B[34m" // Синий }; var reset = "\x1B[0m"; var QSocketDebuger = class { /** * Конструктор класса QSocketUtils. * @param {IQSocketConfigBase['debug']} [debugConfig] - Конфигурация для режима отладки. */ constructor(debugConfig) { /** Логгер, используемый для вывода сообщений */ this.logger = console; const { enabled = false, logger = console, prefix = "" } = debugConfig != null ? debugConfig : {}; this.enabled = enabled; this.logger = logger; this.prefix = prefix; } /** * Получает цветной префикс в зависимости от типа сообщения. * @param {string} type - Тип сообщения (log, error, info, warn). * @returns {string} - Цветной префикс. */ getColoredPrefix(type) { var _a; return `${(_a = colors[type]) != null ? _a : colors.log}${this.prefix}${reset}`; } /** * Логирует сообщение, если включен режим отладки. * @param {...any[]} message - Сообщение или данные для логирования. */ log(...message) { if (this.enabled) this.logger.log(this.getColoredPrefix("log"), ...message); } /** * Логирует сообщение об ошибке, если включен режим отладки. * @param {...any[]} message - Сообщение или данные для логирования ошибок. */ error(...message) { if (this.enabled) this.logger.error(this.getColoredPrefix("error"), ...message); } /** * Логирует информационное сообщение, если включен режим отладки. * @param {...any[]} message - Сообщение или данные для информационного логирования. */ info(...message) { if (this.enabled) this.logger.info(this.getColoredPrefix("info"), ...message); } /** * Логирует предупреждение, если включен режим отладки. * @param {...any[]} message - Сообщение или данные для логирования предупреждений. */ warn(...message) { if (this.enabled) this.logger.warn(this.getColoredPrefix("warn"), ...message); } }; // src/core/QSocketUniqueGenerator.ts var MAX_VALUE = Number.MAX_SAFE_INTEGER; var QSocketUniqueGenerator = class { constructor(prefix = "") { /** * Текущий индекс для генерации UUID. * @private */ this.uuidIndex = 0; this.prefix = prefix; } /** * Метод для генерации следующего уникального идентификатора. */ next() { if (++this.uuidIndex > MAX_VALUE) this.uuidIndex = 0; return `${this.prefix}${this.uuidIndex.toString(16)}`; } }; // src/core/QSocketInteraction.ts var QSocketInteraction = class { constructor(id, socket, allNamespaces = /* @__PURE__ */ new Map(), interactions, timeout, debuger, dateFormat = 0 /* Binary */) { this.acks = /* @__PURE__ */ new Map(); this.connectedNamespaces = /* @__PURE__ */ new Map(); this.allNamespaces = /* @__PURE__ */ new Map(); this.messageBuffer = []; this.isProcessing = false; this.id = id; this.uuid = new QSocketUniqueGenerator(`${this.id}-M`); this.debuger = debuger; this.socket = socket; this.interactions = interactions; this.allNamespaces = allNamespaces; this.timeout = timeout; this._dataFormat = dateFormat; this.socket.on("message", this.onHandle.bind(this)); } set dateFormat(dateFormat) { this._dataFormat = dateFormat; } static close(interaction) { interaction.socket.close(); interaction.closeHandle(); } closeHandle() { this.debuger.log("The connection termination process has started.", this.id); this.acks.forEach((fn) => fn(void 0)); this.acks.clear(); this.socket.close(); this.connectedNamespaces.forEach((namespace) => QSocketNamespace.deleteClient(namespace, this)); } //#region ПРОСЛУШИВАНИЕ СОБЫТИЙ async onHandle(data) { if (this._dataFormat === 0 /* Binary */) { if (typeof data === "string") { this.debuger.error("The current QSocket instance is configured to work via binary data. Communication via base64 is not possible!"); return; } const type = data && data.constructor ? data.constructor.name : ""; if (type !== "Buffer" && type !== "ArrayBuffer" && type !== "Uint8Array") { this.debuger.error("The current QSocket instance is configured to work via binary data. Communication via base64 is not possible!"); return; } } else if (this._dataFormat === 1 /* Base64 */) { if (typeof data === "string") { data = base64ToUint8Array(data); } else { this.debuger.error("The current QSocket instance is configured to work via base64 data. Interaction through binary data is not possible!"); return; } } else { this.debuger.error("The current QSocket instance is not configured correctly. There is no data format for communication!"); return; } const buffer = new Uint8Array(data); let message; try { message = from(buffer); } catch (error) { this.debuger.error(error instanceof Error ? error.message : String(error)); return; } const ackChunks = []; const dataChunks = []; const controlChunks = []; let chunk; for (let i = 0; i < message.length; i++) { chunk = message[i]; switch (chunk.meta.type) { case EQSocketProtocolMessageType.DATA: dataChunks.push(chunk); break; case EQSocketProtocolMessageType.ACK: ackChunks.push(chunk); break; case EQSocketProtocolMessageType.CONTROL: controlChunks.push(chunk); break; } } if (ackChunks.length > 0) this.onAck(ackChunks); if (controlChunks.length > 0) await this.onControl(controlChunks); if (dataChunks.length > 0) await this.onData(dataChunks); } onControl(message) { let errorInstance; let data; return Promise.all( message.map(async (chunk) => { var _a, _b, _c, _d, _e, _f; data = chunk.payload.data; if (data.command === "join-namespace") { const infoMessage = `[namespace: ${data.namespace} | interaction: ${this.id}]`; const namespace = this.allNamespaces.get(data.namespace); if (!namespace) { errorInstance = new Error(`An error occurred while executing the command "join-namespace". Namespace not found ${infoMessage}`); this.debuger.error(errorInstance.message); await this.send([createErrorAckChunk(chunk, errorInstance)]); return; } const handshake = this.uuid.next(); const payload = await this.sendHandshake([createHandshakeAckChunk(chunk, handshake)]); const ackHandshake = (_c = (_b = (_a = payload == null ? void 0 : payload[0]) == null ? void 0 : _a[0]) == null ? void 0 : _b.data) == null ? void 0 : _c.handshake; if (typeof ackHandshake !== "string") { this.debuger.error(`The last handshake is missing a hash key ${infoMessage}`); } if (ackHandshake !== handshake) { this.debuger.error(`Handshake hash does not match expected (Expected: ${handshake} | Received: ${ackHandshake}) ${infoMessage}`); return; } if (!this.connectedNamespaces.has(namespace.name)) this.connectedNamespaces.set(namespace.name, namespace); await QSocketNamespace.addClient(namespace, this); } else if (data.command === "leave-namespace") { const infoMessage = `[namespace: ${data.namespace} | interaction: ${this.id}]`; const namespace = this.connectedNamespaces.get(data.namespace); if (!namespace) { errorInstance = new Error(`An error occurred while executing the command "leave-namespace". Namespace not found ${infoMessage}`); this.debuger.error(errorInstance.message); await this.send([createErrorAckChunk(chunk, errorInstance)]); return; } const handshake = this.uuid.next(); const payload = await this.sendHandshake([createHandshakeAckChunk(chunk, handshake)]); const ackHandshake = (_f = (_e = (_d = payload == null ? void 0 : payload[0]) == null ? void 0 : _d[0]) == null ? void 0 : _e.data) == null ? void 0 : _f.handshake; if (typeof ackHandshake !== "string") { this.debuger.error(`The last handshake is missing a hash key ${infoMessage}`); } if (ackHandshake !== handshake) { this.debuger.error(`Handshake hash does not match expected (Expected: ${handshake} | Received: ${ackHandshake}) ${infoMessage}`); return; } this.connectedNamespaces.delete(namespace.name); QSocketNamespace.deleteClient(namespace, this); } else if (data.command === "handshake") { const resolver = this.acks.get(chunk.meta.uuid); if (resolver !== void 0) { resolver([chunk.payload]); await this.send([createConfirmAckChunk(chunk, true)]); } else { this.debuger.error(`There is no resolver on handshake ${data.handshake}`); await this.send([createConfirmAckChunk(chunk, false)]); } } else { errorInstance = new Error(`Unknown control command`); this.debuger.error(errorInstance.message); await this.send([createErrorAckChunk(chunk, errorInstance)]); } }) ); } onData(message) { let errorInstance; return Promise.all( message.map(async (chunk) => { const namespaceInstance = this.connectedNamespaces.get(chunk.meta.namespace); if (!namespaceInstance) { errorInstance = new Error(`Namespace "${chunk.meta.namespace}" does not exist`); this.debuger.error(errorInstance.message); await this.send([createErrorAckChunk(chunk, errorInstance)]); return; } try { const result = await QSocketNamespace.pipe(this, namespaceInstance, chunk); await this.send(result); } catch (error) { if (error instanceof Error) errorInstance = error; else errorInstance = new Error(String(error)); this.debuger.error(`\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u043A\u0435 \u0434\u0430\u043D\u043D\u044B\u0445: ${errorInstance.message}`); await this.send([createErrorAckChunk(chunk, errorInstance)]); } }) ); } onAck(message) { const ackMap = /* @__PURE__ */ new Map(); message.forEach((chunk) => { let ack = ackMap.get(chunk.meta.uuid) || []; ack.push(chunk); ackMap.set(chunk.meta.uuid, ack); }); ackMap.forEach((value, uuid) => { const resolve = this.acks.get(uuid); if (!resolve) { this.debuger.error(`Return message UUID not found [id: ${this.id}, uuid: ${uuid}]`); return; } resolve(value.map((item) => item.payload)); this.acks.delete(uuid); }); } //#endregion //#region ОТПРАВКА ДАННЫХ async send(message) { this.messageBuffer.push(message); if (!this.isProcessing) { this.isProcessing = true; setTimeout(() => { this.sendCumulative(); this.isProcessing = false; }, 0); } } sendCumulative() { const totalLength = this.messageBuffer.reduce((sum, chunks) => sum + chunks.length, 0); const combinedMessages = new Array(totalLength); let offset = 0; const length = this.messageBuffer.length; let message; for (let i = 0; i < length; i++) { message = this.messageBuffer[i]; combinedMessages.splice(offset, message.length, ...message); offset += message.length; } this.messageBuffer = []; try { this.sendBuffer(to(combinedMessages)); } catch (error) { this.debuger.error(`An error occurred while encoding ${combinedMessages.length} messages. I send messages one at a time`); for (let i = 0; i < combinedMessages.length; i++) { try { this.sendBuffer(to([combinedMessages[i]])); } catch (error2) { const problemMeta = combinedMessages[i].meta; this.debuger.error( `Problem message found. Interaction: ${this.id} TYPE: ${problemMeta.type}, UUID: ${problemMeta.uuid}${problemMeta.type === EQSocketProtocolMessageType.DATA ? `, NAMESPACE: ${problemMeta.namespace}, EVENT:${problemMeta.event}` : ""} ERROR: ${error2 instanceof Error ? error2.message : String(error2)}` ); } } return; } } sendBuffer(buffer) { if (this._dataFormat === 0 /* Binary */) { this.socket.send(buffer.buffer); } else { this.socket.send(uint8ArrayToBase64(buffer)); } } async broadcast(message, timeout = this.timeout.value) { let buffer; message.forEach((chunk) => chunk.meta.uuid = `BROADCAST-FROM-${this.id}-${chunk.meta.uuid}`); try { buffer = to(message); } catch (e) { this.debuger.error(`An error occurred while encoding broadcast message.`); return [[[]]]; } const promises = []; this.interactions.forEach((interaction) => { if (interaction !== this) { promises.push(interaction.addAckResolver(message, "", timeout)); interaction.sendBuffer(buffer); } }); const interactionsResults = await Promise.allSettled(promises).then( (result) => result.filter((item) => item.status === "fulfilled").map(({ value }) => value).filter((value) => value !== void 0) ); return interactionsResults; } async sendData(message, timeout = this.timeout.value) { this.send(message); return await this.addAckResolver(message, "", timeout); } async sendHandshake(message, timeout = this.timeout.value) { this.send(message); return await this.addAckResolver(message, "HANDSHAKE-", timeout); } async sendCommand(message, timeout = this.timeout.value) { this.send(message); return await this.addAckResolver(message, "", timeout); } async addAckResolver(message, prefix, timeout = this.timeout.value) { return (await Promise.allSettled( message.map((chunk) => { return new Promise((emitResolve, emitReject) => { const uuid = prefix + chunk.meta.uuid; const ackResolver = (ackResult) => { clearTimeout(timer); this.acks.delete(uuid); if (ackResult === void 0) emitReject(); else emitResolve(ackResult); }; this.acks.set(uuid, ackResolver); const timer = setTimeout(() => { this.acks.delete(uuid); this.debuger.error(`Waiting time expired [${uuid}]`); emitReject(); }, timeout); }); }) )).filter((res) => res.status === "fulfilled").map(({ value }) => value); } //#endregion //#region NAMESPACES static joinNamespace(interaction, namespace) { return interaction.joinNamespace(namespace); } joinNamespace(namespace) { this.connectedNamespaces.set(namespace.name, namespace); return QSocketNamespace.addClient(namespace, this); } static leaveNamespace(interaction, namespace) { return interaction.leaveNamespace(namespace); } leaveNamespace(namespace) { this.connectedNamespaces.delete(namespace.name); return QSocketNamespace.deleteClient(namespace, this); } //#endregion }; var clientUUID = new QSocketUniqueGenerator(); var serverUUID = new QSocketUniqueGenerator(); var QSocketBase = class { constructor(type, config) { this.namespaces = /* @__PURE__ */ new Map(); this.interactions = /* @__PURE__ */ new Map(); this.dateFormat = 0 /* Binary */; this.timeout = { value: 6e4, actionAfrer: "none" }; this.middlewares = []; var _a, _b; this.type = type; this.debuger = new QSocketDebuger(config == null ? void 0 : config.debug); if (((_a = config == null ? void 0 : config.timeout) == null ? void 0 : _a.value) !== void 0) this.timeout.value = config.timeout.value; if (((_b = config == null ? void 0 : config.timeout) == null ? void 0 : _b.actionAfrer) !== void 0) this.timeout.actionAfrer = config.timeout.actionAfrer; const prefix = type === "server" ? "S" : "C"; const generator = type === "server" ? serverUUID : clientUUID; this.id = `${prefix}${generator.next()}`; this.uuid = new QSocketUniqueGenerator(`${this.id}-SM`); this.interactionUUID = new QSocketUniqueGenerator(`${this.id}-I`); } connectionHandle(socket) { const interactionId = this.interactionUUID.next(); const interaction = new QSocketInteraction(interactionId, socket, this.namespaces, this.interactions, this.timeout, this.debuger, this.dateFormat); this.interactions.set(interactionId, interaction); socket.on("close", () => this.closeInteraction(interactionId, interaction)); } closeInteraction(interactionId, interaction) { QSocketInteraction.close(interaction); this.interactions.delete(interactionId); } /** * Создаёт новое пространство имён или возвращает существующее. * @param {string} name - Имя создаваемого пространства имён. * @returns {QSocketNamespace} Пространство имён QSocket. */ createNamespace(name) { if (this.namespaces.has(name)) { this.debuger.warn(`[QSOCKET] The namespace "${name}" already exists.`); return this.namespaces.get(name); } const namespace = new QSocketNamespace(name, this.type === "server", this.debuger); this.namespaces.set(name, namespace); this.namespaceControl(namespace, "join-namespace"); return namespace; } /** * Удаляет существующее пространство имён. * @param {string} name - Имя удаляемого пространства имён. * @returns {boolean} Возвращает `true`, если пространство имён было удалено, иначе `false`. */ async deleteNamespace(name) { const namespace = this.namespaces.get(name); if (namespace === void 0) { this.debuger.warn(`[QSOCKET] The namespace '${name}' does not exist.`); return false; } this.namespaces.delete(name); QSocketNamespace.destroy(namespace); return await this.namespaceControl(namespace, "leave-namespace"); } /** * @description Добавляет промежуточный обработчик сообщений * @param handler */ use(handler) { this.middlewares.push(handler); } //#region Методы, работающие ТОЛЬКО НА КЛИЕНТЕ async namespaceControl(namespace, cmd) { if (this.type !== "client") return true; const handshakeUUID = this.uuid.next(); const message = [ { meta: { type: EQSocketProtocolMessageType.CONTROL, uuid: handshakeUUID }, payload: { data: { command: cmd, namespace: namespace.name }, "Content-Type": EQSocketProtocolContentType.JSON } } ]; const interactions = Array.from(this.interactions, ([_, interaction]) => interaction); const promises = interactions.map(async (interaction) => { var _a, _b; let sendingResult; try { sendingResult = await interaction.sendCommand(message); } catch (e) { throw new Error(`An error occurred while sending the command "${cmd}" (${namespace.name}) to server "${interaction.id}"`); } if (sendingResult === void 0) { throw new Error(`Failed to send command "${cmd}" (${namespace.name}) to server "${interaction.id}"`); } const handshake = sendingResult[0][0].data; if (typeof handshake !== "string") throw new Error(`The handshake is damaged.`); if (cmd === "join-namespace") { await QSocketInteraction.joinNamespace(interaction, namespace).then(() => handshake); } else { await QSocketInteraction.leaveNamespace(interaction, namespace).then(() => handshake); } try { sendingResult = await interaction.sendCommand([ { meta: { type: EQSocketProtocolMessageType.CONTROL, uuid: `HANDSHAKE-${handshakeUUID}` }, payload: { data: { command: "handshake", handshake }, "Content-Type": EQSocketProtocolContentType.JSON } } ]); } catch (e) { throw new Error(`Failed to send handshake confirmation for "${cmd}" (${namespace.name}) to server "${interaction.id}" [handshake: ${handshake}].`); } const canBeActivated = (_b = (_a = sendingResult == null ? void 0 : sendingResult[0]) == null ? void 0 : _a[0]) == null ? void 0 : _b.data; if (canBeActivated === void 0) { throw new Error(`Failed to send handshake confirmation for "${cmd}" (${namespace.name}) to server "${interaction.id}" [handshake: ${handshake}].`); } if (canBeActivated) { if (cmd === "join-namespace") { QSocketNamespace.activate(namespace); } else if (cmd === "leave-namespace") { QSocketNamespace.diactivate(namespace); } } else { if (cmd === "join-namespace") { QSocketInteraction.leaveNamespace(interaction, namespace); } else if (cmd === "leave-namespace") { QSocketInteraction.joinNamespace(interaction, namespace); } throw new Error(`Failed to establish connection to namespace "${namespace.name}"`); } }); return (await Promise.allSettled(promises)).every((item) => item.status === "fulfilled"); } //#endregion /** * Изменяет формат передачи данных для всех соединений * По умолчанию "binary" */ setDateFormat(dataFormat) { switch (dataFormat) { case "base64": this.interactions.forEach((interaction) => interaction.dateFormat = 1 /* Base64 */); break; case "binary": this.interactions.forEach((interaction) => interaction.dateFormat = 0 /* Binary */); break; default: this.debuger.error("[QSOCKET] Incorrect data format"); } } }; // src/interfaces/QSocketClient.ts var QSocketClient = class extends QSocketBase { constructor(socketBuilder, config) { super("client", config); this.isConnected = false; this.reconnectionAttempts = 0; this.reconnecting = false; this.reconnectionConfig = config == null ? void 0 : config.reconnection; this.transportBuilder = socketBuilder; } async connect() { if (this.isConnected) return; let isTimeout = false; let timeout; try { const transport = await Promise.race([ new Promise((resolve) => { const transport2 = this.transportBuilder(); const handleOpen = () => { if (isTimeout) { transport2.close(); transport2.off("open", handleOpen); return; } if (timeout !== void 0) { clearTimeout(timeout); } transport2.off("open", handleOpen); resolve(transport2); }; transport2.on("open", handleOpen); }), new Promise((_, reject) => { timeout = window.setTimeout(() => { isTimeout = true; reject(new Error("Connection timed out")); }, 1e4); }) ]); this.isConnected = true; this.reconnectionAttempts = 0; this.connectionHandle(transport); this.namespaces.forEach((namespace) => this.namespaceControl(namespace, "join-namespace")); transport.on("close", () => { this.isConnected = false; this.attemptReconnect(); }); } catch (error) { this.debuger.error("Connection failed:", error); this.isConnected = false; this.attemptReconnect(); } finally { if (timeout !== void 0) { clearTimeout(timeout); } } } /** * Метод для переподключения с учетом конфигурации. */ async attemptReconnect() { var _a; if (!((_a = this.reconnectionConfig) == null ? void 0 : _a.enabled) || this.reconnecting) return; this.reconnecting = true; this.debuger.log(`Connection restoration process started.`); while (this.reconnectionConfig.enabled && (this.reconnectionConfig.maxAttempts === void 0 || this.reconnectionAttempts < this.reconnectionConfig.maxAttempts)) { this.reconnectionAttempts++; const delay = this.calculateDelay(); await new Promise((resolve) => setTimeout(resolve, delay)); this.debuger.log(`Attempting to reconnect... (attempt ${this.reconnectionAttempts})`); try { await this.connect(); if (this.isConnected) { this.debuger.log("Reconnected successfully."); break; } } catch (error) { this.debuger.error("Reconnection attempt failed:", error); } } this.reconnecting = false; } /** * Вычисляет задержку для следующей попытки переподключения. * @returns {number} Задержка в миллисекундах */ calculateDelay() { var _a, _b, _c; const baseDelay = (_b = (_a = this.reconnectionConfig) == null ? void 0 : _a.delay) != null ? _b : 1e3; const maxDelay = 6e4; if ((_c = this.reconnectionConfig) == null ? void 0 : _c.exponentialBackoff) { const delay = baseDelay * Math.log1p(this.reconnectionAttempts); return Math.min(delay, maxDelay); } return baseDelay; } //#endregion }; // src/interfaces/QSocketServer.ts var QSocketServer = class extends QSocketBase { constructor(transport, config) { super("server", config); this.server = transport; this.server.on("connection", (socket) => this.connectionHandle(socket)); } }; export { QSocketClient, QSocketServer }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map