@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
JavaScript
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