From b9016c983d1ecc09044b14db9aa1fa3ea186e1d8 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Tue, 3 Mar 2026 15:05:14 +0200 Subject: [PATCH 01/70] update logs --- app/cluster/cluster_manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index 10757ebc..1c2459f2 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -141,7 +141,7 @@ class ClusterManager { } const clusterPort = uWS.us_socket_local_port(listenSocket) - logger.debug("[WS] listening on port %s", clusterPort) + logger.debug("[ClusterManager][WS] listening on port %s", clusterPort) return resolve(clusterPort) }) From c4be3e5bcea00b59ef0e9aa922339ff04a85e28f Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Wed, 21 Jan 2026 19:45:32 +0200 Subject: [PATCH 02/70] sync activity cluster --- app/providers/index.js | 4 +- .../activity_cluster_manager/Provider.js | 15 +++ .../activity_cluster_manager/index.js | 117 ++++++++++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 app/providers/services/activity_cluster_manager/Provider.js create mode 100644 app/providers/services/activity_cluster_manager/index.js diff --git a/app/providers/index.js b/app/providers/index.js index ba84305a..54a47806 100644 --- a/app/providers/index.js +++ b/app/providers/index.js @@ -34,6 +34,7 @@ import BlockListServiceProvider from "./services/block_list/Provider.js" import StorageServiceProvider from "./services/storage/Provider.js" import OperationsLogServiceProvider from "./services/operation_logs/Provider.js" import ActivityManagerServiceProvider from "./services/activity_manager/Provider.js" +import ActivityClusterManagerServiceProvider from "./services/activity_cluster_manager/Provider.js" import ConversationServiceProvider from "./services/conversation/Provider.js" import ConversationHandlerServiceProvider from "./services/conversation_handler/Provider.js" import EncryptionServiceProvider from "./services/encryption/Provider.js" @@ -170,7 +171,8 @@ const providers = [ BlockListServiceProvider, StorageServiceProvider, OperationsLogServiceProvider, - ActivityManagerServiceProvider, + // ActivityManagerServiceProvider, + ActivityClusterManagerServiceProvider, ConversationServiceProvider, ConversationHandlerServiceProvider, MessageServiceProvider, diff --git a/app/providers/services/activity_cluster_manager/Provider.js b/app/providers/services/activity_cluster_manager/Provider.js new file mode 100644 index 00000000..45787ac0 --- /dev/null +++ b/app/providers/services/activity_cluster_manager/Provider.js @@ -0,0 +1,15 @@ +import RegisterProvider from "../../../common/RegisterProvider.js" +import ActivityClusterManagerService from "./index.js" + +const name = "ActivityManagerService" + +class ActivityClusterManagerServiceRegisterProvider extends RegisterProvider { + register(slc) { + const redisClient = slc.use("RedisClient") + const userService = slc.use("UserService") + + return new ActivityClusterManagerService(redisClient, userService) + } +} + +export default new ActivityClusterManagerServiceRegisterProvider({ name, implementationName: ActivityClusterManagerService.name }) diff --git a/app/providers/services/activity_cluster_manager/index.js b/app/providers/services/activity_cluster_manager/index.js new file mode 100644 index 00000000..968f8e65 --- /dev/null +++ b/app/providers/services/activity_cluster_manager/index.js @@ -0,0 +1,117 @@ +import { CONSTANTS as MAIN_CONSTANTS } from "../../../constants/constants.js" + +/* + Structs: + HSET - sama:subscribed_to -> [observerId] - targetId + HSET - sama:subscribers -> [targetId] -> observerId1,observerId2,observerId3 +*/ + +const SUBSCRIBED_TO_KEY = "sama:subscribed_to" +const SUBSCRIBERS_KEY = "sama:subscribers" + +class ActivityClusterManagerService { + constructor(redisConnection, userService) { + this.redisConnection = redisConnection + this.userService = userService + } + + // observer - targets relations + async subscribeTarget(observerId) { + const targetId = await this.redisConnection.client.hGet(SUBSCRIBED_TO_KEY, `${observerId}`) + + return targetId ? +targetId : void 0 + } + + async addSubscribeTarget(observerId, targetId) { + await this.redisConnection.client.hSet(SUBSCRIBED_TO_KEY, `${observerId}`, `${targetId}`) + } + + async deleteSubscribeTarget(observerId, targetId) { + await this.redisConnection.client.hDel(SUBSCRIBED_TO_KEY, `${observerId}`) + } + + async clearAllSubscriptionTargets(observerId) { + await this.redisConnection.client.hDel(SUBSCRIBED_TO_KEY, `${observerId}`) + } + + // target - observer relations + async subscribers(targetId) { + const observersIds = await this.redisConnection.client.hGet(SUBSCRIBERS_KEY, `${targetId}`) + + return observersIds ? observersIds.split(',').map(observerId => +observerId) : [] + } + + async addSubscriber(targetId, observerId) { + const observersIdsArr = await this.subscribers(targetId) + + const observersIds = new Set(observersIdsArr) + + observersIds.add(observerId) + + const newObserversIds = Array.from(observersIds).join(',') + + await this.redisConnection.client.hSet(SUBSCRIBERS_KEY, `${targetId}`, newObserversIds) + } + + async deleteSubscriber(targetId, observerId) { + const observersIdsArr = await this.subscribers(targetId) + + const observersIds = new Set(observersIdsArr) + + observersIds.delete(observerId) + + if (!observersIds.size) { + await this.clearSubscribed(targetId) + return + } + + const newObserversIds = Array.from(observersIds).join(',') + + await this.redisConnection.client.hSet(SUBSCRIBERS_KEY, `${targetId}`, newObserversIds) + } + + async clearSubscribed(targetId) { + await this.redisConnection.client.hDel(SUBSCRIBERS_KEY, `${targetId}`) + } + + async subscribeObserverToTarget(observerId, targetId) { + await this.unsubscribeObserver(observerId) + + await this.addSubscribeTarget(observerId, targetId) + await this.addSubscriber(targetId, observerId) + } + + async unsubscribeObserver(observerId) { + const targetId = await this.subscribeTarget(observerId) + + await this.deleteSubscribeTarget(observerId) + + if (targetId) { + await this.deleteSubscriber(targetId, observerId) + } + } + + async updateUserActivity(userId, status) { + const currentTime = Math.round(new Date() / 1000) + + if (status !== MAIN_CONSTANTS.LAST_ACTIVITY_STATUS.ONLINE) { + await this.userService.updateActivity(userId, currentTime) + + await this.unsubscribeObserver(userId) + } + + const activitySubscribers = await this.subscribers(userId) + + if (!activitySubscribers.length) { + return + } + + return { + subscribers: activitySubscribers, + targetUserId: userId, + activityStatus: { timestamp: currentTime, status }, + } + } +} + +export default ActivityClusterManagerService From 81f8edecc1150ee1f20e4532494b1fa4b847e6e9 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Tue, 3 Mar 2026 17:36:56 +0200 Subject: [PATCH 03/70] update activity service --- app/providers/index.js | 8 +++++--- .../activity_cluster_manager/Provider.js | 15 --------------- .../services/activity_manager/Provider.js | 9 +++------ .../activity_manager_cluster/Provider.js | 15 +++++++++++++++ .../activity_manager_cluster}/index.js | 6 +++--- .../activity_manager_standalone/Provider.js | 16 ++++++++++++++++ .../{ => activity_manager_standalone}/index.js | 6 +++--- 7 files changed, 45 insertions(+), 30 deletions(-) delete mode 100644 app/providers/services/activity_cluster_manager/Provider.js create mode 100644 app/providers/services/activity_manager/activity_manager_cluster/Provider.js rename app/providers/services/{activity_cluster_manager => activity_manager/activity_manager_cluster}/index.js (95%) create mode 100644 app/providers/services/activity_manager/activity_manager_standalone/Provider.js rename app/providers/services/activity_manager/{ => activity_manager_standalone}/index.js (92%) diff --git a/app/providers/index.js b/app/providers/index.js index 54a47806..6e7ca80b 100644 --- a/app/providers/index.js +++ b/app/providers/index.js @@ -33,8 +33,9 @@ import UserServiceProvider from "./services/user/Provider.js" import BlockListServiceProvider from "./services/block_list/Provider.js" import StorageServiceProvider from "./services/storage/Provider.js" import OperationsLogServiceProvider from "./services/operation_logs/Provider.js" +import ActivityManagerStandaloneServiceProvider from "./services/activity_manager/activity_manager_standalone/Provider.js" +import ActivityManagerClusterServiceProvider from "./services/activity_manager/activity_manager_cluster/Provider.js" import ActivityManagerServiceProvider from "./services/activity_manager/Provider.js" -import ActivityClusterManagerServiceProvider from "./services/activity_cluster_manager/Provider.js" import ConversationServiceProvider from "./services/conversation/Provider.js" import ConversationHandlerServiceProvider from "./services/conversation_handler/Provider.js" import EncryptionServiceProvider from "./services/encryption/Provider.js" @@ -171,8 +172,9 @@ const providers = [ BlockListServiceProvider, StorageServiceProvider, OperationsLogServiceProvider, - // ActivityManagerServiceProvider, - ActivityClusterManagerServiceProvider, + ActivityManagerStandaloneServiceProvider, + ActivityManagerClusterServiceProvider, + ActivityManagerServiceProvider, ConversationServiceProvider, ConversationHandlerServiceProvider, MessageServiceProvider, diff --git a/app/providers/services/activity_cluster_manager/Provider.js b/app/providers/services/activity_cluster_manager/Provider.js deleted file mode 100644 index 45787ac0..00000000 --- a/app/providers/services/activity_cluster_manager/Provider.js +++ /dev/null @@ -1,15 +0,0 @@ -import RegisterProvider from "../../../common/RegisterProvider.js" -import ActivityClusterManagerService from "./index.js" - -const name = "ActivityManagerService" - -class ActivityClusterManagerServiceRegisterProvider extends RegisterProvider { - register(slc) { - const redisClient = slc.use("RedisClient") - const userService = slc.use("UserService") - - return new ActivityClusterManagerService(redisClient, userService) - } -} - -export default new ActivityClusterManagerServiceRegisterProvider({ name, implementationName: ActivityClusterManagerService.name }) diff --git a/app/providers/services/activity_manager/Provider.js b/app/providers/services/activity_manager/Provider.js index 4a2b5ed3..9ff79a7d 100644 --- a/app/providers/services/activity_manager/Provider.js +++ b/app/providers/services/activity_manager/Provider.js @@ -1,16 +1,13 @@ import RegisterProvider from "../../../common/RegisterProvider.js" -import ActivityManagerService from "./index.js" - -import { ACTIVITY } from "../../../store/activity.js" const name = "ActivityManagerService" class ActivityManagerServiceRegisterProvider extends RegisterProvider { register(slc) { - const userService = slc.use("UserService") + const config = slc.use("Config") - return new ActivityManagerService(ACTIVITY, userService) + return config.get("app.isStandAloneNode") ? slc.use("ActivityManagerStandaloneService") : slc.use("ActivityManagerClusterService") } } -export default new ActivityManagerServiceRegisterProvider({ name, implementationName: ActivityManagerService.name }) +export default new ActivityManagerServiceRegisterProvider({ name, implementationName: name }) diff --git a/app/providers/services/activity_manager/activity_manager_cluster/Provider.js b/app/providers/services/activity_manager/activity_manager_cluster/Provider.js new file mode 100644 index 00000000..97b1d639 --- /dev/null +++ b/app/providers/services/activity_manager/activity_manager_cluster/Provider.js @@ -0,0 +1,15 @@ +import RegisterProvider from "../../../../common/RegisterProvider.js" +import ActivityManagerClusterService from "./index.js" + +const name = "ActivityManagerClusterService" + +class ActivityManagerClusterServiceRegisterProvider extends RegisterProvider { + register(slc) { + const redisClient = slc.use("RedisClient") + const userService = slc.use("UserService") + + return new ActivityManagerClusterService(redisClient, userService) + } +} + +export default new ActivityManagerClusterServiceRegisterProvider({ name, implementationName: ActivityManagerClusterService.name }) diff --git a/app/providers/services/activity_cluster_manager/index.js b/app/providers/services/activity_manager/activity_manager_cluster/index.js similarity index 95% rename from app/providers/services/activity_cluster_manager/index.js rename to app/providers/services/activity_manager/activity_manager_cluster/index.js index 968f8e65..249c3a8f 100644 --- a/app/providers/services/activity_cluster_manager/index.js +++ b/app/providers/services/activity_manager/activity_manager_cluster/index.js @@ -1,4 +1,4 @@ -import { CONSTANTS as MAIN_CONSTANTS } from "../../../constants/constants.js" +import { CONSTANTS as MAIN_CONSTANTS } from "../../../../constants/constants.js" /* Structs: @@ -9,7 +9,7 @@ import { CONSTANTS as MAIN_CONSTANTS } from "../../../constants/constants.js" const SUBSCRIBED_TO_KEY = "sama:subscribed_to" const SUBSCRIBERS_KEY = "sama:subscribers" -class ActivityClusterManagerService { +class ActivityManagerClusterService { constructor(redisConnection, userService) { this.redisConnection = redisConnection this.userService = userService @@ -114,4 +114,4 @@ class ActivityClusterManagerService { } } -export default ActivityClusterManagerService +export default ActivityManagerClusterService diff --git a/app/providers/services/activity_manager/activity_manager_standalone/Provider.js b/app/providers/services/activity_manager/activity_manager_standalone/Provider.js new file mode 100644 index 00000000..6a389cc3 --- /dev/null +++ b/app/providers/services/activity_manager/activity_manager_standalone/Provider.js @@ -0,0 +1,16 @@ +import RegisterProvider from "../../../../common/RegisterProvider.js" +import ActivityManagerStandaloneService from "./index.js" + +import { ACTIVITY } from "../../../../store/activity.js" + +const name = "ActivityManagerStandaloneService" + +class ActivityManagerStandaloneServiceRegisterProvider extends RegisterProvider { + register(slc) { + const userService = slc.use("UserService") + + return new ActivityManagerStandaloneService(ACTIVITY, userService) + } +} + +export default new ActivityManagerStandaloneServiceRegisterProvider({ name, implementationName: ActivityManagerStandaloneService.name }) diff --git a/app/providers/services/activity_manager/index.js b/app/providers/services/activity_manager/activity_manager_standalone/index.js similarity index 92% rename from app/providers/services/activity_manager/index.js rename to app/providers/services/activity_manager/activity_manager_standalone/index.js index c314bd82..dda04c62 100644 --- a/app/providers/services/activity_manager/index.js +++ b/app/providers/services/activity_manager/activity_manager_standalone/index.js @@ -1,6 +1,6 @@ -import { CONSTANTS as MAIN_CONSTANTS } from "../../../constants/constants.js" +import { CONSTANTS as MAIN_CONSTANTS } from "../../../../constants/constants.js" -class ActivityManagerService { +class ActivityManagerStandaloneService { constructor(ACTIVITY, userService) { this.ACTIVITY = ACTIVITY this.userService = userService @@ -83,4 +83,4 @@ class ActivityManagerService { } } -export default ActivityManagerService +export default ActivityManagerStandaloneService From 6473625bfc122969eab724107dfbaa426e740dfc Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Wed, 4 Mar 2026 15:51:25 +0200 Subject: [PATCH 04/70] imp base clean node data --- app/cluster/cluster_manager.js | 190 +++++++++++++------ app/cluster/cluster_syncer.js | 80 -------- app/providers/services/cluster_node/index.js | 12 +- app/providers/services/session/index.js | 34 ++-- app/utils/build_ws_endpoint.js | 4 +- app/utils/get_ip_from_ws_url.js | 4 +- app/utils/pass_through_mapper.js | 3 - app/utils/split_ws_endpoint.js | 10 +- index.js | 3 +- 9 files changed, 178 insertions(+), 162 deletions(-) delete mode 100644 app/cluster/cluster_syncer.js delete mode 100644 app/utils/pass_through_mapper.js diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index 1c2459f2..1fdc74c5 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -5,92 +5,151 @@ import { StringDecoder } from "node:string_decoder" import config from "../config/index.js" import mainLogger from "../logger/index.js" +import ServiceLocatorContainer from "../common/ServiceLocatorContainer.js" + import packetManager from "../networking/packet_manager.js" import { buildWsEndpoint } from "../utils/build_ws_endpoint.js" -import { getIpFromWsUrl } from "../utils/get_ip_from_ws_url.js" const logger = mainLogger.child("[ClusterManager]") +const loggerSender = logger.child("[Sender]") +const loggerReceiver = logger.child("[Receiver]") const decoder = new StringDecoder("utf8") class ClusterManager { - #clusterNodesWS = {} + #nodesSyncInterval = void 0 + + #localSocket = void 0 + clusterNodesConnections = {} + + async startSyncingClusterNodes() { + if (this.#nodesSyncInterval) { + return + } + + this.#nodesSyncInterval = setInterval(() => this.#syncCluster(), config.get("ws.cluster.nodeExpiresIn") - 3_000) + + this.#syncCluster() + } + + async stopSyncingClusterNodes() { + clearInterval(this.#nodesSyncInterval) + this.#nodesSyncInterval = void 0 + } + + async #syncCluster() { + const sessionService = ServiceLocatorContainer.use("SessionService") + + await this.#storeCurrentNode(sessionService.sessionsTotal) + + await this.#retrieveExistingClusterNodes() + } + + async #storeCurrentNode(usersCount) { + const clusterNodeService = ServiceLocatorContainer.use("ClusterNodeService") + + const addressParams = { + ip_address: config.get("app.ip"), + hostname: config.get("app.hostName"), + port: config.get("ws.cluster.port"), + } + + const optionalParams = { + users_count: usersCount, + } + + await clusterNodeService.create(addressParams, optionalParams) + } + + async #retrieveExistingClusterNodes() { + const clusterNodeService = ServiceLocatorContainer.use("ClusterNodeService") + const nodeList = await clusterNodeService.retrieveAll() + + loggerSender.debug("[nodes] %j %s", nodeList, nodeList.length) - #localSocket = null + const destroyedNodes = [] - get clusterNodesWS() { - return this.#clusterNodesWS + for (const nodeInfo of nodeList) { + const nodeEndpoint = buildWsEndpoint(nodeInfo.ip_address, nodeInfo.port) + const isCurrentNode = config.get("ws.cluster.endpoint") === nodeEndpoint + if (isCurrentNode) { + continue + } + + try { + await this.retrieveConnectionWithNode(nodeEndpoint) + } catch (error) { + loggerSender.error(error, "[connect node][failed] %s", nodeEndpoint) + destroyedNodes.push(nodeEndpoint) + } + } + + for (const nodeEndpoint of destroyedNodes) { + await this.cleanDestroyedNodeData(nodeEndpoint) + } } #shareCurrentNodeInfo(ws) { ws.send( JSON.stringify({ node_info: { + endpoint: config.get("ws.cluster.endpoint"), ip: config.get("app.ip"), + host: config.get("app.hostName"), + port: config.get("ws.cluster.port") }, }) ) } - async senderClusterDeliverPacket(nodeUrl, deliverPacket) { - const ip = getIpFromWsUrl(nodeUrl) - const port = nodeUrl.split(":").at(2) - - let recipientClusterNodeWS = this.clusterNodesWS[getIpFromWsUrl(nodeUrl)] - - if (!recipientClusterNodeWS) { - recipientClusterNodeWS = await this.createSocketWithNode(ip, port) + async retrieveConnectionWithNode(nodeEndpoint) { + const existingWS = this.clusterNodesConnections[nodeEndpoint] + if (existingWS) { + return existingWS } - const clusterPacket = { deliverPacket } - recipientClusterNodeWS.send(JSON.stringify(clusterPacket)) + loggerSender.debug("[connect node] %s", nodeEndpoint) + + return await this.createConnectionWithNode(nodeEndpoint) } - async createSocketWithNode(ip, port) { - return new Promise((resolve, reject) => { - const existingWS = this.clusterNodesWS[ip] - if (existingWS) { - resolve(existingWS) - return - } + async createConnectionWithNode(nodeEndpoint) { + const nodeSederLogger = loggerSender.child(`[${nodeEndpoint}]`) - const url = buildWsEndpoint(ip, port) - if (!url) { - reject(`[ClusterManager][createSocketWithNode] can't create To Node Socket w/o url`) - return - } + return new Promise((resolve, reject) => { + const ws = new WebSocket(nodeEndpoint) - const ws = new WebSocket(url) + ws.nodeEndpoint = nodeEndpoint ws.on("error", async (event) => { - logger.error(event, "[createSocketWithNode] ws on Error") - reject("[ClusterManager][createSocketWithNode] ws on error") + nodeSederLogger.error(event, "[error]") + reject(new Error(`[connection][error] ${nodeEndpoint}`)) }) ws.on("open", async () => { - logger.debug("[createSocketWithNode] ws on Open url %s", ws.url) + nodeSederLogger.debug("[Open] %s", ws.nodeEndpoint) this.#shareCurrentNodeInfo(ws) }) ws.on("message", async (data) => { const json = JSON.parse(decoder.write(Buffer.from(data))) - logger.trace("ws on Message %j", json) + nodeSederLogger.trace("[packet] %j", json) if (json.node_info) { const nodeInfo = json.node_info - this.clusterNodesWS[nodeInfo.ip] = ws + this.clusterNodesConnections[nodeInfo.endpoint] = ws + + nodeSederLogger.debug("[node handshake finished] %s", nodeInfo.endpoint) resolve(ws) return } - - await packetManager.deliverClusterMessageToUser(json.deliverPacket) }) ws.on("close", async () => { - logger.debug("[createSocketWithNode] ws on Close %s", ws.url) - delete this.clusterNodesWS[getIpFromWsUrl(ws.url)] + nodeSederLogger.debug("[Close] %s", ws.nodeEndpoint) + await this.cleanDestroyedNodeData(ws.nodeEndpoint) }) }) } @@ -103,50 +162,69 @@ class ClusterManager { ...wsOptions, open: (ws) => { - logger.debug("[WS] on Open IP: %s", Buffer.from(ws.getRemoteAddressAsText()).toString()) - }, - - close: async (ws, code, message) => { - logger.debug("[WS] on Close") - for (const nodeIp in this.clusterNodesWS) { - if (this.clusterNodesWS[nodeIp] !== ws) { - continue - } - - delete this.clusterNodesWS[nodeIp] - return - } + loggerReceiver.debug("[Open] IP: %s", Buffer.from(ws.getRemoteAddressAsText()).toString()) }, message: async (ws, message, isBinary) => { const json = JSON.parse(decoder.write(Buffer.from(message))) - logger.trace("[WS] on Message %j", json) + loggerReceiver.trace("[packet] %j", json) if (json.node_info) { const nodeInfo = json.node_info - this.clusterNodesWS[nodeInfo.ip] = ws - this.#shareCurrentNodeInfo(ws) + ws.nodeEndpoint = nodeInfo.endpoint + this.clusterNodesConnections[nodeInfo.endpoint] = ws + loggerReceiver.debug("[node handshake finished] %s", nodeInfo.endpoint) + this.#shareCurrentNodeInfo(ws) return } - await packetManager.deliverClusterMessageToUser(json.deliverPacket) + if (json.deliverPacket) { + await packetManager.deliverClusterMessageToUser(json.deliverPacket) + } + }, + + close: async (ws, code, message) => { + logger.debug("[Close] %s Code: %s", ws.nodeEndpoint, code) + await this.cleanDestroyedNodeData(ws.nodeEndpoint) }, }) this.#localSocket.listen(0, listenOptions, (listenSocket) => { if (!listenSocket) { - throw new Error(`[ClusterManager][WS] socket.listen error: can't allocate port`) + throw new Error(`[ClusterManager] error: can't allocate port`) } const clusterPort = uWS.us_socket_local_port(listenSocket) - logger.debug("[ClusterManager][WS] listening on port %s", clusterPort) + loggerReceiver.debug(" listening on port %s", clusterPort) return resolve(clusterPort) }) }) } + + async senderClusterDeliverPacket(nodeUrl, deliverPacket) { + let recipientClusterNodeWS = this.clusterNodesConnections[nodeUrl] + + if (!recipientClusterNodeWS) { + recipientClusterNodeWS = await this.retrieveConnectionWithNode(ip, port) + } + + const clusterPacket = { deliverPacket } + recipientClusterNodeWS.send(JSON.stringify(clusterPacket)) + } + + async cleanDestroyedNodeData(nodeEndpoint) { + logger.debug("[clean node] %s", nodeEndpoint) + + delete this.clusterNodesConnections[nodeEndpoint] + + const sessionService = ServiceLocatorContainer.use("SessionService") + + await sessionService.clearNodeUsersSession(nodeEndpoint) + .catch(error => logger.error(error, "[clean node]")) + } } export default new ClusterManager() diff --git a/app/cluster/cluster_syncer.js b/app/cluster/cluster_syncer.js deleted file mode 100644 index b39d3e4f..00000000 --- a/app/cluster/cluster_syncer.js +++ /dev/null @@ -1,80 +0,0 @@ -import config from "../config/index.js" -import logger from "../logger/index.js" - -import clusterManager from "./cluster_manager.js" - -import ServiceLocatorContainer from "@sama/common/ServiceLocatorContainer.js" - -class ClusterSyncer { - constructor() { - this.nodes = {} - - this.nodesSyncInterval = null - } - - async startSyncingClusterNodes() { - if (this.nodesSyncInterval) { - return - } - - this.#syncCluster() - - this.nodesSyncInterval = setInterval(() => { - this.#syncCluster() - }, config.get("ws.cluster.nodeExpiresIn")) - } - - async stopSyncingClusterNodes() { - clearInterval(this.nodesSyncInterval) - this.nodesSyncInterval = null - } - - async #retrieveExistingClusterNodes() { - const clusterNodeService = ServiceLocatorContainer.use("ClusterNodeService") - const nodeList = await clusterNodeService.retrieveAll() - - // initiate connect to other node - nodeList.forEach(async (n) => { - if ( - config.get("ws.cluster.port") !== n.port && - `${config.get("app.hostName")}${config.get("ws.cluster.port")}` < `${n.hostname}${n.port}` - ) - try { - await clusterManager.createSocketWithNode(n.ip_address, n.port) - } catch (err) { - logger.error(err, "[ClusterSyncer][createSocketWithNode]") - } - }) - - this.nodes = nodeList - - // TODO - // if some node is gone, we may need to do some cleaning ? - } - - async #storeCurrentNode(usersCount) { - const clusterNodeService = ServiceLocatorContainer.use("ClusterNodeService") - - const addressParams = { - ip_address: config.get("app.ip"), - hostname: config.get("app.hostName"), - port: config.get("ws.cluster.port"), - } - - const optionalParams = { - users_count: usersCount, - } - - await clusterNodeService.create(addressParams, optionalParams) - } - - async #syncCluster() { - const sessionService = ServiceLocatorContainer.use("SessionService") - - await this.#storeCurrentNode(sessionService.sessionsTotal) - - await this.#retrieveExistingClusterNodes() - } -} - -export default new ClusterSyncer() diff --git a/app/providers/services/cluster_node/index.js b/app/providers/services/cluster_node/index.js index 2f95e762..014a7741 100644 --- a/app/providers/services/cluster_node/index.js +++ b/app/providers/services/cluster_node/index.js @@ -6,7 +6,17 @@ class ClusterNodeService { async retrieveAll() { const clusterNodes = await this.clusterNodeRepo.findAll({}) - return clusterNodes + return this.normalizeClusterNodes(clusterNodes) + } + + normalizeClusterNodes(clusterNodes) { + const clusterNodeObj = clusterNodes.reduce((acc, clusterNode) => { + const key = `${clusterNode.ip_address}:${clusterNode.port}` + acc[key] = clusterNode + return acc + }, {}) + + return Object.values(clusterNodeObj) } async create(addressParams, optionalParams) { diff --git a/app/providers/services/session/index.js b/app/providers/services/session/index.js index 55f0ce29..d25a4fc1 100644 --- a/app/providers/services/session/index.js +++ b/app/providers/services/session/index.js @@ -49,16 +49,16 @@ class SessionService { return `sama-node:${nodeEndpoint ? nodeEndpoint : buildWsEndpoint(nodeIp, nodePort)}` } - async addUserDeviceToNode(nodeIp, nodePort, userId, deviceId) { + async addUserDeviceToNode(nodeIp, nodePort, organizationId, userId, deviceId) { const nodeKey = this.#nodesSetCacheKey(nodeIp, nodePort) - const userConnectionMember = `${userId}:${deviceId}` + const userConnectionMember = `${organizationId}:${userId}:${deviceId}` await this.redisConnection.client.sAdd(nodeKey, userConnectionMember) } - async removeUserDeviceFromNode(nodeIp, nodePort, userId, deviceId) { + async removeUserDeviceFromNode(nodeIp, nodePort, organizationId, userId, deviceId) { const nodeKey = this.#nodesSetCacheKey(nodeIp, nodePort) - const userConnectionMember = `${userId}:${deviceId}` + const userConnectionMember = `${organizationId}:${userId}:${deviceId}` await this.redisConnection.client.sRem(nodeKey, userConnectionMember) } @@ -68,9 +68,9 @@ class SessionService { const usersConnections = await this.redisConnection.client.sMembers(nodeKey) const users = usersConnections.map((userConnection) => { - const [userId, deviceId] = userConnection.split(":") + const [organizationId, userId, deviceId] = userConnection.split(":") - return { userId, deviceId } + return { organizationId, userId, deviceId } }) return users @@ -98,6 +98,12 @@ class SessionService { const userKey = this.#usersSetCacheKey(organizationId, userId) await this.redisConnection.client.sRem(userKey, deviceId) + + const leftUserDevices = await this.listUserDevice(organizationId, userId) + + if (!leftUserDevices?.length) { + await this.deleteUserDevices(organizationId, userId) + } } async listUserDevice(organizationId, userId) { @@ -200,12 +206,12 @@ class SessionService { if (userDeviceIds.includes(deviceId)) { await this.removeUserData(organizationId, userId, deviceId) - await this.removeUserDeviceFromNode(nodeIp, nodePort, userId, deviceId) + await this.removeUserDeviceFromNode(nodeIp, nodePort, organizationId, userId, deviceId) } await this.addUserDevice(organizationId, userId, deviceId) await this.addUserExtraParams(userId, deviceId, { [CONSTANTS.SESSION_NODE_KEY]: nodeEndpoint }) - await this.addUserDeviceToNode(nodeIp, nodePort, userId, deviceId) + await this.addUserDeviceToNode(nodeIp, nodePort, organizationId, userId, deviceId) } async clearNodeUsersSession(nodeUrl) { @@ -213,8 +219,8 @@ class SessionService { const userConnections = await this.listNodeUserDevices(void 0, void 0, nodeUrl) - for (const { userId, deviceId } of userConnections) { - await this.removeUserData(null, userId, deviceId) + for (const { organizationId, userId, deviceId } of userConnections) { + await this.removeUserData(organizationId, userId, deviceId) } await this.deleteNodeConnections(void 0, void 0, nodeUrl) @@ -305,7 +311,7 @@ class SessionService { } const [, nodeId, nodePort] = splitWsEndpoint(extraParams[CONSTANTS.SESSION_NODE_KEY]) - await this.removeUserDeviceFromNode(nodeId, nodePort, userId, deviceId) + await this.removeUserDeviceFromNode(nodeId, nodePort, organizationId, userId, deviceId) } await this.deleteUserData(organizationId, userId) @@ -314,7 +320,7 @@ class SessionService { async removeUserSession(socket, userId, deviceId) { userId = userId ?? this.getSessionUserId(socket) deviceId = deviceId ?? this.getDeviceId(socket, userId) - const orgId = this.getSession(socket)?.organizationId + const organizationId = this.getSession(socket)?.organizationId const leftActiveConnections = this.getUserDevices(userId).filter(({ deviceId: activeDeviceId }) => activeDeviceId !== deviceId) @@ -333,7 +339,7 @@ class SessionService { const extraParams = await this.retrieveUserExtraParams(userId, deviceId) - await this.removeUserDevice(orgId, userId, deviceId) + await this.removeUserDevice(organizationId, userId, deviceId) await this.deleteUserExtraParams(userId, deviceId) const nodeEndpoint = extraParams?.[CONSTANTS.SESSION_NODE_KEY] @@ -342,7 +348,7 @@ class SessionService { } const [, nodeId, nodePort] = splitWsEndpoint(nodeEndpoint) - await this.removeUserDeviceFromNode(nodeId, nodePort, userId, deviceId) + await this.removeUserDeviceFromNode(nodeId, nodePort, organizationId, userId, deviceId) } async onlineUsersList(organizationId, offset = 0, limit = 10) { diff --git a/app/utils/build_ws_endpoint.js b/app/utils/build_ws_endpoint.js index bca1cc91..3cb87b45 100644 --- a/app/utils/build_ws_endpoint.js +++ b/app/utils/build_ws_endpoint.js @@ -1,3 +1,5 @@ +import { URL } from 'node:url' + import config from "@sama/config/index.js" -export const buildWsEndpoint = (ip, port) => `${config.get("ws.options.isSecure") ? "wss" : "ws"}://${ip}:${port}` +export const buildWsEndpoint = (ip, port) => new URL(`${config.get("ws.options.isSecure") ? "wss" : "ws"}://${ip}:${port}`).toString() diff --git a/app/utils/get_ip_from_ws_url.js b/app/utils/get_ip_from_ws_url.js index d29ffce3..9c93172e 100644 --- a/app/utils/get_ip_from_ws_url.js +++ b/app/utils/get_ip_from_ws_url.js @@ -1 +1,3 @@ -export const getIpFromWsUrl = (wsUrl) => wsUrl.split(":")[1].slice(2) +import { URL } from "node:url" + +export const getIpFromWsUrl = wsUrl => new URL(wsUrl).hostname diff --git a/app/utils/pass_through_mapper.js b/app/utils/pass_through_mapper.js deleted file mode 100644 index 8ab30ab4..00000000 --- a/app/utils/pass_through_mapper.js +++ /dev/null @@ -1,3 +0,0 @@ -const passThroughMapper = async (model) => model - -export default passThroughMapper diff --git a/app/utils/split_ws_endpoint.js b/app/utils/split_ws_endpoint.js index 937e1e8c..a2bce86f 100644 --- a/app/utils/split_ws_endpoint.js +++ b/app/utils/split_ws_endpoint.js @@ -1,5 +1,7 @@ -export const splitWsEndpoint = (url) => { - const regex = /^(.*?):\/\/(.*?)(?::([0-9]+))?(\/.*?)?$/ - const [, protocol, ip, port, path] = url.match(regex) - return [protocol, ip, port, path] +import { URL } from "node:url" + +export const splitWsEndpoint = url => { + const parsedUrl = new URL(url) + + return [parsedUrl.protocol, parsedUrl.hostname, parsedUrl.port, parsedUrl.pathname] } diff --git a/index.js b/index.js index fee58008..733d0854 100644 --- a/index.js +++ b/index.js @@ -11,7 +11,6 @@ import RegisterProvider from "./app/common/RegisterProvider.js" import providers from "./app/providers/index.js" import clusterManager from "./app/cluster/cluster_manager.js" -import clusterSyncer from "./app/cluster/cluster_syncer.js" import WsProtocol from "./app/networking/protocol_processors/ws.js" import TcpProtocol from "./app/networking/protocol_processors/tcp.js" @@ -177,7 +176,7 @@ await ServiceLocatorContainer.createAllSingletonInstances() if (!config.get("app.isStandAloneNode")) { // Start Cluster Sync logger.debug("[Start sync]") - await clusterSyncer.startSyncingClusterNodes() + await clusterManager.startSyncingClusterNodes() } // Start public protocols From bd7907d9807982ecb806fc34764e22c8d82547df Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 5 Mar 2026 14:16:21 +0200 Subject: [PATCH 05/70] it can send offline status for died node users --- APIs/JSON/controllers/http/auth.js | 12 ++-- APIs/JSON/controllers/users.js | 12 ++-- APIs/XMPP | 2 +- app/cluster/cluster_manager.js | 62 ++++++++++++------- app/networking/activity_sender.js | 2 +- app/networking/packet_manager.js | 6 +- app/networking/protocol_processors/base.js | 13 ++-- app/networking/protocol_processors/http.js | 3 - app/networking/protocol_processors/tcp.js | 2 +- app/networking/protocol_processors/ws.js | 3 +- .../operations/http/user/logout/index.js | 8 ++- app/providers/operations/user/logout/index.js | 4 +- .../activity_manager_cluster/index.js | 4 +- app/providers/services/session/index.js | 32 +++++++--- index.js | 2 +- 15 files changed, 106 insertions(+), 61 deletions(-) diff --git a/APIs/JSON/controllers/http/auth.js b/APIs/JSON/controllers/http/auth.js index efb5ecfe..3227030f 100644 --- a/APIs/JSON/controllers/http/auth.js +++ b/APIs/JSON/controllers/http/auth.js @@ -43,7 +43,7 @@ class HttpAuthController extends BaseHttpController { async logout(res, req) { const httpLogoutOperation = ServiceLocatorContainer.use("HttpUserLogoutOperation") - const refreshTokenRecord = await httpLogoutOperation.perform(res.fakeWsSessionKey, res.parsedHeaders, res.parsedSignedCookies) + const { refreshTokenRecord, isWasLastUserSession } = await httpLogoutOperation.perform(res.fakeWsSessionKey, res.parsedHeaders, res.parsedSignedCookies) const httpResponse = new HttpResponse(200, {}, { success: true }).addCookie("refresh_token", refreshTokenRecord.token, { maxAge: 0, @@ -52,15 +52,19 @@ class HttpAuthController extends BaseHttpController { sameSite: "lax", }) - return new Response() - .setHttpResponse(httpResponse) - .updateLastActivityStatus( + const response = new Response().setHttpResponse(httpResponse) + + if (isWasLastUserSession) { + response.updateLastActivityStatus( new LastActivityStatusResponse( refreshTokenRecord.organization_id, refreshTokenRecord.user_id, MAIN_CONSTANTS.LAST_ACTIVITY_STATUS.OFFLINE ) ) + } + + return response } } diff --git a/APIs/JSON/controllers/users.js b/APIs/JSON/controllers/users.js index 87e74b51..e295cc7a 100644 --- a/APIs/JSON/controllers/users.js +++ b/APIs/JSON/controllers/users.js @@ -56,11 +56,15 @@ class UsersController extends BaseJSONController { const { id: requestId } = data const userLogoutOperation = ServiceLocatorContainer.use("UserLogoutOperation") - const { organizationId, userId } = await userLogoutOperation.perform(ws) + const { organizationId, userId, isWasLastUserSession } = await userLogoutOperation.perform(ws) - return new Response() - .addBackMessage({ response: { id: requestId, success: true } }) - .updateLastActivityStatus(new LastActivityStatusResponse(organizationId, userId, MAIN_CONSTANTS.LAST_ACTIVITY_STATUS.OFFLINE)) + const response = new Response().addBackMessage({ response: { id: requestId, success: true } }) + + if (isWasLastUserSession) { + response.updateLastActivityStatus(new LastActivityStatusResponse(organizationId, userId, MAIN_CONSTANTS.LAST_ACTIVITY_STATUS.OFFLINE)) + } + + return response } async send_otp(ws, data) { diff --git a/APIs/XMPP b/APIs/XMPP index ba4c3af7..901be2bf 160000 --- a/APIs/XMPP +++ b/APIs/XMPP @@ -1 +1 @@ -Subproject commit ba4c3af760d78fb9da77e64949e6186b99878f0f +Subproject commit 901be2bfff124a97645b0271fc0ba55b44590a99 diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index 1fdc74c5..a4c977d6 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -1,12 +1,16 @@ +import { StringDecoder } from "node:string_decoder" + import WebSocket from "ws" import uWS from "uWebSockets.js" -import { StringDecoder } from "node:string_decoder" import config from "../config/index.js" import mainLogger from "../logger/index.js" +import BaseProtocolProcessor from "../networking/protocol_processors/base.js" + import ServiceLocatorContainer from "../common/ServiceLocatorContainer.js" +import activitySender from "../networking/activity_sender.js" import packetManager from "../networking/packet_manager.js" import { buildWsEndpoint } from "../utils/build_ws_endpoint.js" @@ -17,20 +21,24 @@ const loggerReceiver = logger.child("[Receiver]") const decoder = new StringDecoder("utf8") -class ClusterManager { +class ClusterManager extends BaseProtocolProcessor { #nodesSyncInterval = void 0 #localSocket = void 0 clusterNodesConnections = {} - async startSyncingClusterNodes() { + async startSyncingClusterNodes(isColdStart) { if (this.#nodesSyncInterval) { return } - this.#nodesSyncInterval = setInterval(() => this.#syncCluster(), config.get("ws.cluster.nodeExpiresIn") - 3_000) + if (isColdStart) { + await this.cleanDestroyedNodeData(config.get("ws.cluster.endpoint")) + } + + this.#nodesSyncInterval = setInterval(() => this.#syncCluster(), config.get("ws.cluster.nodeExpiresIn") - 2_000) - this.#syncCluster() + this.#syncCluster(isColdStart) } async stopSyncingClusterNodes() { @@ -133,12 +141,12 @@ class ClusterManager { }) ws.on("message", async (data) => { - const json = JSON.parse(decoder.write(Buffer.from(data))) + const clusterPacket = JSON.parse(decoder.write(Buffer.from(data))) - nodeSederLogger.trace("[packet] %j", json) + nodeSederLogger.trace("[packet] %j", clusterPacket) - if (json.node_info) { - const nodeInfo = json.node_info + if (clusterPacket.node_info) { + const nodeInfo = clusterPacket.node_info this.clusterNodesConnections[nodeInfo.endpoint] = ws nodeSederLogger.debug("[node handshake finished] %s", nodeInfo.endpoint) @@ -166,12 +174,12 @@ class ClusterManager { }, message: async (ws, message, isBinary) => { - const json = JSON.parse(decoder.write(Buffer.from(message))) + const clusterPacket = JSON.parse(decoder.write(Buffer.from(message))) - loggerReceiver.trace("[packet] %j", json) + loggerReceiver.trace("[packet] %j", clusterPacket) - if (json.node_info) { - const nodeInfo = json.node_info + if (clusterPacket.node_info) { + const nodeInfo = clusterPacket.node_info ws.nodeEndpoint = nodeInfo.endpoint this.clusterNodesConnections[nodeInfo.endpoint] = ws @@ -180,8 +188,8 @@ class ClusterManager { return } - if (json.deliverPacket) { - await packetManager.deliverClusterMessageToUser(json.deliverPacket) + if (clusterPacket.deliverPacket) { + await packetManager.deliverClusterMessageToUser(clusterPacket.deliverPacket) } }, @@ -191,7 +199,7 @@ class ClusterManager { }, }) - this.#localSocket.listen(0, listenOptions, (listenSocket) => { + this.#localSocket.listen(parseInt(config.get("ws.cluster.port")) || 0, listenOptions, (listenSocket) => { if (!listenSocket) { throw new Error(`[ClusterManager] error: can't allocate port`) } @@ -205,14 +213,14 @@ class ClusterManager { } async senderClusterDeliverPacket(nodeUrl, deliverPacket) { - let recipientClusterNodeWS = this.clusterNodesConnections[nodeUrl] + let recipientClusterNodeConnection = this.clusterNodesConnections[nodeUrl] - if (!recipientClusterNodeWS) { - recipientClusterNodeWS = await this.retrieveConnectionWithNode(ip, port) + if (!recipientClusterNodeConnection) { + recipientClusterNodeConnection = await this.retrieveConnectionWithNode(ip, port) } const clusterPacket = { deliverPacket } - recipientClusterNodeWS.send(JSON.stringify(clusterPacket)) + recipientClusterNodeConnection.send(JSON.stringify(clusterPacket)) } async cleanDestroyedNodeData(nodeEndpoint) { @@ -222,9 +230,19 @@ class ClusterManager { const sessionService = ServiceLocatorContainer.use("SessionService") - await sessionService.clearNodeUsersSession(nodeEndpoint) + const lastUserSessions = await sessionService.clearNodeUsersSession(nodeEndpoint) .catch(error => logger.error(error, "[clean node]")) + + if (!lastUserSessions) { + return + } + + const offlineUserResponses = lastUserSessions.map(userSession => activitySender.buildOfflineActivityResponse(userSession.organizationId, userSession.userId)) + + for (const offlineUserResponse of offlineUserResponses) { + await this.processAPIResponse(void 0, offlineUserResponse) + } } } -export default new ClusterManager() +export default new ClusterManager(void 0, void 0) diff --git a/app/networking/activity_sender.js b/app/networking/activity_sender.js index d5d0612e..bed9adfd 100644 --- a/app/networking/activity_sender.js +++ b/app/networking/activity_sender.js @@ -11,7 +11,7 @@ import LastActivityStatusResponse from "@sama/networking/models/LastActivityStat class ActivitySender { detectSocketAPI(socket) { - const api = APIs[socket.apiType ?? BASE_API] + const api = APIs[socket?.apiType ?? BASE_API] return api } diff --git a/app/networking/packet_manager.js b/app/networking/packet_manager.js index fb3a0812..73aa7ff8 100644 --- a/app/networking/packet_manager.js +++ b/app/networking/packet_manager.js @@ -57,7 +57,7 @@ class PacketManager { } } - #deliverToUserDevices(socket, nodeConnections, userId, packet, ignoreSelf) { + #deliverToUserDevices(socket, orgId, nodeConnections, userId, packet, ignoreSelf) { const sessionService = ServiceLocatorContainer.use("SessionService") const senderUserSession = sessionService.getSession(socket) const senderDeviceId = senderUserSession ? sessionService.getDeviceId(socket, senderUserSession.userId) : null @@ -66,7 +66,7 @@ class PacketManager { const senderInfo = { apiType: socket?.apiType, - session: senderUserSession, + session: senderUserSession ?? { organizationId: orgId, userId }, deviceId: senderDeviceId, node: currentNodeUrl, } @@ -138,7 +138,7 @@ class PacketManager { offlineUsersByPackets.push(userId) } - this.#deliverToUserDevices(socket, userNodeData, userId, packet, ignoreSelf) + this.#deliverToUserDevices(socket, orgId, userNodeData, userId, packet, ignoreSelf) } if (pushQueueMessage && offlineUsersByPackets.length) { diff --git a/app/networking/protocol_processors/base.js b/app/networking/protocol_processors/base.js index ca9abf0c..db1e5c98 100644 --- a/app/networking/protocol_processors/base.js +++ b/app/networking/protocol_processors/base.js @@ -2,7 +2,7 @@ import { StringDecoder } from "node:string_decoder" import { v4 as uuid } from "uuid" import logger from "../../logger/index.js" -import { createStore, updateStoreContext } from "../../logger/async_store.js" +import { createStore } from "../../logger/async_store.js" import { ERROR_STATUES } from "../../constants/errors.js" import { CONSTANTS as MAIN_CONSTANTS } from "../../constants/constants.js" import { BASE_API, APIs, detectAPIType } from "../APIs.js" @@ -16,7 +16,7 @@ import MappableMessage from "../models/MappableMessage.js" const decoder = new StringDecoder("utf8") -const mapBackMessageFunc = async (ws, packet) => packetMapper.mapPacket(null, ws.apiType, packet, {}, {}) +const mapBackMessageFunc = async (socket, packet) => packetMapper.mapPacket(null, socket.apiType, packet, {}, {}) class BaseProtocolProcessor { constructor(sessionService, conversationService) { @@ -140,7 +140,7 @@ class BaseProtocolProcessor { } async processUpdateLastActivityResponse(socket, response) { - let { organizationId, userId } = this.sessionService.getSession(socket) ?? {} + let { organizationId, userId } = this.sessionService?.getSession(socket) ?? {} organizationId = response.lastActivityStatusResponse.orgId ?? organizationId userId = response.lastActivityStatusResponse.userId ?? userId @@ -152,6 +152,7 @@ class BaseProtocolProcessor { userId, response.lastActivityStatusResponse.status ) + responses.forEach((activityResponse) => response.merge(activityResponse)) return response @@ -220,7 +221,11 @@ class BaseProtocolProcessor { return } - await this.sessionService.removeUserSession(socket, userId) + const isWasLastUserSession = await this.sessionService.removeUserSession(socket, userId) + + if (!isWasLastUserSession) { + return + } await this.processAPIResponse(socket, activitySender.buildOfflineActivityResponse(organizationId, userId), true) } diff --git a/app/networking/protocol_processors/http.js b/app/networking/protocol_processors/http.js index e1f1380c..0d821928 100644 --- a/app/networking/protocol_processors/http.js +++ b/app/networking/protocol_processors/http.js @@ -1,6 +1,3 @@ -import os from "node:os" - -import { v4 as uuid } from "uuid" import { CONSTANTS as MAIN_CONSTANTS } from "../../constants/constants.js" import { ERROR_STATUES } from "../../constants/errors.js" diff --git a/app/networking/protocol_processors/tcp.js b/app/networking/protocol_processors/tcp.js index 255411f6..b1fa8fef 100644 --- a/app/networking/protocol_processors/tcp.js +++ b/app/networking/protocol_processors/tcp.js @@ -3,7 +3,7 @@ import tls from "node:tls" import { CONSTANTS as MAIN_CONSTANTS } from "../../constants/constants.js" import logger from "../../logger/index.js" -import { asyncLoggerContextStore, createStore, updateStoreContext } from "../../logger/async_store.js" +import { asyncLoggerContextStore, updateStoreContext } from "../../logger/async_store.js" import BaseProtocolProcessor from "./base.js" import { APIs, detectAPIType } from "../APIs.js" diff --git a/app/networking/protocol_processors/ws.js b/app/networking/protocol_processors/ws.js index 29f213e9..68999893 100644 --- a/app/networking/protocol_processors/ws.js +++ b/app/networking/protocol_processors/ws.js @@ -1,8 +1,7 @@ import uWS from "uWebSockets.js" -import { CONSTANTS as MAIN_CONSTANTS } from "../../constants/constants.js" import logger from "../../logger/index.js" -import { asyncLoggerContextStore, createStore } from "../../logger/async_store.js" +import { asyncLoggerContextStore } from "../../logger/async_store.js" import BaseProtocolProcessor from "./base.js" import { wsSafeSend } from "../../utils/sockets-utils.js" diff --git a/app/providers/operations/http/user/logout/index.js b/app/providers/operations/http/user/logout/index.js index 8e44a99b..db07e470 100644 --- a/app/providers/operations/http/user/logout/index.js +++ b/app/providers/operations/http/user/logout/index.js @@ -29,13 +29,17 @@ class HttpUserLogoutOperation { const { user_id: userId, device_id: deviceId } = refreshTokenRecord const socket = this.sessionService.getUserDevices(userId).find((el) => el.deviceId === deviceId)?.socket + const userDevices = await this.sessionService.listUserDevice(refreshTokenRecord.organization_id, refreshTokenRecord.user_id) + let isWasLastUserSession = !userDevices?.length + if (socket) { - await this.userLogoutOperation.perform(socket) + const logoutResult = await this.userLogoutOperation.perform(socket) + isWasLastUserSession = logoutResult.isWasLastUserSession } else { await this.userTokenRepo.deleteByUserId(userId, deviceId) } - return refreshTokenRecord + return { refreshTokenRecord, isWasLastUserSession } } } diff --git a/app/providers/operations/user/logout/index.js b/app/providers/operations/user/logout/index.js index 7e4fa99a..47ba7761 100644 --- a/app/providers/operations/user/logout/index.js +++ b/app/providers/operations/user/logout/index.js @@ -20,11 +20,11 @@ class UserLogoutOperation { await this.encryptionRepo.removeByDeviceId(userId, deviceId) - await this.sessionService.removeUserSession(ws, userId, deviceId) + const isWasLastUserSession = await this.sessionService.removeUserSession(ws, userId, deviceId) await this.userTokenRepo.deleteByUserId(userId, deviceId) - return { organizationId, userId } + return { organizationId, userId, isWasLastUserSession } } } diff --git a/app/providers/services/activity_manager/activity_manager_cluster/index.js b/app/providers/services/activity_manager/activity_manager_cluster/index.js index 249c3a8f..e7570368 100644 --- a/app/providers/services/activity_manager/activity_manager_cluster/index.js +++ b/app/providers/services/activity_manager/activity_manager_cluster/index.js @@ -19,7 +19,7 @@ class ActivityManagerClusterService { async subscribeTarget(observerId) { const targetId = await this.redisConnection.client.hGet(SUBSCRIBED_TO_KEY, `${observerId}`) - return targetId ? +targetId : void 0 + return targetId ? targetId : void 0 } async addSubscribeTarget(observerId, targetId) { @@ -38,7 +38,7 @@ class ActivityManagerClusterService { async subscribers(targetId) { const observersIds = await this.redisConnection.client.hGet(SUBSCRIBERS_KEY, `${targetId}`) - return observersIds ? observersIds.split(',').map(observerId => +observerId) : [] + return observersIds ? observersIds.split(',') : [] } async addSubscriber(targetId, observerId) { diff --git a/app/providers/services/session/index.js b/app/providers/services/session/index.js index d25a4fc1..ff0451f3 100644 --- a/app/providers/services/session/index.js +++ b/app/providers/services/session/index.js @@ -103,6 +103,8 @@ class SessionService { if (!leftUserDevices?.length) { await this.deleteUserDevices(organizationId, userId) + + return true } } @@ -151,8 +153,11 @@ class SessionService { } async removeUserData(organizationId, userId, deviceId) { - await this.removeUserDevice(organizationId, userId, deviceId) + const isWasLastUserSession = await this.removeUserDevice(organizationId, userId, deviceId) + await this.deleteUserExtraParams(userId, deviceId) + + return isWasLastUserSession } async deleteUserData(organizationId, userId) { @@ -217,13 +222,18 @@ class SessionService { async clearNodeUsersSession(nodeUrl) { if (this.config.get("app.isStandAloneNode")) return + const lastUserSessions = [] + const userConnections = await this.listNodeUserDevices(void 0, void 0, nodeUrl) - for (const { organizationId, userId, deviceId } of userConnections) { - await this.removeUserData(organizationId, userId, deviceId) + for (const userData of userConnections) { + const isLastUserSession = await this.removeUserData(userData.organizationId, userData.userId, userData.deviceId) + if (isLastUserSession) lastUserSessions.push(userData) } await this.deleteNodeConnections(void 0, void 0, nodeUrl) + + return lastUserSessions } setSessionUserId(socket, organizationId, userId, extraParams) { @@ -323,32 +333,36 @@ class SessionService { const organizationId = this.getSession(socket)?.organizationId const leftActiveConnections = this.getUserDevices(userId).filter(({ deviceId: activeDeviceId }) => activeDeviceId !== deviceId) + let isLastConnection = !leftActiveConnections?.length if (leftActiveConnections?.length) { this.activeSessions.DEVICES[userId] = leftActiveConnections } else { delete this.activeSessions.DEVICES[userId] } + this.activeSessions.SESSIONS.delete(socket) if (!deviceId) { - return + return isLastConnection } - if (this.config.get("app.isStandAloneNode")) return - - const extraParams = await this.retrieveUserExtraParams(userId, deviceId) + if (this.config.get("app.isStandAloneNode")) return isLastConnection - await this.removeUserDevice(organizationId, userId, deviceId) + isLastConnection = await this.removeUserDevice(organizationId, userId, deviceId) await this.deleteUserExtraParams(userId, deviceId) + const extraParams = await this.retrieveUserExtraParams(userId, deviceId) + const nodeEndpoint = extraParams?.[CONSTANTS.SESSION_NODE_KEY] if (!nodeEndpoint) { - return + return isLastConnection } const [, nodeId, nodePort] = splitWsEndpoint(nodeEndpoint) await this.removeUserDeviceFromNode(nodeId, nodePort, organizationId, userId, deviceId) + + return isLastConnection } async onlineUsersList(organizationId, offset = 0, limit = 10) { diff --git a/index.js b/index.js index 733d0854..11fef946 100644 --- a/index.js +++ b/index.js @@ -176,7 +176,7 @@ await ServiceLocatorContainer.createAllSingletonInstances() if (!config.get("app.isStandAloneNode")) { // Start Cluster Sync logger.debug("[Start sync]") - await clusterManager.startSyncingClusterNodes() + await clusterManager.startSyncingClusterNodes(true) } // Start public protocols From 3eb2c1a3885ad73caa4334dadb48c72ee1ac9578 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 5 Mar 2026 14:33:51 +0200 Subject: [PATCH 06/70] refactoring --- app/cluster/cluster_manager.js | 4 +- app/networking/packet_manager.js | 14 +++---- app/providers/services/session/index.js | 49 +++++++++++-------------- app/utils/get_ip_from_ws_url.js | 3 -- app/utils/split_ws_endpoint.js | 7 ---- 5 files changed, 31 insertions(+), 46 deletions(-) delete mode 100644 app/utils/get_ip_from_ws_url.js delete mode 100644 app/utils/split_ws_endpoint.js diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index a4c977d6..c2518727 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -212,8 +212,8 @@ class ClusterManager extends BaseProtocolProcessor { }) } - async senderClusterDeliverPacket(nodeUrl, deliverPacket) { - let recipientClusterNodeConnection = this.clusterNodesConnections[nodeUrl] + async senderClusterDeliverPacket(nodeEndpoint, deliverPacket) { + let recipientClusterNodeConnection = this.clusterNodesConnections[nodeEndpoint] if (!recipientClusterNodeConnection) { recipientClusterNodeConnection = await this.retrieveConnectionWithNode(ip, port) diff --git a/app/networking/packet_manager.js b/app/networking/packet_manager.js index 73aa7ff8..51e3e148 100644 --- a/app/networking/packet_manager.js +++ b/app/networking/packet_manager.js @@ -62,13 +62,13 @@ class PacketManager { const senderUserSession = sessionService.getSession(socket) const senderDeviceId = senderUserSession ? sessionService.getDeviceId(socket, senderUserSession.userId) : null - const currentNodeUrl = config.get("ws.cluster.endpoint") + const currentNodeEndpoint = config.get("ws.cluster.endpoint") const senderInfo = { apiType: socket?.apiType, session: senderUserSession ?? { organizationId: orgId, userId }, deviceId: senderDeviceId, - node: currentNodeUrl, + node: currentNodeEndpoint, } if (config.get("app.isStandAloneNode")) { @@ -81,13 +81,13 @@ class PacketManager { }) } else { Object.entries(nodeConnections).forEach(async ([nodeDeviceId, extraParams]) => { - const nodeUrl = extraParams[MAIN_CONSTANTS.SESSION_NODE_KEY] + const nodeEndpoint = extraParams[MAIN_CONSTANTS.SESSION_NODE_KEY] - if (!nodeUrl) { + if (!nodeEndpoint) { return } - if (currentNodeUrl === nodeUrl) { + if (currentNodeEndpoint === nodeEndpoint) { if (senderDeviceId === nodeDeviceId && ignoreSelf) { return } @@ -99,9 +99,9 @@ class PacketManager { try { const clusterPacket = { userId, packet, senderInfo } - await clusterManager.senderClusterDeliverPacket(nodeUrl, clusterPacket) + await clusterManager.senderClusterDeliverPacket(nodeEndpoint, clusterPacket) } catch (error) { - await sessionService.clearNodeUsersSession(nodeUrl) + await sessionService.clearNodeUsersSession(nodeEndpoint) logger.error(error, "[deliverToUserDevices] createSocketWithNode error") } }) diff --git a/app/providers/services/session/index.js b/app/providers/services/session/index.js index ff0451f3..a437edfa 100644 --- a/app/providers/services/session/index.js +++ b/app/providers/services/session/index.js @@ -1,10 +1,8 @@ -import { buildWsEndpoint } from "../../../utils/build_ws_endpoint.js" -import { splitWsEndpoint } from "../../../utils/split_ws_endpoint.js" import { CONSTANTS } from "../../../constants/constants.js" /* Structs: - SET - sama-node:{node-endpoint} -> {userId}:{deviceId} + SET - sama-node:{node-endpoint} -> {organizationId}:{userId}:{deviceId} SET - sama-user:{organizationId}:{userId} -> {deviceId} HASH - sama-user-data:{userId}:{deviceId} -> extra params */ @@ -45,26 +43,26 @@ class SessionService { return socketsToClose } - #nodesSetCacheKey(nodeIp, nodePort, nodeEndpoint) { - return `sama-node:${nodeEndpoint ? nodeEndpoint : buildWsEndpoint(nodeIp, nodePort)}` + #nodesSetCacheKey(nodeEndpoint) { + return `sama-node:${nodeEndpoint}` } - async addUserDeviceToNode(nodeIp, nodePort, organizationId, userId, deviceId) { - const nodeKey = this.#nodesSetCacheKey(nodeIp, nodePort) + async addUserDeviceToNode(nodeEndpoint, organizationId, userId, deviceId) { + const nodeKey = this.#nodesSetCacheKey(nodeEndpoint) const userConnectionMember = `${organizationId}:${userId}:${deviceId}` await this.redisConnection.client.sAdd(nodeKey, userConnectionMember) } - async removeUserDeviceFromNode(nodeIp, nodePort, organizationId, userId, deviceId) { - const nodeKey = this.#nodesSetCacheKey(nodeIp, nodePort) + async removeUserDeviceFromNode(nodeEndpoint, organizationId, userId, deviceId) { + const nodeKey = this.#nodesSetCacheKey(nodeEndpoint) const userConnectionMember = `${organizationId}:${userId}:${deviceId}` await this.redisConnection.client.sRem(nodeKey, userConnectionMember) } - async listNodeUserDevices(nodeIp, nodePort, nodeEndpoint) { - const nodeKey = this.#nodesSetCacheKey(nodeIp, nodePort, nodeEndpoint) + async listNodeUserDevices(nodeEndpoint) { + const nodeKey = this.#nodesSetCacheKey(nodeEndpoint) const usersConnections = await this.redisConnection.client.sMembers(nodeKey) const users = usersConnections.map((userConnection) => { @@ -76,8 +74,8 @@ class SessionService { return users } - async deleteNodeConnections(nodeIp, nodePort, nodeEndpoint) { - const nodeKey = this.#nodesSetCacheKey(nodeIp, nodePort, nodeEndpoint) + async deleteNodeConnections(nodeEndpoint) { + const nodeKey = this.#nodesSetCacheKey(nodeEndpoint) await this.redisConnection.client.del(nodeKey) } @@ -195,10 +193,8 @@ class SessionService { return userData } - async storeUserNodeData(socket, organizationId, userId, deviceId, nodeIp, nodePort) { - nodeIp ??= this.config.get("app.ip") - nodePort ??= this.config.get("ws.cluster.port") - const nodeEndpoint = buildWsEndpoint(nodeIp, nodePort) + async storeUserNodeData(socket, organizationId, userId, deviceId) { + const nodeEndpoint = this.config.get("ws.cluster.endpoint") const session = this.getSession(socket) if (session?.extraParams) { @@ -211,27 +207,27 @@ class SessionService { if (userDeviceIds.includes(deviceId)) { await this.removeUserData(organizationId, userId, deviceId) - await this.removeUserDeviceFromNode(nodeIp, nodePort, organizationId, userId, deviceId) + await this.removeUserDeviceFromNode(nodeEndpoint, organizationId, userId, deviceId) } await this.addUserDevice(organizationId, userId, deviceId) await this.addUserExtraParams(userId, deviceId, { [CONSTANTS.SESSION_NODE_KEY]: nodeEndpoint }) - await this.addUserDeviceToNode(nodeIp, nodePort, organizationId, userId, deviceId) + await this.addUserDeviceToNode(nodeEndpoint, organizationId, userId, deviceId) } - async clearNodeUsersSession(nodeUrl) { + async clearNodeUsersSession(nodeEndpoint) { if (this.config.get("app.isStandAloneNode")) return const lastUserSessions = [] - const userConnections = await this.listNodeUserDevices(void 0, void 0, nodeUrl) + const userConnections = await this.listNodeUserDevices(nodeEndpoint) for (const userData of userConnections) { const isLastUserSession = await this.removeUserData(userData.organizationId, userData.userId, userData.deviceId) if (isLastUserSession) lastUserSessions.push(userData) } - await this.deleteNodeConnections(void 0, void 0, nodeUrl) + await this.deleteNodeConnections(nodeEndpoint) return lastUserSessions } @@ -290,7 +286,7 @@ class SessionService { getDeviceId(socket, userId) { if (this.activeSessions.DEVICES[userId]) { - return this.activeSessions.DEVICES[userId].find((el) => el.socket === socket)?.deviceId + return this.activeSessions.DEVICES[userId].find((connection) => connection.socket === socket)?.deviceId } return null @@ -320,8 +316,8 @@ class SessionService { continue } - const [, nodeId, nodePort] = splitWsEndpoint(extraParams[CONSTANTS.SESSION_NODE_KEY]) - await this.removeUserDeviceFromNode(nodeId, nodePort, organizationId, userId, deviceId) + const nodeEndpoint = extraParams[CONSTANTS.SESSION_NODE_KEY] + await this.removeUserDeviceFromNode(nodeEndpoint, organizationId, userId, deviceId) } await this.deleteUserData(organizationId, userId) @@ -359,8 +355,7 @@ class SessionService { return isLastConnection } - const [, nodeId, nodePort] = splitWsEndpoint(nodeEndpoint) - await this.removeUserDeviceFromNode(nodeId, nodePort, organizationId, userId, deviceId) + await this.removeUserDeviceFromNode(nodeEndpoint, organizationId, userId, deviceId) return isLastConnection } diff --git a/app/utils/get_ip_from_ws_url.js b/app/utils/get_ip_from_ws_url.js deleted file mode 100644 index 9c93172e..00000000 --- a/app/utils/get_ip_from_ws_url.js +++ /dev/null @@ -1,3 +0,0 @@ -import { URL } from "node:url" - -export const getIpFromWsUrl = wsUrl => new URL(wsUrl).hostname diff --git a/app/utils/split_ws_endpoint.js b/app/utils/split_ws_endpoint.js deleted file mode 100644 index a2bce86f..00000000 --- a/app/utils/split_ws_endpoint.js +++ /dev/null @@ -1,7 +0,0 @@ -import { URL } from "node:url" - -export const splitWsEndpoint = url => { - const parsedUrl = new URL(url) - - return [parsedUrl.protocol, parsedUrl.hostname, parsedUrl.port, parsedUrl.pathname] -} From bc89bdcb72914780f9e4636aaa8e4bde50cac73d Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 5 Mar 2026 15:47:46 +0200 Subject: [PATCH 07/70] fix spec --- app/networking/models/DeliverMessage.js | 6 ++-- app/networking/protocol_processors/base.js | 4 +-- .../activity_manager_cluster/index.js | 32 ++++++++++++------- .../activity_manager_standalone/index.js | 4 +-- test/activities.spec.js | 19 ++++++----- test/cluster.spec.js | 14 ++++++-- test/push_notifications.spec.js | 2 +- 7 files changed, 48 insertions(+), 33 deletions(-) diff --git a/app/networking/models/DeliverMessage.js b/app/networking/models/DeliverMessage.js index a592e6bb..39692115 100644 --- a/app/networking/models/DeliverMessage.js +++ b/app/networking/models/DeliverMessage.js @@ -1,5 +1,5 @@ class DeliverMessage { - ws = null + socket = null orgId = null @@ -13,11 +13,11 @@ class DeliverMessage { notSaveInOfflineStorage = false pushQueueMessage = null - constructor(orgId, packet, notSaveInOfflineStorage, ws) { + constructor(orgId, packet, notSaveInOfflineStorage, socket) { this.orgId = orgId this.packet = packet this.notSaveInOfflineStorage = !!notSaveInOfflineStorage - this.ws = ws + this.socket = socket } setOrgId(orgId) { diff --git a/app/networking/protocol_processors/base.js b/app/networking/protocol_processors/base.js index db1e5c98..6e9bf760 100644 --- a/app/networking/protocol_processors/base.js +++ b/app/networking/protocol_processors/base.js @@ -162,7 +162,7 @@ class BaseProtocolProcessor { for (const deliverPackage of deliverPackages) { logger.trace("[DELIVER] %o", deliverPackage) - deliverPackage.ws ??= socket + deliverPackage.socket ??= socket if (deliverPackage.userIds?.length) { await this.processDeliverUserMessage(deliverPackage) @@ -176,7 +176,7 @@ class BaseProtocolProcessor { try { await packetManager.deliverToUserOrUsers( deliverMessage.orgId, - deliverMessage.ws, + deliverMessage.socket, deliverMessage.packet, deliverMessage.pushQueueMessage, participantIds ?? deliverMessage.userIds, diff --git a/app/providers/services/activity_manager/activity_manager_cluster/index.js b/app/providers/services/activity_manager/activity_manager_cluster/index.js index e7570368..594b6bd6 100644 --- a/app/providers/services/activity_manager/activity_manager_cluster/index.js +++ b/app/providers/services/activity_manager/activity_manager_cluster/index.js @@ -15,30 +15,38 @@ class ActivityManagerClusterService { this.userService = userService } + serializeUserId(userId) { + return `${userId}` + } + + deserializeUserId(userId) { + return userId && `${userId}` + } + // observer - targets relations async subscribeTarget(observerId) { - const targetId = await this.redisConnection.client.hGet(SUBSCRIBED_TO_KEY, `${observerId}`) + const targetId = await this.redisConnection.client.hGet(SUBSCRIBED_TO_KEY, this.serializeUserId(observerId)) - return targetId ? targetId : void 0 + return targetId ? this.deserializeUserId(targetId) : void 0 } async addSubscribeTarget(observerId, targetId) { - await this.redisConnection.client.hSet(SUBSCRIBED_TO_KEY, `${observerId}`, `${targetId}`) + await this.redisConnection.client.hSet(SUBSCRIBED_TO_KEY, this.serializeUserId(observerId), this.serializeUserId(targetId)) } async deleteSubscribeTarget(observerId, targetId) { - await this.redisConnection.client.hDel(SUBSCRIBED_TO_KEY, `${observerId}`) + await this.redisConnection.client.hDel(SUBSCRIBED_TO_KEY, this.serializeUserId(observerId)) } async clearAllSubscriptionTargets(observerId) { - await this.redisConnection.client.hDel(SUBSCRIBED_TO_KEY, `${observerId}`) + await this.redisConnection.client.hDel(SUBSCRIBED_TO_KEY, this.serializeUserId(observerId)) } // target - observer relations async subscribers(targetId) { - const observersIds = await this.redisConnection.client.hGet(SUBSCRIBERS_KEY, `${targetId}`) + const observersIds = await this.redisConnection.client.hGet(SUBSCRIBERS_KEY, this.serializeUserId(targetId)) - return observersIds ? observersIds.split(',') : [] + return observersIds ? observersIds.split(',').map(userId => this.deserializeUserId(userId)) : [] } async addSubscriber(targetId, observerId) { @@ -46,11 +54,11 @@ class ActivityManagerClusterService { const observersIds = new Set(observersIdsArr) - observersIds.add(observerId) + observersIds.add(this.deserializeUserId(observerId)) const newObserversIds = Array.from(observersIds).join(',') - await this.redisConnection.client.hSet(SUBSCRIBERS_KEY, `${targetId}`, newObserversIds) + await this.redisConnection.client.hSet(SUBSCRIBERS_KEY, this.serializeUserId(targetId), newObserversIds) } async deleteSubscriber(targetId, observerId) { @@ -58,7 +66,7 @@ class ActivityManagerClusterService { const observersIds = new Set(observersIdsArr) - observersIds.delete(observerId) + observersIds.delete(this.deserializeUserId(observerId)) if (!observersIds.size) { await this.clearSubscribed(targetId) @@ -67,11 +75,11 @@ class ActivityManagerClusterService { const newObserversIds = Array.from(observersIds).join(',') - await this.redisConnection.client.hSet(SUBSCRIBERS_KEY, `${targetId}`, newObserversIds) + await this.redisConnection.client.hSet(SUBSCRIBERS_KEY, this.serializeUserId(targetId), newObserversIds) } async clearSubscribed(targetId) { - await this.redisConnection.client.hDel(SUBSCRIBERS_KEY, `${targetId}`) + await this.redisConnection.client.hDel(SUBSCRIBERS_KEY, this.serializeUserId(targetId)) } async subscribeObserverToTarget(observerId, targetId) { diff --git a/app/providers/services/activity_manager/activity_manager_standalone/index.js b/app/providers/services/activity_manager/activity_manager_standalone/index.js index dda04c62..6c762bd4 100644 --- a/app/providers/services/activity_manager/activity_manager_standalone/index.js +++ b/app/providers/services/activity_manager/activity_manager_standalone/index.js @@ -25,7 +25,7 @@ class ActivityManagerStandaloneService { // target - observer relations subscribers(targetId) { - return this.ACTIVITY.SUBSCRIBERS[targetId] || {} + return Object.keys(this.ACTIVITY.SUBSCRIBERS[targetId] || {}) } addSubscriber(targetId, observerId) { @@ -69,7 +69,7 @@ class ActivityManagerStandaloneService { this.unsubscribeObserver(userId) } - const activitySubscribers = Object.keys(this.subscribers(userId)) + const activitySubscribers = this.subscribers(userId) if (!activitySubscribers.length) { return diff --git a/test/activities.spec.js b/test/activities.spec.js index d983e3eb..44ef8169 100644 --- a/test/activities.spec.js +++ b/test/activities.spec.js @@ -1,7 +1,6 @@ import assert from "node:assert" import ServiceLocatorContainer from "../app/common/ServiceLocatorContainer.js" -import { ACTIVITY } from "../app/store/activity.js" import packetJsonProcessor from "../APIs/JSON/routes/packet_processor.js" import { generateNewOrganizationId, createUserArray, sendLogin, sendLogout } from "./tools/utils.js" @@ -18,8 +17,8 @@ describe("User activities", async () => { before(async () => { orgId = await generateNewOrganizationId() usersIds = await createUserArray(orgId, 3) - currentUserToken1 = (await sendLogin("line_2", orgId, "user_3")).response.user.token currentUserToken = (await sendLogin("line_1", orgId, "user_1")).response.user.token + currentUserToken1 = (await sendLogin("line_2", orgId, "user_3")).response.user.token }) it("should work online list invalid limit", async () => { @@ -97,8 +96,8 @@ describe("User activities", async () => { responseData = responseData.backMessages.at(0) assert.strictEqual(responseData.response.id, requestData.request.id) - assert.equal(activityManagerService.subscribeTarget(usersIds[0]), usersIds[1]) - assert.notEqual(activityManagerService.subscribers(usersIds[1])[usersIds[0]], undefined) + assert.equal(await activityManagerService.subscribeTarget(usersIds[0]), `${usersIds[1]}`) + assert.notEqual((await activityManagerService.subscribers(usersIds[1])).at(0), undefined) assert.notEqual(responseData.response.last_activity, undefined) }) @@ -125,7 +124,7 @@ describe("User activities", async () => { let requestData = { request: { user_last_activity_subscribe: { - id: usersIds[1], + id: `${usersIds[1]}`, }, id: "1", }, @@ -150,9 +149,9 @@ describe("User activities", async () => { assert.strictEqual(responseData.response.id, requestData.request.id) assert.strictEqual(responseData.response.success, true) - assert.equal(ACTIVITY.SUBSCRIBED_TO[usersIds[0]], undefined) - assert.notEqual(ACTIVITY.SUBSCRIBERS[usersIds[1]], undefined) - assert.equal(ACTIVITY.SUBSCRIBERS[usersIds[1]][usersIds[0]], undefined) + assert.equal(await activityManagerService.subscribeTarget(usersIds[0]), undefined) + assert.notEqual(await activityManagerService.subscribers(usersIds[1]), undefined) + assert.equal((await activityManagerService.subscribers(usersIds[1])).find(uId => uId === `${usersIds[0]}`), undefined) }) it("should work getUserStatus", async () => { @@ -188,8 +187,8 @@ describe("User activities", async () => { assert.strictEqual(responseData.response.id, requestData.request.id) assert.strictEqual(responseData.response.success, true) - assert.equal(ACTIVITY.SUBSCRIBED_TO[usersIds[2]], undefined) - assert.deepEqual(ACTIVITY.SUBSCRIBERS[usersIds[1]], {}) + assert.equal(await activityManagerService.subscribeTarget(usersIds[2]), undefined) + assert.equal((await activityManagerService.subscribers(usersIds[1])).length, 0) await sendLogout("line_2", currentUserToken1) }) diff --git a/test/cluster.spec.js b/test/cluster.spec.js index 892de390..f892098f 100644 --- a/test/cluster.spec.js +++ b/test/cluster.spec.js @@ -5,6 +5,7 @@ import assert from "node:assert" import ServiceLocatorContainer from "../app/common/ServiceLocatorContainer.js" import clusterManager from "../app/cluster/cluster_manager.js" import { generateNewOrganizationId, createConversation, createUserArray, mockedWS, sendLogin } from "./tools/utils.js" +import { buildWsEndpoint } from "../app/utils/build_ws_endpoint.js" import packetJsonProcessor from "../APIs/JSON/routes/packet_processor.js" import packetManager from "../app/networking/packet_manager.js" @@ -56,10 +57,17 @@ describe("Cluster Message function", async () => { secondSocketResponse = data }, } - clusterManager.clusterNodesWS[ip.address()] = mockedWS2 + const newEndpoint = buildWsEndpoint(ip.address(), secondClusterPort) + clusterManager.clusterNodesConnections[newEndpoint] = mockedWS2 sessionService.addUserDeviceConnection(mockedWS2, orgId, usersIds[1], deviceId) - await sessionService.storeUserNodeData(mockedWS2, orgId, usersIds[1], deviceId, ip.address(), secondClusterPort) + + const prevEndpoint = sessionService.config.get("ws.cluster.endpoint") + sessionService.config.set("ws.cluster.endpoint", newEndpoint) + + await sessionService.storeUserNodeData(mockedWS2, orgId, usersIds[1], deviceId) + + sessionService.config.set("ws.cluster.endpoint", prevEndpoint) }) describe("Send Message to other node", async () => { @@ -80,7 +88,7 @@ describe("Cluster Message function", async () => { await packetManager.deliverToUserOrUsers( orgId, - deliverMessage.ws || mockedWS, + deliverMessage.socket || mockedWS, JSON.stringify(deliverMessage.packet), null, usersIds, diff --git a/test/push_notifications.spec.js b/test/push_notifications.spec.js index a423a568..345ab6e0 100644 --- a/test/push_notifications.spec.js +++ b/test/push_notifications.spec.js @@ -579,7 +579,7 @@ describe("PushNotification functions", async () => { await packetManager.deliverToUserOrUsers( deliverMessage.orgId, - deliverMessage.ws, + deliverMessage.socket, deliverMessage.packet, deliverMessage.pushQueueMessage, deliverMessage.userIds, From 1638d7062336371ac4bea9b11d76ee52aaf4355a Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 5 Mar 2026 16:10:26 +0200 Subject: [PATCH 08/70] update config --- app/config/index.js | 10 ++++++---- index.js | 15 +++++++-------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/config/index.js b/app/config/index.js index 966fe912..27ee5b24 100644 --- a/app/config/index.js +++ b/app/config/index.js @@ -3,20 +3,22 @@ import ip from "ip" import CONFIG_VALUES from "./default.js" import Config from "./config.js" +import { buildWsEndpoint } from "../utils/build_ws_endpoint.js" const config = new Config(CONFIG_VALUES) config.set("app.hostName", os.hostname(), true) config.set("app.ip", ip.address()) -if (config.get("app.isStandAloneNode")) { - config.set("ws.cluster.port", 1000) -} - const isSecureWs = config.get("ws.options.ssl.key") && config.get("ws.options.ssl.cert") config.set("ws.options.isSecure", !!isSecureWs) const isTls = config.get("tcp.options.tls.key") && config.get("tcp.options.tls.cert") config.set("tcp.options.isTls", !!isTls) +if (config.get("app.isStandAloneNode")) { + config.set("ws.cluster.port", 1000) + config.set("ws.cluster.endpoint", buildWsEndpoint(config.get("app.ip"), config.get("ws.cluster.port"))) +} + export default config diff --git a/index.js b/index.js index 11fef946..43eebcc3 100644 --- a/index.js +++ b/index.js @@ -66,12 +66,6 @@ if (config.get("tcp.options.isTls")) { }) } -if (!config.get("app.isStandAloneNode")) { - const clusterPort = await clusterManager.createLocalSocket(uWSOptions) - config.set("ws.cluster.port", clusterPort) -} -config.set("ws.cluster.endpoint", buildWsEndpoint(config.get("app.ip"), config.get("ws.cluster.port"))) - logger.debug("[Config] %s", JSON.stringify(config.toObject(), null, 5)) // perform a database connection when the server starts @@ -164,8 +158,6 @@ for (const api of Object.values(APIs)) { } } -logger.debug("[Config][Merged] %s", JSON.stringify(config.toObject(), null, 5)) - // Boot providers logger.debug("[Boot]") await ServiceLocatorContainer.boot() @@ -174,11 +166,18 @@ logger.debug("[Create singleton]") await ServiceLocatorContainer.createAllSingletonInstances() if (!config.get("app.isStandAloneNode")) { + const clusterPort = await clusterManager.createLocalSocket(uWSOptions) + + config.set("ws.cluster.port", clusterPort) + config.set("ws.cluster.endpoint", buildWsEndpoint(config.get("app.ip"), config.get("ws.cluster.port"))) + // Start Cluster Sync logger.debug("[Start sync]") await clusterManager.startSyncingClusterNodes(true) } +logger.debug("[Config][Merged] %s", JSON.stringify(config.toObject(), null, 5)) + // Start public protocols const sessionService = ServiceLocatorContainer.use("SessionService") const conversationService = ServiceLocatorContainer.use("ConversationService") From e455b0a950fdd59d1491dc55c3f97ae83ad0584b Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Fri, 6 Mar 2026 15:19:20 +0200 Subject: [PATCH 09/70] refactor PacketManager --- app/cluster/cluster_manager.js | 6 +- app/networking/packet_manager.js | 134 ++++++++++----------- app/networking/protocol_processors/base.js | 24 ++-- test/cluster.spec.js | 23 +++- test/push_notifications.spec.js | 24 ++-- 5 files changed, 120 insertions(+), 91 deletions(-) diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index c2518727..0b188299 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -181,10 +181,11 @@ class ClusterManager extends BaseProtocolProcessor { if (clusterPacket.node_info) { const nodeInfo = clusterPacket.node_info ws.nodeEndpoint = nodeInfo.endpoint - this.clusterNodesConnections[nodeInfo.endpoint] = ws - loggerReceiver.debug("[node handshake finished] %s", nodeInfo.endpoint) + loggerReceiver.debug("[node handshake pong] %s", nodeInfo.endpoint) this.#shareCurrentNodeInfo(ws) + + await this.retrieveConnectionWithNode(nodeInfo.endpoint) return } @@ -220,6 +221,7 @@ class ClusterManager extends BaseProtocolProcessor { } const clusterPacket = { deliverPacket } + loggerSender.trace("[%s][deliver cluster] %j", nodeEndpoint, clusterPacket) recipientClusterNodeConnection.send(JSON.stringify(clusterPacket)) } diff --git a/app/networking/packet_manager.js b/app/networking/packet_manager.js index 51e3e148..94a079b6 100644 --- a/app/networking/packet_manager.js +++ b/app/networking/packet_manager.js @@ -11,17 +11,13 @@ import { CONSTANTS as MAIN_CONSTANTS } from "../constants/constants.js" const logger = maiLogger.child("[PacketManager]") class PacketManager { - async deliverToUserOnThisNode(userId, packet, deviceId, senderInfo) { + async deliverToUserOnThisNode(userId, exceptDeviceId, packet, senderInfo) { const sessionService = ServiceLocatorContainer.use("SessionService") - let activeDevices = sessionService + const activeDevices = sessionService .getUserDevices(userId) .filter((activeDevice) => activeDevice?.deviceId !== MAIN_CONSTANTS.HTTP_DEVICE_ID) - - if (deviceId) { - activeDevices = activeDevices.filter((recipient) => recipient?.deviceId === deviceId) - } - - activeDevices = activeDevices.filter((recipient) => !!recipient?.socket) + .filter((activeDevice) => !!activeDevice?.socket) + .filter((activeDevice) => activeDevice?.deviceId !== exceptDeviceId) for (const recipient of activeDevices) { try { @@ -57,100 +53,100 @@ class PacketManager { } } - #deliverToUserDevices(socket, orgId, nodeConnections, userId, packet, ignoreSelf) { + // async deliverToUserOrUsers(orgId, socket, packet, pushQueueMessage, usersIds, notSaveInOfflineStorage, ignoreSelf) + async deliverToUserOrUsers(sourceOptions, destinationUserIds, payloadOptions) { const sessionService = ServiceLocatorContainer.use("SessionService") - const senderUserSession = sessionService.getSession(socket) - const senderDeviceId = senderUserSession ? sessionService.getDeviceId(socket, senderUserSession.userId) : null + const opLogsService = ServiceLocatorContainer.use("OperationLogsService") + const pushQueueService = ServiceLocatorContainer.use("PushQueueService") - const currentNodeEndpoint = config.get("ws.cluster.endpoint") + if (!destinationUserIds?.length) { + return + } const senderInfo = { - apiType: socket?.apiType, - session: senderUserSession ?? { organizationId: orgId, userId }, - deviceId: senderDeviceId, - node: currentNodeEndpoint, + organizationId: sourceOptions.organizationId, + session: { organizationId: sourceOptions.organizationId }, + node: config.get("ws.cluster.endpoint"), } - if (config.get("app.isStandAloneNode")) { - Object.keys(nodeConnections).forEach(async (nodeDeviceId) => { - if (senderDeviceId === nodeDeviceId && ignoreSelf) { - return // carbon message - } + if (sourceOptions.socket) { + senderInfo.apiType = sourceOptions.socket?.apiType + senderInfo.session = sessionService.getSession(sourceOptions.socket) ?? senderInfo.session + senderInfo.deviceId = sessionService.getDeviceId(sourceOptions.socket, senderInfo.session.userId) + } - await this.deliverToUserOnThisNode(userId, packet, nodeDeviceId, senderInfo) - }) - } else { - Object.entries(nodeConnections).forEach(async ([nodeDeviceId, extraParams]) => { - const nodeEndpoint = extraParams[MAIN_CONSTANTS.SESSION_NODE_KEY] + const offlineUsersIds = [] + let currentNodesUserIds = {} + let otherNodes = {} - if (!nodeEndpoint) { - return - } + for (const userId of destinationUserIds) { + const userConnections = await sessionService.listUserData(sourceOptions.organizationId, userId) + const isOffline = !Object.keys(userConnections ?? {}).length + const isInactive = Object.values(userConnections).some((extraParams) => sessionService.isUserInactive(null, extraParams)) - if (currentNodeEndpoint === nodeEndpoint) { - if (senderDeviceId === nodeDeviceId && ignoreSelf) { - return - } + if (isOffline && !payloadOptions.notSaveInOfflineStorage) { + opLogsService.savePacket(userId, payloadOptions.packet) + } - await this.deliverToUserOnThisNode(userId, packet, nodeDeviceId, senderInfo) // carbon message + if (isOffline || isInactive) { + offlineUsersIds.push(userId) + } - return - } + if (isOffline) { + continue + } + + this.reduceUserNodeConnections(senderInfo.node, userId, userConnections, currentNodesUserIds, otherNodes) + } + Object.keys(currentNodesUserIds).forEach(async userId => { + const exceptDeviceId = payloadOptions.ignoreSelf ? senderInfo.deviceId : void 0 + + await this.deliverToUserOnThisNode(userId, exceptDeviceId, payloadOptions.packet, senderInfo) + }) + + Object.entries(otherNodes).forEach(([nodeEndpoint, userIds]) => { + Object.keys(userIds).forEach(async userId => { try { - const clusterPacket = { userId, packet, senderInfo } + const clusterPacket = { userId, packet: payloadOptions.packet, senderInfo } await clusterManager.senderClusterDeliverPacket(nodeEndpoint, clusterPacket) } catch (error) { await sessionService.clearNodeUsersSession(nodeEndpoint) - logger.error(error, "[deliverToUserDevices] createSocketWithNode error") + logger.error(error, "[deliver to other node]") } }) - } - } + }) - async deliverToUserOrUsers(orgId, socket, packet, pushQueueMessage, usersIds, notSaveInOfflineStorage, ignoreSelf) { - const sessionService = ServiceLocatorContainer.use("SessionService") - const opLogsService = ServiceLocatorContainer.use("OperationLogsService") - const pushQueueService = ServiceLocatorContainer.use("PushQueueService") - - if (!usersIds?.length) { - return + if (payloadOptions.pushQueueMessage && offlineUsersIds.length) { + payloadOptions.pushQueueMessage.setRecipientIds(offlineUsersIds) + await pushQueueService.createPush(payloadOptions.pushQueueMessage) } + } - const offlineUsersByPackets = [] - - for (const userId of usersIds) { - const userNodeData = await sessionService.listUserData(orgId, userId) - const isNoConnections = !userNodeData || !Object.keys(userNodeData).length - - if (isNoConnections) { - if (!notSaveInOfflineStorage) { - opLogsService.savePacket(userId, packet) - } - - offlineUsersByPackets.push(userId) + reduceUserNodeConnections(targetNodeEndpoint, userId, nodeConnections, currentNodesAcc, otherNodesAcc) { + for (const connectionsParams of Object.values(nodeConnections)) { + const connectionEndpoint = connectionsParams?.[MAIN_CONSTANTS.SESSION_NODE_KEY] + if (!connectionEndpoint) { continue } - const isInactive = Object.values(userNodeData).some((extraParams) => sessionService.isUserInactive(null, extraParams)) - - if (isInactive) { - offlineUsersByPackets.push(userId) + if (connectionEndpoint === targetNodeEndpoint) { + currentNodesAcc[userId] = connectionEndpoint + continue } - this.#deliverToUserDevices(socket, orgId, userNodeData, userId, packet, ignoreSelf) - } + if (!otherNodesAcc[connectionEndpoint]) { + otherNodesAcc[connectionEndpoint] = {} + } - if (pushQueueMessage && offlineUsersByPackets.length) { - pushQueueMessage.setRecipientIds(offlineUsersByPackets) - await pushQueueService.createPush(pushQueueMessage) + otherNodesAcc[connectionEndpoint][userId] = connectionEndpoint } } async deliverClusterMessageToUser(deliverPacket) { try { const { userId, packet, senderInfo } = deliverPacket - await this.deliverToUserOnThisNode(userId, packet, null, senderInfo) + await this.deliverToUserOnThisNode(userId, void 0, packet, senderInfo) } catch (error) { logger.error(error, "[deliverClusterMessageToUser]") } diff --git a/app/networking/protocol_processors/base.js b/app/networking/protocol_processors/base.js index 6e9bf760..46f21893 100644 --- a/app/networking/protocol_processors/base.js +++ b/app/networking/protocol_processors/base.js @@ -174,14 +174,24 @@ class BaseProtocolProcessor { async processDeliverUserMessage(deliverMessage, participantIds) { try { + const sourceOptions = { + organizationId: deliverMessage.orgId, + socket: deliverMessage.socket, + } + + const destinationUserIds = participantIds ?? deliverMessage.userIds + + const payloadOptions = { + packet: deliverMessage.packet, + notSaveInOfflineStorage: deliverMessage.notSaveInOfflineStorage, + ignoreSelf: deliverMessage.ignoreSelf, + pushQueueMessage: deliverMessage.pushQueueMessage + } + await packetManager.deliverToUserOrUsers( - deliverMessage.orgId, - deliverMessage.socket, - deliverMessage.packet, - deliverMessage.pushQueueMessage, - participantIds ?? deliverMessage.userIds, - deliverMessage.notSaveInOfflineStorage, - deliverMessage.ignoreSelf + sourceOptions, + destinationUserIds, + payloadOptions ) } catch (error) { logger.error(error, "[PacketManager][error]") diff --git a/test/cluster.spec.js b/test/cluster.spec.js index f892098f..443b17db 100644 --- a/test/cluster.spec.js +++ b/test/cluster.spec.js @@ -86,13 +86,24 @@ describe("Cluster Message function", async () => { assert.notEqual(deliverMessage, undefined) assert.equal(`${deliverMessage.cId}`, `${currentConversationId}`) + const sourceOptions = { + organizationId: orgId, + socket: deliverMessage.socket || mockedWS, + } + + const destinationUserIds = usersIds + + const payloadOptions = { + packet: JSON.stringify(deliverMessage.packet), + notSaveInOfflineStorage: deliverMessage.notSaveInOfflineStorage, + ignoreSelf: false, + pushQueueMessage: void 0 + } + await packetManager.deliverToUserOrUsers( - orgId, - deliverMessage.socket || mockedWS, - JSON.stringify(deliverMessage.packet), - null, - usersIds, - deliverMessage.notSaveInOfflineStorage + sourceOptions, + destinationUserIds, + payloadOptions ) const response = JSON.parse(secondSocketResponse).deliverPacket diff --git a/test/push_notifications.spec.js b/test/push_notifications.spec.js index 345ab6e0..bf7398d2 100644 --- a/test/push_notifications.spec.js +++ b/test/push_notifications.spec.js @@ -577,14 +577,24 @@ describe("PushNotification functions", async () => { const deliverMessage = responseOnMessage.deliverMessages.at(0) + const sourceOptions = { + organizationId: deliverMessage.orgId, + socket: deliverMessage.socket, + } + + const destinationUserIds = deliverMessage.userIds + + const payloadOptions = { + packet: deliverMessage.packet, + notSaveInOfflineStorage: deliverMessage.notSaveInOfflineStorage, + ignoreSelf: deliverMessage.ignoreSelf, + pushQueueMessage: deliverMessage.pushQueueMessage + } + await packetManager.deliverToUserOrUsers( - deliverMessage.orgId, - deliverMessage.socket, - deliverMessage.packet, - deliverMessage.pushQueueMessage, - deliverMessage.userIds, - deliverMessage.notSaveInOfflineStorage, - deliverMessage.ignoreSelf + sourceOptions, + destinationUserIds, + payloadOptions ) assert.ok(pushQueueItem) From b7739e57482b24bb0d684bd59869c98cc77e0429 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Wed, 11 Mar 2026 15:21:04 +0200 Subject: [PATCH 10/70] it can clear destoyed node cache data --- APIs/JSON/controllers/http/auth.js | 6 ++- APIs/XMPP | 2 +- app/cluster/cluster_manager.js | 31 ++++++----- app/constants/constants.js | 6 +++ app/lib/redis.js | 8 ++- app/networking/packet_manager.js | 8 +-- app/networking/protocol_processors/base.js | 8 +-- .../activity_manager_cluster/index.js | 6 +-- .../services/cluster_node/Provider.js | 5 +- app/providers/services/cluster_node/index.js | 51 ++++++++++--------- app/providers/services/session/index.js | 15 +++--- app/utils/build_ws_endpoint.js | 2 +- test/activities.spec.js | 5 +- test/cluster.spec.js | 8 +-- test/push_notifications.spec.js | 8 +-- 15 files changed, 92 insertions(+), 77 deletions(-) diff --git a/APIs/JSON/controllers/http/auth.js b/APIs/JSON/controllers/http/auth.js index 3227030f..552043f5 100644 --- a/APIs/JSON/controllers/http/auth.js +++ b/APIs/JSON/controllers/http/auth.js @@ -43,7 +43,11 @@ class HttpAuthController extends BaseHttpController { async logout(res, req) { const httpLogoutOperation = ServiceLocatorContainer.use("HttpUserLogoutOperation") - const { refreshTokenRecord, isWasLastUserSession } = await httpLogoutOperation.perform(res.fakeWsSessionKey, res.parsedHeaders, res.parsedSignedCookies) + const { refreshTokenRecord, isWasLastUserSession } = await httpLogoutOperation.perform( + res.fakeWsSessionKey, + res.parsedHeaders, + res.parsedSignedCookies + ) const httpResponse = new HttpResponse(200, {}, { success: true }).addCookie("refresh_token", refreshTokenRecord.token, { maxAge: 0, diff --git a/APIs/XMPP b/APIs/XMPP index 901be2bf..0210f07b 160000 --- a/APIs/XMPP +++ b/APIs/XMPP @@ -1 +1 @@ -Subproject commit 901be2bfff124a97645b0271fc0ba55b44590a99 +Subproject commit 0210f07bf51005d72279c7278e3b196f069d13a4 diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index 0b188299..e81cc0d6 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -36,9 +36,11 @@ class ClusterManager extends BaseProtocolProcessor { await this.cleanDestroyedNodeData(config.get("ws.cluster.endpoint")) } - this.#nodesSyncInterval = setInterval(() => this.#syncCluster(), config.get("ws.cluster.nodeExpiresIn") - 2_000) + this.#nodesSyncInterval = setInterval(() => this.#syncCluster(), config.get("ws.cluster.nodeExpiresIn")) - this.#syncCluster(isColdStart) + if (isColdStart) { + await this.#syncCluster() + } } async stopSyncingClusterNodes() { @@ -49,7 +51,7 @@ class ClusterManager extends BaseProtocolProcessor { async #syncCluster() { const sessionService = ServiceLocatorContainer.use("SessionService") - await this.#storeCurrentNode(sessionService.sessionsTotal) + await this.#storeCurrentNode(sessionService.totalSessions()) await this.#retrieveExistingClusterNodes() } @@ -67,19 +69,23 @@ class ClusterManager extends BaseProtocolProcessor { users_count: usersCount, } - await clusterNodeService.create(addressParams, optionalParams) + await clusterNodeService.upsert(addressParams, optionalParams) } async #retrieveExistingClusterNodes() { const clusterNodeService = ServiceLocatorContainer.use("ClusterNodeService") - const nodeList = await clusterNodeService.retrieveAll() - loggerSender.debug("[nodes] %j %s", nodeList, nodeList.length) + const nodeList = await clusterNodeService.retrieveActive() + const activeNodeEndpoints = nodeList.map((node) => buildWsEndpoint(node.ip_address, node.port)) + loggerSender.debug("[active] %j %s", nodeList, nodeList.length) + const storedNodeEndpoints = await clusterNodeService.retrieveStored() + loggerSender.debug("[stored] %j %s", storedNodeEndpoints, storedNodeEndpoints.length) + + const nodeEndpoints = activeNodeEndpoints.concat(storedNodeEndpoints) const destroyedNodes = [] - for (const nodeInfo of nodeList) { - const nodeEndpoint = buildWsEndpoint(nodeInfo.ip_address, nodeInfo.port) + for (const nodeEndpoint of nodeEndpoints) { const isCurrentNode = config.get("ws.cluster.endpoint") === nodeEndpoint if (isCurrentNode) { continue @@ -105,7 +111,7 @@ class ClusterManager extends BaseProtocolProcessor { endpoint: config.get("ws.cluster.endpoint"), ip: config.get("app.ip"), host: config.get("app.hostName"), - port: config.get("ws.cluster.port") + port: config.get("ws.cluster.port"), }, }) ) @@ -232,14 +238,15 @@ class ClusterManager extends BaseProtocolProcessor { const sessionService = ServiceLocatorContainer.use("SessionService") - const lastUserSessions = await sessionService.clearNodeUsersSession(nodeEndpoint) - .catch(error => logger.error(error, "[clean node]")) + const lastUserSessions = await sessionService.clearNodeUsersSession(nodeEndpoint).catch((error) => logger.error(error, "[clean node]")) if (!lastUserSessions) { return } - const offlineUserResponses = lastUserSessions.map(userSession => activitySender.buildOfflineActivityResponse(userSession.organizationId, userSession.userId)) + const offlineUserResponses = lastUserSessions.map((userSession) => + activitySender.buildOfflineActivityResponse(userSession.organizationId, userSession.userId) + ) for (const offlineUserResponse of offlineUserResponses) { await this.processAPIResponse(void 0, offlineUserResponse) diff --git a/app/constants/constants.js b/app/constants/constants.js index 93362362..518b46a1 100644 --- a/app/constants/constants.js +++ b/app/constants/constants.js @@ -39,4 +39,10 @@ export const CONSTANTS = { TESTING: "testing", }, ENV_TRUE: "true", + REDIS_PREFIXES: { + NODE_DATA: "sama-node-data", + NODE_USERS: "sama-node-users", + USER_DEVICES: "sama-user-devices", + USER_DATA: "sama-user-data", + }, } diff --git a/app/lib/redis.js b/app/lib/redis.js index be7c8b91..09be9b88 100644 --- a/app/lib/redis.js +++ b/app/lib/redis.js @@ -70,10 +70,14 @@ class RedisManager { return matchCount } - async findKeyByPattern(pattern) { + async findKeysByPattern(pattern) { const keys = await this.client.keys(pattern) - return keys?.at(0) + if (!keys?.length) { + return keys + } + + return keys } } diff --git a/app/networking/packet_manager.js b/app/networking/packet_manager.js index 94a079b6..c79b3b16 100644 --- a/app/networking/packet_manager.js +++ b/app/networking/packet_manager.js @@ -71,7 +71,7 @@ class PacketManager { if (sourceOptions.socket) { senderInfo.apiType = sourceOptions.socket?.apiType - senderInfo.session = sessionService.getSession(sourceOptions.socket) ?? senderInfo.session + senderInfo.session = sessionService.getSession(sourceOptions.socket) ?? senderInfo.session senderInfo.deviceId = sessionService.getDeviceId(sourceOptions.socket, senderInfo.session.userId) } @@ -99,14 +99,14 @@ class PacketManager { this.reduceUserNodeConnections(senderInfo.node, userId, userConnections, currentNodesUserIds, otherNodes) } - Object.keys(currentNodesUserIds).forEach(async userId => { + Object.keys(currentNodesUserIds).forEach(async (userId) => { const exceptDeviceId = payloadOptions.ignoreSelf ? senderInfo.deviceId : void 0 await this.deliverToUserOnThisNode(userId, exceptDeviceId, payloadOptions.packet, senderInfo) }) Object.entries(otherNodes).forEach(([nodeEndpoint, userIds]) => { - Object.keys(userIds).forEach(async userId => { + Object.keys(userIds).forEach(async (userId) => { try { const clusterPacket = { userId, packet: payloadOptions.packet, senderInfo } await clusterManager.senderClusterDeliverPacket(nodeEndpoint, clusterPacket) @@ -130,7 +130,7 @@ class PacketManager { continue } - if (connectionEndpoint === targetNodeEndpoint) { + if (connectionEndpoint === targetNodeEndpoint || config.get("app.isStandAloneNode")) { currentNodesAcc[userId] = connectionEndpoint continue } diff --git a/app/networking/protocol_processors/base.js b/app/networking/protocol_processors/base.js index 46f21893..9b9a150b 100644 --- a/app/networking/protocol_processors/base.js +++ b/app/networking/protocol_processors/base.js @@ -185,14 +185,10 @@ class BaseProtocolProcessor { packet: deliverMessage.packet, notSaveInOfflineStorage: deliverMessage.notSaveInOfflineStorage, ignoreSelf: deliverMessage.ignoreSelf, - pushQueueMessage: deliverMessage.pushQueueMessage + pushQueueMessage: deliverMessage.pushQueueMessage, } - await packetManager.deliverToUserOrUsers( - sourceOptions, - destinationUserIds, - payloadOptions - ) + await packetManager.deliverToUserOrUsers(sourceOptions, destinationUserIds, payloadOptions) } catch (error) { logger.error(error, "[PacketManager][error]") } diff --git a/app/providers/services/activity_manager/activity_manager_cluster/index.js b/app/providers/services/activity_manager/activity_manager_cluster/index.js index 594b6bd6..efac9f12 100644 --- a/app/providers/services/activity_manager/activity_manager_cluster/index.js +++ b/app/providers/services/activity_manager/activity_manager_cluster/index.js @@ -46,7 +46,7 @@ class ActivityManagerClusterService { async subscribers(targetId) { const observersIds = await this.redisConnection.client.hGet(SUBSCRIBERS_KEY, this.serializeUserId(targetId)) - return observersIds ? observersIds.split(',').map(userId => this.deserializeUserId(userId)) : [] + return observersIds ? observersIds.split(",").map((userId) => this.deserializeUserId(userId)) : [] } async addSubscriber(targetId, observerId) { @@ -56,7 +56,7 @@ class ActivityManagerClusterService { observersIds.add(this.deserializeUserId(observerId)) - const newObserversIds = Array.from(observersIds).join(',') + const newObserversIds = Array.from(observersIds).join(",") await this.redisConnection.client.hSet(SUBSCRIBERS_KEY, this.serializeUserId(targetId), newObserversIds) } @@ -73,7 +73,7 @@ class ActivityManagerClusterService { return } - const newObserversIds = Array.from(observersIds).join(',') + const newObserversIds = Array.from(observersIds).join(",") await this.redisConnection.client.hSet(SUBSCRIBERS_KEY, this.serializeUserId(targetId), newObserversIds) } diff --git a/app/providers/services/cluster_node/Provider.js b/app/providers/services/cluster_node/Provider.js index 9a65b09e..8ce6a22b 100644 --- a/app/providers/services/cluster_node/Provider.js +++ b/app/providers/services/cluster_node/Provider.js @@ -5,9 +5,10 @@ const name = "ClusterNodeService" class ClusterNodeServiceRegisterProvider extends RegisterProvider { register(slc) { - const clusterNodeRepo = slc.use("ClusterNodeRepository") + const config = slc.use("Config") + const redisClient = slc.use("RedisClient") - return new ClusterNodeService(clusterNodeRepo) + return new ClusterNodeService(config, redisClient) } } diff --git a/app/providers/services/cluster_node/index.js b/app/providers/services/cluster_node/index.js index 014a7741..14f72455 100644 --- a/app/providers/services/cluster_node/index.js +++ b/app/providers/services/cluster_node/index.js @@ -1,42 +1,45 @@ +import { CONSTANTS as MAIN_CONSTANTS } from "../../../constants/constants.js" + class ClusterNodeService { - constructor(clusterNodeRepo) { - this.clusterNodeRepo = clusterNodeRepo + constructor(config, redisClient) { + this.config = config + this.redisClient = redisClient } - async retrieveAll() { - const clusterNodes = await this.clusterNodeRepo.findAll({}) - - return this.normalizeClusterNodes(clusterNodes) + #nodeInfoKey(endpoint) { + return `${MAIN_CONSTANTS.REDIS_PREFIXES.NODE_DATA}:${endpoint}` } - normalizeClusterNodes(clusterNodes) { - const clusterNodeObj = clusterNodes.reduce((acc, clusterNode) => { - const key = `${clusterNode.ip_address}:${clusterNode.port}` - acc[key] = clusterNode - return acc - }, {}) + async retrieveActive() { + const nodeKeys = await this.redisClient.findKeysByPattern(this.#nodeInfoKey("*")) - return Object.values(clusterNodeObj) - } + const nodesInfo = [] - async create(addressParams, optionalParams) { - const clusterNodeParams = Object.assign({}, addressParams, optionalParams) + for (const nodeKey of nodeKeys) { + const nodeInfo = await this.redisClient.client.hGetAll(nodeKey) + nodesInfo.push(nodeInfo) + } - const clusterNode = await this.clusterNodeRepo.create(clusterNodeParams) + return nodesInfo + } + + async retrieveStored() { + const nodeKeys = await this.redisClient.findKeysByPattern(`${MAIN_CONSTANTS.REDIS_PREFIXES.NODE_USERS}:*`) - return clusterNode + return nodeKeys.map((nodeKey) => nodeKey.replace(`${MAIN_CONSTANTS.REDIS_PREFIXES.NODE_USERS}:`, "")) } async upsert(addressParams, optionalParams) { - const existedNode = await this.clusterNodeRepo.findOne(addressParams) + const currentNodeKey = this.#nodeInfoKey(this.config.get("ws.cluster.endpoint")) - if (existedNode) { - await this.clusterNodeRepo.updateStats(existedNode._id, optionalParams) + const keyValuePairs = Object.entries(Object.assign({}, addressParams, optionalParams)) + .flat() + .map((val) => `${val}`) - return - } + await this.redisClient.client.hSet(currentNodeKey, keyValuePairs) - await this.create(addressParams, optionalParams) + const expireSeconds = Math.round(this.config.get("ws.cluster.nodeExpiresIn") / 1_000) + 5 + await this.redisClient.client.expire(currentNodeKey, expireSeconds) } } diff --git a/app/providers/services/session/index.js b/app/providers/services/session/index.js index a437edfa..5a8de978 100644 --- a/app/providers/services/session/index.js +++ b/app/providers/services/session/index.js @@ -2,8 +2,8 @@ import { CONSTANTS } from "../../../constants/constants.js" /* Structs: - SET - sama-node:{node-endpoint} -> {organizationId}:{userId}:{deviceId} - SET - sama-user:{organizationId}:{userId} -> {deviceId} + SET - sama-node-users:{node-endpoint} -> {organizationId}:{userId}:{deviceId} + SET - sama-user-devices:{organizationId}:{userId} -> {deviceId} HASH - sama-user-data:{userId}:{deviceId} -> extra params */ @@ -44,7 +44,7 @@ class SessionService { } #nodesSetCacheKey(nodeEndpoint) { - return `sama-node:${nodeEndpoint}` + return `${CONSTANTS.REDIS_PREFIXES.NODE_USERS}:${nodeEndpoint}` } async addUserDeviceToNode(nodeEndpoint, organizationId, userId, deviceId) { @@ -80,11 +80,11 @@ class SessionService { } #usersSetCacheKey(organizationId, userId) { - return `sama-user:${organizationId}:${userId}` + return `${CONSTANTS.REDIS_PREFIXES.USER_DEVICES}:${organizationId}:${userId}` } #usersHashCacheKey(userId, deviceId) { - return `sama-user-data:${userId}:${deviceId}` + return `${CONSTANTS.REDIS_PREFIXES.USER_DATA}:${userId}:${deviceId}` } async addUserDevice(organizationId, userId, deviceId) { @@ -131,7 +131,7 @@ class SessionService { .flat() .map((val) => `${val}`) - await this.redisConnection.client.hSet(userHashKey, ...keyValuePairs) + await this.redisConnection.client.hSet(userHashKey, keyValuePairs) } async retrieveUserExtraParams(userId, deviceId) { @@ -346,9 +346,8 @@ class SessionService { if (this.config.get("app.isStandAloneNode")) return isLastConnection isLastConnection = await this.removeUserDevice(organizationId, userId, deviceId) - await this.deleteUserExtraParams(userId, deviceId) - const extraParams = await this.retrieveUserExtraParams(userId, deviceId) + await this.deleteUserExtraParams(userId, deviceId) const nodeEndpoint = extraParams?.[CONSTANTS.SESSION_NODE_KEY] if (!nodeEndpoint) { diff --git a/app/utils/build_ws_endpoint.js b/app/utils/build_ws_endpoint.js index 3cb87b45..b9b93a73 100644 --- a/app/utils/build_ws_endpoint.js +++ b/app/utils/build_ws_endpoint.js @@ -1,4 +1,4 @@ -import { URL } from 'node:url' +import { URL } from "node:url" import config from "@sama/config/index.js" diff --git a/test/activities.spec.js b/test/activities.spec.js index 44ef8169..77ba6191 100644 --- a/test/activities.spec.js +++ b/test/activities.spec.js @@ -151,7 +151,10 @@ describe("User activities", async () => { assert.strictEqual(responseData.response.success, true) assert.equal(await activityManagerService.subscribeTarget(usersIds[0]), undefined) assert.notEqual(await activityManagerService.subscribers(usersIds[1]), undefined) - assert.equal((await activityManagerService.subscribers(usersIds[1])).find(uId => uId === `${usersIds[0]}`), undefined) + assert.equal( + (await activityManagerService.subscribers(usersIds[1])).find((uId) => uId === `${usersIds[0]}`), + undefined + ) }) it("should work getUserStatus", async () => { diff --git a/test/cluster.spec.js b/test/cluster.spec.js index 443b17db..95455b8b 100644 --- a/test/cluster.spec.js +++ b/test/cluster.spec.js @@ -97,14 +97,10 @@ describe("Cluster Message function", async () => { packet: JSON.stringify(deliverMessage.packet), notSaveInOfflineStorage: deliverMessage.notSaveInOfflineStorage, ignoreSelf: false, - pushQueueMessage: void 0 + pushQueueMessage: void 0, } - await packetManager.deliverToUserOrUsers( - sourceOptions, - destinationUserIds, - payloadOptions - ) + await packetManager.deliverToUserOrUsers(sourceOptions, destinationUserIds, payloadOptions) const response = JSON.parse(secondSocketResponse).deliverPacket response.packet = JSON.parse(response.packet) diff --git a/test/push_notifications.spec.js b/test/push_notifications.spec.js index bf7398d2..5dbd7ede 100644 --- a/test/push_notifications.spec.js +++ b/test/push_notifications.spec.js @@ -588,14 +588,10 @@ describe("PushNotification functions", async () => { packet: deliverMessage.packet, notSaveInOfflineStorage: deliverMessage.notSaveInOfflineStorage, ignoreSelf: deliverMessage.ignoreSelf, - pushQueueMessage: deliverMessage.pushQueueMessage + pushQueueMessage: deliverMessage.pushQueueMessage, } - await packetManager.deliverToUserOrUsers( - sourceOptions, - destinationUserIds, - payloadOptions - ) + await packetManager.deliverToUserOrUsers(sourceOptions, destinationUserIds, payloadOptions) assert.ok(pushQueueItem) }) From 1ecf067cf10a2956420cd64787aac05181f1499e Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Fri, 13 Mar 2026 14:39:30 +0200 Subject: [PATCH 11/70] update clean node conditions --- app/cluster/cluster_manager.js | 74 ++++++++++++++------ app/networking/packet_manager.js | 1 - app/providers/services/cluster_node/index.js | 11 ++- 3 files changed, 61 insertions(+), 25 deletions(-) diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index e81cc0d6..d56abfa4 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -25,7 +25,9 @@ class ClusterManager extends BaseProtocolProcessor { #nodesSyncInterval = void 0 #localSocket = void 0 - clusterNodesConnections = {} + + clusterNodesConnections = new Map() + closeNodesConnections = new Set() async startSyncingClusterNodes(isColdStart) { if (this.#nodesSyncInterval) { @@ -73,29 +75,48 @@ class ClusterManager extends BaseProtocolProcessor { } async #retrieveExistingClusterNodes() { + const destroyedNodes = new Set() + const clusterNodeService = ServiceLocatorContainer.use("ClusterNodeService") - const nodeList = await clusterNodeService.retrieveActive() - const activeNodeEndpoints = nodeList.map((node) => buildWsEndpoint(node.ip_address, node.port)) - loggerSender.debug("[active] %j %s", nodeList, nodeList.length) + const activeNodes = await clusterNodeService.retrieveActive() + loggerSender.debug("[active] %j %s", activeNodes, activeNodes.size) - const storedNodeEndpoints = await clusterNodeService.retrieveStored() - loggerSender.debug("[stored] %j %s", storedNodeEndpoints, storedNodeEndpoints.length) + const storedNodes = await clusterNodeService.retrieveStored() + loggerSender.debug("[stored] %j %s", storedNodes, storedNodes.size) + + // check node-users stored and no active + storedNodes.forEach((storedNode) => { + if (!activeNodes.has(storedNode)) { + destroyedNodes.add(storedNode) + } + }) - const nodeEndpoints = activeNodeEndpoints.concat(storedNodeEndpoints) - const destroyedNodes = [] + // check ws closed and not active + this.closeNodesConnections.forEach((closeEndpoint) => { + if (!activeNodes.has(closeEndpoint)) { + destroyedNodes.add(closeEndpoint) + } + }) - for (const nodeEndpoint of nodeEndpoints) { + // check current connects but not active (probable ws close did not call) + for (const clusterNode of this.clusterNodesConnections.keys()) { + if (!activeNodes.has(clusterNode)) { + destroyedNodes.add(clusterNode) + } + } + + for (const nodeEndpoint of activeNodes) { const isCurrentNode = config.get("ws.cluster.endpoint") === nodeEndpoint if (isCurrentNode) { continue } try { - await this.retrieveConnectionWithNode(nodeEndpoint) + await this.createOrRetrieveConnectionWithNode(nodeEndpoint) } catch (error) { loggerSender.error(error, "[connect node][failed] %s", nodeEndpoint) - destroyedNodes.push(nodeEndpoint) + destroyedNodes.add(nodeEndpoint) } } @@ -117,15 +138,20 @@ class ClusterManager extends BaseProtocolProcessor { ) } - async retrieveConnectionWithNode(nodeEndpoint) { - const existingWS = this.clusterNodesConnections[nodeEndpoint] + async createOrRetrieveConnectionWithNode(nodeEndpoint) { + const existingWS = this.clusterNodesConnections.get(nodeEndpoint) if (existingWS) { return existingWS } loggerSender.debug("[connect node] %s", nodeEndpoint) - return await this.createConnectionWithNode(nodeEndpoint) + const ws = await this.createConnectionWithNode(nodeEndpoint) + + this.clusterNodesConnections.set(nodeEndpoint, ws) + this.closeNodesConnections.delete(nodeEndpoint) + + return ws } async createConnectionWithNode(nodeEndpoint) { @@ -153,8 +179,6 @@ class ClusterManager extends BaseProtocolProcessor { if (clusterPacket.node_info) { const nodeInfo = clusterPacket.node_info - this.clusterNodesConnections[nodeInfo.endpoint] = ws - nodeSederLogger.debug("[node handshake finished] %s", nodeInfo.endpoint) resolve(ws) return @@ -163,7 +187,7 @@ class ClusterManager extends BaseProtocolProcessor { ws.on("close", async () => { nodeSederLogger.debug("[Close] %s", ws.nodeEndpoint) - await this.cleanDestroyedNodeData(ws.nodeEndpoint) + this.onCloseNode(ws.nodeEndpoint) }) }) } @@ -191,7 +215,7 @@ class ClusterManager extends BaseProtocolProcessor { loggerReceiver.debug("[node handshake pong] %s", nodeInfo.endpoint) this.#shareCurrentNodeInfo(ws) - await this.retrieveConnectionWithNode(nodeInfo.endpoint) + await this.createOrRetrieveConnectionWithNode(nodeInfo.endpoint) return } @@ -202,7 +226,7 @@ class ClusterManager extends BaseProtocolProcessor { close: async (ws, code, message) => { logger.debug("[Close] %s Code: %s", ws.nodeEndpoint, code) - await this.cleanDestroyedNodeData(ws.nodeEndpoint) + this.onCloseNode(ws.nodeEndpoint) }, }) @@ -220,10 +244,10 @@ class ClusterManager extends BaseProtocolProcessor { } async senderClusterDeliverPacket(nodeEndpoint, deliverPacket) { - let recipientClusterNodeConnection = this.clusterNodesConnections[nodeEndpoint] + let recipientClusterNodeConnection = this.clusterNodesConnections.get(nodeEndpoint) if (!recipientClusterNodeConnection) { - recipientClusterNodeConnection = await this.retrieveConnectionWithNode(ip, port) + recipientClusterNodeConnection = await this.createOrRetrieveConnectionWithNode(ip, port) } const clusterPacket = { deliverPacket } @@ -231,10 +255,16 @@ class ClusterManager extends BaseProtocolProcessor { recipientClusterNodeConnection.send(JSON.stringify(clusterPacket)) } + onCloseNode(nodeEndpoint) { + this.closeNodesConnections.add(nodeEndpoint) + this.clusterNodesConnections.delete(nodeEndpoint) + } + async cleanDestroyedNodeData(nodeEndpoint) { logger.debug("[clean node] %s", nodeEndpoint) - delete this.clusterNodesConnections[nodeEndpoint] + this.clusterNodesConnections.delete(nodeEndpoint) + this.closeNodesConnections.delete(nodeEndpoint) const sessionService = ServiceLocatorContainer.use("SessionService") diff --git a/app/networking/packet_manager.js b/app/networking/packet_manager.js index c79b3b16..35a648bf 100644 --- a/app/networking/packet_manager.js +++ b/app/networking/packet_manager.js @@ -111,7 +111,6 @@ class PacketManager { const clusterPacket = { userId, packet: payloadOptions.packet, senderInfo } await clusterManager.senderClusterDeliverPacket(nodeEndpoint, clusterPacket) } catch (error) { - await sessionService.clearNodeUsersSession(nodeEndpoint) logger.error(error, "[deliver to other node]") } }) diff --git a/app/providers/services/cluster_node/index.js b/app/providers/services/cluster_node/index.js index 14f72455..065b56f9 100644 --- a/app/providers/services/cluster_node/index.js +++ b/app/providers/services/cluster_node/index.js @@ -1,4 +1,5 @@ import { CONSTANTS as MAIN_CONSTANTS } from "../../../constants/constants.js" +import { buildWsEndpoint } from "../../../utils/build_ws_endpoint.js" class ClusterNodeService { constructor(config, redisClient) { @@ -20,13 +21,19 @@ class ClusterNodeService { nodesInfo.push(nodeInfo) } - return nodesInfo + const nodeEndpoints = nodesInfo.map((node) => buildWsEndpoint(node.ip_address, node.port)) + + const activeNodeEndpoints = new Set(nodeEndpoints) + + return activeNodeEndpoints } async retrieveStored() { const nodeKeys = await this.redisClient.findKeysByPattern(`${MAIN_CONSTANTS.REDIS_PREFIXES.NODE_USERS}:*`) - return nodeKeys.map((nodeKey) => nodeKey.replace(`${MAIN_CONSTANTS.REDIS_PREFIXES.NODE_USERS}:`, "")) + const nodeEndpoints = nodeKeys.map((nodeKey) => nodeKey.replace(`${MAIN_CONSTANTS.REDIS_PREFIXES.NODE_USERS}:`, "")) + + return new Set(nodeEndpoints) } async upsert(addressParams, optionalParams) { From 8bfe459f0073532a476d2eb7f70e8a2f6f08a0cf Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Mon, 16 Mar 2026 23:16:03 +0200 Subject: [PATCH 12/70] add reconnecting --- app/cluster/cluster_manager.js | 103 ++++++++++++++++++---- app/utils/promise-queue-with-jittering.js | 61 +++++++++++++ 2 files changed, 147 insertions(+), 17 deletions(-) create mode 100644 app/utils/promise-queue-with-jittering.js diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index d56abfa4..f0c574de 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -13,7 +13,7 @@ import ServiceLocatorContainer from "../common/ServiceLocatorContainer.js" import activitySender from "../networking/activity_sender.js" import packetManager from "../networking/packet_manager.js" -import { buildWsEndpoint } from "../utils/build_ws_endpoint.js" +import { promiseQueueWithJittering, CancelQueueError } from "../utils/promise-queue-with-jittering.js" const logger = mainLogger.child("[ClusterManager]") const loggerSender = logger.child("[Sender]") @@ -21,13 +21,39 @@ const loggerReceiver = logger.child("[Receiver]") const decoder = new StringDecoder("utf8") +class ReconnectManager { + nodeEndpoint = void 0 + startFn = void 0 + cancelFn = void 0 + + constructor(nodeEndpoint, promiseFn) { + this.nodeEndpoint = nodeEndpoint + + const { start, cancel } = promiseQueueWithJittering(promiseFn, 3, 1_000) + + this.startFn = start + this.cancelFn = cancel + } + + start() { + return this.startFn?.() + } + + cancel() { + this.cancelFn?.() + + this.startFn = void 0 + this.cancelFn = void 0 + } +} + class ClusterManager extends BaseProtocolProcessor { #nodesSyncInterval = void 0 #localSocket = void 0 clusterNodesConnections = new Map() - closeNodesConnections = new Set() + closeNodesConnections = new Map() async startSyncingClusterNodes(isColdStart) { if (this.#nodesSyncInterval) { @@ -80,10 +106,10 @@ class ClusterManager extends BaseProtocolProcessor { const clusterNodeService = ServiceLocatorContainer.use("ClusterNodeService") const activeNodes = await clusterNodeService.retrieveActive() - loggerSender.debug("[active] %j %s", activeNodes, activeNodes.size) + loggerSender.debug("[active] %j %s", Array.from(activeNodes), activeNodes.size) const storedNodes = await clusterNodeService.retrieveStored() - loggerSender.debug("[stored] %j %s", storedNodes, storedNodes.size) + loggerSender.debug("[stored] %j %s", Array.from(storedNodes), storedNodes.size) // check node-users stored and no active storedNodes.forEach((storedNode) => { @@ -93,11 +119,11 @@ class ClusterManager extends BaseProtocolProcessor { }) // check ws closed and not active - this.closeNodesConnections.forEach((closeEndpoint) => { - if (!activeNodes.has(closeEndpoint)) { - destroyedNodes.add(closeEndpoint) + for (const clusterNode of this.closeNodesConnections.keys()) { + if (!activeNodes.has(clusterNode)) { + destroyedNodes.add(clusterNode) } - }) + } // check current connects but not active (probable ws close did not call) for (const clusterNode of this.clusterNodesConnections.keys()) { @@ -112,11 +138,15 @@ class ClusterManager extends BaseProtocolProcessor { continue } + if (this.closeNodesConnections.has(nodeEndpoint)) { + continue + } + try { - await this.createOrRetrieveConnectionWithNode(nodeEndpoint) + await this.createOrRetrieveConnectionWithNode(nodeEndpoint, true) } catch (error) { loggerSender.error(error, "[connect node][failed] %s", nodeEndpoint) - destroyedNodes.add(nodeEndpoint) + this.startNodeReconnecting(nodeEndpoint, false) } } @@ -138,22 +168,61 @@ class ClusterManager extends BaseProtocolProcessor { ) } - async createOrRetrieveConnectionWithNode(nodeEndpoint) { + async createOrRetrieveConnectionWithNode(nodeEndpoint, throwIfActiveReconnect) { const existingWS = this.clusterNodesConnections.get(nodeEndpoint) if (existingWS) { return existingWS } + if (throwIfActiveReconnect && this.closeNodesConnections.has(nodeEndpoint)) { + return new Error("Node reconnecting") + } + loggerSender.debug("[connect node] %s", nodeEndpoint) const ws = await this.createConnectionWithNode(nodeEndpoint) this.clusterNodesConnections.set(nodeEndpoint, ws) - this.closeNodesConnections.delete(nodeEndpoint) + this.cancelNodeReconnecting(nodeEndpoint) return ws } + async startNodeReconnecting(nodeEndpoint, skipIfHashActiveReconnection) { + if (skipIfHashActiveReconnection && this.closeNodesConnections.has(nodeEndpoint)) { + return + } + + const reconnectLogger = logger.child(`[Reconnecting][${nodeEndpoint}]`) + reconnectLogger.debug("[start]") + + this.cancelNodeReconnecting(nodeEndpoint) + + const connectFn = () => this.createOrRetrieveConnectionWithNode(nodeEndpoint) + const reconnectManager = new ReconnectManager(nodeEndpoint, connectFn) + + this.closeNodesConnections.set(nodeEndpoint, reconnectManager) + + try { + const result = await reconnectManager.start() + + reconnectLogger.debug("[finish]") + + this.cancelNodeReconnecting(nodeEndpoint) + + return result + } catch (error) { + reconnectLogger.error(error, "[error]") + } + } + + cancelNodeReconnecting(nodeEndpoint) { + const existingReconnectManager = this.closeNodesConnections.get(nodeEndpoint) + existingReconnectManager?.cancel?.() + + this.closeNodesConnections.delete(nodeEndpoint) + } + async createConnectionWithNode(nodeEndpoint) { const nodeSederLogger = loggerSender.child(`[${nodeEndpoint}]`) @@ -164,7 +233,7 @@ class ClusterManager extends BaseProtocolProcessor { ws.on("error", async (event) => { nodeSederLogger.error(event, "[error]") - reject(new Error(`[connection][error] ${nodeEndpoint}`)) + reject(new Error(`Error on ${nodeEndpoint}`)) }) ws.on("open", async () => { @@ -215,7 +284,7 @@ class ClusterManager extends BaseProtocolProcessor { loggerReceiver.debug("[node handshake pong] %s", nodeInfo.endpoint) this.#shareCurrentNodeInfo(ws) - await this.createOrRetrieveConnectionWithNode(nodeInfo.endpoint) + await this.createOrRetrieveConnectionWithNode(nodeInfo.endpoint, true) return } @@ -247,7 +316,7 @@ class ClusterManager extends BaseProtocolProcessor { let recipientClusterNodeConnection = this.clusterNodesConnections.get(nodeEndpoint) if (!recipientClusterNodeConnection) { - recipientClusterNodeConnection = await this.createOrRetrieveConnectionWithNode(ip, port) + recipientClusterNodeConnection = await this.createOrRetrieveConnectionWithNode(nodeEndpoint, true) } const clusterPacket = { deliverPacket } @@ -256,15 +325,15 @@ class ClusterManager extends BaseProtocolProcessor { } onCloseNode(nodeEndpoint) { - this.closeNodesConnections.add(nodeEndpoint) this.clusterNodesConnections.delete(nodeEndpoint) + this.startNodeReconnecting(nodeEndpoint, true) } async cleanDestroyedNodeData(nodeEndpoint) { logger.debug("[clean node] %s", nodeEndpoint) this.clusterNodesConnections.delete(nodeEndpoint) - this.closeNodesConnections.delete(nodeEndpoint) + this.cancelNodeReconnecting(nodeEndpoint) const sessionService = ServiceLocatorContainer.use("SessionService") diff --git a/app/utils/promise-queue-with-jittering.js b/app/utils/promise-queue-with-jittering.js new file mode 100644 index 00000000..29900901 --- /dev/null +++ b/app/utils/promise-queue-with-jittering.js @@ -0,0 +1,61 @@ +import { setTimeout } from "node:timers/promises" + +class CancelQueueError extends Error {} + +const promiseQueueWithJittering = (executablePromise, tryCount, delay) => { + let isCanceled = false + + const cancel = () => isCanceled = true + + const start = async () => { + if (isCanceled) { + throw new CancelQueueError("Canceled") + } + + const tryDelays = new Array(tryCount).fill(0).map((_, index) => { + const tryCount = index + 1 + const tryDelay = Math.pow(tryCount, 2) * delay + return tryDelay + }) + + let lastError = void 0 + + console.log('[tryDelays]', tryDelays, isCanceled) + + for (const tryDelay of tryDelays) { + try { + console.log('[try]', tryDelay, isCanceled) + + if (isCanceled) { + throw new CancelQueueError("Canceled") + } + + await setTimeout(tryDelay) + + if (isCanceled) { + throw new CancelQueueError("Canceled") + } + + const successResult = await executablePromise() + + if (isCanceled) { + throw new CancelQueueError("Canceled") + } + + return successResult + } catch (error) { + lastError = error + } + } + + if (isCanceled) { + throw new CancelQueueError("Canceled") + } + + throw lastError + } + + return { start, cancel } +} + +export { promiseQueueWithJittering, CancelQueueError } \ No newline at end of file From c6c720089c6f9da9c60af97eaeea89a8ca91e918 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Tue, 17 Mar 2026 16:01:02 +0200 Subject: [PATCH 13/70] add Node Clustering readme --- README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/README.md b/README.md index c29fa599..a8c60cec 100644 --- a/README.md +++ b/README.md @@ -77,3 +77,68 @@ Want to support us? Buy Me A Coffee + +## Node Clustering + +### What data is stored in Redis by each node ? + +#### sama-node-data + +Record key in redis like `sama-node-data:{node-endpoint}` where `{node-endpoint}` ws url like `ws://192.168.1.13:55495/`, so redis record will be `sama-node-data:ws://192.168.1.13:55495/`. The record is HSET with ttl that equal config value `ws.cluster.nodeExpiresIn` in seconds + 5 seconds. The record has key/values like `ip/port/host` etc. + +#### sama-node-users + +Record key in redis like `sama-node-users:{node-endpoint}`. The record is SET that contains list user users connected to this node, user item is string like `{organizationId}:{userId}:{deviceId}`. Item example: `683db8ecdb9dee54f53304c0:683db9ed29aecb9feb4afb73:38400000-8cf0-11bd-b23e-10b96e40000d` + +#### sama-user-devices + +Record key in redis like `sama-user-devices:{organizationId}:{userId}`. The record is SET that contains list user devices ids connected to all nodes. Item example: `38400000-8cf0-11bd-b23e-10b96e40000d` + +#### sama-user-data + +Record key in redis like `sama-user-data:{userId}:{deviceId}`. The record in HSET that contains extra user session data e.g. active/inactive status + +### How nodes discover each other ? + +Each node write record key in Redis like `sama-node-data:{node-endpoint}` where `{node-endpoint}` it is own ws url. Node create this record on start and update on sync cluster (check method `syncCluster`). In sync method node retrieve all records from Redis with prefix `sama-node-data:`, the parse record ws url part (`{node-endpoint}`) and try establish ws connection if it doesn't already exist. + + +### How nodes connect to each other ? + +If node-A does not have establish connection with node-B, node-A will create ws connection (check method `createConnectionWithNode`). When ws with node-B endpoint (`node-endpoint`) successfully opened - node-A send like 'ping' message with own network data (`ip/port/host` etc.) to node-B (check method `shareCurrentNodeInfo`), node-B send to node-A like 'pong' message with own network data, node-A after receiving 'pong' message - connection successfully established and handshake finished, app resolve promise and save ws connection in local object (check prop `clusterNodesConnections`). Node-B after send 'pong' message start connecting with node-A by same flow + +#### What happens when connection breaks between 2 nodes ? + +If connecting breaks between 2 nodes, node detect it by ws event close, and start reconnecting (check method `startNodeReconnecting` and `promiseQueueWithJittering`, prop `closeNodesConnections`), this method try 3 time create connection until the first successful with delay '1/4/9 seconds', flow like 1 second delay - try connect (if success return) - 4 seconds delay - try connect (if success return) - 9 seconds delay - try connect (if success return) - failed + +### What happens when new node added to cluster ? + +2 Ways (node-A,node-B - running, node-C - new node in cluster) + +- Flow `How nodes discover each other` and then `How nodes connect to each other`. Node-C retrieve redis records (`sama-node-data`) created by node-A,node-B and create ws connections with each node (open ws and then connect flow with 'ping'/'pong'). + +- Node-A,node-B in method `syncCluster` with call with interval that equal config value `ws.cluster.nodeExpiresIn` in ms, retrieve `sama-node-data` records and see new node record created by node-C and then create connection with node-C + +### What happens when existing node removed from cluster ? + +Example with node-A and node-B + +When node-B disconnect from cluster, ws connection breaks and node-A start reconnecting with node-B but it will not be success. +Then on next `syncCluster` iteration (or next + 1 iteration) node-A can't find node-B record in `sama-node-data` records, but has active reconnecting state with node-B, then mean that node-B destroyed because no-one update node-B `sama-node-data` record ttl, then node-A cancel reconnecting and clean redis data creates by node-B (`sama-node-users`/`sama-user-devices`/`sama-user-data`), `sama-node-data` already deleted by redis ttl. + +#### Conditions when node mark other nodes like destroyed and should clean data ? + +It can be detected no `syncCluster` iteration + +- If have active node record `sama-node-users` w/o ttl, but do not have record `sama-node-data` - no-one update record so node is destroyed +- If do not have node record `sama-node-data` but has reconnecting state +- If have connection (check prop `clusterNodesConnections`) but do not have record `sama-node-data` (case when some how ws close was not called) + +#### How its data is cleared from Redis ? + +Check method `cleanDestroyedNodeData` that clean destroyed node data by node-endpoint. +Flow: +- retrieve `sama-node-users` - list of user who was connected to destroyed node +- delete 'smembers' from `sama-user-devices` +- delete `sama-user-data` record +- delete `sama-node-users` From 7420a8a06952ac6a9e8139c5b084adf08bdba28a Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 19 Mar 2026 13:44:44 +0200 Subject: [PATCH 14/70] it can close ws with code --- app/cluster/cluster_manager.js | 97 ++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 41 deletions(-) diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index f0c574de..cc282037 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -21,6 +21,9 @@ const loggerReceiver = logger.child("[Receiver]") const decoder = new StringDecoder("utf8") +const DUPLICATE_CLOSE_WS_CODE = 1004 +const DUPLICATE_CLOSE_WS_REASON = "Duplicate" + class ReconnectManager { nodeEndpoint = void 0 startFn = void 0 @@ -182,45 +185,16 @@ class ClusterManager extends BaseProtocolProcessor { const ws = await this.createConnectionWithNode(nodeEndpoint) - this.clusterNodesConnections.set(nodeEndpoint, ws) this.cancelNodeReconnecting(nodeEndpoint) - return ws - } - - async startNodeReconnecting(nodeEndpoint, skipIfHashActiveReconnection) { - if (skipIfHashActiveReconnection && this.closeNodesConnections.has(nodeEndpoint)) { - return - } - - const reconnectLogger = logger.child(`[Reconnecting][${nodeEndpoint}]`) - reconnectLogger.debug("[start]") - - this.cancelNodeReconnecting(nodeEndpoint) - - const connectFn = () => this.createOrRetrieveConnectionWithNode(nodeEndpoint) - const reconnectManager = new ReconnectManager(nodeEndpoint, connectFn) - - this.closeNodesConnections.set(nodeEndpoint, reconnectManager) - - try { - const result = await reconnectManager.start() - - reconnectLogger.debug("[finish]") - - this.cancelNodeReconnecting(nodeEndpoint) - - return result - } catch (error) { - reconnectLogger.error(error, "[error]") + if (this.clusterNodesConnections.get(nodeEndpoint)) { + ws.close(DUPLICATE_CLOSE_WS_CODE, DUPLICATE_CLOSE_WS_REASON) + return this.clusterNodesConnections.get(nodeEndpoint) } - } - cancelNodeReconnecting(nodeEndpoint) { - const existingReconnectManager = this.closeNodesConnections.get(nodeEndpoint) - existingReconnectManager?.cancel?.() + this.clusterNodesConnections.set(nodeEndpoint, ws) - this.closeNodesConnections.delete(nodeEndpoint) + return ws } async createConnectionWithNode(nodeEndpoint) { @@ -254,13 +228,49 @@ class ClusterManager extends BaseProtocolProcessor { } }) - ws.on("close", async () => { - nodeSederLogger.debug("[Close] %s", ws.nodeEndpoint) - this.onCloseNode(ws.nodeEndpoint) + ws.on("close", (code, reason) => { + reason = reason.toString("utf-8") + nodeSederLogger.debug("[Close] %s Code: %s Reason: %s", ws.nodeEndpoint, code, reason) + this.onCloseNode(ws.nodeEndpoint, +code, reason) }) }) } + async startNodeReconnecting(nodeEndpoint, skipIfHashActiveReconnection) { + if (skipIfHashActiveReconnection && this.closeNodesConnections.has(nodeEndpoint)) { + return + } + + const reconnectLogger = logger.child(`[Reconnecting][${nodeEndpoint}]`) + reconnectLogger.debug("[start]") + + this.cancelNodeReconnecting(nodeEndpoint) + + const connectFn = () => this.createOrRetrieveConnectionWithNode(nodeEndpoint) + const reconnectManager = new ReconnectManager(nodeEndpoint, connectFn) + + this.closeNodesConnections.set(nodeEndpoint, reconnectManager) + + try { + const result = await reconnectManager.start() + + reconnectLogger.debug("[finish]") + + this.cancelNodeReconnecting(nodeEndpoint) + + return result + } catch (error) { + reconnectLogger.error(error, "[error]") + } + } + + cancelNodeReconnecting(nodeEndpoint) { + const existingReconnectManager = this.closeNodesConnections.get(nodeEndpoint) + existingReconnectManager?.cancel?.() + + this.closeNodesConnections.delete(nodeEndpoint) + } + async createLocalSocket({ isSSL, appOptions, wsOptions, listenOptions }) { return new Promise((resolve) => { this.#localSocket = isSSL ? uWS.SSLApp(appOptions) : uWS.App(appOptions) @@ -293,9 +303,10 @@ class ClusterManager extends BaseProtocolProcessor { } }, - close: async (ws, code, message) => { - logger.debug("[Close] %s Code: %s", ws.nodeEndpoint, code) - this.onCloseNode(ws.nodeEndpoint) + close: (ws, code, reason) => { + reason = decoder.write(Buffer.from(reason)) + logger.debug("[Close] %s Code: %s Reason: %s", ws.nodeEndpoint, code, reason) + this.onCloseNode(ws.nodeEndpoint, +code, reason) }, }) @@ -324,7 +335,11 @@ class ClusterManager extends BaseProtocolProcessor { recipientClusterNodeConnection.send(JSON.stringify(clusterPacket)) } - onCloseNode(nodeEndpoint) { + onCloseNode(nodeEndpoint, closeCode, reason) { + if (closeCode === DUPLICATE_CLOSE_WS_CODE && reason === DUPLICATE_CLOSE_WS_REASON) { + return + } + this.clusterNodesConnections.delete(nodeEndpoint) this.startNodeReconnecting(nodeEndpoint, true) } From 822cd6fefce6819207f5808cc34f95fda084d8ac Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 19 Mar 2026 17:43:25 +0200 Subject: [PATCH 15/70] reconnecty only ifWas opened --- app/cluster/cluster_manager.js | 49 +++++++++++++++-------- app/utils/promise-queue-with-jittering.js | 12 ++++-- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index cc282037..2b0fd6e3 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -26,8 +26,11 @@ const DUPLICATE_CLOSE_WS_REASON = "Duplicate" class ReconnectManager { nodeEndpoint = void 0 + startFn = void 0 - cancelFn = void 0 + cancelFn = void 0 + + isFinished = false constructor(nodeEndpoint, promiseFn) { this.nodeEndpoint = nodeEndpoint @@ -47,6 +50,8 @@ class ReconnectManager { this.startFn = void 0 this.cancelFn = void 0 + + this.isFinished = true } } @@ -56,7 +61,7 @@ class ClusterManager extends BaseProtocolProcessor { #localSocket = void 0 clusterNodesConnections = new Map() - closeNodesConnections = new Map() + reconnectingNodesConnections = new Map() async startSyncingClusterNodes(isColdStart) { if (this.#nodesSyncInterval) { @@ -84,7 +89,7 @@ class ClusterManager extends BaseProtocolProcessor { await this.#storeCurrentNode(sessionService.totalSessions()) - await this.#retrieveExistingClusterNodes() + await this.#connectToExistingClusterNodes() } async #storeCurrentNode(usersCount) { @@ -103,7 +108,7 @@ class ClusterManager extends BaseProtocolProcessor { await clusterNodeService.upsert(addressParams, optionalParams) } - async #retrieveExistingClusterNodes() { + async #connectToExistingClusterNodes() { const destroyedNodes = new Set() const clusterNodeService = ServiceLocatorContainer.use("ClusterNodeService") @@ -122,7 +127,7 @@ class ClusterManager extends BaseProtocolProcessor { }) // check ws closed and not active - for (const clusterNode of this.closeNodesConnections.keys()) { + for (const clusterNode of this.reconnectingNodesConnections.keys()) { if (!activeNodes.has(clusterNode)) { destroyedNodes.add(clusterNode) } @@ -141,7 +146,8 @@ class ClusterManager extends BaseProtocolProcessor { continue } - if (this.closeNodesConnections.has(nodeEndpoint)) { + const existingReconnectManager = this.reconnectingNodesConnections.get(nodeEndpoint) + if (existingReconnectManager && !existingReconnectManager.isFinished) { continue } @@ -177,7 +183,7 @@ class ClusterManager extends BaseProtocolProcessor { return existingWS } - if (throwIfActiveReconnect && this.closeNodesConnections.has(nodeEndpoint)) { + if (throwIfActiveReconnect && this.reconnectingNodesConnections.has(nodeEndpoint)) { return new Error("Node reconnecting") } @@ -212,6 +218,7 @@ class ClusterManager extends BaseProtocolProcessor { ws.on("open", async () => { nodeSederLogger.debug("[Open] %s", ws.nodeEndpoint) + ws.isWasOpened = true this.#shareCurrentNodeInfo(ws) }) @@ -230,14 +237,19 @@ class ClusterManager extends BaseProtocolProcessor { ws.on("close", (code, reason) => { reason = reason.toString("utf-8") - nodeSederLogger.debug("[Close] %s Code: %s Reason: %s", ws.nodeEndpoint, code, reason) + nodeSederLogger.debug("[Close][%s] IsWasOpened: %s Code: %s Reason: %s", ws.nodeEndpoint, ws.isWasOpened, code, reason) + + if (!ws.isWasOpened) { + return + } + this.onCloseNode(ws.nodeEndpoint, +code, reason) }) }) } async startNodeReconnecting(nodeEndpoint, skipIfHashActiveReconnection) { - if (skipIfHashActiveReconnection && this.closeNodesConnections.has(nodeEndpoint)) { + if (skipIfHashActiveReconnection && this.reconnectingNodesConnections.has(nodeEndpoint)) { return } @@ -246,29 +258,32 @@ class ClusterManager extends BaseProtocolProcessor { this.cancelNodeReconnecting(nodeEndpoint) - const connectFn = () => this.createOrRetrieveConnectionWithNode(nodeEndpoint) + const connectFn = () => this.createOrRetrieveConnectionWithNode(nodeEndpoint, false) const reconnectManager = new ReconnectManager(nodeEndpoint, connectFn) - this.closeNodesConnections.set(nodeEndpoint, reconnectManager) + this.reconnectingNodesConnections.set(nodeEndpoint, reconnectManager) try { const result = await reconnectManager.start() reconnectLogger.debug("[finish]") - this.cancelNodeReconnecting(nodeEndpoint) + this.cancelNodeReconnecting(nodeEndpoint, true) return result } catch (error) { reconnectLogger.error(error, "[error]") + reconnectManager.cancel() } } - cancelNodeReconnecting(nodeEndpoint) { - const existingReconnectManager = this.closeNodesConnections.get(nodeEndpoint) - existingReconnectManager?.cancel?.() + cancelNodeReconnecting(nodeEndpoint, isSuccess) { + if (!isSuccess) { + const existingReconnectManager = this.reconnectingNodesConnections.get(nodeEndpoint) + existingReconnectManager?.cancel?.() + } - this.closeNodesConnections.delete(nodeEndpoint) + this.reconnectingNodesConnections.delete(nodeEndpoint) } async createLocalSocket({ isSSL, appOptions, wsOptions, listenOptions }) { @@ -305,7 +320,7 @@ class ClusterManager extends BaseProtocolProcessor { close: (ws, code, reason) => { reason = decoder.write(Buffer.from(reason)) - logger.debug("[Close] %s Code: %s Reason: %s", ws.nodeEndpoint, code, reason) + logger.debug("[Close][%s] Code: %s Reason: %s", ws.nodeEndpoint, code, reason) this.onCloseNode(ws.nodeEndpoint, +code, reason) }, }) diff --git a/app/utils/promise-queue-with-jittering.js b/app/utils/promise-queue-with-jittering.js index 29900901..f0863dac 100644 --- a/app/utils/promise-queue-with-jittering.js +++ b/app/utils/promise-queue-with-jittering.js @@ -6,6 +6,7 @@ const promiseQueueWithJittering = (executablePromise, tryCount, delay) => { let isCanceled = false const cancel = () => isCanceled = true + const checkIsCanceled = () => isCanceled const start = async () => { if (isCanceled) { @@ -36,7 +37,7 @@ const promiseQueueWithJittering = (executablePromise, tryCount, delay) => { throw new CancelQueueError("Canceled") } - const successResult = await executablePromise() + const successResult = await executablePromise(checkIsCanceled) if (isCanceled) { throw new CancelQueueError("Canceled") @@ -45,14 +46,19 @@ const promiseQueueWithJittering = (executablePromise, tryCount, delay) => { return successResult } catch (error) { lastError = error + if (error instanceof CancelQueueError) { + break + } } } + if (lastError) { + throw lastError + } + if (isCanceled) { throw new CancelQueueError("Canceled") } - - throw lastError } return { start, cancel } From 8438ec5141cb55f416e995728790f198292ed6ae Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Fri, 20 Mar 2026 13:57:58 +0200 Subject: [PATCH 16/70] fix typo --- app/cluster/cluster_manager.js | 4 ++-- app/networking/packet_manager.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index 2b0fd6e3..c5131f4c 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -184,7 +184,7 @@ class ClusterManager extends BaseProtocolProcessor { } if (throwIfActiveReconnect && this.reconnectingNodesConnections.has(nodeEndpoint)) { - return new Error("Node reconnecting") + throw new Error("Node reconnecting") } loggerSender.debug("[connect node] %s", nodeEndpoint) @@ -346,7 +346,7 @@ class ClusterManager extends BaseProtocolProcessor { } const clusterPacket = { deliverPacket } - loggerSender.trace("[%s][deliver cluster] %j", nodeEndpoint, clusterPacket) + loggerSender.trace("[%s][send] %j", nodeEndpoint, clusterPacket) recipientClusterNodeConnection.send(JSON.stringify(clusterPacket)) } diff --git a/app/networking/packet_manager.js b/app/networking/packet_manager.js index 35a648bf..c3478814 100644 --- a/app/networking/packet_manager.js +++ b/app/networking/packet_manager.js @@ -109,9 +109,10 @@ class PacketManager { Object.keys(userIds).forEach(async (userId) => { try { const clusterPacket = { userId, packet: payloadOptions.packet, senderInfo } + logger.trace("[Cluster][%s][deliver] %j", nodeEndpoint, clusterPacket) await clusterManager.senderClusterDeliverPacket(nodeEndpoint, clusterPacket) } catch (error) { - logger.error(error, "[deliver to other node]") + logger.error(error, "[%s][deliver to other node]", nodeEndpoint) } }) }) From 482f4aba1fa5f5505f063c95b8681cf02efa8b91 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Tue, 31 Mar 2026 19:15:39 +0300 Subject: [PATCH 17/70] start testing --- package-lock.json | 960 +++++++++++++++++- package.json | 3 +- test/cluster/cluster.spec.js | 227 +++++ .../sama-communications-sdk-0.16.1.tgz | Bin 0 -> 24046 bytes test/cluster/utils.js | 0 5 files changed, 1176 insertions(+), 14 deletions(-) create mode 100644 test/cluster/cluster.spec.js create mode 100644 test/cluster/sama-communications-sdk-0.16.1.tgz create mode 100644 test/cluster/utils.js diff --git a/package-lock.json b/package-lock.json index d95e7999..572d3a03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "ws": "^8.12.0" }, "devDependencies": { + "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.16.1.tgz", "c8": "^10.1.3", "mocha": "^10.0.0", "nodemon": "2.0.19", @@ -1011,6 +1012,42 @@ "node": ">=14.0.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz", @@ -1022,6 +1059,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -1188,10 +1239,11 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.20", @@ -1257,6 +1309,134 @@ "tslib": "2" } }, + "node_modules/@microsoft/api-extractor": { + "version": "7.57.7", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.57.7.tgz", + "integrity": "sha512-kmnmVs32MFWbV5X6BInC1/TfCs7y1ugwxv1xHsAIj/DyUfoe7vtO0alRUgbQa57+yRGHBBjlNcEk33SCAt5/dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/api-extractor-model": "7.33.4", + "@microsoft/tsdoc": "~0.16.0", + "@microsoft/tsdoc-config": "~0.18.1", + "@rushstack/node-core-library": "5.20.3", + "@rushstack/rig-package": "0.7.2", + "@rushstack/terminal": "0.22.3", + "@rushstack/ts-command-line": "5.3.3", + "diff": "~8.0.2", + "lodash": "~4.17.23", + "minimatch": "10.2.3", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "source-map": "~0.6.1", + "typescript": "5.8.2" + }, + "bin": { + "api-extractor": "bin/api-extractor" + } + }, + "node_modules/@microsoft/api-extractor-model": { + "version": "7.33.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.33.4.tgz", + "integrity": "sha512-u1LTaNTikZAQ9uK6KG1Ms7nvNedsnODnspq/gH2dcyETWvH4hVNGNDvRAEutH66kAmxA4/necElqGNs1FggC8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "~0.16.0", + "@microsoft/tsdoc-config": "~0.18.1", + "@rushstack/node-core-library": "5.20.3" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/api-extractor/node_modules/minimatch": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz", + "integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@microsoft/api-extractor/node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", + "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.18.1.tgz", + "integrity": "sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.16.0", + "ajv": "~8.18.0", + "jju": "~1.4.0", + "resolve": "~1.22.2" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz", @@ -1415,6 +1595,162 @@ "@redis/client": "^1.0.0" } }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rushstack/node-core-library": { + "version": "5.20.3", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.20.3.tgz", + "integrity": "sha512-95JgEPq2k7tHxhF9/OJnnyHDXfC9cLhhta0An/6MlkDsX2A6dTzDrTUG18vx4vjc280V0fi0xDH9iQczpSuWsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "~8.18.0", + "ajv-draft-04": "~1.0.0", + "ajv-formats": "~3.0.1", + "fs-extra": "~11.3.0", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@rushstack/problem-matcher": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@rushstack/problem-matcher/-/problem-matcher-0.2.1.tgz", + "integrity": "sha512-gulfhBs6n+I5b7DvjKRfhMGyUejtSgOHTclF/eONr8hcgF1APEDjhxIsfdUYYMzC3rvLwGluqLjbwCFZ8nxrog==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/rig-package": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.7.2.tgz", + "integrity": "sha512-9XbFWuqMYcHUso4mnETfhGVUSaADBRj6HUAAEYk50nMPn8WRICmBuCphycQGNB3duIR6EEZX3Xj3SYc2XiP+9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" + } + }, + "node_modules/@rushstack/terminal": { + "version": "0.22.3", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.22.3.tgz", + "integrity": "sha512-gHC9pIMrUPzAbBiI4VZMU7Q+rsCzb8hJl36lFIulIzoceKotyKL3Rd76AZ2CryCTKEg+0bnTj406HE5YY5OQvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/node-core-library": "5.20.3", + "@rushstack/problem-matcher": "0.2.1", + "supports-color": "~8.1.1" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.3.3.tgz", + "integrity": "sha512-c+ltdcvC7ym+10lhwR/vWiOhsrm/bP3By2VsFcs5qTKv+6tTmxgbVrtJ5NdNjANiV5TcmOZgUN+5KYQ4llsvEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rushstack/terminal": "0.22.3", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "string-argv": "~0.3.1" + } + }, + "node_modules/@rushstack/ts-command-line/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@sama-communications/sdk": { + "version": "0.16.1", + "resolved": "file:test/cluster/sama-communications-sdk-0.16.1.tgz", + "integrity": "sha512-Ls1FBvMo+SamOPK+X1W7dpq+/IqmXgWfSSIg5GdsMrilvR69Rkzn7inzvu2Z4Def02H+PwK52cvxz5Nl3FcTRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "vite-plugin-dts": "^4.5.4", + "ws": "8.18.3" + } + }, "node_modules/@sebastianwessel/quickjs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@sebastianwessel/quickjs/-/quickjs-2.1.1.tgz", @@ -2086,6 +2422,20 @@ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "license": "MIT" }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2107,12 +2457,138 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz", + "integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.31", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz", + "integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/language-core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.0.tgz", + "integrity": "sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "~2.4.11", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^0.4.9", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz", + "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==", + "dev": true, + "license": "MIT" + }, "node_modules/@zxing/text-encoding": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", "optional": true }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ai": { "version": "5.0.51", "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.51.tgz", @@ -2131,6 +2607,63 @@ "zod": "^3.25.76 || ^4" } }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/alien-signals": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-0.4.14.tgz", + "integrity": "sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -2236,10 +2769,11 @@ "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2613,12 +3147,26 @@ "node": "^12.20.0 || >=14" } }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -2710,6 +3258,13 @@ "node": "*" } }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2803,6 +3358,19 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -2824,6 +3392,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/eventsource-parser": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", @@ -2833,11 +3408,25 @@ "node": ">=18.0.0" } }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-copy": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-redact": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", @@ -2852,6 +3441,23 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fast-xml-parser": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", @@ -3144,9 +3750,10 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -3191,6 +3798,16 @@ "dev": true, "license": "ISC" }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3282,6 +3899,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3453,6 +4086,13 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true, + "license": "MIT" + }, "node_modules/joi": { "version": "17.11.0", "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", @@ -3492,6 +4132,13 @@ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "license": "(AFL-2.1 OR BSD-3-Clause)" }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/json-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-stream/-/json-stream-1.0.0.tgz", @@ -3556,6 +4203,31 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3682,6 +4354,16 @@ "node": ">=12" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/memfs": { "version": "4.17.0", "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.0.tgz", @@ -3809,6 +4491,38 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/mocha": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", @@ -3953,6 +4667,13 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" } }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -4169,6 +4890,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4196,6 +4924,13 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -4218,6 +4953,13 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -4291,6 +5033,18 @@ "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", "license": "MIT" }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/prettier": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", @@ -4362,6 +5116,23 @@ "node": ">=6" } }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/query-string": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", @@ -4489,6 +5260,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resend": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/resend/-/resend-6.0.3.tgz", @@ -4506,6 +5287,27 @@ } } }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4677,6 +5479,26 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -4703,6 +5525,13 @@ "node": ">= 10.x" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -4724,6 +5553,16 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4807,6 +5646,19 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/thingies": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", @@ -4891,6 +5743,28 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -4975,6 +5849,65 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/vite-plugin-dts": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-4.5.4.tgz", + "integrity": "sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/api-extractor": "^7.50.1", + "@rollup/pluginutils": "^5.1.4", + "@volar/typescript": "^2.4.11", + "@vue/language-core": "2.2.0", + "compare-versions": "^6.1.1", + "debug": "^4.4.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.0.0", + "magic-string": "^0.30.17" + }, + "peerDependencies": { + "typescript": "*", + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vite-plugin-dts/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/vite-plugin-dts/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, "node_modules/web-encoding": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", @@ -5088,9 +6021,10 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index d6320e5c..0ad8c6fc 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,8 @@ "c8": "^10.1.3", "mocha": "^10.0.0", "nodemon": "2.0.19", - "prettier": "3.2.5" + "prettier": "3.2.5", + "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.16.1.tgz" }, "engines": { "node": "22.x" diff --git a/test/cluster/cluster.spec.js b/test/cluster/cluster.spec.js new file mode 100644 index 00000000..5daa9164 --- /dev/null +++ b/test/cluster/cluster.spec.js @@ -0,0 +1,227 @@ +import { spawn } from "node:child_process" +import { Transform } from "node:stream" +import process from "node:process" + +import { v4 } from "uuid" +import { SAMAClient } from "@sama-communications/sdk" +import assert from "node:assert" + +console.log(process.env.RUN_NODE_1_CMD, process.env.RUN_NODE_2_CMD) + +const createTransformStream = (prefix, nodeSubprocess) => new (class extends Transform { + _transform(chunk, encoding, cb) { + const withPrefixChunk = `|-${prefix}-|${chunk}` + cb(void 0, withPrefixChunk) + + if (chunk.includes('[TCP] listening on port')) { + nodeSubprocess.emit('node_ready', nodeSubprocess) + } + } +}) + +const nodeA = spawn(process.env.RUN_NODE_1_CMD, [], { + shell: true, + stdio: ['ignore', 'pipe', 'pipe'] +}) + +const nodeAReadyPromise = new Promise((resolve, reject) => { + const rejectTimerId = setTimeout(() => reject(new Error('Timeout')), 5_000) + + nodeA.once('node_ready', (nodeSubprocess) => { + console.log('[Node][Ready]', nodeSubprocess.pid) + resolve(nodeSubprocess) + clearTimeout(rejectTimerId) + }) +}) + +nodeA.stdout.pipe(createTransformStream('NODE_A', nodeA)).pipe(process.stdout) +nodeA.stderr.pipe(createTransformStream('NODE_A', nodeA)).pipe(process.stderr) + +await nodeAReadyPromise + +const nodeB = spawn(process.env.RUN_NODE_2_CMD, [], { + shell: true, + stdio: ['ignore', 'pipe', 'pipe'] +}) + +const nodeBReadyPromise = new Promise((resolve, reject) => { + const rejectTimerId = setTimeout(() => reject(new Error('Timeout')), 5_000) + + nodeB.once('node_ready', (nodeSubprocess) => { + console.log('[Node][Ready]', nodeSubprocess.pid) + resolve(nodeSubprocess) + clearTimeout(rejectTimerId) + }) +}) + +nodeB.stdout.pipe(createTransformStream('NODE_B', nodeB)).pipe(process.stdout) +nodeB.stderr.pipe(createTransformStream('NODE_B', nodeB)).pipe(process.stderr) + +await nodeBReadyPromise + +console.log("\n=== SETUP FINISHED ===\n") + +const configNodeA = { + endpoint: { + ws: process.env.NODE_1_WS_ENDPOINT, + http: process.env.NODE_1_HTTP_ENDPOINT, + }, + organization_id: process.env.TEST_ORG_ID, + disableAutoReconnect: true, +} + +const configNodeB = { + endpoint: { + ws: process.env.NODE_2_WS_ENDPOINT, + http: process.env.NODE_2_HTTP_ENDPOINT, + }, + organization_id: process.env.TEST_ORG_ID, + disableAutoReconnect: true, +} + +const samaClientA = new SAMAClient(configNodeA) +const samaClientB = new SAMAClient(configNodeB) + +let userANativeId = void 0 +let userBNativeId = void 0 + +describe("Connect", () => { + it("connect client A", async () => { + await samaClientA.connect() + const user = await samaClientA.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' }, deviceId: v4() }) + userANativeId = user.native_id + }) + + it("connect client B", async () => { + await samaClientB.connect() + const user = await samaClientB.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' }, deviceId: v4() }) + userBNativeId = user.native_id + }) +}) + +describe("Base messaging", () => { + describe("System", () => { + describe("A -> B", () => { + it("event", (done) => { + const mId = v4() + samaClientA.messageSystem({ mid: mId, uids: [userBNativeId], x: { one: '1' } }) + + samaClientB.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userANativeId) + + done() + } + }) + }) + + describe("B -> A", () => { + it("event", (done) => { + const mId = v4() + samaClientB.messageSystem({ mid: mId, uids: [userANativeId], x: { two: '2' } }) + + samaClientA.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userBNativeId) + + done() + } + }) + }) + }) + + describe("Private", () => { + describe("A -> B", () => { + let messageId = void 0 + + it("typing", (done) => { + samaClientA.sendTypingStatus({ cid: process.env.TEST_U_CONV_ID, status: 'start' }) + + samaClientB.onUserTypingListener = (typing) => { + assert.equal(typing.cid, process.env.TEST_U_CONV_ID) + assert.equal(typing.from, userANativeId) + done() + } + }) + + it("simple message", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientA.messageCreate({ cid: process.env.TEST_U_CONV_ID, body: body, mid: mId }) + + samaClientB.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, process.env.TEST_U_CONV_ID) + assert.equal(message.from, userANativeId) + + messageId = message._id + + done() + } + }) + + it("read status", (done) => { + samaClientB.markConversationAsRead({ cid: process.env.TEST_U_CONV_ID, mids: [messageId] }) + + samaClientA.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, process.env.TEST_U_CONV_ID) + assert.equal(status.from, userBNativeId) + + done() + } + }) + }) + + describe("B -> A", () => { + let messageId = void 0 + + it("typing", (done) => { + samaClientB.sendTypingStatus({ cid: process.env.TEST_U_CONV_ID, status: 'start' }) + + samaClientA.onUserTypingListener = (typing) => { + assert.equal(typing.cid, process.env.TEST_U_CONV_ID) + assert.equal(typing.from, userBNativeId) + done() + } + }) + + it("simple message", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientB.messageCreate({ cid: process.env.TEST_U_CONV_ID, body: body, mid: mId }) + + samaClientA.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, process.env.TEST_U_CONV_ID) + assert.equal(message.from, userBNativeId) + + messageId = message._id + + done() + } + }) + + it("read status", (done) => { + samaClientA.markConversationAsRead({ cid: process.env.TEST_U_CONV_ID, mids: [messageId] }) + + samaClientB.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, process.env.TEST_U_CONV_ID) + assert.equal(status.from, userANativeId) + + done() + } + }) + }) + }) +}) + +const onExit = () => { + nodeA.kill() + nodeB.kill() +} + +process.on('SIGTERM', onExit) + +after(onExit) \ No newline at end of file diff --git a/test/cluster/sama-communications-sdk-0.16.1.tgz b/test/cluster/sama-communications-sdk-0.16.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..ea8263052b4786bc72cef7f822a899777c9bd007 GIT binary patch literal 24046 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 literal 0 HcmV?d00001 diff --git a/test/cluster/utils.js b/test/cluster/utils.js new file mode 100644 index 00000000..e69de29b From b60acbd152755f8564db0c1956ac566e9b03aaed Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Wed, 1 Apr 2026 14:58:07 +0300 Subject: [PATCH 18/70] add activty listening --- .../{cluster.spec.js => cross-nodes.spec.js} | 137 ++++++++++++++++-- 1 file changed, 128 insertions(+), 9 deletions(-) rename test/cluster/{cluster.spec.js => cross-nodes.spec.js} (59%) diff --git a/test/cluster/cluster.spec.js b/test/cluster/cross-nodes.spec.js similarity index 59% rename from test/cluster/cluster.spec.js rename to test/cluster/cross-nodes.spec.js index 5daa9164..9208a89c 100644 --- a/test/cluster/cluster.spec.js +++ b/test/cluster/cross-nodes.spec.js @@ -1,10 +1,11 @@ +import assert from "node:assert" import { spawn } from "node:child_process" import { Transform } from "node:stream" import process from "node:process" +import { setTimeout as setTimeoutPromise } from "node:timers/promises" import { v4 } from "uuid" import { SAMAClient } from "@sama-communications/sdk" -import assert from "node:assert" console.log(process.env.RUN_NODE_1_CMD, process.env.RUN_NODE_2_CMD) @@ -82,8 +83,8 @@ const configNodeB = { const samaClientA = new SAMAClient(configNodeA) const samaClientB = new SAMAClient(configNodeB) -let userANativeId = void 0 -let userBNativeId = void 0 +let userANativeId = '69cbe9757fc0a44161188215' +let userBNativeId = '69cbe9a27fc0a44161188218' describe("Connect", () => { it("connect client A", async () => { @@ -92,10 +93,22 @@ describe("Connect", () => { userANativeId = user.native_id }) - it("connect client B", async () => { - await samaClientB.connect() - const user = await samaClientB.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' }, deviceId: v4() }) - userBNativeId = user.native_id + it ("subscribe user B last activity", async () => { + const activity = await samaClientA.subscribeToUserActivity(userBNativeId) + assert.ok(activity[userBNativeId] > 0) + }) + + it("connect client B and check last activity", (done) => { + samaClientB.connect().then(() => samaClientB.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' }, deviceId: v4() })) + samaClientA.onUserActivityListener = (activity) => { + assert.equal(activity[userBNativeId], 0) + done() + } + }) + + it("subscribe user A last activity", async () => { + const activity = await samaClientB.subscribeToUserActivity(userANativeId) + assert.ok(activity[userANativeId] === 0) }) }) @@ -147,12 +160,13 @@ describe("Base messaging", () => { it("simple message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientA.messageCreate({ cid: process.env.TEST_U_CONV_ID, body: body, mid: mId }) + samaClientA.messageCreate({ cid: process.env.TEST_U_CONV_ID, body: body, mid: mId, x: { two: '2' } }) samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, process.env.TEST_U_CONV_ID) assert.equal(message.from, userANativeId) + assert.equal(message.x['two'], '2') messageId = message._id @@ -189,12 +203,13 @@ describe("Base messaging", () => { it("simple message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientB.messageCreate({ cid: process.env.TEST_U_CONV_ID, body: body, mid: mId }) + samaClientB.messageCreate({ cid: process.env.TEST_U_CONV_ID, body: body, mid: mId, x: { two: '22' } }) samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, process.env.TEST_U_CONV_ID) assert.equal(message.from, userBNativeId) + assert.equal(message.x['two'], '22') messageId = message._id @@ -215,6 +230,110 @@ describe("Base messaging", () => { }) }) }) + + describe("Group", () => { + describe("A -> B", () => { + let messageId = void 0 + + it("typing", (done) => { + samaClientA.sendTypingStatus({ cid: process.env.TEST_G_CONV_ID, status: 'start' }) + + samaClientB.onUserTypingListener = (typing) => { + assert.equal(typing.cid, process.env.TEST_G_CONV_ID) + assert.equal(typing.from, userANativeId) + done() + } + }) + + it("simple message", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientA.messageCreate({ cid: process.env.TEST_G_CONV_ID, body: body, mid: mId, x: { one: '1' } }) + + samaClientB.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, process.env.TEST_G_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['one'], '1') + + messageId = message._id + + done() + } + }) + + it("read status", (done) => { + samaClientB.markConversationAsRead({ cid: process.env.TEST_G_CONV_ID, mids: [messageId] }) + + samaClientA.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, process.env.TEST_G_CONV_ID) + assert.equal(status.from, userBNativeId) + + done() + } + }) + }) + + describe("B -> A", () => { + let messageId = void 0 + + it("typing", (done) => { + samaClientB.sendTypingStatus({ cid: process.env.TEST_G_CONV_ID, status: 'start' }) + + samaClientA.onUserTypingListener = (typing) => { + assert.equal(typing.cid, process.env.TEST_G_CONV_ID) + assert.equal(typing.from, userBNativeId) + done() + } + }) + + it("simple message", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientB.messageCreate({ cid: process.env.TEST_G_CONV_ID, body: body, mid: mId, x: { one: '11' } }) + + samaClientA.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, process.env.TEST_G_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['one'], '11') + + messageId = message._id + + done() + } + }) + + it("read status", (done) => { + samaClientA.markConversationAsRead({ cid: process.env.TEST_G_CONV_ID, mids: [messageId] }) + + samaClientB.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, process.env.TEST_G_CONV_ID) + assert.equal(status.from, userANativeId) + + done() + } + }) + }) + }) +}) + +describe("Activity on disconnect", () => { + it("user A logout", (done) => { + samaClientA.disconnect() + + samaClientB.onUserActivityListener = (activity) => { + assert.ok(activity[userANativeId] > 0) + done() + } + }) + + it("disconnect B", async () => { + samaClientB.disconnect() + await setTimeoutPromise(200) + }) }) const onExit = () => { From 7c60e88d554ced6d98b40a40c472f62747cb0ce4 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Wed, 1 Apr 2026 17:33:53 +0300 Subject: [PATCH 19/70] update same-node / cross-node specs --- .cluster-mocharc.json | 12 + test/cluster/cross-node-multi-devices.spec.js | 545 ++++++++++++++++++ test/cluster/cross-node.spec.js | 304 ++++++++++ test/cluster/cross-nodes.spec.js | 346 ----------- test/cluster/same-node.spec.js | 289 ++++++++++ test/cluster/utils.js | 66 +++ 6 files changed, 1216 insertions(+), 346 deletions(-) create mode 100644 .cluster-mocharc.json create mode 100644 test/cluster/cross-node-multi-devices.spec.js create mode 100644 test/cluster/cross-node.spec.js delete mode 100644 test/cluster/cross-nodes.spec.js create mode 100644 test/cluster/same-node.spec.js diff --git a/.cluster-mocharc.json b/.cluster-mocharc.json new file mode 100644 index 00000000..30183bd5 --- /dev/null +++ b/.cluster-mocharc.json @@ -0,0 +1,12 @@ +{ + "recursive": true, + "sort": false, + "color": true, + "allow-uncaught": true, + "reporter": "list", + "node-option": ["import=dotenv/config"], + "exit": true, + "spec": ["./test/cluster/same-node.spec.js", "./test/cluster/cross-node.spec.js", "./test/cluster/cross-node-multi-devices.spec.js"], + "exclude": "test/**/*.spec.js", + "timeout": 120000 +} diff --git a/test/cluster/cross-node-multi-devices.spec.js b/test/cluster/cross-node-multi-devices.spec.js new file mode 100644 index 00000000..b0f40da5 --- /dev/null +++ b/test/cluster/cross-node-multi-devices.spec.js @@ -0,0 +1,545 @@ +import assert from "node:assert" +import { setTimeout as setTimeoutPromise } from "node:timers/promises" + +import { v4 } from "uuid" +import { SAMAClient } from "@sama-communications/sdk" + +import { + startOrAccessNodeA, startOrAccessNodeB, + TEST_ORG_ID, TEST_U_CONV_ID, TEST_G_CONV_ID, + NODE_1_WS_ENDPOINT, NODE_1_HTTP_ENDPOINT, + NODE_2_WS_ENDPOINT, NODE_2_HTTP_ENDPOINT, + userANativeId, userBNativeId +} from "./utils.js" + +const configNodeA = { + endpoint: { + ws: NODE_1_WS_ENDPOINT, + http: NODE_1_HTTP_ENDPOINT, + }, + organization_id: TEST_ORG_ID, + disableAutoReconnect: true, +} + +const configNodeB = { + endpoint: { + ws: NODE_2_WS_ENDPOINT, + http: NODE_2_HTTP_ENDPOINT, + }, + organization_id: TEST_ORG_ID, + disableAutoReconnect: true, +} + +const samaClientA_Device1 = new SAMAClient(configNodeA) +const samaClientB_Device1 = new SAMAClient(configNodeB) +const samaClientB_Device2 = new SAMAClient(configNodeB) + +let nodeA = void 0 +let nodeB = void 0 + +describe("Cross-node behavior", () => { + before(async () => { + console.log('[Before][cross-node]') + + nodeA = await startOrAccessNodeA() + nodeB = await startOrAccessNodeB() + + await setTimeoutPromise(500) + }) + + describe("Node-A (U_A_D1) - Node-B (U_B_D1,U_B_D2)", () => { + describe("Connect", () => { + it("connect U_A_D1", async () => { + await samaClientA_Device1.connect() + await samaClientA_Device1.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' }, deviceId: v4() }) + }) + + it ("subscribe user B last activity", async () => { + const activity = await samaClientA_Device1.subscribeToUserActivity(userBNativeId) + assert.ok(activity[userBNativeId] > 0) + }) + + it("connect U_B_D1 and check last activity", (done) => { + samaClientB_Device1.connect().then(() => samaClientB_Device1.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' }, deviceId: v4() })) + samaClientA_Device1.onUserActivityListener = (activity) => { + assert.equal(activity[userBNativeId], 0) + done() + } + }) + + it("connect U_B_D2 and check last activity", (done) => { + samaClientB_Device2.connect().then(() => samaClientB_Device2.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' }, deviceId: v4() })) + samaClientA_Device1.onUserActivityListener = (activity) => { + assert.equal(activity[userBNativeId], 0) + done() + } + }) + + it("subscribe user A last activity", async () => { + let activity = await samaClientB_Device1.subscribeToUserActivity(userANativeId) + assert.ok(activity[userANativeId] === 0) + + activity = await samaClientB_Device2.subscribeToUserActivity(userANativeId) + assert.ok(activity[userANativeId] === 0) + }) + }) + + describe("Base messaging", () => { + describe("System", () => { + describe("U_A_D1 -> U_B_D1,U_B_D2", async () => { + it("event", async () => { + const mId = v4() + samaClientA_Device1.messageSystem({ mid: mId, uids: [userBNativeId], x: { one: '1' } }) + + const uA_D1_promise = new Promise((resolve, reject) => { + samaClientB_Device1.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userANativeId) + + resolve() + } + }) + + const uA_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userANativeId) + + resolve() + } + }) + + await Promise.all([uA_D1_promise, uA_D2_promise]) + }) + }) + + describe("U_B_D1 sender", () => { + it("event to U_A_D1", (done) => { + const mId = v4() + samaClientB_Device1.messageSystem({ mid: mId, uids: [userANativeId], x: { two: '2' } }) + + samaClientA_Device1.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userBNativeId) + + done() + } + }) + + it("event to U_A_D1,U_B_D2", async () => { + const mId = v4() + samaClientB_Device1.messageSystem({ mid: mId, uids: [userANativeId, userBNativeId], x: { two: '22' } }) + + const uA_D1_promise = new Promise((resolve, reject) => { + samaClientA_Device1.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userBNativeId) + + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userBNativeId) + + resolve() + } + }) + + await Promise.all([uA_D1_promise, uB_D2_promise]) + }) + }) + + describe("U_B_D2 sender", () => { + it("event to conversation", async () => { + const mId = v4() + samaClientB_Device2.messageSystem({ mid: mId, cid: TEST_U_CONV_ID, x: { four: '44' } }) + + const uA_D1_promise = new Promise((resolve, reject) => { + samaClientA_Device1.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userBNativeId) + + resolve() + } + }) + + const uB_D1_promise = new Promise((resolve, reject) => { + samaClientB_Device1.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userBNativeId) + + resolve() + } + }) + + await Promise.all([uA_D1_promise, uB_D1_promise]) + }) + }) + }) + + describe("Private", () => { + describe("U_A_D1 -> U_B_D1,U_B_D2", () => { + let messageId = void 0 + + it("typing", async () => { + samaClientA_Device1.sendTypingStatus({ cid: TEST_U_CONV_ID, status: 'start' }) + + const uB_D1_promise = new Promise((resolve, reject) => { + samaClientB_Device1.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_U_CONV_ID) + assert.equal(typing.from, userANativeId) + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_U_CONV_ID) + assert.equal(typing.from, userANativeId) + resolve() + } + }) + + await Promise.all([uB_D1_promise, uB_D2_promise]) + }) + + it("simple message", async () => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientA_Device1.messageCreate({ cid: TEST_U_CONV_ID, body: body, mid: mId, x: { two: '2' } }) + + const uB_D1_promise = new Promise((resolve, reject) => { + samaClientB_Device1.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_U_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['two'], '2') + + messageId = message._id + + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_U_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['two'], '2') + + messageId = message._id + + resolve() + } + }) + + await Promise.all([uB_D1_promise, uB_D2_promise]) + }) + + it("read status", (done) => { + console.log(messageId) + samaClientB_Device2.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) + + samaClientA_Device1.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_U_CONV_ID) + assert.equal(status.from, userBNativeId) + + done() + } + }) + }) + + describe("U_B_D1 -> U_A_D1,U_B_D2", () => { + let messageId = void 0 + + it("typing", async () => { + samaClientB_Device1.sendTypingStatus({ cid: TEST_U_CONV_ID, status: 'start' }) + + const uA_D1_promise = new Promise((resolve, reject) => { + samaClientA_Device1.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_U_CONV_ID) + assert.equal(typing.from, userBNativeId) + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_U_CONV_ID) + assert.equal(typing.from, userBNativeId) + resolve() + } + }) + + await Promise.all([uA_D1_promise, uB_D2_promise]) + }) + + it("simple message", async () => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientB_Device1.messageCreate({ cid: TEST_U_CONV_ID, body: body, mid: mId, x: { two: '22' } }) + + const uA_D1_promise = new Promise((resolve, reject) => { + samaClientA_Device1.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_U_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['two'], '22') + + messageId = message._id + + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_U_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['two'], '22') + + messageId = message._id + + resolve() + } + }) + + await Promise.all([uA_D1_promise, uB_D2_promise]) + }) + + it("read status", async () => { + samaClientA_Device1.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) + + const uB_D1_promise = new Promise((resolve, reject) => { + samaClientB_Device1.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_U_CONV_ID) + assert.equal(status.from, userANativeId) + + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_U_CONV_ID) + assert.equal(status.from, userANativeId) + + resolve() + } + }) + + await Promise.all([uB_D1_promise, uB_D2_promise]) + }) + }) + }) + + describe("Group", () => { + describe("U_A_D1 -> U_B_D1,U_B_D2", () => { + let messageId = void 0 + + it("typing", async () => { + samaClientA_Device1.sendTypingStatus({ cid: TEST_G_CONV_ID, status: 'start' }) + + const uB_D1_promise = new Promise((resolve, reject) => { + samaClientB_Device1.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_G_CONV_ID) + assert.equal(typing.from, userANativeId) + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_G_CONV_ID) + assert.equal(typing.from, userANativeId) + resolve() + } + }) + + await Promise.all([uB_D1_promise, uB_D2_promise]) + }) + + it("simple message", async () => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientA_Device1.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '2' } }) + + const uB_D1_promise = new Promise((resolve, reject) => { + samaClientB_Device1.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['two'], '2') + + messageId = message._id + + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['two'], '2') + + messageId = message._id + + resolve() + } + }) + + await Promise.all([uB_D1_promise, uB_D2_promise]) + }) + + it("read status", (done) => { + console.log(messageId) + samaClientB_Device2.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) + + samaClientA_Device1.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_G_CONV_ID) + assert.equal(status.from, userBNativeId) + + done() + } + }) + }) + + describe("U_B_D1 -> U_A_D1,U_B_D2", () => { + let messageId = void 0 + + it("typing", async () => { + samaClientB_Device1.sendTypingStatus({ cid: TEST_G_CONV_ID, status: 'start' }) + + const uA_D1_promise = new Promise((resolve, reject) => { + samaClientA_Device1.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_G_CONV_ID) + assert.equal(typing.from, userBNativeId) + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_G_CONV_ID) + assert.equal(typing.from, userBNativeId) + resolve() + } + }) + + await Promise.all([uA_D1_promise, uB_D2_promise]) + }) + + it("simple message", async () => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientB_Device1.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '22' } }) + + const uA_D1_promise = new Promise((resolve, reject) => { + samaClientA_Device1.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['two'], '22') + + messageId = message._id + + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['two'], '22') + + messageId = message._id + + resolve() + } + }) + + await Promise.all([uA_D1_promise, uB_D2_promise]) + }) + + it("read status", async () => { + samaClientA_Device1.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) + + const uB_D1_promise = new Promise((resolve, reject) => { + samaClientB_Device1.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_G_CONV_ID) + assert.equal(status.from, userANativeId) + + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_G_CONV_ID) + assert.equal(status.from, userANativeId) + + resolve() + } + }) + + await Promise.all([uB_D1_promise, uB_D2_promise]) + }) + }) + }) + }) + + describe("Activity on disconnect", () => { + it("U_B_D1 logout (not last activity)", async () => { + samaClientB_Device1.disconnect() + + const promise = new Promise((resolve, reject) => { + setTimeout(() => resolve(), 500) + + samaClientA_Device1.onUserActivityListener = (activity) => { + if (activity[userBNativeId] > 0) { + reject(new Error("Should not be called")) + } + } + }) + + await promise + }) + + it("U_B_D1 logout (last activity)", (done) => { + samaClientB_Device2.disconnect() + + samaClientA_Device1.onUserActivityListener = (activity) => { + assert.ok(activity[userBNativeId] > 0) + done() + } + }) + + it("disconnect A", async () => { + samaClientA_Device1.disconnect() + await setTimeoutPromise(200) + }) + }) + }) + + const onExit = () => { + nodeA.kill() + nodeB.kill() + } + + after(onExit) +}) \ No newline at end of file diff --git a/test/cluster/cross-node.spec.js b/test/cluster/cross-node.spec.js new file mode 100644 index 00000000..e0a27fe4 --- /dev/null +++ b/test/cluster/cross-node.spec.js @@ -0,0 +1,304 @@ +import assert from "node:assert" +import { setTimeout as setTimeoutPromise } from "node:timers/promises" + +import { v4 } from "uuid" +import { SAMAClient } from "@sama-communications/sdk" + +import { + startOrAccessNodeA, startOrAccessNodeB, + TEST_ORG_ID, TEST_U_CONV_ID, TEST_G_CONV_ID, + NODE_1_WS_ENDPOINT, NODE_1_HTTP_ENDPOINT, + NODE_2_WS_ENDPOINT, NODE_2_HTTP_ENDPOINT, + userANativeId, userBNativeId +} from "./utils.js" + +const configNodeA = { + endpoint: { + ws: NODE_1_WS_ENDPOINT, + http: NODE_1_HTTP_ENDPOINT, + }, + organization_id: TEST_ORG_ID, + disableAutoReconnect: true, +} + +const configNodeB = { + endpoint: { + ws: NODE_2_WS_ENDPOINT, + http: NODE_2_HTTP_ENDPOINT, + }, + organization_id: TEST_ORG_ID, + disableAutoReconnect: true, +} + +const samaClientA = new SAMAClient(configNodeA) +const samaClientB = new SAMAClient(configNodeB) + +let nodeA = void 0 +let nodeB = void 0 + +describe("Cross-node behavior", () => { + before(async () => { + console.log('[Before][cross-node]') + + nodeA = await startOrAccessNodeA() + nodeB = await startOrAccessNodeB() + + await setTimeoutPromise(500) + }) + + describe("Connect", () => { + it("connect client A", async () => { + await samaClientA.connect() + await samaClientA.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' }, deviceId: v4() }) + }) + + it ("subscribe user B last activity", async () => { + const activity = await samaClientA.subscribeToUserActivity(userBNativeId) + assert.ok(activity[userBNativeId] > 0) + }) + + it("connect client B and check last activity", (done) => { + samaClientB.connect().then(() => samaClientB.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' }, deviceId: v4() })) + samaClientA.onUserActivityListener = (activity) => { + assert.equal(activity[userBNativeId], 0) + done() + } + }) + + it("subscribe user A last activity", async () => { + const activity = await samaClientB.subscribeToUserActivity(userANativeId) + assert.ok(activity[userANativeId] === 0) + }) + }) + + describe("Base messaging", () => { + describe("System", () => { + describe("A -> B", () => { + it("event", (done) => { + const mId = v4() + samaClientA.messageSystem({ mid: mId, uids: [userBNativeId], x: { one: '1' } }) + + samaClientB.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userANativeId) + + done() + } + }) + }) + + describe("B -> A", () => { + it("event", (done) => { + const mId = v4() + samaClientB.messageSystem({ mid: mId, uids: [userANativeId], x: { two: '2' } }) + + samaClientA.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userBNativeId) + + done() + } + }) + }) + }) + + describe("Private", () => { + describe("A -> B", () => { + let messageId = void 0 + + it("typing", (done) => { + samaClientA.sendTypingStatus({ cid: TEST_U_CONV_ID, status: 'start' }) + + samaClientB.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_U_CONV_ID) + assert.equal(typing.from, userANativeId) + done() + } + }) + + it("simple message", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientA.messageCreate({ cid: TEST_U_CONV_ID, body: body, mid: mId, x: { two: '2' } }) + + samaClientB.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_U_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['two'], '2') + + messageId = message._id + + done() + } + }) + + it("read status", (done) => { + samaClientB.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) + + samaClientA.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_U_CONV_ID) + assert.equal(status.from, userBNativeId) + + done() + } + }) + }) + + describe("B -> A", () => { + let messageId = void 0 + + it("typing", (done) => { + samaClientB.sendTypingStatus({ cid: TEST_U_CONV_ID, status: 'start' }) + + samaClientA.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_U_CONV_ID) + assert.equal(typing.from, userBNativeId) + done() + } + }) + + it("simple message", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientB.messageCreate({ cid: TEST_U_CONV_ID, body: body, mid: mId, x: { two: '22' } }) + + samaClientA.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_U_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['two'], '22') + + messageId = message._id + + done() + } + }) + + it("read status", (done) => { + samaClientA.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) + + samaClientB.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_U_CONV_ID) + assert.equal(status.from, userANativeId) + + done() + } + }) + }) + }) + + describe("Group", () => { + describe("A -> B", () => { + let messageId = void 0 + + it("typing", (done) => { + samaClientA.sendTypingStatus({ cid: TEST_G_CONV_ID, status: 'start' }) + + samaClientB.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_G_CONV_ID) + assert.equal(typing.from, userANativeId) + done() + } + }) + + it("simple message", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientA.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { one: '1' } }) + + samaClientB.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['one'], '1') + + messageId = message._id + + done() + } + }) + + it("read status", (done) => { + samaClientB.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) + + samaClientA.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_G_CONV_ID) + assert.equal(status.from, userBNativeId) + + done() + } + }) + }) + + describe("B -> A", () => { + let messageId = void 0 + + it("typing", (done) => { + samaClientB.sendTypingStatus({ cid: TEST_G_CONV_ID, status: 'start' }) + + samaClientA.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_G_CONV_ID) + assert.equal(typing.from, userBNativeId) + done() + } + }) + + it("simple message", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientB.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { one: '11' } }) + + samaClientA.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['one'], '11') + + messageId = message._id + + done() + } + }) + + it("read status", (done) => { + samaClientA.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) + + samaClientB.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_G_CONV_ID) + assert.equal(status.from, userANativeId) + + done() + } + }) + }) + }) + }) + + describe("Activity on disconnect", () => { + it("user A logout", (done) => { + samaClientA.disconnect() + + samaClientB.onUserActivityListener = (activity) => { + assert.ok(activity[userANativeId] > 0) + done() + } + }) + + it("disconnect B", async () => { + samaClientB.disconnect() + await setTimeoutPromise(200) + }) + }) + + const onExit = () => { + nodeA.kill() + nodeB.kill() + } + + // after(onExit) +}) \ No newline at end of file diff --git a/test/cluster/cross-nodes.spec.js b/test/cluster/cross-nodes.spec.js deleted file mode 100644 index 9208a89c..00000000 --- a/test/cluster/cross-nodes.spec.js +++ /dev/null @@ -1,346 +0,0 @@ -import assert from "node:assert" -import { spawn } from "node:child_process" -import { Transform } from "node:stream" -import process from "node:process" -import { setTimeout as setTimeoutPromise } from "node:timers/promises" - -import { v4 } from "uuid" -import { SAMAClient } from "@sama-communications/sdk" - -console.log(process.env.RUN_NODE_1_CMD, process.env.RUN_NODE_2_CMD) - -const createTransformStream = (prefix, nodeSubprocess) => new (class extends Transform { - _transform(chunk, encoding, cb) { - const withPrefixChunk = `|-${prefix}-|${chunk}` - cb(void 0, withPrefixChunk) - - if (chunk.includes('[TCP] listening on port')) { - nodeSubprocess.emit('node_ready', nodeSubprocess) - } - } -}) - -const nodeA = spawn(process.env.RUN_NODE_1_CMD, [], { - shell: true, - stdio: ['ignore', 'pipe', 'pipe'] -}) - -const nodeAReadyPromise = new Promise((resolve, reject) => { - const rejectTimerId = setTimeout(() => reject(new Error('Timeout')), 5_000) - - nodeA.once('node_ready', (nodeSubprocess) => { - console.log('[Node][Ready]', nodeSubprocess.pid) - resolve(nodeSubprocess) - clearTimeout(rejectTimerId) - }) -}) - -nodeA.stdout.pipe(createTransformStream('NODE_A', nodeA)).pipe(process.stdout) -nodeA.stderr.pipe(createTransformStream('NODE_A', nodeA)).pipe(process.stderr) - -await nodeAReadyPromise - -const nodeB = spawn(process.env.RUN_NODE_2_CMD, [], { - shell: true, - stdio: ['ignore', 'pipe', 'pipe'] -}) - -const nodeBReadyPromise = new Promise((resolve, reject) => { - const rejectTimerId = setTimeout(() => reject(new Error('Timeout')), 5_000) - - nodeB.once('node_ready', (nodeSubprocess) => { - console.log('[Node][Ready]', nodeSubprocess.pid) - resolve(nodeSubprocess) - clearTimeout(rejectTimerId) - }) -}) - -nodeB.stdout.pipe(createTransformStream('NODE_B', nodeB)).pipe(process.stdout) -nodeB.stderr.pipe(createTransformStream('NODE_B', nodeB)).pipe(process.stderr) - -await nodeBReadyPromise - -console.log("\n=== SETUP FINISHED ===\n") - -const configNodeA = { - endpoint: { - ws: process.env.NODE_1_WS_ENDPOINT, - http: process.env.NODE_1_HTTP_ENDPOINT, - }, - organization_id: process.env.TEST_ORG_ID, - disableAutoReconnect: true, -} - -const configNodeB = { - endpoint: { - ws: process.env.NODE_2_WS_ENDPOINT, - http: process.env.NODE_2_HTTP_ENDPOINT, - }, - organization_id: process.env.TEST_ORG_ID, - disableAutoReconnect: true, -} - -const samaClientA = new SAMAClient(configNodeA) -const samaClientB = new SAMAClient(configNodeB) - -let userANativeId = '69cbe9757fc0a44161188215' -let userBNativeId = '69cbe9a27fc0a44161188218' - -describe("Connect", () => { - it("connect client A", async () => { - await samaClientA.connect() - const user = await samaClientA.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' }, deviceId: v4() }) - userANativeId = user.native_id - }) - - it ("subscribe user B last activity", async () => { - const activity = await samaClientA.subscribeToUserActivity(userBNativeId) - assert.ok(activity[userBNativeId] > 0) - }) - - it("connect client B and check last activity", (done) => { - samaClientB.connect().then(() => samaClientB.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' }, deviceId: v4() })) - samaClientA.onUserActivityListener = (activity) => { - assert.equal(activity[userBNativeId], 0) - done() - } - }) - - it("subscribe user A last activity", async () => { - const activity = await samaClientB.subscribeToUserActivity(userANativeId) - assert.ok(activity[userANativeId] === 0) - }) -}) - -describe("Base messaging", () => { - describe("System", () => { - describe("A -> B", () => { - it("event", (done) => { - const mId = v4() - samaClientA.messageSystem({ mid: mId, uids: [userBNativeId], x: { one: '1' } }) - - samaClientB.onSystemMessageEvent = (message) => { - assert.equal(message._id, mId) - assert.equal(message.from, userANativeId) - - done() - } - }) - }) - - describe("B -> A", () => { - it("event", (done) => { - const mId = v4() - samaClientB.messageSystem({ mid: mId, uids: [userANativeId], x: { two: '2' } }) - - samaClientA.onSystemMessageEvent = (message) => { - assert.equal(message._id, mId) - assert.equal(message.from, userBNativeId) - - done() - } - }) - }) - }) - - describe("Private", () => { - describe("A -> B", () => { - let messageId = void 0 - - it("typing", (done) => { - samaClientA.sendTypingStatus({ cid: process.env.TEST_U_CONV_ID, status: 'start' }) - - samaClientB.onUserTypingListener = (typing) => { - assert.equal(typing.cid, process.env.TEST_U_CONV_ID) - assert.equal(typing.from, userANativeId) - done() - } - }) - - it("simple message", (done) => { - const mId = v4() - const body = `Cluster Test Hello m: ${mId}` - samaClientA.messageCreate({ cid: process.env.TEST_U_CONV_ID, body: body, mid: mId, x: { two: '2' } }) - - samaClientB.onMessageEvent = (message) => { - assert.equal(message.body, body) - assert.equal(message.cid, process.env.TEST_U_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['two'], '2') - - messageId = message._id - - done() - } - }) - - it("read status", (done) => { - samaClientB.markConversationAsRead({ cid: process.env.TEST_U_CONV_ID, mids: [messageId] }) - - samaClientA.onMessageStatusListener = (status) => { - assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, process.env.TEST_U_CONV_ID) - assert.equal(status.from, userBNativeId) - - done() - } - }) - }) - - describe("B -> A", () => { - let messageId = void 0 - - it("typing", (done) => { - samaClientB.sendTypingStatus({ cid: process.env.TEST_U_CONV_ID, status: 'start' }) - - samaClientA.onUserTypingListener = (typing) => { - assert.equal(typing.cid, process.env.TEST_U_CONV_ID) - assert.equal(typing.from, userBNativeId) - done() - } - }) - - it("simple message", (done) => { - const mId = v4() - const body = `Cluster Test Hello m: ${mId}` - samaClientB.messageCreate({ cid: process.env.TEST_U_CONV_ID, body: body, mid: mId, x: { two: '22' } }) - - samaClientA.onMessageEvent = (message) => { - assert.equal(message.body, body) - assert.equal(message.cid, process.env.TEST_U_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['two'], '22') - - messageId = message._id - - done() - } - }) - - it("read status", (done) => { - samaClientA.markConversationAsRead({ cid: process.env.TEST_U_CONV_ID, mids: [messageId] }) - - samaClientB.onMessageStatusListener = (status) => { - assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, process.env.TEST_U_CONV_ID) - assert.equal(status.from, userANativeId) - - done() - } - }) - }) - }) - - describe("Group", () => { - describe("A -> B", () => { - let messageId = void 0 - - it("typing", (done) => { - samaClientA.sendTypingStatus({ cid: process.env.TEST_G_CONV_ID, status: 'start' }) - - samaClientB.onUserTypingListener = (typing) => { - assert.equal(typing.cid, process.env.TEST_G_CONV_ID) - assert.equal(typing.from, userANativeId) - done() - } - }) - - it("simple message", (done) => { - const mId = v4() - const body = `Cluster Test Hello m: ${mId}` - samaClientA.messageCreate({ cid: process.env.TEST_G_CONV_ID, body: body, mid: mId, x: { one: '1' } }) - - samaClientB.onMessageEvent = (message) => { - assert.equal(message.body, body) - assert.equal(message.cid, process.env.TEST_G_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['one'], '1') - - messageId = message._id - - done() - } - }) - - it("read status", (done) => { - samaClientB.markConversationAsRead({ cid: process.env.TEST_G_CONV_ID, mids: [messageId] }) - - samaClientA.onMessageStatusListener = (status) => { - assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, process.env.TEST_G_CONV_ID) - assert.equal(status.from, userBNativeId) - - done() - } - }) - }) - - describe("B -> A", () => { - let messageId = void 0 - - it("typing", (done) => { - samaClientB.sendTypingStatus({ cid: process.env.TEST_G_CONV_ID, status: 'start' }) - - samaClientA.onUserTypingListener = (typing) => { - assert.equal(typing.cid, process.env.TEST_G_CONV_ID) - assert.equal(typing.from, userBNativeId) - done() - } - }) - - it("simple message", (done) => { - const mId = v4() - const body = `Cluster Test Hello m: ${mId}` - samaClientB.messageCreate({ cid: process.env.TEST_G_CONV_ID, body: body, mid: mId, x: { one: '11' } }) - - samaClientA.onMessageEvent = (message) => { - assert.equal(message.body, body) - assert.equal(message.cid, process.env.TEST_G_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['one'], '11') - - messageId = message._id - - done() - } - }) - - it("read status", (done) => { - samaClientA.markConversationAsRead({ cid: process.env.TEST_G_CONV_ID, mids: [messageId] }) - - samaClientB.onMessageStatusListener = (status) => { - assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, process.env.TEST_G_CONV_ID) - assert.equal(status.from, userANativeId) - - done() - } - }) - }) - }) -}) - -describe("Activity on disconnect", () => { - it("user A logout", (done) => { - samaClientA.disconnect() - - samaClientB.onUserActivityListener = (activity) => { - assert.ok(activity[userANativeId] > 0) - done() - } - }) - - it("disconnect B", async () => { - samaClientB.disconnect() - await setTimeoutPromise(200) - }) -}) - -const onExit = () => { - nodeA.kill() - nodeB.kill() -} - -process.on('SIGTERM', onExit) - -after(onExit) \ No newline at end of file diff --git a/test/cluster/same-node.spec.js b/test/cluster/same-node.spec.js new file mode 100644 index 00000000..2e856009 --- /dev/null +++ b/test/cluster/same-node.spec.js @@ -0,0 +1,289 @@ +import assert from "node:assert" +import { setTimeout as setTimeoutPromise } from "node:timers/promises" + +import { v4 } from "uuid" +import { SAMAClient } from "@sama-communications/sdk" + +import { + startOrAccessNodeA, + TEST_ORG_ID, TEST_U_CONV_ID, TEST_G_CONV_ID, + NODE_1_WS_ENDPOINT, NODE_1_HTTP_ENDPOINT, + userANativeId, userBNativeId +} from "./utils.js" + +const configNodeA = { + endpoint: { + ws: NODE_1_WS_ENDPOINT, + http: NODE_1_HTTP_ENDPOINT, + }, + organization_id: TEST_ORG_ID, + disableAutoReconnect: true, +} + +const samaClientA = new SAMAClient(configNodeA) +const samaClientB = new SAMAClient(configNodeA) + +let nodeA = void 0 + +describe("Same-node behavior", () => { + before(async () => { + console.log('[Before][same-node]') + + nodeA = await startOrAccessNodeA() + }) + + describe("Connect", () => { + it("connect client A", async () => { + await samaClientA.connect() + await samaClientA.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' }, deviceId: v4() }) + }) + + it ("subscribe user B last activity", async () => { + const activity = await samaClientA.subscribeToUserActivity(userBNativeId) + assert.ok(activity[userBNativeId] > 0) + }) + + it("connect client B and check last activity", (done) => { + samaClientB.connect().then(() => samaClientB.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' }, deviceId: v4() })) + samaClientA.onUserActivityListener = (activity) => { + assert.equal(activity[userBNativeId], 0) + done() + } + }) + + it("subscribe user A last activity", async () => { + const activity = await samaClientB.subscribeToUserActivity(userANativeId) + assert.ok(activity[userANativeId] === 0) + }) + }) + + describe("Base messaging", () => { + describe("System", () => { + describe("A -> B", () => { + it("event", (done) => { + const mId = v4() + samaClientA.messageSystem({ mid: mId, uids: [userBNativeId], x: { one: '1' } }) + + samaClientB.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userANativeId) + + done() + } + }) + }) + + describe("B -> A", () => { + it("event", (done) => { + const mId = v4() + samaClientB.messageSystem({ mid: mId, uids: [userANativeId], x: { two: '2' } }) + + samaClientA.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userBNativeId) + + done() + } + }) + }) + }) + + describe("Private", () => { + describe("A -> B", () => { + let messageId = void 0 + + it("typing", (done) => { + samaClientA.sendTypingStatus({ cid: TEST_U_CONV_ID, status: 'start' }) + + samaClientB.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_U_CONV_ID) + assert.equal(typing.from, userANativeId) + done() + } + }) + + it("simple message", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientA.messageCreate({ cid: TEST_U_CONV_ID, body: body, mid: mId, x: { two: '2' } }) + + samaClientB.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_U_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['two'], '2') + + messageId = message._id + + done() + } + }) + + it("read status", (done) => { + samaClientB.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) + + samaClientA.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_U_CONV_ID) + assert.equal(status.from, userBNativeId) + + done() + } + }) + }) + + describe("B -> A", () => { + let messageId = void 0 + + it("typing", (done) => { + samaClientB.sendTypingStatus({ cid: TEST_U_CONV_ID, status: 'start' }) + + samaClientA.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_U_CONV_ID) + assert.equal(typing.from, userBNativeId) + done() + } + }) + + it("simple message", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientB.messageCreate({ cid: TEST_U_CONV_ID, body: body, mid: mId, x: { two: '22' } }) + + samaClientA.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_U_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['two'], '22') + + messageId = message._id + + done() + } + }) + + it("read status", (done) => { + samaClientA.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) + + samaClientB.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_U_CONV_ID) + assert.equal(status.from, userANativeId) + + done() + } + }) + }) + }) + + describe("Group", () => { + describe("A -> B", () => { + let messageId = void 0 + + it("typing", (done) => { + samaClientA.sendTypingStatus({ cid: TEST_G_CONV_ID, status: 'start' }) + + samaClientB.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_G_CONV_ID) + assert.equal(typing.from, userANativeId) + done() + } + }) + + it("simple message", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientA.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { one: '1' } }) + + samaClientB.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['one'], '1') + + messageId = message._id + + done() + } + }) + + it("read status", (done) => { + samaClientB.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) + + samaClientA.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_G_CONV_ID) + assert.equal(status.from, userBNativeId) + + done() + } + }) + }) + + describe("B -> A", () => { + let messageId = void 0 + + it("typing", (done) => { + samaClientB.sendTypingStatus({ cid: TEST_G_CONV_ID, status: 'start' }) + + samaClientA.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_G_CONV_ID) + assert.equal(typing.from, userBNativeId) + done() + } + }) + + it("simple message", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientB.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { one: '11' } }) + + samaClientA.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['one'], '11') + + messageId = message._id + + done() + } + }) + + it("read status", (done) => { + samaClientA.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) + + samaClientB.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_G_CONV_ID) + assert.equal(status.from, userANativeId) + + done() + } + }) + }) + }) + }) + + describe("Activity on disconnect", () => { + it("user A logout", (done) => { + samaClientA.disconnect() + + samaClientB.onUserActivityListener = (activity) => { + assert.ok(activity[userANativeId] > 0) + done() + } + }) + + it("disconnect B", async () => { + samaClientB.disconnect() + await setTimeoutPromise(200) + }) + }) + + const onExit = () => { + nodeA?.kill() + } + + // after(onExit) +}) \ No newline at end of file diff --git a/test/cluster/utils.js b/test/cluster/utils.js index e69de29b..c17ff9b1 100644 --- a/test/cluster/utils.js +++ b/test/cluster/utils.js @@ -0,0 +1,66 @@ +import { spawn } from "node:child_process" +import { Transform } from "node:stream" +import process from "node:process" + +export const RUN_NODE_1_CMD = process.env.RUN_NODE_1_CMD +export const RUN_NODE_2_CMD = process.env.RUN_NODE_2_CMD + +export const NODE_1_WS_ENDPOINT = process.env.NODE_1_WS_ENDPOINT +export const NODE_2_WS_ENDPOINT = process.env.NODE_2_WS_ENDPOINT +export const NODE_1_HTTP_ENDPOINT = process.env.NODE_1_HTTP_ENDPOINT +export const NODE_2_HTTP_ENDPOINT = process.env.NODE_2_HTTP_ENDPOINT + +export const TEST_ORG_ID = process.env.TEST_ORG_ID +export const TEST_U_CONV_ID = process.env.TEST_U_CONV_ID +export const TEST_G_CONV_ID = process.env.TEST_G_CONV_ID + +export const userANativeId = '69cbe9757fc0a44161188215' +export const userBNativeId = '69cbe9a27fc0a44161188218' + +let nodeA = void 0 +let nodeB = void 0 + +console.log(RUN_NODE_1_CMD, RUN_NODE_2_CMD) + +const createTransformStream = (tag, nodeSubprocess) => new (class extends Transform { + _transform(chunk, encoding, cb) { + const withPrefixChunk = `|-${tag}-|${chunk}` + cb(void 0, withPrefixChunk) + + if (chunk.includes('[TCP] listening on port')) { + nodeSubprocess.emit('node_ready', nodeSubprocess) + } + } +}) + +export const spawnNode = async (cmd, tag, createPipeStream = createTransformStream) => { + const nodeSubprocess = spawn(cmd, [], { + shell: true, + stdio: ['ignore', 'pipe', 'pipe'] + }) + + const nodeSubprocessReadyPromise = new Promise((resolve, reject) => { + const rejectTimerId = setTimeout(() => reject(new Error('Timeout')), 5_000) + + nodeSubprocess.once('node_ready', (nodeSubprocess) => { + console.log('[Node][Ready]', tag, nodeSubprocess.pid) + resolve(nodeSubprocess) + clearTimeout(rejectTimerId) + }) + }) + + nodeSubprocess.stdout.pipe(createPipeStream(tag, nodeSubprocess)).pipe(process.stdout) + nodeSubprocess.stderr.pipe(createPipeStream(tag, nodeSubprocess)).pipe(process.stderr) + + await nodeSubprocessReadyPromise + + return nodeSubprocess +} + +export const startOrAccessNodeA = async (tag = 'NODE_A', createPipeStream) => { + return nodeA ? nodeA : await spawnNode(RUN_NODE_1_CMD, tag, createPipeStream).then(node => nodeA = node) +} + +export const startOrAccessNodeB = async (tag = 'NODE_B', createPipeStream) => { + return nodeB ? nodeB : await spawnNode(RUN_NODE_2_CMD, tag, createPipeStream).then(node => nodeB = node) +} From 9ae07116374ccf2492954dc9fb4dabe093fcb526 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Wed, 1 Apr 2026 18:34:14 +0300 Subject: [PATCH 20/70] add fixtures --- .cluster-mocharc.json | 2 +- package-lock.json | 29 +++++++++++++++++++ package.json | 5 ++-- test/cluster/cross-node-multi-devices.spec.js | 9 +----- test/cluster/cross-node.spec.js | 7 ----- test/cluster/fixtures.js | 29 +++++++++++++++++++ test/cluster/same-node.spec.js | 6 ---- test/cluster/utils.js | 3 ++ 8 files changed, 66 insertions(+), 24 deletions(-) create mode 100644 test/cluster/fixtures.js diff --git a/.cluster-mocharc.json b/.cluster-mocharc.json index 30183bd5..99a7b9b8 100644 --- a/.cluster-mocharc.json +++ b/.cluster-mocharc.json @@ -4,7 +4,7 @@ "color": true, "allow-uncaught": true, "reporter": "list", - "node-option": ["import=dotenv/config"], + "require": ["dotenv/config", "./test/cluster/utils.js", "./test/cluster/fixtures.js"], "exit": true, "spec": ["./test/cluster/same-node.spec.js", "./test/cluster/cross-node.spec.js", "./test/cluster/cross-node-multi-devices.spec.js"], "exclude": "test/**/*.spec.js", diff --git a/package-lock.json b/package-lock.json index 572d3a03..abb5356e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "devDependencies": { "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.16.1.tgz", "c8": "^10.1.3", + "kill-port": "^2.0.1", "mocha": "^10.0.0", "nodemon": "2.0.19", "prettier": "3.2.5" @@ -3623,6 +3624,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-them-args": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/get-them-args/-/get-them-args-1.3.2.tgz", + "integrity": "sha512-LRn8Jlk+DwZE4GTlDbT3Hikd1wSHgLMme/+7ddlqKd7ldwR6LjJgTVWzBnR01wnYGe4KgrXjg287RaI22UHmAw==", + "dev": true, + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -4203,6 +4211,20 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/kill-port": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kill-port/-/kill-port-2.0.1.tgz", + "integrity": "sha512-e0SVOV5jFo0mx8r7bS29maVWp17qGqLBZ5ricNSajON6//kmb7qqqNnml4twNE8Dtj97UQD+gNFOaipS/q1zzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-them-args": "1.3.2", + "shell-exec": "1.0.2" + }, + "bin": { + "kill-port": "cli.js" + } + }, "node_modules/kolorist": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", @@ -5421,6 +5443,13 @@ "node": ">=8" } }, + "node_modules/shell-exec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/shell-exec/-/shell-exec-1.0.2.tgz", + "integrity": "sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg==", + "dev": true, + "license": "MIT" + }, "node_modules/simple-update-notifier": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", diff --git a/package.json b/package.json index 0ad8c6fc..652ee2ed 100644 --- a/package.json +++ b/package.json @@ -50,11 +50,12 @@ "ws": "^8.12.0" }, "devDependencies": { + "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.16.1.tgz", "c8": "^10.1.3", + "kill-port": "^2.0.1", "mocha": "^10.0.0", "nodemon": "2.0.19", - "prettier": "3.2.5", - "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.16.1.tgz" + "prettier": "3.2.5" }, "engines": { "node": "22.x" diff --git a/test/cluster/cross-node-multi-devices.spec.js b/test/cluster/cross-node-multi-devices.spec.js index b0f40da5..b8031592 100644 --- a/test/cluster/cross-node-multi-devices.spec.js +++ b/test/cluster/cross-node-multi-devices.spec.js @@ -39,7 +39,7 @@ let nodeB = void 0 describe("Cross-node behavior", () => { before(async () => { - console.log('[Before][cross-node]') + console.log('[Before][cross-node-multi-devices]') nodeA = await startOrAccessNodeA() nodeB = await startOrAccessNodeB() @@ -535,11 +535,4 @@ describe("Cross-node behavior", () => { }) }) }) - - const onExit = () => { - nodeA.kill() - nodeB.kill() - } - - after(onExit) }) \ No newline at end of file diff --git a/test/cluster/cross-node.spec.js b/test/cluster/cross-node.spec.js index e0a27fe4..fda96776 100644 --- a/test/cluster/cross-node.spec.js +++ b/test/cluster/cross-node.spec.js @@ -294,11 +294,4 @@ describe("Cross-node behavior", () => { await setTimeoutPromise(200) }) }) - - const onExit = () => { - nodeA.kill() - nodeB.kill() - } - - // after(onExit) }) \ No newline at end of file diff --git a/test/cluster/fixtures.js b/test/cluster/fixtures.js new file mode 100644 index 00000000..0e3be2b6 --- /dev/null +++ b/test/cluster/fixtures.js @@ -0,0 +1,29 @@ +import killPort from "kill-port" +import { killNodeA, killNodeB } from "./utils.js" + +const killPortWrap = (port, method) => killPort(port, method) + .then(() => console.log('[Port]', port, 'killed')) + .catch(error => console.log('[Port]', port, error.message)) + +const killPorts = async () => { + await killPortWrap(9001) + await killPortWrap(9002) + + await killPortWrap(8001) + await killPortWrap(8002) +} + +export async function mochaGlobalSetup() { + console.log("[Global][Start]") + + await killPorts() +} + +export async function mochaGlobalTeardown() { + console.log("[Global][Shutdown]") + + killNodeA() + killNodeB() + + await killPorts() +} \ No newline at end of file diff --git a/test/cluster/same-node.spec.js b/test/cluster/same-node.spec.js index 2e856009..3e1320fd 100644 --- a/test/cluster/same-node.spec.js +++ b/test/cluster/same-node.spec.js @@ -280,10 +280,4 @@ describe("Same-node behavior", () => { await setTimeoutPromise(200) }) }) - - const onExit = () => { - nodeA?.kill() - } - - // after(onExit) }) \ No newline at end of file diff --git a/test/cluster/utils.js b/test/cluster/utils.js index c17ff9b1..6cf85275 100644 --- a/test/cluster/utils.js +++ b/test/cluster/utils.js @@ -64,3 +64,6 @@ export const startOrAccessNodeA = async (tag = 'NODE_A', createPipeStream) => { export const startOrAccessNodeB = async (tag = 'NODE_B', createPipeStream) => { return nodeB ? nodeB : await spawnNode(RUN_NODE_2_CMD, tag, createPipeStream).then(node => nodeB = node) } + +export const killNodeA = () => nodeA?.kill() +export const killNodeB = () => nodeB?.kill() From 89cb45e08da79205c81ac90283e2847739bc7486 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 2 Apr 2026 17:45:50 +0300 Subject: [PATCH 21/70] update specs --- .cluster-mocharc.json | 3 +- index.js | 2 + ...-devices.spec.js => multi-devices.spec.js} | 0 test/cluster/node-crash.spec.js | 182 ++++++++++++++++++ test/cluster/utils.js | 56 +++++- 5 files changed, 232 insertions(+), 11 deletions(-) rename test/cluster/{cross-node-multi-devices.spec.js => multi-devices.spec.js} (100%) create mode 100644 test/cluster/node-crash.spec.js diff --git a/.cluster-mocharc.json b/.cluster-mocharc.json index 99a7b9b8..f078381c 100644 --- a/.cluster-mocharc.json +++ b/.cluster-mocharc.json @@ -3,10 +3,11 @@ "sort": false, "color": true, "allow-uncaught": true, + "parallel": false, "reporter": "list", "require": ["dotenv/config", "./test/cluster/utils.js", "./test/cluster/fixtures.js"], "exit": true, - "spec": ["./test/cluster/same-node.spec.js", "./test/cluster/cross-node.spec.js", "./test/cluster/cross-node-multi-devices.spec.js"], + "spec": ["./test/cluster/node-crash.spec.js"], "exclude": "test/**/*.spec.js", "timeout": 120000 } diff --git a/index.js b/index.js index 43eebcc3..3139eccf 100644 --- a/index.js +++ b/index.js @@ -193,4 +193,6 @@ if (config.get("tcp.isEnabled")) { await tcpProtocolImp.listen(tcpOptions) } +logger.debug("[Ready] cluster-ws: %s", config.get("ws.cluster.endpoint")) + // https://dev.to/mattkrick/replacing-express-with-uwebsockets-48ph diff --git a/test/cluster/cross-node-multi-devices.spec.js b/test/cluster/multi-devices.spec.js similarity index 100% rename from test/cluster/cross-node-multi-devices.spec.js rename to test/cluster/multi-devices.spec.js diff --git a/test/cluster/node-crash.spec.js b/test/cluster/node-crash.spec.js new file mode 100644 index 00000000..47b8e8a8 --- /dev/null +++ b/test/cluster/node-crash.spec.js @@ -0,0 +1,182 @@ +import assert from "node:assert" +import { setTimeout as setTimeoutPromise } from "node:timers/promises" + +import { v4 } from "uuid" +import { SAMAClient } from "@sama-communications/sdk" + +import { + startOrAccessNodeA, startOrAccessNodeB, + TEST_ORG_ID, TEST_U_CONV_ID, TEST_G_CONV_ID, + NODE_1_WS_ENDPOINT, NODE_1_HTTP_ENDPOINT, + NODE_2_WS_ENDPOINT, NODE_2_HTTP_ENDPOINT, + userANativeId, userBNativeId +} from "./utils.js" + +const configNodeA = { + endpoint: { + ws: NODE_1_WS_ENDPOINT, + http: NODE_1_HTTP_ENDPOINT, + }, + organization_id: TEST_ORG_ID, + disableAutoReconnect: true, +} + +const configNodeB = { + endpoint: { + ws: NODE_2_WS_ENDPOINT, + http: NODE_2_HTTP_ENDPOINT, + }, + organization_id: TEST_ORG_ID, + disableAutoReconnect: true, +} + +const samaClientA = new SAMAClient(configNodeA) +const samaClientB = new SAMAClient(configNodeB) + +let nodeA = void 0 +let nodeB = void 0 + +describe("Cross-node behavior:", () => { + before(async () => { + console.log('[Before][node-crash]') + + nodeA = await startOrAccessNodeA() + nodeB = await startOrAccessNodeB() + + await setTimeoutPromise(500) + }) + + describe("NodeA(U_A) - NodeB(U_B) - NodeB crash:", () => { + it("connect client A", async () => { + await samaClientA.connect() + await samaClientA.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' }, deviceId: v4() }) + }) + + it ("subscribe user B last activity", async () => { + const activity = await samaClientA.subscribeToUserActivity(userBNativeId) + assert.ok(activity[userBNativeId] > 0) + }) + + it("connect client B and check last activity", (done) => { + samaClientB.connect().then(() => samaClientB.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' }, deviceId: v4() })) + samaClientA.onUserActivityListener = (activity) => { + assert.equal(activity[userBNativeId], 0) + done() + } + }) + + it("messaging A -> B", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientA.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '2' } }) + + samaClientB.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['two'], '2') + + done() + } + }) + + it("messaging B -> A", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientB.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '22' } }) + + samaClientA.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['two'], '22') + + done() + } + }) + + it("kill NodeB events with U_B last activity from NodeA", async () => { + const closePromise = new Promise((resolve, reject) => { + nodeA.once(`node_close[${nodeB.clusterEndpoint}]`, () => { + console.log('[node][close]', nodeB.clusterEndpoint) + resolve() + }) + }) + + const startReconnectPromise = new Promise((resolve, reject) => { + nodeA.once(`node_reconnect_start[${nodeB.clusterEndpoint}]`, () => { + console.log('[node][reconnect][start]', nodeB.clusterEndpoint) + resolve() + }) + }) + + const cleanNodeBPromise = new Promise((resolve, reject) => { + nodeA.once(`node_clean[${nodeB.clusterEndpoint}]`, () => { + console.log('[node][clean]', nodeB.clusterEndpoint) + resolve() + }) + }) + + const userBOfflinePromise = new Promise((resolve, reject) => { + samaClientA.onUserActivityListener = async (activity) => { + assert.ok(activity[userBNativeId] > 0) + + activity = await samaClientA.subscribeToUserActivity(userBNativeId) + assert.ok(activity[userBNativeId] > 0) + + resolve() + } + }) + + nodeB.kill() + + await Promise.all([closePromise, startReconnectPromise, cleanNodeBPromise, userBOfflinePromise]) + }) + + describe("Run new NodeB and reconnect U_B", () => { + it("start new NodeB", async () => { + nodeB = await startOrAccessNodeB(void 0, true) + }) + + it("connect client B and check last activity", (done) => { + samaClientB.connect().then(() => samaClientB.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' }, deviceId: v4() })) + samaClientA.onUserActivityListener = (activity) => { + assert.equal(activity[userBNativeId], 0) + done() + } + }) + + describe("it can send message to each other:", () => { + it("A -> B", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientA.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '2' } }) + + samaClientB.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['two'], '2') + + done() + } + }) + + it("B -> A", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientB.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '22' } }) + + samaClientA.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['two'], '22') + + done() + } + }) + }) + }) + }) +}) \ No newline at end of file diff --git a/test/cluster/utils.js b/test/cluster/utils.js index 6cf85275..dcffb41e 100644 --- a/test/cluster/utils.js +++ b/test/cluster/utils.js @@ -22,18 +22,53 @@ let nodeB = void 0 console.log(RUN_NODE_1_CMD, RUN_NODE_2_CMD) -const createTransformStream = (tag, nodeSubprocess) => new (class extends Transform { +// ws endpoint reg: /wss?:\/\/wss?:\/\/.+\// + +const createPipeStream = (tag, nodeSubprocess) => new (class extends Transform { _transform(chunk, encoding, cb) { + chunk = chunk.toString() const withPrefixChunk = `|-${tag}-|${chunk}` cb(void 0, withPrefixChunk) - if (chunk.includes('[TCP] listening on port')) { - nodeSubprocess.emit('node_ready', nodeSubprocess) + let regResult = chunk.match(/\[Ready\] cluster-ws: (wss?:\/\/.+\/)/) + if (regResult) { + const nodeClusterWsEndpoint = regResult.at(1) + nodeSubprocess.emit('node_ready', nodeClusterWsEndpoint) + } + + regResult = chunk.match(/\[node handshake finished\] (wss?:\/\/.+\/)/) + if (regResult) { + const nodeClusterWsEndpoint = regResult.at(1) + nodeSubprocess.emit(`node_connected[${nodeClusterWsEndpoint}]`, nodeClusterWsEndpoint) + } + + regResult = chunk.match(/\[clean node\] (wss?:\/\/.+\/)/) + if (regResult) { + const nodeClusterWsEndpoint = regResult.at(1) + nodeSubprocess.emit(`node_clean[${nodeClusterWsEndpoint}]`, nodeClusterWsEndpoint) + } + + regResult = chunk.match(/\[Close\]\[(wss?:\/\/.+\/)\] IsWasOpened/) + if (regResult) { + const nodeClusterWsEndpoint = regResult.at(1) + nodeSubprocess.emit(`node_close[${nodeClusterWsEndpoint}]`, nodeClusterWsEndpoint) + } + + regResult = chunk.match(/\[Reconnecting\]\[(wss?:\/\/.+\/)\]\[start\]/) + if (regResult) { + const nodeClusterWsEndpoint = regResult.at(1) + nodeSubprocess.emit(`node_reconnect_start[${nodeClusterWsEndpoint}]`, nodeClusterWsEndpoint) + } + + regResult = chunk.match(/\[Reconnecting\]\[(wss?:\/\/.+\/)\]\[finish\]/) + if (regResult) { + const nodeClusterWsEndpoint = regResult.at(1) + nodeSubprocess.emit(`node_reconnect_finish[${nodeClusterWsEndpoint}]`, nodeClusterWsEndpoint) } } }) -export const spawnNode = async (cmd, tag, createPipeStream = createTransformStream) => { +export const spawnNode = async (cmd, tag) => { const nodeSubprocess = spawn(cmd, [], { shell: true, stdio: ['ignore', 'pipe', 'pipe'] @@ -42,8 +77,9 @@ export const spawnNode = async (cmd, tag, createPipeStream = createTransformStre const nodeSubprocessReadyPromise = new Promise((resolve, reject) => { const rejectTimerId = setTimeout(() => reject(new Error('Timeout')), 5_000) - nodeSubprocess.once('node_ready', (nodeSubprocess) => { - console.log('[Node][Ready]', tag, nodeSubprocess.pid) + nodeSubprocess.once('node_ready', (nodeClusterWsEndpoint) => { + nodeSubprocess.clusterEndpoint = nodeClusterWsEndpoint + console.log('[Node][Ready]', tag, nodeSubprocess.pid, nodeSubprocess.clusterEndpoint) resolve(nodeSubprocess) clearTimeout(rejectTimerId) }) @@ -57,12 +93,12 @@ export const spawnNode = async (cmd, tag, createPipeStream = createTransformStre return nodeSubprocess } -export const startOrAccessNodeA = async (tag = 'NODE_A', createPipeStream) => { - return nodeA ? nodeA : await spawnNode(RUN_NODE_1_CMD, tag, createPipeStream).then(node => nodeA = node) +export const startOrAccessNodeA = async (tag = 'NODE_A', force) => { + return (nodeA && !force) ? nodeA : await spawnNode(RUN_NODE_1_CMD, tag).then(node => nodeA = node) } -export const startOrAccessNodeB = async (tag = 'NODE_B', createPipeStream) => { - return nodeB ? nodeB : await spawnNode(RUN_NODE_2_CMD, tag, createPipeStream).then(node => nodeB = node) +export const startOrAccessNodeB = async (tag = 'NODE_B', force) => { + return (nodeB && !force) ? nodeB : await spawnNode(RUN_NODE_2_CMD, tag).then(node => nodeB = node) } export const killNodeA = () => nodeA?.kill() From 73a5f855545eaa1d8636f589022a92df17c6a854 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Fri, 3 Apr 2026 14:59:28 +0300 Subject: [PATCH 22/70] it can connect with token --- .cluster-mocharc.json | 2 +- package-lock.json | 84 +++---- package.json | 2 +- test/cluster/cross-node.spec.js | 15 +- test/cluster/multi-devices.spec.js | 18 +- test/cluster/node-crash.spec.js | 231 +++++++++++++----- .../sama-communications-sdk-0.16.1.tgz | Bin 24046 -> 0 bytes .../sama-communications-sdk-0.16.2.tgz | Bin 0 -> 24080 bytes test/cluster/same-node.spec.js | 15 +- test/cluster/utils.js | 14 +- 10 files changed, 257 insertions(+), 124 deletions(-) delete mode 100644 test/cluster/sama-communications-sdk-0.16.1.tgz create mode 100644 test/cluster/sama-communications-sdk-0.16.2.tgz diff --git a/.cluster-mocharc.json b/.cluster-mocharc.json index f078381c..d1600834 100644 --- a/.cluster-mocharc.json +++ b/.cluster-mocharc.json @@ -7,7 +7,7 @@ "reporter": "list", "require": ["dotenv/config", "./test/cluster/utils.js", "./test/cluster/fixtures.js"], "exit": true, - "spec": ["./test/cluster/node-crash.spec.js"], + "spec": ["./test/cluster/same-node.spec.js", "./test/cluster/cross-node.spec.js", "./test/cluster/multi-devices.spec.js", "./test/cluster/node-crash.spec.js"], "exclude": "test/**/*.spec.js", "timeout": 120000 } diff --git a/package-lock.json b/package-lock.json index abb5356e..df8268cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "ws": "^8.12.0" }, "devDependencies": { - "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.16.1.tgz", + "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.16.2.tgz", "c8": "^10.1.3", "kill-port": "^2.0.1", "mocha": "^10.0.0", @@ -1311,41 +1311,41 @@ } }, "node_modules/@microsoft/api-extractor": { - "version": "7.57.7", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.57.7.tgz", - "integrity": "sha512-kmnmVs32MFWbV5X6BInC1/TfCs7y1ugwxv1xHsAIj/DyUfoe7vtO0alRUgbQa57+yRGHBBjlNcEk33SCAt5/dA==", + "version": "7.58.0", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.58.0.tgz", + "integrity": "sha512-XcHlDylX5GblbGbs1eBoexjVyvdJMioTPuBCgCorE2rqijzTYxi6eudXyez3xACRxtH9aDtahoL9fYM4XTvQmg==", "dev": true, "license": "MIT", "dependencies": { - "@microsoft/api-extractor-model": "7.33.4", + "@microsoft/api-extractor-model": "7.33.5", "@microsoft/tsdoc": "~0.16.0", "@microsoft/tsdoc-config": "~0.18.1", - "@rushstack/node-core-library": "5.20.3", + "@rushstack/node-core-library": "5.21.0", "@rushstack/rig-package": "0.7.2", - "@rushstack/terminal": "0.22.3", - "@rushstack/ts-command-line": "5.3.3", + "@rushstack/terminal": "0.22.4", + "@rushstack/ts-command-line": "5.3.4", "diff": "~8.0.2", "lodash": "~4.17.23", "minimatch": "10.2.3", "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", - "typescript": "5.8.2" + "typescript": "5.9.3" }, "bin": { "api-extractor": "bin/api-extractor" } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.33.4", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.33.4.tgz", - "integrity": "sha512-u1LTaNTikZAQ9uK6KG1Ms7nvNedsnODnspq/gH2dcyETWvH4hVNGNDvRAEutH66kAmxA4/necElqGNs1FggC8w==", + "version": "7.33.5", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.33.5.tgz", + "integrity": "sha512-Xh4dXuusndVQqVz4nEN9xOp0DyzsKxeD2FFJkSPg4arAjDSKPcy6cAc7CaeBPA7kF2wV1fuDlo2p/bNMpVr8yg==", "dev": true, "license": "MIT", "dependencies": { "@microsoft/tsdoc": "~0.16.0", "@microsoft/tsdoc-config": "~0.18.1", - "@rushstack/node-core-library": "5.20.3" + "@rushstack/node-core-library": "5.21.0" } }, "node_modules/@microsoft/api-extractor/node_modules/balanced-match": { @@ -1405,9 +1405,9 @@ } }, "node_modules/@microsoft/api-extractor/node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1633,9 +1633,9 @@ } }, "node_modules/@rushstack/node-core-library": { - "version": "5.20.3", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.20.3.tgz", - "integrity": "sha512-95JgEPq2k7tHxhF9/OJnnyHDXfC9cLhhta0An/6MlkDsX2A6dTzDrTUG18vx4vjc280V0fi0xDH9iQczpSuWsw==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.21.0.tgz", + "integrity": "sha512-LFzN+1lyWROit/P8Md6yxAth7lLYKn37oCKJHirEE2TQB25NDUM7bALf0ar+JAtwFfRCH+D+DGOA7DAzIi2r+g==", "dev": true, "license": "MIT", "dependencies": { @@ -1699,13 +1699,13 @@ } }, "node_modules/@rushstack/terminal": { - "version": "0.22.3", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.22.3.tgz", - "integrity": "sha512-gHC9pIMrUPzAbBiI4VZMU7Q+rsCzb8hJl36lFIulIzoceKotyKL3Rd76AZ2CryCTKEg+0bnTj406HE5YY5OQvw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.22.4.tgz", + "integrity": "sha512-fhtLjnXCc/4WleVbVl6aoc7jcWnU6yqjS1S8WoaNREG3ycu/viZ9R/9QM7Y/b4CDvcXoiDyMNIay7JMwBptM3g==", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/node-core-library": "5.20.3", + "@rushstack/node-core-library": "5.21.0", "@rushstack/problem-matcher": "0.2.1", "supports-color": "~8.1.1" }, @@ -1719,13 +1719,13 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.3.3.tgz", - "integrity": "sha512-c+ltdcvC7ym+10lhwR/vWiOhsrm/bP3By2VsFcs5qTKv+6tTmxgbVrtJ5NdNjANiV5TcmOZgUN+5KYQ4llsvEw==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.3.4.tgz", + "integrity": "sha512-MLkVKVEN6/2clKTrjN2B2KqKCuPxRwnNsWY7a+FCAq2EMdkj10cM8YgiBSMeGFfzM0mDMzargpHNnNzaBi9Whg==", "dev": true, "license": "MIT", "dependencies": { - "@rushstack/terminal": "0.22.3", + "@rushstack/terminal": "0.22.4", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" @@ -1742,9 +1742,9 @@ } }, "node_modules/@sama-communications/sdk": { - "version": "0.16.1", - "resolved": "file:test/cluster/sama-communications-sdk-0.16.1.tgz", - "integrity": "sha512-Ls1FBvMo+SamOPK+X1W7dpq+/IqmXgWfSSIg5GdsMrilvR69Rkzn7inzvu2Z4Def02H+PwK52cvxz5Nl3FcTRQ==", + "version": "0.16.2", + "resolved": "file:test/cluster/sama-communications-sdk-0.16.2.tgz", + "integrity": "sha512-BewG6wzTxtFx/tXdAgkDS2psEHSU40FFz3QxAKvriMes2W2Epuunl/07hi+EwIh+QLwpNDj4pK9lAYyM8U6CrQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2488,28 +2488,28 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz", - "integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", + "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.2", - "@vue/shared": "3.5.31", + "@vue/shared": "3.5.32", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz", - "integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", + "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.31", - "@vue/shared": "3.5.31" + "@vue/compiler-core": "3.5.32", + "@vue/shared": "3.5.32" } }, "node_modules/@vue/compiler-vue2": { @@ -2565,9 +2565,9 @@ } }, "node_modules/@vue/shared": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz", - "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", + "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 652ee2ed..473fde55 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "ws": "^8.12.0" }, "devDependencies": { - "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.16.1.tgz", + "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.16.2.tgz", "c8": "^10.1.3", "kill-port": "^2.0.1", "mocha": "^10.0.0", diff --git a/test/cluster/cross-node.spec.js b/test/cluster/cross-node.spec.js index fda96776..a270c858 100644 --- a/test/cluster/cross-node.spec.js +++ b/test/cluster/cross-node.spec.js @@ -31,7 +31,12 @@ const configNodeB = { } const samaClientA = new SAMAClient(configNodeA) +samaClientA.deviceId = v4() const samaClientB = new SAMAClient(configNodeB) +samaClientB.deviceId = v4() + +let userAToken = void 0 +let userBToken = void 0 let nodeA = void 0 let nodeB = void 0 @@ -49,7 +54,8 @@ describe("Cross-node behavior", () => { describe("Connect", () => { it("connect client A", async () => { await samaClientA.connect() - await samaClientA.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' }, deviceId: v4() }) + const { token } = await samaClientA.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' } }) + userAToken = token }) it ("subscribe user B last activity", async () => { @@ -58,7 +64,12 @@ describe("Cross-node behavior", () => { }) it("connect client B and check last activity", (done) => { - samaClientB.connect().then(() => samaClientB.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' }, deviceId: v4() })) + samaClientB + .connect() + .then(() => samaClientB.socketLogin({ + user: { login: 'banshiAnton2', password: '12345678' }, + })).then(({ token }) => userBToken = token) + samaClientA.onUserActivityListener = (activity) => { assert.equal(activity[userBNativeId], 0) done() diff --git a/test/cluster/multi-devices.spec.js b/test/cluster/multi-devices.spec.js index b8031592..11bd098f 100644 --- a/test/cluster/multi-devices.spec.js +++ b/test/cluster/multi-devices.spec.js @@ -31,8 +31,14 @@ const configNodeB = { } const samaClientA_Device1 = new SAMAClient(configNodeA) +samaClientA_Device1.deviceId = v4() const samaClientB_Device1 = new SAMAClient(configNodeB) +samaClientB_Device1.deviceId = v4() const samaClientB_Device2 = new SAMAClient(configNodeB) +samaClientB_Device2.deviceId = v4() + +let userAToken = void 0 +let userBToken = void 0 let nodeA = void 0 let nodeB = void 0 @@ -51,7 +57,8 @@ describe("Cross-node behavior", () => { describe("Connect", () => { it("connect U_A_D1", async () => { await samaClientA_Device1.connect() - await samaClientA_Device1.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' }, deviceId: v4() }) + const { token } = await samaClientA_Device1.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' } }) + userAToken = token }) it ("subscribe user B last activity", async () => { @@ -60,7 +67,12 @@ describe("Cross-node behavior", () => { }) it("connect U_B_D1 and check last activity", (done) => { - samaClientB_Device1.connect().then(() => samaClientB_Device1.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' }, deviceId: v4() })) + samaClientB_Device1 + .connect() + .then(() => samaClientB_Device1.socketLogin({ + user: { login: 'banshiAnton2', password: '12345678' }, + })).then(({ token }) => userBToken = token) + samaClientA_Device1.onUserActivityListener = (activity) => { assert.equal(activity[userBNativeId], 0) done() @@ -68,7 +80,7 @@ describe("Cross-node behavior", () => { }) it("connect U_B_D2 and check last activity", (done) => { - samaClientB_Device2.connect().then(() => samaClientB_Device2.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' }, deviceId: v4() })) + samaClientB_Device2.connect().then(() => samaClientB_Device2.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' } })) samaClientA_Device1.onUserActivityListener = (activity) => { assert.equal(activity[userBNativeId], 0) done() diff --git a/test/cluster/node-crash.spec.js b/test/cluster/node-crash.spec.js index 47b8e8a8..d8905ae9 100644 --- a/test/cluster/node-crash.spec.js +++ b/test/cluster/node-crash.spec.js @@ -31,12 +31,17 @@ const configNodeB = { } const samaClientA = new SAMAClient(configNodeA) +samaClientA.deviceId = v4() const samaClientB = new SAMAClient(configNodeB) +samaClientB.deviceId = v4() + +let userAToken = void 0 +let userBToken = void 0 let nodeA = void 0 let nodeB = void 0 -describe("Cross-node behavior:", () => { +describe("Crash Node behavior:", () => { before(async () => { console.log('[Before][node-crash]') @@ -46,10 +51,11 @@ describe("Cross-node behavior:", () => { await setTimeoutPromise(500) }) - describe("NodeA(U_A) - NodeB(U_B) - NodeB crash:", () => { + describe("NodeA(U_A) - NodeB(U_B) - NodeB crash (wait fro clean):", () => { it("connect client A", async () => { await samaClientA.connect() - await samaClientA.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' }, deviceId: v4() }) + const { token } = await samaClientA.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' } }) + userAToken = token }) it ("subscribe user B last activity", async () => { @@ -58,88 +64,114 @@ describe("Cross-node behavior:", () => { }) it("connect client B and check last activity", (done) => { - samaClientB.connect().then(() => samaClientB.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' }, deviceId: v4() })) + samaClientB + .connect() + .then(() => samaClientB.socketLogin({ + user: { login: 'banshiAnton2', password: '12345678' }, + })).then(({ token }) => userBToken = token) + samaClientA.onUserActivityListener = (activity) => { assert.equal(activity[userBNativeId], 0) done() } }) - it("messaging A -> B", (done) => { - const mId = v4() - const body = `Cluster Test Hello m: ${mId}` - samaClientA.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '2' } }) - - samaClientB.onMessageEvent = (message) => { - assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['two'], '2') - - done() - } - }) - - it("messaging B -> A", (done) => { - const mId = v4() - const body = `Cluster Test Hello m: ${mId}` - samaClientB.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '22' } }) - - samaClientA.onMessageEvent = (message) => { - assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['two'], '22') - - done() - } + describe("Messaging:", () => { + it("A -> B", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientA.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '2' } }) + + samaClientB.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['two'], '2') + + done() + } + }) + + it("messaging B -> A", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientB.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '22' } }) + + samaClientA.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['two'], '22') + + done() + } + }) }) - it("kill NodeB events with U_B last activity from NodeA", async () => { - const closePromise = new Promise((resolve, reject) => { - nodeA.once(`node_close[${nodeB.clusterEndpoint}]`, () => { - console.log('[node][close]', nodeB.clusterEndpoint) - resolve() + describe("NodeB Crash", () => { + it("kill NodeB events with U_B last activity from NodeA", async () => { + const closePromise = new Promise((resolve, reject) => { + nodeA.once(`node_close[${nodeB.clusterEndpoint}]`, () => { + console.log('[node][close]', nodeB.clusterEndpoint) + resolve() + }) }) - }) - - const startReconnectPromise = new Promise((resolve, reject) => { - nodeA.once(`node_reconnect_start[${nodeB.clusterEndpoint}]`, () => { - console.log('[node][reconnect][start]', nodeB.clusterEndpoint) - resolve() + + const startReconnectPromise = new Promise((resolve, reject) => { + nodeA.once(`node_reconnect_start[${nodeB.clusterEndpoint}]`, () => { + console.log('[node][reconnect][start]', nodeB.clusterEndpoint) + resolve() + }) }) - }) - - const cleanNodeBPromise = new Promise((resolve, reject) => { - nodeA.once(`node_clean[${nodeB.clusterEndpoint}]`, () => { - console.log('[node][clean]', nodeB.clusterEndpoint) - resolve() + + const cleanNodeBPromise = new Promise((resolve, reject) => { + nodeA.once(`node_clean[${nodeB.clusterEndpoint}]`, () => { + console.log('[node][clean]', nodeB.clusterEndpoint) + resolve() + }) }) + + const userBOfflinePromise = new Promise((resolve, reject) => { + samaClientA.onUserActivityListener = async (activity) => { + assert.ok(activity[userBNativeId] > 0) + + activity = await samaClientA.subscribeToUserActivity(userBNativeId) + assert.ok(activity[userBNativeId] > 0) + + resolve() + } + }) + + nodeB.kill() + + await Promise.all([closePromise, startReconnectPromise, cleanNodeBPromise, userBOfflinePromise]) }) - - const userBOfflinePromise = new Promise((resolve, reject) => { - samaClientA.onUserActivityListener = async (activity) => { - assert.ok(activity[userBNativeId] > 0) - - activity = await samaClientA.subscribeToUserActivity(userBNativeId) - assert.ok(activity[userBNativeId] > 0) - - resolve() - } - }) - - nodeB.kill() - - await Promise.all([closePromise, startReconnectPromise, cleanNodeBPromise, userBOfflinePromise]) }) describe("Run new NodeB and reconnect U_B", () => { it("start new NodeB", async () => { - nodeB = await startOrAccessNodeB(void 0, true) + nodeB = await startOrAccessNodeB(void 0, true, true) + + const nodeReadyPromise = new Promise((resolve, reject) => { + nodeB.once('node_ready', (nodeClusterWsEndpoint) => { + nodeB.clusterEndpoint = nodeClusterWsEndpoint + console.log('[node][ready]', nodeB.pid, nodeB.clusterEndpoint) + resolve() + }) + }) + + const nodeHandShackedPromise = new Promise((resolve, reject) => { + nodeB.once(`node_connected[${nodeA.clusterEndpoint}]`, (nodeClusterWsEndpoint) => { + console.log('[node][connected]', nodeClusterWsEndpoint) + resolve() + }) + }) + + await Promise.all([nodeReadyPromise, nodeHandShackedPromise]) }) it("connect client B and check last activity", (done) => { - samaClientB.connect().then(() => samaClientB.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' }, deviceId: v4() })) + samaClientB.connect().then(() => samaClientB.socketLogin({ token: userBToken })) samaClientA.onUserActivityListener = (activity) => { assert.equal(activity[userBNativeId], 0) done() @@ -179,4 +211,71 @@ describe("Cross-node behavior:", () => { }) }) }) + + describe("NodeA(U_A) - NodeB(U_B) - NodeA crush - connect to NodeB:", () => { + it("crash NodeA", async () => { + const closePromise = new Promise((resolve, reject) => { + nodeB.once(`node_close[${nodeA.clusterEndpoint}]`, () => { + console.log('[node][close]', nodeA.clusterEndpoint) + resolve() + }) + }) + + const startReconnectPromise = new Promise((resolve, reject) => { + nodeB.once(`node_reconnect_start[${nodeA.clusterEndpoint}]`, () => { + console.log('[node][reconnect][start]', nodeA.clusterEndpoint) + resolve() + }) + }) + + nodeA.kill() + + await Promise.all([closePromise, startReconnectPromise]) + }) + + it ("subscribe last activity U_B -> U_A", async () => { + const activity = await samaClientB.subscribeToUserActivity(userANativeId) + assert.ok(activity[userANativeId] === 0) + }) + + it("connect U_A to NodeB", (done) => { + samaClientA.connect(NODE_2_WS_ENDPOINT, NODE_2_HTTP_ENDPOINT).then(() => samaClientA.socketLogin({ token: userAToken })) + samaClientB.onUserActivityListener = (activity) => { + assert.equal(activity[userANativeId], 0) + done() + } + }) + + describe("it can send message to each other:", () => { + it("A -> B", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientA.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '2' } }) + + samaClientB.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['two'], '2') + + done() + } + }) + + it("B -> A", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientB.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '22' } }) + + samaClientA.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['two'], '22') + + done() + } + }) + }) + }) }) \ No newline at end of file diff --git a/test/cluster/sama-communications-sdk-0.16.1.tgz b/test/cluster/sama-communications-sdk-0.16.1.tgz deleted file mode 100644 index ea8263052b4786bc72cef7f822a899777c9bd007..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24046 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 diff --git a/test/cluster/sama-communications-sdk-0.16.2.tgz b/test/cluster/sama-communications-sdk-0.16.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..89fb0a853e1995fef550d045617f777c036e2696 GIT binary patch literal 24080 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 { describe("Connect", () => { it("connect client A", async () => { await samaClientA.connect() - await samaClientA.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' }, deviceId: v4() }) + const { token } = await samaClientA.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' } }) + userAToken = token }) it ("subscribe user B last activity", async () => { @@ -44,7 +50,12 @@ describe("Same-node behavior", () => { }) it("connect client B and check last activity", (done) => { - samaClientB.connect().then(() => samaClientB.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' }, deviceId: v4() })) + samaClientB + .connect() + .then(() => samaClientB.socketLogin({ + user: { login: 'banshiAnton2', password: '12345678' }, + })).then(({ token }) => userBToken = token) + samaClientA.onUserActivityListener = (activity) => { assert.equal(activity[userBNativeId], 0) done() diff --git a/test/cluster/utils.js b/test/cluster/utils.js index dcffb41e..9666f8f9 100644 --- a/test/cluster/utils.js +++ b/test/cluster/utils.js @@ -68,13 +68,13 @@ const createPipeStream = (tag, nodeSubprocess) => new (class extends Transform { } }) -export const spawnNode = async (cmd, tag) => { +export const spawnNode = async (cmd, tag, notWaitReady) => { const nodeSubprocess = spawn(cmd, [], { shell: true, stdio: ['ignore', 'pipe', 'pipe'] }) - - const nodeSubprocessReadyPromise = new Promise((resolve, reject) => { + + const nodeSubprocessReadyPromise = notWaitReady ? true : new Promise((resolve, reject) => { const rejectTimerId = setTimeout(() => reject(new Error('Timeout')), 5_000) nodeSubprocess.once('node_ready', (nodeClusterWsEndpoint) => { @@ -93,12 +93,12 @@ export const spawnNode = async (cmd, tag) => { return nodeSubprocess } -export const startOrAccessNodeA = async (tag = 'NODE_A', force) => { - return (nodeA && !force) ? nodeA : await spawnNode(RUN_NODE_1_CMD, tag).then(node => nodeA = node) +export const startOrAccessNodeA = async (tag = 'NODE_A', force, notWaitReady) => { + return (nodeA && !force) ? nodeA : await spawnNode(RUN_NODE_1_CMD, tag, notWaitReady).then(node => nodeA = node) } -export const startOrAccessNodeB = async (tag = 'NODE_B', force) => { - return (nodeB && !force) ? nodeB : await spawnNode(RUN_NODE_2_CMD, tag).then(node => nodeB = node) +export const startOrAccessNodeB = async (tag = 'NODE_B', force, notWaitReady) => { + return (nodeB && !force) ? nodeB : await spawnNode(RUN_NODE_2_CMD, tag, notWaitReady).then(node => nodeB = node) } export const killNodeA = () => nodeA?.kill() From 2211b82a9920ff9eb52e3d6e50fb8aab5de937af Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Fri, 3 Apr 2026 16:11:42 +0300 Subject: [PATCH 23/70] test reconnecting --- app/cluster/cluster_manager.js | 1 + app/utils/promise-queue-with-jittering.js | 4 - test/cluster/node-crash.spec.js | 91 ++++++++++++++++++++++- test/cluster/utils.js | 11 +-- 4 files changed, 97 insertions(+), 10 deletions(-) diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index c5131f4c..0b4fcad0 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -310,6 +310,7 @@ class ClusterManager extends BaseProtocolProcessor { this.#shareCurrentNodeInfo(ws) await this.createOrRetrieveConnectionWithNode(nodeInfo.endpoint, true) + .catch(error => loggerReceiver.error(error, "[create connection on receiver]")) return } diff --git a/app/utils/promise-queue-with-jittering.js b/app/utils/promise-queue-with-jittering.js index f0863dac..dbeb0ac7 100644 --- a/app/utils/promise-queue-with-jittering.js +++ b/app/utils/promise-queue-with-jittering.js @@ -39,10 +39,6 @@ const promiseQueueWithJittering = (executablePromise, tryCount, delay) => { const successResult = await executablePromise(checkIsCanceled) - if (isCanceled) { - throw new CancelQueueError("Canceled") - } - return successResult } catch (error) { lastError = error diff --git a/test/cluster/node-crash.spec.js b/test/cluster/node-crash.spec.js index d8905ae9..b5daa5f5 100644 --- a/test/cluster/node-crash.spec.js +++ b/test/cluster/node-crash.spec.js @@ -150,7 +150,7 @@ describe("Crash Node behavior:", () => { describe("Run new NodeB and reconnect U_B", () => { it("start new NodeB", async () => { - nodeB = await startOrAccessNodeB(void 0, true, true) + nodeB = await startOrAccessNodeB(true) const nodeReadyPromise = new Promise((resolve, reject) => { nodeB.once('node_ready', (nodeClusterWsEndpoint) => { @@ -212,6 +212,95 @@ describe("Crash Node behavior:", () => { }) }) + describe("NodeA(U_A) - NodeB(U_B) - NodeA restarting", () => { + it("NodeA crash", async () => { + const closePromise = new Promise((resolve, reject) => { + nodeB.once(`node_close[${nodeA.clusterEndpoint}]`, () => { + console.log('[node][close]', nodeA.clusterEndpoint) + resolve() + }) + }) + + const startReconnectPromise = new Promise((resolve, reject) => { + nodeB.once(`node_reconnect_start[${nodeA.clusterEndpoint}]`, () => { + console.log('[node][reconnect][start]', nodeA.clusterEndpoint) + resolve() + }) + }) + + nodeA.kill() + + await Promise.all([closePromise, startReconnectPromise]) + }) + + it ("check last activity", async () => { + const activity = await samaClientB.subscribeToUserActivity(userANativeId) + assert.ok(activity[userANativeId] === 0) + }) + + it("restart NodeA", async () => { + const nodeAClusterEndpoint = nodeA.clusterEndpoint + + const finishReconnectPromise = new Promise((resolve, reject) => { + nodeB.once(`node_reconnect_finish[${nodeAClusterEndpoint}]`, () => { + console.log('[node][reconnect][finish]', nodeAClusterEndpoint) + resolve() + }) + }) + + const { port } = new URL(nodeAClusterEndpoint) + const nodeEnv = { 'APP_CLUSTER_PORT': `${port}` } + + nodeA = await startOrAccessNodeA(false, nodeEnv) + + await finishReconnectPromise + }) + + it("connect U_A to NodeA", (done) => { + samaClientA + .connect() + .then(() => samaClientA.socketLogin({ token: userAToken })) + .then(({ token }) => userAToken = token) + + samaClientB.onUserActivityListener = (activity) => { + assert.equal(activity[userANativeId], 0) + done() + } + }) + + describe("it can send message to each other:", () => { + it("A -> B", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientA.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '2' } }) + + samaClientB.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['two'], '2') + + done() + } + }) + + it("B -> A", (done) => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientB.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '22' } }) + + samaClientA.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['two'], '22') + + done() + } + }) + }) + }) + describe("NodeA(U_A) - NodeB(U_B) - NodeA crush - connect to NodeB:", () => { it("crash NodeA", async () => { const closePromise = new Promise((resolve, reject) => { diff --git a/test/cluster/utils.js b/test/cluster/utils.js index 9666f8f9..95a249b0 100644 --- a/test/cluster/utils.js +++ b/test/cluster/utils.js @@ -68,9 +68,10 @@ const createPipeStream = (tag, nodeSubprocess) => new (class extends Transform { } }) -export const spawnNode = async (cmd, tag, notWaitReady) => { +export const spawnNode = async (cmd, env, tag, notWaitReady) => { const nodeSubprocess = spawn(cmd, [], { shell: true, + env: env ? { ...process.env, ...env } : void 0, stdio: ['ignore', 'pipe', 'pipe'] }) @@ -93,12 +94,12 @@ export const spawnNode = async (cmd, tag, notWaitReady) => { return nodeSubprocess } -export const startOrAccessNodeA = async (tag = 'NODE_A', force, notWaitReady) => { - return (nodeA && !force) ? nodeA : await spawnNode(RUN_NODE_1_CMD, tag, notWaitReady).then(node => nodeA = node) +export const startOrAccessNodeA = async (notWaitReady, env) => { + return (nodeA && !nodeA.killed) ? nodeA : await spawnNode(RUN_NODE_1_CMD, env, 'NODE_A', notWaitReady).then(node => nodeA = node) } -export const startOrAccessNodeB = async (tag = 'NODE_B', force, notWaitReady) => { - return (nodeB && !force) ? nodeB : await spawnNode(RUN_NODE_2_CMD, tag, notWaitReady).then(node => nodeB = node) +export const startOrAccessNodeB = async (notWaitReady, env) => { + return (nodeB && !nodeB.killed) ? nodeB : await spawnNode(RUN_NODE_2_CMD, env, 'NODE_B', notWaitReady).then(node => nodeB = node) } export const killNodeA = () => nodeA?.kill() From 28a5f028e8ee9f6b9d8c2e13ca4c8c931afa6785 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Fri, 3 Apr 2026 17:15:55 +0300 Subject: [PATCH 24/70] update specs --- .mocharc.json | 1 + package.json | 1 + test/cluster.spec.js | 128 -------- test/cluster/multi-devices.spec.js | 495 ++++++++++++++++++++++++++++- test/cluster/node-crash.spec.js | 95 +++++- test/cluster/utils.js | 2 - 6 files changed, 589 insertions(+), 133 deletions(-) delete mode 100644 test/cluster.spec.js diff --git a/.mocharc.json b/.mocharc.json index 96bc1053..02f2b2b3 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -6,5 +6,6 @@ "node-option": ["experimental-loader=./sama-loader.mjs", "import=dotenv/config"], "exit": true, "spec": "test/**/*.spec.js", + "exclude": "test/cluster/*.spec.js", "timeout": 60000 } diff --git a/package.json b/package.json index 473fde55..d8a2a508 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "start": "node --experimental-loader ./sama-loader.mjs --import dotenv/config index.js", "start:dev": "nodemon --experimental-loader ./sama-loader.mjs --import dotenv/config index.js", "test": "mocha", + "test:cluster": "mocha --config ./.cluster-mocharc.json", "test:cover": "c8 npm run test" }, "repository": { diff --git a/test/cluster.spec.js b/test/cluster.spec.js deleted file mode 100644 index 95455b8b..00000000 --- a/test/cluster.spec.js +++ /dev/null @@ -1,128 +0,0 @@ -import ip from "ip" -import uWS from "uWebSockets.js" -import assert from "node:assert" - -import ServiceLocatorContainer from "../app/common/ServiceLocatorContainer.js" -import clusterManager from "../app/cluster/cluster_manager.js" -import { generateNewOrganizationId, createConversation, createUserArray, mockedWS, sendLogin } from "./tools/utils.js" -import { buildWsEndpoint } from "../app/utils/build_ws_endpoint.js" -import packetJsonProcessor from "../APIs/JSON/routes/packet_processor.js" -import packetManager from "../app/networking/packet_manager.js" - -const config = ServiceLocatorContainer.use("Config") -const logger = ServiceLocatorContainer.use("Logger") -const sessionService = ServiceLocatorContainer.use("SessionService") -const userRepo = ServiceLocatorContainer.use("UserRepository") -const conversationRepo = ServiceLocatorContainer.use("ConversationRepository") -const conversationParticipantRepo = ServiceLocatorContainer.use("ConversationParticipantRepository") -const messageRepo = ServiceLocatorContainer.use("MessageRepository") -const messageStatusRepo = ServiceLocatorContainer.use("MessageStatusRepository") - -let orgId = void 0 -let currentConversationId = "" -let usersIds = [] -let deviceId = null -let secondClusterPort = null -let secondSocketResponse = null - -describe("Cluster Message function", async () => { - before(async () => { - orgId = await generateNewOrganizationId() - usersIds = await createUserArray(orgId, 2) - - await sendLogin(mockedWS, orgId, "user_1") - - currentConversationId = await createConversation(mockedWS, null, null, "g", [usersIds[1], usersIds[0]]) - - // emulate user2 connect in other node - deviceId = sessionService.activeSessions.DEVICES[usersIds[0]][0].deviceId - - const SSL_APP_OPTIONS = { - key_file_name: config.get("ws.options.ssl.key"), - cert_file_name: config.get("ws.options.ssl.cert"), - } - const CLUSTER_SOCKET = uWS.SSLApp(SSL_APP_OPTIONS) - CLUSTER_SOCKET.listen(0, (listenSocket) => { - if (listenSocket) { - const clusterPort = uWS.us_socket_local_port(listenSocket) - logger.trace("CLUSTER listening on port %s", clusterPort) - secondClusterPort = clusterPort - } else { - throw "CLUSTER_SOCKET.listen error" - } - }) - - const mockedWS2 = { - send: (data) => { - secondSocketResponse = data - }, - } - const newEndpoint = buildWsEndpoint(ip.address(), secondClusterPort) - clusterManager.clusterNodesConnections[newEndpoint] = mockedWS2 - - sessionService.addUserDeviceConnection(mockedWS2, orgId, usersIds[1], deviceId) - - const prevEndpoint = sessionService.config.get("ws.cluster.endpoint") - sessionService.config.set("ws.cluster.endpoint", newEndpoint) - - await sessionService.storeUserNodeData(mockedWS2, orgId, usersIds[1], deviceId) - - sessionService.config.set("ws.cluster.endpoint", prevEndpoint) - }) - - describe("Send Message to other node", async () => { - it("should work", async () => { - const requestData = { - message: { - id: "xyz", - body: "hey how is going?", - cid: currentConversationId, - }, - } - - const controllerResponse = await packetJsonProcessor.processMessageOrError(mockedWS, JSON.stringify(requestData)) - const deliverMessage = controllerResponse.deliverMessages.at(0) - - assert.notEqual(deliverMessage, undefined) - assert.equal(`${deliverMessage.cId}`, `${currentConversationId}`) - - const sourceOptions = { - organizationId: orgId, - socket: deliverMessage.socket || mockedWS, - } - - const destinationUserIds = usersIds - - const payloadOptions = { - packet: JSON.stringify(deliverMessage.packet), - notSaveInOfflineStorage: deliverMessage.notSaveInOfflineStorage, - ignoreSelf: false, - pushQueueMessage: void 0, - } - - await packetManager.deliverToUserOrUsers(sourceOptions, destinationUserIds, payloadOptions) - - const response = JSON.parse(secondSocketResponse).deliverPacket - response.packet = JSON.parse(response.packet) - - assert.notEqual(response, undefined) - - assert.strictEqual(response.userId, usersIds[1].toString()) - assert.strictEqual(response.senderInfo.session.userId, usersIds[0].toString()) - - assert.notEqual(response.packet, undefined) - assert.strictEqual(response.packet.message.from, usersIds[0].toString()) - assert.strictEqual(response.packet.message.body, "hey how is going?") - }) - }) - - after(async () => { - await userRepo.deleteMany({}) - await messageRepo.deleteMany({}) - await messageStatusRepo.deleteMany({}) - await conversationRepo.deleteMany({}) - await conversationParticipantRepo.deleteMany({}) - - usersIds = [] - }) -}) diff --git a/test/cluster/multi-devices.spec.js b/test/cluster/multi-devices.spec.js index 11bd098f..01a1eb5e 100644 --- a/test/cluster/multi-devices.spec.js +++ b/test/cluster/multi-devices.spec.js @@ -253,7 +253,6 @@ describe("Cross-node behavior", () => { }) it("read status", (done) => { - console.log(messageId) samaClientB_Device2.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) samaClientA_Device1.onMessageStatusListener = (status) => { @@ -414,7 +413,499 @@ describe("Cross-node behavior", () => { }) it("read status", (done) => { - console.log(messageId) + samaClientB_Device2.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) + + samaClientA_Device1.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_G_CONV_ID) + assert.equal(status.from, userBNativeId) + + done() + } + }) + }) + + describe("U_B_D1 -> U_A_D1,U_B_D2", () => { + let messageId = void 0 + + it("typing", async () => { + samaClientB_Device1.sendTypingStatus({ cid: TEST_G_CONV_ID, status: 'start' }) + + const uA_D1_promise = new Promise((resolve, reject) => { + samaClientA_Device1.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_G_CONV_ID) + assert.equal(typing.from, userBNativeId) + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_G_CONV_ID) + assert.equal(typing.from, userBNativeId) + resolve() + } + }) + + await Promise.all([uA_D1_promise, uB_D2_promise]) + }) + + it("simple message", async () => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientB_Device1.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '22' } }) + + const uA_D1_promise = new Promise((resolve, reject) => { + samaClientA_Device1.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['two'], '22') + + messageId = message._id + + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['two'], '22') + + messageId = message._id + + resolve() + } + }) + + await Promise.all([uA_D1_promise, uB_D2_promise]) + }) + + it("read status", async () => { + samaClientA_Device1.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) + + const uB_D1_promise = new Promise((resolve, reject) => { + samaClientB_Device1.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_G_CONV_ID) + assert.equal(status.from, userANativeId) + + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_G_CONV_ID) + assert.equal(status.from, userANativeId) + + resolve() + } + }) + + await Promise.all([uB_D1_promise, uB_D2_promise]) + }) + }) + }) + }) + + describe("Activity on disconnect", () => { + it("U_B_D1 logout (not last activity)", async () => { + samaClientB_Device1.disconnect() + + const promise = new Promise((resolve, reject) => { + setTimeout(() => resolve(), 500) + + samaClientA_Device1.onUserActivityListener = (activity) => { + if (activity[userBNativeId] > 0) { + reject(new Error("Should not be called")) + } + } + }) + + await promise + }) + + it("U_B_D1 logout (last activity)", (done) => { + samaClientB_Device2.disconnect() + + samaClientA_Device1.onUserActivityListener = (activity) => { + assert.ok(activity[userBNativeId] > 0) + done() + } + }) + + it("disconnect A", async () => { + samaClientA_Device1.disconnect() + await setTimeoutPromise(200) + }) + }) + }) + + describe("Node-A (U_A_D1, U_B_D2) - Node-B (U_B_D1)", () => { + describe("Connect", () => { + it("connect U_A_D1", async () => { + await samaClientA_Device1.connect() + const { token } = await samaClientA_Device1.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' } }) + userAToken = token + }) + + it ("subscribe user B last activity", async () => { + const activity = await samaClientA_Device1.subscribeToUserActivity(userBNativeId) + assert.ok(activity[userBNativeId] > 0) + }) + + it("connect U_B_D1 and check last activity", (done) => { + samaClientB_Device1 + .connect() + .then(() => samaClientB_Device1.socketLogin({ + user: { login: 'banshiAnton2', password: '12345678' }, + })).then(({ token }) => userBToken = token) + + samaClientA_Device1.onUserActivityListener = (activity) => { + assert.equal(activity[userBNativeId], 0) + done() + } + }) + + it("connect U_B_D2 and check last activity", (done) => { + samaClientB_Device2.connect(NODE_1_WS_ENDPOINT, NODE_1_HTTP_ENDPOINT).then(() => samaClientB_Device2.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' } })) + samaClientA_Device1.onUserActivityListener = (activity) => { + assert.equal(activity[userBNativeId], 0) + done() + } + }) + + it("subscribe user A last activity", async () => { + let activity = await samaClientB_Device1.subscribeToUserActivity(userANativeId) + assert.ok(activity[userANativeId] === 0) + + activity = await samaClientB_Device2.subscribeToUserActivity(userANativeId) + assert.ok(activity[userANativeId] === 0) + }) + }) + + describe("Base messaging", () => { + describe("System", () => { + describe("U_A_D1 -> U_B_D1,U_B_D2", async () => { + it("event", async () => { + const mId = v4() + samaClientA_Device1.messageSystem({ mid: mId, uids: [userBNativeId], x: { one: '1' } }) + + const uA_D1_promise = new Promise((resolve, reject) => { + samaClientB_Device1.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userANativeId) + + resolve() + } + }) + + const uA_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userANativeId) + + resolve() + } + }) + + await Promise.all([uA_D1_promise, uA_D2_promise]) + }) + }) + + describe("U_B_D1 sender", () => { + it("event to U_A_D1", (done) => { + const mId = v4() + samaClientB_Device1.messageSystem({ mid: mId, uids: [userANativeId], x: { two: '2' } }) + + samaClientA_Device1.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userBNativeId) + + done() + } + }) + + it("event to U_A_D1,U_B_D2", async () => { + const mId = v4() + samaClientB_Device1.messageSystem({ mid: mId, uids: [userANativeId, userBNativeId], x: { two: '22' } }) + + const uA_D1_promise = new Promise((resolve, reject) => { + samaClientA_Device1.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userBNativeId) + + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userBNativeId) + + resolve() + } + }) + + await Promise.all([uA_D1_promise, uB_D2_promise]) + }) + }) + + describe("U_B_D2 sender", () => { + it("event to conversation", async () => { + const mId = v4() + samaClientB_Device2.messageSystem({ mid: mId, cid: TEST_U_CONV_ID, x: { four: '44' } }) + + const uA_D1_promise = new Promise((resolve, reject) => { + samaClientA_Device1.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userBNativeId) + + resolve() + } + }) + + const uB_D1_promise = new Promise((resolve, reject) => { + samaClientB_Device1.onSystemMessageEvent = (message) => { + assert.equal(message._id, mId) + assert.equal(message.from, userBNativeId) + + resolve() + } + }) + + await Promise.all([uA_D1_promise, uB_D1_promise]) + }) + }) + }) + + describe("Private", () => { + describe("U_A_D1 -> U_B_D1,U_B_D2", () => { + let messageId = void 0 + + it("typing", async () => { + samaClientA_Device1.sendTypingStatus({ cid: TEST_U_CONV_ID, status: 'start' }) + + const uB_D1_promise = new Promise((resolve, reject) => { + samaClientB_Device1.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_U_CONV_ID) + assert.equal(typing.from, userANativeId) + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_U_CONV_ID) + assert.equal(typing.from, userANativeId) + resolve() + } + }) + + await Promise.all([uB_D1_promise, uB_D2_promise]) + }) + + it("simple message", async () => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientA_Device1.messageCreate({ cid: TEST_U_CONV_ID, body: body, mid: mId, x: { two: '2' } }) + + const uB_D1_promise = new Promise((resolve, reject) => { + samaClientB_Device1.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_U_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['two'], '2') + + messageId = message._id + + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_U_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['two'], '2') + + messageId = message._id + + resolve() + } + }) + + await Promise.all([uB_D1_promise, uB_D2_promise]) + }) + + it("read status", (done) => { + samaClientB_Device2.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) + + samaClientA_Device1.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_U_CONV_ID) + assert.equal(status.from, userBNativeId) + + done() + } + }) + }) + + describe("U_B_D1 -> U_A_D1,U_B_D2", () => { + let messageId = void 0 + + it("typing", async () => { + samaClientB_Device1.sendTypingStatus({ cid: TEST_U_CONV_ID, status: 'start' }) + + const uA_D1_promise = new Promise((resolve, reject) => { + samaClientA_Device1.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_U_CONV_ID) + assert.equal(typing.from, userBNativeId) + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_U_CONV_ID) + assert.equal(typing.from, userBNativeId) + resolve() + } + }) + + await Promise.all([uA_D1_promise, uB_D2_promise]) + }) + + it("simple message", async () => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientB_Device1.messageCreate({ cid: TEST_U_CONV_ID, body: body, mid: mId, x: { two: '22' } }) + + const uA_D1_promise = new Promise((resolve, reject) => { + samaClientA_Device1.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_U_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['two'], '22') + + messageId = message._id + + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_U_CONV_ID) + assert.equal(message.from, userBNativeId) + assert.equal(message.x['two'], '22') + + messageId = message._id + + resolve() + } + }) + + await Promise.all([uA_D1_promise, uB_D2_promise]) + }) + + it("read status", async () => { + samaClientA_Device1.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) + + const uB_D1_promise = new Promise((resolve, reject) => { + samaClientB_Device1.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_U_CONV_ID) + assert.equal(status.from, userANativeId) + + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onMessageStatusListener = (status) => { + assert.equal(status.ids.at(0), messageId) + assert.equal(status.cid, TEST_U_CONV_ID) + assert.equal(status.from, userANativeId) + + resolve() + } + }) + + await Promise.all([uB_D1_promise, uB_D2_promise]) + }) + }) + }) + + describe("Group", () => { + describe("U_A_D1 -> U_B_D1,U_B_D2", () => { + let messageId = void 0 + + it("typing", async () => { + samaClientA_Device1.sendTypingStatus({ cid: TEST_G_CONV_ID, status: 'start' }) + + const uB_D1_promise = new Promise((resolve, reject) => { + samaClientB_Device1.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_G_CONV_ID) + assert.equal(typing.from, userANativeId) + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onUserTypingListener = (typing) => { + assert.equal(typing.cid, TEST_G_CONV_ID) + assert.equal(typing.from, userANativeId) + resolve() + } + }) + + await Promise.all([uB_D1_promise, uB_D2_promise]) + }) + + it("simple message", async () => { + const mId = v4() + const body = `Cluster Test Hello m: ${mId}` + samaClientA_Device1.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '2' } }) + + const uB_D1_promise = new Promise((resolve, reject) => { + samaClientB_Device1.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['two'], '2') + + messageId = message._id + + resolve() + } + }) + + const uB_D2_promise = new Promise((resolve, reject) => { + samaClientB_Device2.onMessageEvent = (message) => { + assert.equal(message.body, body) + assert.equal(message.cid, TEST_G_CONV_ID) + assert.equal(message.from, userANativeId) + assert.equal(message.x['two'], '2') + + messageId = message._id + + resolve() + } + }) + + await Promise.all([uB_D1_promise, uB_D2_promise]) + }) + + it("read status", (done) => { samaClientB_Device2.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) samaClientA_Device1.onMessageStatusListener = (status) => { diff --git a/test/cluster/node-crash.spec.js b/test/cluster/node-crash.spec.js index b5daa5f5..8b7327a4 100644 --- a/test/cluster/node-crash.spec.js +++ b/test/cluster/node-crash.spec.js @@ -34,6 +34,8 @@ const samaClientA = new SAMAClient(configNodeA) samaClientA.deviceId = v4() const samaClientB = new SAMAClient(configNodeB) samaClientB.deviceId = v4() +const samaClientB_Device2 = new SAMAClient(configNodeA) +samaClientB_Device2.deviceId = v4() let userAToken = void 0 let userBToken = void 0 @@ -171,7 +173,9 @@ describe("Crash Node behavior:", () => { }) it("connect client B and check last activity", (done) => { - samaClientB.connect().then(() => samaClientB.socketLogin({ token: userBToken })) + samaClientB.connect() + .then(() => samaClientB.socketLogin({ token: userBToken })) + .then(({ token }) => userBToken = token) samaClientA.onUserActivityListener = (activity) => { assert.equal(activity[userBNativeId], 0) done() @@ -301,6 +305,95 @@ describe("Crash Node behavior:", () => { }) }) + describe("NodeA(U_A,U_B_D2) - NodeB(U_B_D1) - NodeB Crush check last activity behavior:", () => { + describe("Connect User B Device 2", () => { + it("subscribe last activity U_B", async () => { + const activity = await samaClientA.subscribeToUserActivity(userBNativeId) + assert.ok(activity[userBNativeId] === 0) + }) + + it("connect U_B_D2 to NodeA", (done) => { + samaClientB_Device2 + .connect() + .then(() => samaClientB_Device2.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' } })) + + samaClientA.onUserActivityListener = (activity) => { + assert.equal(activity[userBNativeId], 0) + done() + } + }) + }) + + describe("kill NodeB check last activity behavior", () => { + it ("kill NodeB wait no last activity", async () => { + const closePromise = new Promise((resolve, reject) => { + nodeA.once(`node_close[${nodeB.clusterEndpoint}]`, () => { + console.log('[node][close]', nodeB.clusterEndpoint) + resolve() + }) + }) + + const startReconnectPromise = new Promise((resolve, reject) => { + nodeA.once(`node_reconnect_start[${nodeB.clusterEndpoint}]`, () => { + console.log('[node][reconnect][start]', nodeB.clusterEndpoint) + resolve() + }) + }) + + const cleanNodeBPromise = new Promise((resolve, reject) => { + nodeA.once(`node_clean[${nodeB.clusterEndpoint}]`, () => { + console.log('[node][clean]', nodeB.clusterEndpoint) + resolve() + }) + }) + + const userBNoOfflinePromise = new Promise((resolve, reject) => { + cleanNodeBPromise.then(() => setTimeoutPromise(1_000)).then(() => resolve()) + + samaClientA.onUserActivityListener = async (activity) => { + if (activity[userBNativeId] > 0) { + reject(new Error("Should not be called")) + } + } + }) + + nodeB.kill() + + await Promise.all([closePromise, startReconnectPromise, cleanNodeBPromise, userBNoOfflinePromise]) + }) + + it("check user B activity", async () => { + const activity = await samaClientA.subscribeToUserActivity(userBNativeId) + assert.ok(activity[userBNativeId] === 0) + }) + + it("close last U_B connection on NodeA", (done) => { + samaClientB_Device2.disconnect() + + samaClientA.onUserActivityListener = (activity) => { + assert.ok(activity[userBNativeId] > 0) + done() + } + }) + }) + + describe("Start new NodeB", () => { + it("start", async () => { + nodeB = await startOrAccessNodeB() + + await setTimeoutPromise(500) + }) + + it("connect U_B", (done) => { + samaClientB.connect().then(() => samaClientB.socketLogin({ token: userBToken })) + samaClientA.onUserActivityListener = (activity) => { + assert.equal(activity[userBNativeId], 0) + done() + } + }) + }) + }) + describe("NodeA(U_A) - NodeB(U_B) - NodeA crush - connect to NodeB:", () => { it("crash NodeA", async () => { const closePromise = new Promise((resolve, reject) => { diff --git a/test/cluster/utils.js b/test/cluster/utils.js index 95a249b0..8619e7e0 100644 --- a/test/cluster/utils.js +++ b/test/cluster/utils.js @@ -20,8 +20,6 @@ export const userBNativeId = '69cbe9a27fc0a44161188218' let nodeA = void 0 let nodeB = void 0 -console.log(RUN_NODE_1_CMD, RUN_NODE_2_CMD) - // ws endpoint reg: /wss?:\/\/wss?:\/\/.+\// const createPipeStream = (tag, nodeSubprocess) => new (class extends Transform { From f6044ca1ccdbb5be597bc7e8c1c1f11f166c9972 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Mon, 6 Apr 2026 15:05:34 +0300 Subject: [PATCH 25/70] update specs: add dummy data --- app/cluster/cluster_manager.js | 7 +- app/utils/promise-queue-with-jittering.js | 16 +- package-lock.json | 18 + package.json | 1 + test/cluster/cross-node.spec.js | 254 ++++---- test/cluster/fixtures.js | 26 +- test/cluster/multi-devices.spec.js | 688 ++++++++++++---------- test/cluster/node-crash.spec.js | 274 +++++---- test/cluster/same-node.spec.js | 249 ++++---- test/cluster/utils.js | 205 +++++-- 10 files changed, 976 insertions(+), 762 deletions(-) diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index 0b4fcad0..5a5912ff 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -240,7 +240,7 @@ class ClusterManager extends BaseProtocolProcessor { nodeSederLogger.debug("[Close][%s] IsWasOpened: %s Code: %s Reason: %s", ws.nodeEndpoint, ws.isWasOpened, code, reason) if (!ws.isWasOpened) { - return + return } this.onCloseNode(ws.nodeEndpoint, +code, reason) @@ -309,8 +309,9 @@ class ClusterManager extends BaseProtocolProcessor { loggerReceiver.debug("[node handshake pong] %s", nodeInfo.endpoint) this.#shareCurrentNodeInfo(ws) - await this.createOrRetrieveConnectionWithNode(nodeInfo.endpoint, true) - .catch(error => loggerReceiver.error(error, "[create connection on receiver]")) + await this.createOrRetrieveConnectionWithNode(nodeInfo.endpoint, true).catch((error) => + loggerReceiver.error(error, "[create connection on receiver]") + ) return } diff --git a/app/utils/promise-queue-with-jittering.js b/app/utils/promise-queue-with-jittering.js index dbeb0ac7..b1dc71d3 100644 --- a/app/utils/promise-queue-with-jittering.js +++ b/app/utils/promise-queue-with-jittering.js @@ -1,11 +1,11 @@ import { setTimeout } from "node:timers/promises" class CancelQueueError extends Error {} - + const promiseQueueWithJittering = (executablePromise, tryCount, delay) => { let isCanceled = false - const cancel = () => isCanceled = true + const cancel = () => (isCanceled = true) const checkIsCanceled = () => isCanceled const start = async () => { @@ -18,14 +18,14 @@ const promiseQueueWithJittering = (executablePromise, tryCount, delay) => { const tryDelay = Math.pow(tryCount, 2) * delay return tryDelay }) - + let lastError = void 0 - - console.log('[tryDelays]', tryDelays, isCanceled) - + + console.log("[tryDelays]", tryDelays, isCanceled) + for (const tryDelay of tryDelays) { try { - console.log('[try]', tryDelay, isCanceled) + console.log("[try]", tryDelay, isCanceled) if (isCanceled) { throw new CancelQueueError("Canceled") @@ -60,4 +60,4 @@ const promiseQueueWithJittering = (executablePromise, tryCount, delay) => { return { start, cancel } } -export { promiseQueueWithJittering, CancelQueueError } \ No newline at end of file +export { promiseQueueWithJittering, CancelQueueError } diff --git a/package-lock.json b/package-lock.json index df8268cb..965be663 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "ws": "^8.12.0" }, "devDependencies": { + "@faker-js/faker": "^10.4.0", "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.16.2.tgz", "c8": "^10.1.3", "kill-port": "^2.0.1", @@ -1092,6 +1093,23 @@ "node": ">=0.1.90" } }, + "node_modules/@faker-js/faker": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.4.0.tgz", + "integrity": "sha512-sDBWI3yLy8EcDzgobvJTWq1MJYzAkQdpjXuPukga9wXonhpMRvd1Izuo2Qgwey2OiEoRIBr35RMU9HJRoOHzpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0", + "npm": ">=10" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", diff --git a/package.json b/package.json index d8a2a508..6a5410a2 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "ws": "^8.12.0" }, "devDependencies": { + "@faker-js/faker": "^10.4.0", "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.16.2.tgz", "c8": "^10.1.3", "kill-port": "^2.0.1", diff --git a/test/cluster/cross-node.spec.js b/test/cluster/cross-node.spec.js index a270c858..70da1b71 100644 --- a/test/cluster/cross-node.spec.js +++ b/test/cluster/cross-node.spec.js @@ -5,11 +5,14 @@ import { v4 } from "uuid" import { SAMAClient } from "@sama-communications/sdk" import { - startOrAccessNodeA, startOrAccessNodeB, - TEST_ORG_ID, TEST_U_CONV_ID, TEST_G_CONV_ID, - NODE_1_WS_ENDPOINT, NODE_1_HTTP_ENDPOINT, - NODE_2_WS_ENDPOINT, NODE_2_HTTP_ENDPOINT, - userANativeId, userBNativeId + startOrAccessNodeA, + startOrAccessNodeB, + NODE_1_WS_ENDPOINT, + NODE_1_HTTP_ENDPOINT, + NODE_2_WS_ENDPOINT, + NODE_2_HTTP_ENDPOINT, + dummyDataTestConfig, + userPassword, } from "./utils.js" const configNodeA = { @@ -17,7 +20,6 @@ const configNodeA = { ws: NODE_1_WS_ENDPOINT, http: NODE_1_HTTP_ENDPOINT, }, - organization_id: TEST_ORG_ID, disableAutoReconnect: true, } @@ -26,14 +28,11 @@ const configNodeB = { ws: NODE_2_WS_ENDPOINT, http: NODE_2_HTTP_ENDPOINT, }, - organization_id: TEST_ORG_ID, disableAutoReconnect: true, } -const samaClientA = new SAMAClient(configNodeA) -samaClientA.deviceId = v4() -const samaClientB = new SAMAClient(configNodeB) -samaClientB.deviceId = v4() +let samaClientA = void 0 +let samaClientB = void 0 let userAToken = void 0 let userBToken = void 0 @@ -43,266 +42,277 @@ let nodeB = void 0 describe("Cross-node behavior", () => { before(async () => { - console.log('[Before][cross-node]') - + console.log("[Before][cross-node]") + + configNodeA.organization_id = dummyDataTestConfig.organizationId + configNodeB.organization_id = dummyDataTestConfig.organizationId + + samaClientA = new SAMAClient(configNodeA) + samaClientA.deviceId = v4() + samaClientB = new SAMAClient(configNodeB) + samaClientB.deviceId = v4() + nodeA = await startOrAccessNodeA() nodeB = await startOrAccessNodeB() await setTimeoutPromise(500) }) - + describe("Connect", () => { it("connect client A", async () => { await samaClientA.connect() - const { token } = await samaClientA.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' } }) + const { token } = await samaClientA.socketLogin({ user: { login: dummyDataTestConfig.users.user1.login, password: userPassword } }) userAToken = token }) - - it ("subscribe user B last activity", async () => { - const activity = await samaClientA.subscribeToUserActivity(userBNativeId) - assert.ok(activity[userBNativeId] > 0) + + it("subscribe user B last activity", async () => { + const activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.user2.nativeId) + assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) }) - + it("connect client B and check last activity", (done) => { samaClientB .connect() - .then(() => samaClientB.socketLogin({ - user: { login: 'banshiAnton2', password: '12345678' }, - })).then(({ token }) => userBToken = token) + .then(() => + samaClientB.socketLogin({ + user: { login: dummyDataTestConfig.users.user2.login, password: userPassword }, + }) + ) + .then(({ token }) => (userBToken = token)) samaClientA.onUserActivityListener = (activity) => { - assert.equal(activity[userBNativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) done() } }) - + it("subscribe user A last activity", async () => { - const activity = await samaClientB.subscribeToUserActivity(userANativeId) - assert.ok(activity[userANativeId] === 0) + const activity = await samaClientB.subscribeToUserActivity(dummyDataTestConfig.users.user1.nativeId) + assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] === 0) }) }) - + describe("Base messaging", () => { describe("System", () => { describe("A -> B", () => { it("event", (done) => { const mId = v4() - samaClientA.messageSystem({ mid: mId, uids: [userBNativeId], x: { one: '1' } }) - + samaClientA.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.user2.nativeId], x: { one: "1" } }) + samaClientB.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userANativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + done() } }) }) - + describe("B -> A", () => { it("event", (done) => { const mId = v4() - samaClientB.messageSystem({ mid: mId, uids: [userANativeId], x: { two: '2' } }) - + samaClientB.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.user1.nativeId], x: { two: "2" } }) + samaClientA.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userBNativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + done() } }) }) }) - + describe("Private", () => { describe("A -> B", () => { let messageId = void 0 - + it("typing", (done) => { - samaClientA.sendTypingStatus({ cid: TEST_U_CONV_ID, status: 'start' }) - + samaClientA.sendTypingStatus({ cid: dummyDataTestConfig.conversations.private.nativeId, status: "start" }) + samaClientB.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_U_CONV_ID) - assert.equal(typing.from, userANativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) done() } }) - + it("simple message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientA.messageCreate({ cid: TEST_U_CONV_ID, body: body, mid: mId, x: { two: '2' } }) - + samaClientA.messageCreate({ cid: dummyDataTestConfig.conversations.private.nativeId, body: body, mid: mId, x: { two: "2" } }) + samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_U_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['two'], '2') - + assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.x["two"], "2") + messageId = message._id - + done() } }) - + it("read status", (done) => { - samaClientB.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) - + samaClientB.markConversationAsRead({ cid: dummyDataTestConfig.conversations.private.nativeId, mids: [messageId] }) + samaClientA.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_U_CONV_ID) - assert.equal(status.from, userBNativeId) - + assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user2.nativeId) + done() } }) }) - + describe("B -> A", () => { let messageId = void 0 - + it("typing", (done) => { - samaClientB.sendTypingStatus({ cid: TEST_U_CONV_ID, status: 'start' }) - + samaClientB.sendTypingStatus({ cid: dummyDataTestConfig.conversations.private.nativeId, status: "start" }) + samaClientA.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_U_CONV_ID) - assert.equal(typing.from, userBNativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) done() } }) - + it("simple message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientB.messageCreate({ cid: TEST_U_CONV_ID, body: body, mid: mId, x: { two: '22' } }) - + samaClientB.messageCreate({ cid: dummyDataTestConfig.conversations.private.nativeId, body: body, mid: mId, x: { two: "22" } }) + samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_U_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['two'], '22') - + assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.x["two"], "22") + messageId = message._id - + done() } }) - + it("read status", (done) => { - samaClientA.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) - + samaClientA.markConversationAsRead({ cid: dummyDataTestConfig.conversations.private.nativeId, mids: [messageId] }) + samaClientB.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_U_CONV_ID) - assert.equal(status.from, userANativeId) - + assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + done() } }) }) }) - + describe("Group", () => { describe("A -> B", () => { let messageId = void 0 - + it("typing", (done) => { - samaClientA.sendTypingStatus({ cid: TEST_G_CONV_ID, status: 'start' }) - + samaClientA.sendTypingStatus({ cid: dummyDataTestConfig.conversations.group.nativeId, status: "start" }) + samaClientB.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_G_CONV_ID) - assert.equal(typing.from, userANativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) done() } }) - + it("simple message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientA.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { one: '1' } }) - + samaClientA.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { one: "1" } }) + samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['one'], '1') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.x["one"], "1") + messageId = message._id - + done() } }) - + it("read status", (done) => { - samaClientB.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) - + samaClientB.markConversationAsRead({ cid: dummyDataTestConfig.conversations.group.nativeId, mids: [messageId] }) + samaClientA.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_G_CONV_ID) - assert.equal(status.from, userBNativeId) - + assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user2.nativeId) + done() } }) }) - + describe("B -> A", () => { let messageId = void 0 - + it("typing", (done) => { - samaClientB.sendTypingStatus({ cid: TEST_G_CONV_ID, status: 'start' }) - + samaClientB.sendTypingStatus({ cid: dummyDataTestConfig.conversations.group.nativeId, status: "start" }) + samaClientA.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_G_CONV_ID) - assert.equal(typing.from, userBNativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) done() } }) - + it("simple message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientB.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { one: '11' } }) - + samaClientB.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { one: "11" } }) + samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['one'], '11') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.x["one"], "11") + messageId = message._id - + done() } }) - + it("read status", (done) => { - samaClientA.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) - + samaClientA.markConversationAsRead({ cid: dummyDataTestConfig.conversations.group.nativeId, mids: [messageId] }) + samaClientB.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_G_CONV_ID) - assert.equal(status.from, userANativeId) - + assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + done() } }) }) }) }) - + describe("Activity on disconnect", () => { it("user A logout", (done) => { samaClientA.disconnect() - + samaClientB.onUserActivityListener = (activity) => { - assert.ok(activity[userANativeId] > 0) + assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] > 0) done() } }) - + it("disconnect B", async () => { samaClientB.disconnect() await setTimeoutPromise(200) }) }) -}) \ No newline at end of file +}) diff --git a/test/cluster/fixtures.js b/test/cluster/fixtures.js index 0e3be2b6..fa450d78 100644 --- a/test/cluster/fixtures.js +++ b/test/cluster/fixtures.js @@ -1,9 +1,18 @@ import killPort from "kill-port" -import { killNodeA, killNodeB } from "./utils.js" +import { + NODE_1_HTTP_ENDPOINT, + NODE_1_WS_ENDPOINT, + startOrAccessNodeA, + createDummyData, + dummyDataTestConfig, + killNodeA, + killNodeB, +} from "./utils.js" -const killPortWrap = (port, method) => killPort(port, method) - .then(() => console.log('[Port]', port, 'killed')) - .catch(error => console.log('[Port]', port, error.message)) +const killPortWrap = (port, method) => + killPort(port, method) + .then(() => console.log("[Port]", port, "killed")) + .catch((error) => console.log("[Port]", port, error.message)) const killPorts = async () => { await killPortWrap(9001) @@ -15,8 +24,13 @@ const killPorts = async () => { export async function mochaGlobalSetup() { console.log("[Global][Start]") - + await killPorts() + + await startOrAccessNodeA() + await createDummyData(NODE_1_WS_ENDPOINT, NODE_1_HTTP_ENDPOINT) + + console.log("[Dummy Date Config]", dummyDataTestConfig) } export async function mochaGlobalTeardown() { @@ -26,4 +40,4 @@ export async function mochaGlobalTeardown() { killNodeB() await killPorts() -} \ No newline at end of file +} diff --git a/test/cluster/multi-devices.spec.js b/test/cluster/multi-devices.spec.js index 01a1eb5e..a18fff04 100644 --- a/test/cluster/multi-devices.spec.js +++ b/test/cluster/multi-devices.spec.js @@ -5,11 +5,14 @@ import { v4 } from "uuid" import { SAMAClient } from "@sama-communications/sdk" import { - startOrAccessNodeA, startOrAccessNodeB, - TEST_ORG_ID, TEST_U_CONV_ID, TEST_G_CONV_ID, - NODE_1_WS_ENDPOINT, NODE_1_HTTP_ENDPOINT, - NODE_2_WS_ENDPOINT, NODE_2_HTTP_ENDPOINT, - userANativeId, userBNativeId + startOrAccessNodeA, + startOrAccessNodeB, + NODE_1_WS_ENDPOINT, + NODE_1_HTTP_ENDPOINT, + NODE_2_WS_ENDPOINT, + NODE_2_HTTP_ENDPOINT, + dummyDataTestConfig, + userPassword, } from "./utils.js" const configNodeA = { @@ -17,7 +20,6 @@ const configNodeA = { ws: NODE_1_WS_ENDPOINT, http: NODE_1_HTTP_ENDPOINT, }, - organization_id: TEST_ORG_ID, disableAutoReconnect: true, } @@ -26,16 +28,12 @@ const configNodeB = { ws: NODE_2_WS_ENDPOINT, http: NODE_2_HTTP_ENDPOINT, }, - organization_id: TEST_ORG_ID, disableAutoReconnect: true, } -const samaClientA_Device1 = new SAMAClient(configNodeA) -samaClientA_Device1.deviceId = v4() -const samaClientB_Device1 = new SAMAClient(configNodeB) -samaClientB_Device1.deviceId = v4() -const samaClientB_Device2 = new SAMAClient(configNodeB) -samaClientB_Device2.deviceId = v4() +let samaClientA_Device1 = void 0 +let samaClientB_Device1 = void 0 +let samaClientB_Device2 = void 0 let userAToken = void 0 let userBToken = void 0 @@ -45,69 +43,86 @@ let nodeB = void 0 describe("Cross-node behavior", () => { before(async () => { - console.log('[Before][cross-node-multi-devices]') - + console.log("[Before][cross-node-multi-devices]") + + configNodeA.organization_id = dummyDataTestConfig.organizationId + configNodeB.organization_id = dummyDataTestConfig.organizationId + + samaClientA_Device1 = new SAMAClient(configNodeA) + samaClientA_Device1.deviceId = v4() + samaClientB_Device1 = new SAMAClient(configNodeB) + samaClientB_Device1.deviceId = v4() + samaClientB_Device2 = new SAMAClient(configNodeB) + samaClientB_Device2.deviceId = v4() + nodeA = await startOrAccessNodeA() nodeB = await startOrAccessNodeB() await setTimeoutPromise(500) }) - + describe("Node-A (U_A_D1) - Node-B (U_B_D1,U_B_D2)", () => { describe("Connect", () => { it("connect U_A_D1", async () => { await samaClientA_Device1.connect() - const { token } = await samaClientA_Device1.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' } }) + const { token } = await samaClientA_Device1.socketLogin({ + user: { login: dummyDataTestConfig.users.user1.login, password: userPassword }, + }) userAToken = token }) - - it ("subscribe user B last activity", async () => { - const activity = await samaClientA_Device1.subscribeToUserActivity(userBNativeId) - assert.ok(activity[userBNativeId] > 0) + + it("subscribe user B last activity", async () => { + const activity = await samaClientA_Device1.subscribeToUserActivity(dummyDataTestConfig.users.user2.nativeId) + assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) }) - + it("connect U_B_D1 and check last activity", (done) => { samaClientB_Device1 .connect() - .then(() => samaClientB_Device1.socketLogin({ - user: { login: 'banshiAnton2', password: '12345678' }, - })).then(({ token }) => userBToken = token) + .then(() => + samaClientB_Device1.socketLogin({ + user: { login: dummyDataTestConfig.users.user2.login, password: userPassword }, + }) + ) + .then(({ token }) => (userBToken = token)) samaClientA_Device1.onUserActivityListener = (activity) => { - assert.equal(activity[userBNativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) done() } }) it("connect U_B_D2 and check last activity", (done) => { - samaClientB_Device2.connect().then(() => samaClientB_Device2.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' } })) + samaClientB_Device2 + .connect() + .then(() => samaClientB_Device2.socketLogin({ user: { login: dummyDataTestConfig.users.user2.login, password: userPassword } })) samaClientA_Device1.onUserActivityListener = (activity) => { - assert.equal(activity[userBNativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) done() } }) - + it("subscribe user A last activity", async () => { - let activity = await samaClientB_Device1.subscribeToUserActivity(userANativeId) - assert.ok(activity[userANativeId] === 0) + let activity = await samaClientB_Device1.subscribeToUserActivity(dummyDataTestConfig.users.user1.nativeId) + assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] === 0) - activity = await samaClientB_Device2.subscribeToUserActivity(userANativeId) - assert.ok(activity[userANativeId] === 0) + activity = await samaClientB_Device2.subscribeToUserActivity(dummyDataTestConfig.users.user1.nativeId) + assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] === 0) }) }) - + describe("Base messaging", () => { describe("System", () => { describe("U_A_D1 -> U_B_D1,U_B_D2", async () => { it("event", async () => { const mId = v4() - samaClientA_Device1.messageSystem({ mid: mId, uids: [userBNativeId], x: { one: '1' } }) + samaClientA_Device1.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.user2.nativeId], x: { one: "1" } }) const uA_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userANativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + resolve() } }) @@ -115,8 +130,8 @@ describe("Cross-node behavior", () => { const uA_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userANativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + resolve() } }) @@ -124,29 +139,33 @@ describe("Cross-node behavior", () => { await Promise.all([uA_D1_promise, uA_D2_promise]) }) }) - + describe("U_B_D1 sender", () => { it("event to U_A_D1", (done) => { const mId = v4() - samaClientB_Device1.messageSystem({ mid: mId, uids: [userANativeId], x: { two: '2' } }) - + samaClientB_Device1.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.user1.nativeId], x: { two: "2" } }) + samaClientA_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userBNativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + done() } }) it("event to U_A_D1,U_B_D2", async () => { const mId = v4() - samaClientB_Device1.messageSystem({ mid: mId, uids: [userANativeId, userBNativeId], x: { two: '22' } }) - + samaClientB_Device1.messageSystem({ + mid: mId, + uids: [dummyDataTestConfig.users.user1.nativeId, dummyDataTestConfig.users.user2.nativeId], + x: { two: "22" }, + }) + const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userBNativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + resolve() } }) @@ -154,8 +173,8 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userBNativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + resolve() } }) @@ -167,13 +186,13 @@ describe("Cross-node behavior", () => { describe("U_B_D2 sender", () => { it("event to conversation", async () => { const mId = v4() - samaClientB_Device2.messageSystem({ mid: mId, cid: TEST_U_CONV_ID, x: { four: '44' } }) - + samaClientB_Device2.messageSystem({ mid: mId, cid: dummyDataTestConfig.conversations.private.nativeId, x: { four: "44" } }) + const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userBNativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + resolve() } }) @@ -181,8 +200,8 @@ describe("Cross-node behavior", () => { const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userBNativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + resolve() } }) @@ -191,47 +210,52 @@ describe("Cross-node behavior", () => { }) }) }) - + describe("Private", () => { describe("U_A_D1 -> U_B_D1,U_B_D2", () => { let messageId = void 0 - + it("typing", async () => { - samaClientA_Device1.sendTypingStatus({ cid: TEST_U_CONV_ID, status: 'start' }) - + samaClientA_Device1.sendTypingStatus({ cid: dummyDataTestConfig.conversations.private.nativeId, status: "start" }) + const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_U_CONV_ID) - assert.equal(typing.from, userANativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) resolve() } }) const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_U_CONV_ID) - assert.equal(typing.from, userANativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) resolve() } }) await Promise.all([uB_D1_promise, uB_D2_promise]) }) - + it("simple message", async () => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientA_Device1.messageCreate({ cid: TEST_U_CONV_ID, body: body, mid: mId, x: { two: '2' } }) + samaClientA_Device1.messageCreate({ + cid: dummyDataTestConfig.conversations.private.nativeId, + body: body, + mid: mId, + x: { two: "2" }, + }) const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_U_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['two'], '2') - + assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.x["two"], "2") + messageId = message._id - + resolve() } }) @@ -239,10 +263,10 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_U_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['two'], '2') - + assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.x["two"], "2") + messageId = message._id resolve() @@ -251,59 +275,64 @@ describe("Cross-node behavior", () => { await Promise.all([uB_D1_promise, uB_D2_promise]) }) - + it("read status", (done) => { - samaClientB_Device2.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) - + samaClientB_Device2.markConversationAsRead({ cid: dummyDataTestConfig.conversations.private.nativeId, mids: [messageId] }) + samaClientA_Device1.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_U_CONV_ID) - assert.equal(status.from, userBNativeId) - + assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user2.nativeId) + done() } }) }) - + describe("U_B_D1 -> U_A_D1,U_B_D2", () => { let messageId = void 0 - + it("typing", async () => { - samaClientB_Device1.sendTypingStatus({ cid: TEST_U_CONV_ID, status: 'start' }) + samaClientB_Device1.sendTypingStatus({ cid: dummyDataTestConfig.conversations.private.nativeId, status: "start" }) const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_U_CONV_ID) - assert.equal(typing.from, userBNativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) resolve() } }) const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_U_CONV_ID) - assert.equal(typing.from, userBNativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) resolve() } }) await Promise.all([uA_D1_promise, uB_D2_promise]) }) - + it("simple message", async () => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientB_Device1.messageCreate({ cid: TEST_U_CONV_ID, body: body, mid: mId, x: { two: '22' } }) - + samaClientB_Device1.messageCreate({ + cid: dummyDataTestConfig.conversations.private.nativeId, + body: body, + mid: mId, + x: { two: "22" }, + }) + const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_U_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['two'], '22') - + assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.x["two"], "22") + messageId = message._id - + resolve() } }) @@ -311,10 +340,10 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_U_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['two'], '22') - + assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.x["two"], "22") + messageId = message._id resolve() @@ -323,16 +352,16 @@ describe("Cross-node behavior", () => { await Promise.all([uA_D1_promise, uB_D2_promise]) }) - + it("read status", async () => { - samaClientA_Device1.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) + samaClientA_Device1.markConversationAsRead({ cid: dummyDataTestConfig.conversations.private.nativeId, mids: [messageId] }) const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_U_CONV_ID) - assert.equal(status.from, userANativeId) - + assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + resolve() } }) @@ -340,8 +369,8 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_U_CONV_ID) - assert.equal(status.from, userANativeId) + assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) resolve() } @@ -351,47 +380,52 @@ describe("Cross-node behavior", () => { }) }) }) - + describe("Group", () => { describe("U_A_D1 -> U_B_D1,U_B_D2", () => { let messageId = void 0 - + it("typing", async () => { - samaClientA_Device1.sendTypingStatus({ cid: TEST_G_CONV_ID, status: 'start' }) - + samaClientA_Device1.sendTypingStatus({ cid: dummyDataTestConfig.conversations.group.nativeId, status: "start" }) + const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_G_CONV_ID) - assert.equal(typing.from, userANativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) resolve() } }) const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_G_CONV_ID) - assert.equal(typing.from, userANativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) resolve() } }) await Promise.all([uB_D1_promise, uB_D2_promise]) }) - + it("simple message", async () => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientA_Device1.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '2' } }) + samaClientA_Device1.messageCreate({ + cid: dummyDataTestConfig.conversations.group.nativeId, + body: body, + mid: mId, + x: { two: "2" }, + }) const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['two'], '2') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.x["two"], "2") + messageId = message._id - + resolve() } }) @@ -399,10 +433,10 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['two'], '2') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.x["two"], "2") + messageId = message._id resolve() @@ -411,59 +445,64 @@ describe("Cross-node behavior", () => { await Promise.all([uB_D1_promise, uB_D2_promise]) }) - + it("read status", (done) => { - samaClientB_Device2.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) - + samaClientB_Device2.markConversationAsRead({ cid: dummyDataTestConfig.conversations.group.nativeId, mids: [messageId] }) + samaClientA_Device1.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_G_CONV_ID) - assert.equal(status.from, userBNativeId) - + assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user2.nativeId) + done() } }) }) - + describe("U_B_D1 -> U_A_D1,U_B_D2", () => { let messageId = void 0 - + it("typing", async () => { - samaClientB_Device1.sendTypingStatus({ cid: TEST_G_CONV_ID, status: 'start' }) + samaClientB_Device1.sendTypingStatus({ cid: dummyDataTestConfig.conversations.group.nativeId, status: "start" }) const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_G_CONV_ID) - assert.equal(typing.from, userBNativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) resolve() } }) const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_G_CONV_ID) - assert.equal(typing.from, userBNativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) resolve() } }) await Promise.all([uA_D1_promise, uB_D2_promise]) }) - + it("simple message", async () => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientB_Device1.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '22' } }) - + samaClientB_Device1.messageCreate({ + cid: dummyDataTestConfig.conversations.group.nativeId, + body: body, + mid: mId, + x: { two: "22" }, + }) + const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['two'], '22') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.x["two"], "22") + messageId = message._id - + resolve() } }) @@ -471,10 +510,10 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['two'], '22') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.x["two"], "22") + messageId = message._id resolve() @@ -483,16 +522,16 @@ describe("Cross-node behavior", () => { await Promise.all([uA_D1_promise, uB_D2_promise]) }) - + it("read status", async () => { - samaClientA_Device1.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) + samaClientA_Device1.markConversationAsRead({ cid: dummyDataTestConfig.conversations.group.nativeId, mids: [messageId] }) const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_G_CONV_ID) - assert.equal(status.from, userANativeId) - + assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + resolve() } }) @@ -500,8 +539,8 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_G_CONV_ID) - assert.equal(status.from, userANativeId) + assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) resolve() } @@ -512,7 +551,7 @@ describe("Cross-node behavior", () => { }) }) }) - + describe("Activity on disconnect", () => { it("U_B_D1 logout (not last activity)", async () => { samaClientB_Device1.disconnect() @@ -521,7 +560,7 @@ describe("Cross-node behavior", () => { setTimeout(() => resolve(), 500) samaClientA_Device1.onUserActivityListener = (activity) => { - if (activity[userBNativeId] > 0) { + if (activity[dummyDataTestConfig.users.user2.nativeId] > 0) { reject(new Error("Should not be called")) } } @@ -534,11 +573,11 @@ describe("Cross-node behavior", () => { samaClientB_Device2.disconnect() samaClientA_Device1.onUserActivityListener = (activity) => { - assert.ok(activity[userBNativeId] > 0) + assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) done() } }) - + it("disconnect A", async () => { samaClientA_Device1.disconnect() await setTimeoutPromise(200) @@ -550,57 +589,64 @@ describe("Cross-node behavior", () => { describe("Connect", () => { it("connect U_A_D1", async () => { await samaClientA_Device1.connect() - const { token } = await samaClientA_Device1.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' } }) + const { token } = await samaClientA_Device1.socketLogin({ + user: { login: dummyDataTestConfig.users.user1.login, password: userPassword }, + }) userAToken = token }) - - it ("subscribe user B last activity", async () => { - const activity = await samaClientA_Device1.subscribeToUserActivity(userBNativeId) - assert.ok(activity[userBNativeId] > 0) + + it("subscribe user B last activity", async () => { + const activity = await samaClientA_Device1.subscribeToUserActivity(dummyDataTestConfig.users.user2.nativeId) + assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) }) - + it("connect U_B_D1 and check last activity", (done) => { samaClientB_Device1 .connect() - .then(() => samaClientB_Device1.socketLogin({ - user: { login: 'banshiAnton2', password: '12345678' }, - })).then(({ token }) => userBToken = token) + .then(() => + samaClientB_Device1.socketLogin({ + user: { login: dummyDataTestConfig.users.user2.login, password: userPassword }, + }) + ) + .then(({ token }) => (userBToken = token)) samaClientA_Device1.onUserActivityListener = (activity) => { - assert.equal(activity[userBNativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) done() } }) it("connect U_B_D2 and check last activity", (done) => { - samaClientB_Device2.connect(NODE_1_WS_ENDPOINT, NODE_1_HTTP_ENDPOINT).then(() => samaClientB_Device2.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' } })) + samaClientB_Device2 + .connect(NODE_1_WS_ENDPOINT, NODE_1_HTTP_ENDPOINT) + .then(() => samaClientB_Device2.socketLogin({ user: { login: dummyDataTestConfig.users.user2.login, password: userPassword } })) samaClientA_Device1.onUserActivityListener = (activity) => { - assert.equal(activity[userBNativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) done() } }) - + it("subscribe user A last activity", async () => { - let activity = await samaClientB_Device1.subscribeToUserActivity(userANativeId) - assert.ok(activity[userANativeId] === 0) + let activity = await samaClientB_Device1.subscribeToUserActivity(dummyDataTestConfig.users.user1.nativeId) + assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] === 0) - activity = await samaClientB_Device2.subscribeToUserActivity(userANativeId) - assert.ok(activity[userANativeId] === 0) + activity = await samaClientB_Device2.subscribeToUserActivity(dummyDataTestConfig.users.user1.nativeId) + assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] === 0) }) }) - + describe("Base messaging", () => { describe("System", () => { describe("U_A_D1 -> U_B_D1,U_B_D2", async () => { it("event", async () => { const mId = v4() - samaClientA_Device1.messageSystem({ mid: mId, uids: [userBNativeId], x: { one: '1' } }) + samaClientA_Device1.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.user2.nativeId], x: { one: "1" } }) const uA_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userANativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + resolve() } }) @@ -608,8 +654,8 @@ describe("Cross-node behavior", () => { const uA_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userANativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + resolve() } }) @@ -617,29 +663,33 @@ describe("Cross-node behavior", () => { await Promise.all([uA_D1_promise, uA_D2_promise]) }) }) - + describe("U_B_D1 sender", () => { it("event to U_A_D1", (done) => { const mId = v4() - samaClientB_Device1.messageSystem({ mid: mId, uids: [userANativeId], x: { two: '2' } }) - + samaClientB_Device1.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.user1.nativeId], x: { two: "2" } }) + samaClientA_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userBNativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + done() } }) it("event to U_A_D1,U_B_D2", async () => { const mId = v4() - samaClientB_Device1.messageSystem({ mid: mId, uids: [userANativeId, userBNativeId], x: { two: '22' } }) - + samaClientB_Device1.messageSystem({ + mid: mId, + uids: [dummyDataTestConfig.users.user1.nativeId, dummyDataTestConfig.users.user2.nativeId], + x: { two: "22" }, + }) + const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userBNativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + resolve() } }) @@ -647,8 +697,8 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userBNativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + resolve() } }) @@ -660,13 +710,13 @@ describe("Cross-node behavior", () => { describe("U_B_D2 sender", () => { it("event to conversation", async () => { const mId = v4() - samaClientB_Device2.messageSystem({ mid: mId, cid: TEST_U_CONV_ID, x: { four: '44' } }) - + samaClientB_Device2.messageSystem({ mid: mId, cid: dummyDataTestConfig.conversations.private.nativeId, x: { four: "44" } }) + const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userBNativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + resolve() } }) @@ -674,8 +724,8 @@ describe("Cross-node behavior", () => { const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userBNativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + resolve() } }) @@ -684,47 +734,52 @@ describe("Cross-node behavior", () => { }) }) }) - + describe("Private", () => { describe("U_A_D1 -> U_B_D1,U_B_D2", () => { let messageId = void 0 - + it("typing", async () => { - samaClientA_Device1.sendTypingStatus({ cid: TEST_U_CONV_ID, status: 'start' }) - + samaClientA_Device1.sendTypingStatus({ cid: dummyDataTestConfig.conversations.private.nativeId, status: "start" }) + const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_U_CONV_ID) - assert.equal(typing.from, userANativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) resolve() } }) const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_U_CONV_ID) - assert.equal(typing.from, userANativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) resolve() } }) await Promise.all([uB_D1_promise, uB_D2_promise]) }) - + it("simple message", async () => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientA_Device1.messageCreate({ cid: TEST_U_CONV_ID, body: body, mid: mId, x: { two: '2' } }) + samaClientA_Device1.messageCreate({ + cid: dummyDataTestConfig.conversations.private.nativeId, + body: body, + mid: mId, + x: { two: "2" }, + }) const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_U_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['two'], '2') - + assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.x["two"], "2") + messageId = message._id - + resolve() } }) @@ -732,10 +787,10 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_U_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['two'], '2') - + assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.x["two"], "2") + messageId = message._id resolve() @@ -744,59 +799,64 @@ describe("Cross-node behavior", () => { await Promise.all([uB_D1_promise, uB_D2_promise]) }) - + it("read status", (done) => { - samaClientB_Device2.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) - + samaClientB_Device2.markConversationAsRead({ cid: dummyDataTestConfig.conversations.private.nativeId, mids: [messageId] }) + samaClientA_Device1.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_U_CONV_ID) - assert.equal(status.from, userBNativeId) - + assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user2.nativeId) + done() } }) }) - + describe("U_B_D1 -> U_A_D1,U_B_D2", () => { let messageId = void 0 - + it("typing", async () => { - samaClientB_Device1.sendTypingStatus({ cid: TEST_U_CONV_ID, status: 'start' }) + samaClientB_Device1.sendTypingStatus({ cid: dummyDataTestConfig.conversations.private.nativeId, status: "start" }) const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_U_CONV_ID) - assert.equal(typing.from, userBNativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) resolve() } }) const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_U_CONV_ID) - assert.equal(typing.from, userBNativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) resolve() } }) await Promise.all([uA_D1_promise, uB_D2_promise]) }) - + it("simple message", async () => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientB_Device1.messageCreate({ cid: TEST_U_CONV_ID, body: body, mid: mId, x: { two: '22' } }) - + samaClientB_Device1.messageCreate({ + cid: dummyDataTestConfig.conversations.private.nativeId, + body: body, + mid: mId, + x: { two: "22" }, + }) + const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_U_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['two'], '22') - + assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.x["two"], "22") + messageId = message._id - + resolve() } }) @@ -804,10 +864,10 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_U_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['two'], '22') - + assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.x["two"], "22") + messageId = message._id resolve() @@ -816,16 +876,16 @@ describe("Cross-node behavior", () => { await Promise.all([uA_D1_promise, uB_D2_promise]) }) - + it("read status", async () => { - samaClientA_Device1.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) + samaClientA_Device1.markConversationAsRead({ cid: dummyDataTestConfig.conversations.private.nativeId, mids: [messageId] }) const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_U_CONV_ID) - assert.equal(status.from, userANativeId) - + assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + resolve() } }) @@ -833,8 +893,8 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_U_CONV_ID) - assert.equal(status.from, userANativeId) + assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) resolve() } @@ -844,47 +904,52 @@ describe("Cross-node behavior", () => { }) }) }) - + describe("Group", () => { describe("U_A_D1 -> U_B_D1,U_B_D2", () => { let messageId = void 0 - + it("typing", async () => { - samaClientA_Device1.sendTypingStatus({ cid: TEST_G_CONV_ID, status: 'start' }) - + samaClientA_Device1.sendTypingStatus({ cid: dummyDataTestConfig.conversations.group.nativeId, status: "start" }) + const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_G_CONV_ID) - assert.equal(typing.from, userANativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) resolve() } }) const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_G_CONV_ID) - assert.equal(typing.from, userANativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) resolve() } }) await Promise.all([uB_D1_promise, uB_D2_promise]) }) - + it("simple message", async () => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientA_Device1.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '2' } }) + samaClientA_Device1.messageCreate({ + cid: dummyDataTestConfig.conversations.group.nativeId, + body: body, + mid: mId, + x: { two: "2" }, + }) const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['two'], '2') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.x["two"], "2") + messageId = message._id - + resolve() } }) @@ -892,10 +957,10 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['two'], '2') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.x["two"], "2") + messageId = message._id resolve() @@ -904,59 +969,64 @@ describe("Cross-node behavior", () => { await Promise.all([uB_D1_promise, uB_D2_promise]) }) - + it("read status", (done) => { - samaClientB_Device2.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) - + samaClientB_Device2.markConversationAsRead({ cid: dummyDataTestConfig.conversations.group.nativeId, mids: [messageId] }) + samaClientA_Device1.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_G_CONV_ID) - assert.equal(status.from, userBNativeId) - + assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user2.nativeId) + done() } }) }) - + describe("U_B_D1 -> U_A_D1,U_B_D2", () => { let messageId = void 0 - + it("typing", async () => { - samaClientB_Device1.sendTypingStatus({ cid: TEST_G_CONV_ID, status: 'start' }) + samaClientB_Device1.sendTypingStatus({ cid: dummyDataTestConfig.conversations.group.nativeId, status: "start" }) const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_G_CONV_ID) - assert.equal(typing.from, userBNativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) resolve() } }) const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_G_CONV_ID) - assert.equal(typing.from, userBNativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) resolve() } }) await Promise.all([uA_D1_promise, uB_D2_promise]) }) - + it("simple message", async () => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientB_Device1.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '22' } }) - + samaClientB_Device1.messageCreate({ + cid: dummyDataTestConfig.conversations.group.nativeId, + body: body, + mid: mId, + x: { two: "22" }, + }) + const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['two'], '22') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.x["two"], "22") + messageId = message._id - + resolve() } }) @@ -964,10 +1034,10 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['two'], '22') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.x["two"], "22") + messageId = message._id resolve() @@ -976,16 +1046,16 @@ describe("Cross-node behavior", () => { await Promise.all([uA_D1_promise, uB_D2_promise]) }) - + it("read status", async () => { - samaClientA_Device1.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) + samaClientA_Device1.markConversationAsRead({ cid: dummyDataTestConfig.conversations.group.nativeId, mids: [messageId] }) const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_G_CONV_ID) - assert.equal(status.from, userANativeId) - + assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + resolve() } }) @@ -993,8 +1063,8 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_G_CONV_ID) - assert.equal(status.from, userANativeId) + assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) resolve() } @@ -1005,7 +1075,7 @@ describe("Cross-node behavior", () => { }) }) }) - + describe("Activity on disconnect", () => { it("U_B_D1 logout (not last activity)", async () => { samaClientB_Device1.disconnect() @@ -1014,7 +1084,7 @@ describe("Cross-node behavior", () => { setTimeout(() => resolve(), 500) samaClientA_Device1.onUserActivityListener = (activity) => { - if (activity[userBNativeId] > 0) { + if (activity[dummyDataTestConfig.users.user2.nativeId] > 0) { reject(new Error("Should not be called")) } } @@ -1027,15 +1097,15 @@ describe("Cross-node behavior", () => { samaClientB_Device2.disconnect() samaClientA_Device1.onUserActivityListener = (activity) => { - assert.ok(activity[userBNativeId] > 0) + assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) done() } }) - + it("disconnect A", async () => { samaClientA_Device1.disconnect() await setTimeoutPromise(200) }) }) }) -}) \ No newline at end of file +}) diff --git a/test/cluster/node-crash.spec.js b/test/cluster/node-crash.spec.js index 8b7327a4..1dac6b63 100644 --- a/test/cluster/node-crash.spec.js +++ b/test/cluster/node-crash.spec.js @@ -5,11 +5,14 @@ import { v4 } from "uuid" import { SAMAClient } from "@sama-communications/sdk" import { - startOrAccessNodeA, startOrAccessNodeB, - TEST_ORG_ID, TEST_U_CONV_ID, TEST_G_CONV_ID, - NODE_1_WS_ENDPOINT, NODE_1_HTTP_ENDPOINT, - NODE_2_WS_ENDPOINT, NODE_2_HTTP_ENDPOINT, - userANativeId, userBNativeId + startOrAccessNodeA, + startOrAccessNodeB, + NODE_1_WS_ENDPOINT, + NODE_1_HTTP_ENDPOINT, + NODE_2_WS_ENDPOINT, + NODE_2_HTTP_ENDPOINT, + dummyDataTestConfig, + userPassword, } from "./utils.js" const configNodeA = { @@ -17,7 +20,6 @@ const configNodeA = { ws: NODE_1_WS_ENDPOINT, http: NODE_1_HTTP_ENDPOINT, }, - organization_id: TEST_ORG_ID, disableAutoReconnect: true, } @@ -26,16 +28,12 @@ const configNodeB = { ws: NODE_2_WS_ENDPOINT, http: NODE_2_HTTP_ENDPOINT, }, - organization_id: TEST_ORG_ID, disableAutoReconnect: true, } -const samaClientA = new SAMAClient(configNodeA) -samaClientA.deviceId = v4() -const samaClientB = new SAMAClient(configNodeB) -samaClientB.deviceId = v4() -const samaClientB_Device2 = new SAMAClient(configNodeA) -samaClientB_Device2.deviceId = v4() +let samaClientA = void 0 +let samaClientB = void 0 +let samaClientB_Device2 = void 0 let userAToken = void 0 let userBToken = void 0 @@ -45,8 +43,18 @@ let nodeB = void 0 describe("Crash Node behavior:", () => { before(async () => { - console.log('[Before][node-crash]') - + console.log("[Before][node-crash]") + + configNodeA.organization_id = dummyDataTestConfig.organizationId + configNodeB.organization_id = dummyDataTestConfig.organizationId + + samaClientA = new SAMAClient(configNodeA) + samaClientA.deviceId = v4() + samaClientB = new SAMAClient(configNodeB) + samaClientB.deviceId = v4() + samaClientB_Device2 = new SAMAClient(configNodeA) + samaClientB_Device2.deviceId = v4() + nodeA = await startOrAccessNodeA() nodeB = await startOrAccessNodeB() @@ -56,24 +64,27 @@ describe("Crash Node behavior:", () => { describe("NodeA(U_A) - NodeB(U_B) - NodeB crash (wait fro clean):", () => { it("connect client A", async () => { await samaClientA.connect() - const { token } = await samaClientA.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' } }) + const { token } = await samaClientA.socketLogin({ user: { login: dummyDataTestConfig.users.user1.login, password: userPassword } }) userAToken = token }) - - it ("subscribe user B last activity", async () => { - const activity = await samaClientA.subscribeToUserActivity(userBNativeId) - assert.ok(activity[userBNativeId] > 0) + + it("subscribe user B last activity", async () => { + const activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.user2.nativeId) + assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) }) - + it("connect client B and check last activity", (done) => { samaClientB .connect() - .then(() => samaClientB.socketLogin({ - user: { login: 'banshiAnton2', password: '12345678' }, - })).then(({ token }) => userBToken = token) + .then(() => + samaClientB.socketLogin({ + user: { login: dummyDataTestConfig.users.user2.login, password: userPassword }, + }) + ) + .then(({ token }) => (userBToken = token)) samaClientA.onUserActivityListener = (activity) => { - assert.equal(activity[userBNativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) done() } }) @@ -82,29 +93,29 @@ describe("Crash Node behavior:", () => { it("A -> B", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientA.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '2' } }) - + samaClientA.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { two: "2" } }) + samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['two'], '2') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.x["two"], "2") + done() } }) - + it("messaging B -> A", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientB.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '22' } }) - + samaClientB.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { two: "22" } }) + samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['two'], '22') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.x["two"], "22") + done() } }) @@ -114,38 +125,38 @@ describe("Crash Node behavior:", () => { it("kill NodeB events with U_B last activity from NodeA", async () => { const closePromise = new Promise((resolve, reject) => { nodeA.once(`node_close[${nodeB.clusterEndpoint}]`, () => { - console.log('[node][close]', nodeB.clusterEndpoint) + console.log("[node][close]", nodeB.clusterEndpoint) resolve() }) }) - + const startReconnectPromise = new Promise((resolve, reject) => { nodeA.once(`node_reconnect_start[${nodeB.clusterEndpoint}]`, () => { - console.log('[node][reconnect][start]', nodeB.clusterEndpoint) + console.log("[node][reconnect][start]", nodeB.clusterEndpoint) resolve() }) }) - + const cleanNodeBPromise = new Promise((resolve, reject) => { nodeA.once(`node_clean[${nodeB.clusterEndpoint}]`, () => { - console.log('[node][clean]', nodeB.clusterEndpoint) + console.log("[node][clean]", nodeB.clusterEndpoint) resolve() }) }) - + const userBOfflinePromise = new Promise((resolve, reject) => { samaClientA.onUserActivityListener = async (activity) => { - assert.ok(activity[userBNativeId] > 0) - - activity = await samaClientA.subscribeToUserActivity(userBNativeId) - assert.ok(activity[userBNativeId] > 0) - + assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) + + activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.user2.nativeId) + assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) + resolve() } }) - + nodeB.kill() - + await Promise.all([closePromise, startReconnectPromise, cleanNodeBPromise, userBOfflinePromise]) }) }) @@ -155,16 +166,16 @@ describe("Crash Node behavior:", () => { nodeB = await startOrAccessNodeB(true) const nodeReadyPromise = new Promise((resolve, reject) => { - nodeB.once('node_ready', (nodeClusterWsEndpoint) => { + nodeB.once("node_ready", (nodeClusterWsEndpoint) => { nodeB.clusterEndpoint = nodeClusterWsEndpoint - console.log('[node][ready]', nodeB.pid, nodeB.clusterEndpoint) + console.log("[node][ready]", nodeB.pid, nodeB.clusterEndpoint) resolve() }) }) const nodeHandShackedPromise = new Promise((resolve, reject) => { nodeB.once(`node_connected[${nodeA.clusterEndpoint}]`, (nodeClusterWsEndpoint) => { - console.log('[node][connected]', nodeClusterWsEndpoint) + console.log("[node][connected]", nodeClusterWsEndpoint) resolve() }) }) @@ -173,11 +184,12 @@ describe("Crash Node behavior:", () => { }) it("connect client B and check last activity", (done) => { - samaClientB.connect() + samaClientB + .connect() .then(() => samaClientB.socketLogin({ token: userBToken })) - .then(({ token }) => userBToken = token) + .then(({ token }) => (userBToken = token)) samaClientA.onUserActivityListener = (activity) => { - assert.equal(activity[userBNativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) done() } }) @@ -186,29 +198,29 @@ describe("Crash Node behavior:", () => { it("A -> B", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientA.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '2' } }) - + samaClientA.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { two: "2" } }) + samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['two'], '2') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.x["two"], "2") + done() } }) - + it("B -> A", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientB.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '22' } }) - + samaClientB.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { two: "22" } }) + samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['two'], '22') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.x["two"], "22") + done() } }) @@ -220,26 +232,26 @@ describe("Crash Node behavior:", () => { it("NodeA crash", async () => { const closePromise = new Promise((resolve, reject) => { nodeB.once(`node_close[${nodeA.clusterEndpoint}]`, () => { - console.log('[node][close]', nodeA.clusterEndpoint) + console.log("[node][close]", nodeA.clusterEndpoint) resolve() }) }) const startReconnectPromise = new Promise((resolve, reject) => { nodeB.once(`node_reconnect_start[${nodeA.clusterEndpoint}]`, () => { - console.log('[node][reconnect][start]', nodeA.clusterEndpoint) + console.log("[node][reconnect][start]", nodeA.clusterEndpoint) resolve() }) }) nodeA.kill() - + await Promise.all([closePromise, startReconnectPromise]) }) - it ("check last activity", async () => { - const activity = await samaClientB.subscribeToUserActivity(userANativeId) - assert.ok(activity[userANativeId] === 0) + it("check last activity", async () => { + const activity = await samaClientB.subscribeToUserActivity(dummyDataTestConfig.users.user1.nativeId) + assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] === 0) }) it("restart NodeA", async () => { @@ -247,13 +259,13 @@ describe("Crash Node behavior:", () => { const finishReconnectPromise = new Promise((resolve, reject) => { nodeB.once(`node_reconnect_finish[${nodeAClusterEndpoint}]`, () => { - console.log('[node][reconnect][finish]', nodeAClusterEndpoint) + console.log("[node][reconnect][finish]", nodeAClusterEndpoint) resolve() }) }) const { port } = new URL(nodeAClusterEndpoint) - const nodeEnv = { 'APP_CLUSTER_PORT': `${port}` } + const nodeEnv = { APP_CLUSTER_PORT: `${port}` } nodeA = await startOrAccessNodeA(false, nodeEnv) @@ -264,10 +276,10 @@ describe("Crash Node behavior:", () => { samaClientA .connect() .then(() => samaClientA.socketLogin({ token: userAToken })) - .then(({ token }) => userAToken = token) + .then(({ token }) => (userAToken = token)) samaClientB.onUserActivityListener = (activity) => { - assert.equal(activity[userANativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.user1.nativeId], 0) done() } }) @@ -276,14 +288,14 @@ describe("Crash Node behavior:", () => { it("A -> B", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientA.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '2' } }) - + samaClientA.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { two: "2" } }) + samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['two'], '2') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.x["two"], "2") + done() } }) @@ -291,14 +303,14 @@ describe("Crash Node behavior:", () => { it("B -> A", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientB.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '22' } }) - + samaClientB.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { two: "22" } }) + samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['two'], '22') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.x["two"], "22") + done() } }) @@ -308,70 +320,70 @@ describe("Crash Node behavior:", () => { describe("NodeA(U_A,U_B_D2) - NodeB(U_B_D1) - NodeB Crush check last activity behavior:", () => { describe("Connect User B Device 2", () => { it("subscribe last activity U_B", async () => { - const activity = await samaClientA.subscribeToUserActivity(userBNativeId) - assert.ok(activity[userBNativeId] === 0) + const activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.user2.nativeId) + assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] === 0) }) - + it("connect U_B_D2 to NodeA", (done) => { samaClientB_Device2 .connect() - .then(() => samaClientB_Device2.socketLogin({ user: { login: 'banshiAnton2', password: '12345678' } })) - + .then(() => samaClientB_Device2.socketLogin({ user: { login: dummyDataTestConfig.users.user2.login, password: userPassword } })) + samaClientA.onUserActivityListener = (activity) => { - assert.equal(activity[userBNativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) done() } }) }) describe("kill NodeB check last activity behavior", () => { - it ("kill NodeB wait no last activity", async () => { + it("kill NodeB wait no last activity", async () => { const closePromise = new Promise((resolve, reject) => { nodeA.once(`node_close[${nodeB.clusterEndpoint}]`, () => { - console.log('[node][close]', nodeB.clusterEndpoint) + console.log("[node][close]", nodeB.clusterEndpoint) resolve() }) }) - + const startReconnectPromise = new Promise((resolve, reject) => { nodeA.once(`node_reconnect_start[${nodeB.clusterEndpoint}]`, () => { - console.log('[node][reconnect][start]', nodeB.clusterEndpoint) + console.log("[node][reconnect][start]", nodeB.clusterEndpoint) resolve() }) }) - + const cleanNodeBPromise = new Promise((resolve, reject) => { nodeA.once(`node_clean[${nodeB.clusterEndpoint}]`, () => { - console.log('[node][clean]', nodeB.clusterEndpoint) + console.log("[node][clean]", nodeB.clusterEndpoint) resolve() }) }) - + const userBNoOfflinePromise = new Promise((resolve, reject) => { cleanNodeBPromise.then(() => setTimeoutPromise(1_000)).then(() => resolve()) samaClientA.onUserActivityListener = async (activity) => { - if (activity[userBNativeId] > 0) { + if (activity[dummyDataTestConfig.users.user2.nativeId] > 0) { reject(new Error("Should not be called")) } } }) - + nodeB.kill() - + await Promise.all([closePromise, startReconnectPromise, cleanNodeBPromise, userBNoOfflinePromise]) }) it("check user B activity", async () => { - const activity = await samaClientA.subscribeToUserActivity(userBNativeId) - assert.ok(activity[userBNativeId] === 0) + const activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.user2.nativeId) + assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] === 0) }) it("close last U_B connection on NodeA", (done) => { samaClientB_Device2.disconnect() samaClientA.onUserActivityListener = (activity) => { - assert.ok(activity[userBNativeId] > 0) + assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) done() } }) @@ -387,7 +399,7 @@ describe("Crash Node behavior:", () => { it("connect U_B", (done) => { samaClientB.connect().then(() => samaClientB.socketLogin({ token: userBToken })) samaClientA.onUserActivityListener = (activity) => { - assert.equal(activity[userBNativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) done() } }) @@ -398,14 +410,14 @@ describe("Crash Node behavior:", () => { it("crash NodeA", async () => { const closePromise = new Promise((resolve, reject) => { nodeB.once(`node_close[${nodeA.clusterEndpoint}]`, () => { - console.log('[node][close]', nodeA.clusterEndpoint) + console.log("[node][close]", nodeA.clusterEndpoint) resolve() }) }) const startReconnectPromise = new Promise((resolve, reject) => { nodeB.once(`node_reconnect_start[${nodeA.clusterEndpoint}]`, () => { - console.log('[node][reconnect][start]', nodeA.clusterEndpoint) + console.log("[node][reconnect][start]", nodeA.clusterEndpoint) resolve() }) }) @@ -415,15 +427,15 @@ describe("Crash Node behavior:", () => { await Promise.all([closePromise, startReconnectPromise]) }) - it ("subscribe last activity U_B -> U_A", async () => { - const activity = await samaClientB.subscribeToUserActivity(userANativeId) - assert.ok(activity[userANativeId] === 0) + it("subscribe last activity U_B -> U_A", async () => { + const activity = await samaClientB.subscribeToUserActivity(dummyDataTestConfig.users.user1.nativeId) + assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] === 0) }) it("connect U_A to NodeB", (done) => { samaClientA.connect(NODE_2_WS_ENDPOINT, NODE_2_HTTP_ENDPOINT).then(() => samaClientA.socketLogin({ token: userAToken })) samaClientB.onUserActivityListener = (activity) => { - assert.equal(activity[userANativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.user1.nativeId], 0) done() } }) @@ -432,14 +444,14 @@ describe("Crash Node behavior:", () => { it("A -> B", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientA.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '2' } }) - + samaClientA.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { two: "2" } }) + samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['two'], '2') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.x["two"], "2") + done() } }) @@ -447,17 +459,17 @@ describe("Crash Node behavior:", () => { it("B -> A", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientB.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { two: '22' } }) - + samaClientB.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { two: "22" } }) + samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['two'], '22') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.x["two"], "22") + done() } }) }) }) -}) \ No newline at end of file +}) diff --git a/test/cluster/same-node.spec.js b/test/cluster/same-node.spec.js index 0d8eb038..bbe0d5c2 100644 --- a/test/cluster/same-node.spec.js +++ b/test/cluster/same-node.spec.js @@ -4,26 +4,18 @@ import { setTimeout as setTimeoutPromise } from "node:timers/promises" import { v4 } from "uuid" import { SAMAClient } from "@sama-communications/sdk" -import { - startOrAccessNodeA, - TEST_ORG_ID, TEST_U_CONV_ID, TEST_G_CONV_ID, - NODE_1_WS_ENDPOINT, NODE_1_HTTP_ENDPOINT, - userANativeId, userBNativeId -} from "./utils.js" +import { startOrAccessNodeA, NODE_1_WS_ENDPOINT, NODE_1_HTTP_ENDPOINT, dummyDataTestConfig, userPassword } from "./utils.js" const configNodeA = { endpoint: { ws: NODE_1_WS_ENDPOINT, http: NODE_1_HTTP_ENDPOINT, }, - organization_id: TEST_ORG_ID, disableAutoReconnect: true, } -const samaClientA = new SAMAClient(configNodeA) -samaClientA.deviceId = v4() -const samaClientB = new SAMAClient(configNodeA) -samaClientB.deviceId = v4() +let samaClientA = void 0 +let samaClientB = void 0 let userAToken = void 0 let userBToken = void 0 @@ -32,263 +24,276 @@ let nodeA = void 0 describe("Same-node behavior", () => { before(async () => { - console.log('[Before][same-node]') - + console.log("[Before][same-node]") + + configNodeA.organization_id = dummyDataTestConfig.organizationId + + samaClientA = new SAMAClient(configNodeA) + samaClientA.deviceId = v4() + + samaClientB = new SAMAClient(configNodeA) + samaClientB.deviceId = v4() + + dummyDataTestConfig.conversations.group.nativeId + nodeA = await startOrAccessNodeA() }) - + describe("Connect", () => { it("connect client A", async () => { await samaClientA.connect() - const { token } = await samaClientA.socketLogin({ user: { login: 'banshiAnton1', password: '12345678' } }) + const { token } = await samaClientA.socketLogin({ user: { login: dummyDataTestConfig.users.user1.login, password: userPassword } }) userAToken = token }) - - it ("subscribe user B last activity", async () => { - const activity = await samaClientA.subscribeToUserActivity(userBNativeId) - assert.ok(activity[userBNativeId] > 0) + + it("subscribe user B last activity", async () => { + const activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.user2.nativeId) + assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) }) - + it("connect client B and check last activity", (done) => { samaClientB .connect() - .then(() => samaClientB.socketLogin({ - user: { login: 'banshiAnton2', password: '12345678' }, - })).then(({ token }) => userBToken = token) + .then(() => + samaClientB.socketLogin({ + user: { login: dummyDataTestConfig.users.user2.login, password: userPassword }, + }) + ) + .then(({ token }) => (userBToken = token)) samaClientA.onUserActivityListener = (activity) => { - assert.equal(activity[userBNativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) done() } }) - + it("subscribe user A last activity", async () => { - const activity = await samaClientB.subscribeToUserActivity(userANativeId) - assert.ok(activity[userANativeId] === 0) + const activity = await samaClientB.subscribeToUserActivity(dummyDataTestConfig.users.user1.nativeId) + assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] === 0) }) }) - + describe("Base messaging", () => { describe("System", () => { describe("A -> B", () => { it("event", (done) => { const mId = v4() - samaClientA.messageSystem({ mid: mId, uids: [userBNativeId], x: { one: '1' } }) - + samaClientA.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.user2.nativeId], x: { one: "1" } }) + samaClientB.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userANativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + done() } }) }) - + describe("B -> A", () => { it("event", (done) => { const mId = v4() - samaClientB.messageSystem({ mid: mId, uids: [userANativeId], x: { two: '2' } }) - + samaClientB.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.user1.nativeId], x: { two: "2" } }) + samaClientA.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, userBNativeId) - + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + done() } }) }) }) - + describe("Private", () => { describe("A -> B", () => { let messageId = void 0 - + it("typing", (done) => { - samaClientA.sendTypingStatus({ cid: TEST_U_CONV_ID, status: 'start' }) - + samaClientA.sendTypingStatus({ cid: dummyDataTestConfig.conversations.private.nativeId, status: "start" }) + samaClientB.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_U_CONV_ID) - assert.equal(typing.from, userANativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) done() } }) - + it("simple message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientA.messageCreate({ cid: TEST_U_CONV_ID, body: body, mid: mId, x: { two: '2' } }) - + samaClientA.messageCreate({ cid: dummyDataTestConfig.conversations.private.nativeId, body: body, mid: mId, x: { two: "2" } }) + samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_U_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['two'], '2') - + assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.x["two"], "2") + messageId = message._id - + done() } }) - + it("read status", (done) => { - samaClientB.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) - + samaClientB.markConversationAsRead({ cid: dummyDataTestConfig.conversations.private.nativeId, mids: [messageId] }) + samaClientA.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_U_CONV_ID) - assert.equal(status.from, userBNativeId) - + assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user2.nativeId) + done() } }) }) - + describe("B -> A", () => { let messageId = void 0 - + it("typing", (done) => { - samaClientB.sendTypingStatus({ cid: TEST_U_CONV_ID, status: 'start' }) - + samaClientB.sendTypingStatus({ cid: dummyDataTestConfig.conversations.private.nativeId, status: "start" }) + samaClientA.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_U_CONV_ID) - assert.equal(typing.from, userBNativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) done() } }) - + it("simple message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientB.messageCreate({ cid: TEST_U_CONV_ID, body: body, mid: mId, x: { two: '22' } }) - + samaClientB.messageCreate({ cid: dummyDataTestConfig.conversations.private.nativeId, body: body, mid: mId, x: { two: "22" } }) + samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_U_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['two'], '22') - + assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.x["two"], "22") + messageId = message._id - + done() } }) - + it("read status", (done) => { - samaClientA.markConversationAsRead({ cid: TEST_U_CONV_ID, mids: [messageId] }) - + samaClientA.markConversationAsRead({ cid: dummyDataTestConfig.conversations.private.nativeId, mids: [messageId] }) + samaClientB.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_U_CONV_ID) - assert.equal(status.from, userANativeId) - + assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + done() } }) }) }) - + describe("Group", () => { describe("A -> B", () => { let messageId = void 0 - + it("typing", (done) => { - samaClientA.sendTypingStatus({ cid: TEST_G_CONV_ID, status: 'start' }) - + samaClientA.sendTypingStatus({ cid: dummyDataTestConfig.conversations.group.nativeId, status: "start" }) + samaClientB.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_G_CONV_ID) - assert.equal(typing.from, userANativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) done() } }) - + it("simple message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientA.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { one: '1' } }) - + samaClientA.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { one: "1" } }) + samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userANativeId) - assert.equal(message.x['one'], '1') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.x["one"], "1") + messageId = message._id - + done() } }) - + it("read status", (done) => { - samaClientB.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) - + samaClientB.markConversationAsRead({ cid: dummyDataTestConfig.conversations.group.nativeId, mids: [messageId] }) + samaClientA.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_G_CONV_ID) - assert.equal(status.from, userBNativeId) - + assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user2.nativeId) + done() } }) }) - + describe("B -> A", () => { let messageId = void 0 - + it("typing", (done) => { - samaClientB.sendTypingStatus({ cid: TEST_G_CONV_ID, status: 'start' }) - + samaClientB.sendTypingStatus({ cid: dummyDataTestConfig.conversations.group.nativeId, status: "start" }) + samaClientA.onUserTypingListener = (typing) => { - assert.equal(typing.cid, TEST_G_CONV_ID) - assert.equal(typing.from, userBNativeId) + assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) done() } }) - + it("simple message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` - samaClientB.messageCreate({ cid: TEST_G_CONV_ID, body: body, mid: mId, x: { one: '11' } }) - + samaClientB.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { one: "11" } }) + samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) - assert.equal(message.cid, TEST_G_CONV_ID) - assert.equal(message.from, userBNativeId) - assert.equal(message.x['one'], '11') - + assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.x["one"], "11") + messageId = message._id - + done() } }) - + it("read status", (done) => { - samaClientA.markConversationAsRead({ cid: TEST_G_CONV_ID, mids: [messageId] }) - + samaClientA.markConversationAsRead({ cid: dummyDataTestConfig.conversations.group.nativeId, mids: [messageId] }) + samaClientB.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) - assert.equal(status.cid, TEST_G_CONV_ID) - assert.equal(status.from, userANativeId) - + assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + done() } }) }) }) }) - + describe("Activity on disconnect", () => { it("user A logout", (done) => { samaClientA.disconnect() - + samaClientB.onUserActivityListener = (activity) => { - assert.ok(activity[userANativeId] > 0) + assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] > 0) done() } }) - + it("disconnect B", async () => { samaClientB.disconnect() await setTimeoutPromise(200) }) }) -}) \ No newline at end of file +}) diff --git a/test/cluster/utils.js b/test/cluster/utils.js index 8619e7e0..cfa9824f 100644 --- a/test/cluster/utils.js +++ b/test/cluster/utils.js @@ -1,7 +1,12 @@ import { spawn } from "node:child_process" import { Transform } from "node:stream" +import { setTimeout as setTimeoutPromise } from "node:timers/promises" import process from "node:process" +import { faker } from "@faker-js/faker" +import { SAMAClient } from "@sama-communications/sdk" +import { v4 } from "uuid" + export const RUN_NODE_1_CMD = process.env.RUN_NODE_1_CMD export const RUN_NODE_2_CMD = process.env.RUN_NODE_2_CMD @@ -10,79 +15,88 @@ export const NODE_2_WS_ENDPOINT = process.env.NODE_2_WS_ENDPOINT export const NODE_1_HTTP_ENDPOINT = process.env.NODE_1_HTTP_ENDPOINT export const NODE_2_HTTP_ENDPOINT = process.env.NODE_2_HTTP_ENDPOINT -export const TEST_ORG_ID = process.env.TEST_ORG_ID -export const TEST_U_CONV_ID = process.env.TEST_U_CONV_ID -export const TEST_G_CONV_ID = process.env.TEST_G_CONV_ID - -export const userANativeId = '69cbe9757fc0a44161188215' -export const userBNativeId = '69cbe9a27fc0a44161188218' +export const userPassword = "12345678" +export const dummyDataTestConfig = { + organizationId: void 0, + users: { + user1: { nativeId: void 0, login: void 0, password: userPassword }, + user2: { nativeId: void 0, login: void 0, password: userPassword }, + }, + conversations: { + private: { nativeId: void 0 }, + group: { nativeId: void 0 }, + }, +} let nodeA = void 0 let nodeB = void 0 // ws endpoint reg: /wss?:\/\/wss?:\/\/.+\// -const createPipeStream = (tag, nodeSubprocess) => new (class extends Transform { - _transform(chunk, encoding, cb) { - chunk = chunk.toString() - const withPrefixChunk = `|-${tag}-|${chunk}` - cb(void 0, withPrefixChunk) - - let regResult = chunk.match(/\[Ready\] cluster-ws: (wss?:\/\/.+\/)/) - if (regResult) { - const nodeClusterWsEndpoint = regResult.at(1) - nodeSubprocess.emit('node_ready', nodeClusterWsEndpoint) - } - - regResult = chunk.match(/\[node handshake finished\] (wss?:\/\/.+\/)/) - if (regResult) { - const nodeClusterWsEndpoint = regResult.at(1) - nodeSubprocess.emit(`node_connected[${nodeClusterWsEndpoint}]`, nodeClusterWsEndpoint) - } - - regResult = chunk.match(/\[clean node\] (wss?:\/\/.+\/)/) - if (regResult) { - const nodeClusterWsEndpoint = regResult.at(1) - nodeSubprocess.emit(`node_clean[${nodeClusterWsEndpoint}]`, nodeClusterWsEndpoint) - } - - regResult = chunk.match(/\[Close\]\[(wss?:\/\/.+\/)\] IsWasOpened/) - if (regResult) { - const nodeClusterWsEndpoint = regResult.at(1) - nodeSubprocess.emit(`node_close[${nodeClusterWsEndpoint}]`, nodeClusterWsEndpoint) +const createPipeStream = (tag, nodeSubprocess) => + new (class extends Transform { + _transform(chunk, encoding, cb) { + chunk = chunk.toString() + const withPrefixChunk = `|-${tag}-|${chunk}` + cb(void 0, withPrefixChunk) + + let regResult = chunk.match(/\[Ready\] cluster-ws: (wss?:\/\/.+\/)/) + if (regResult) { + const nodeClusterWsEndpoint = regResult.at(1) + nodeSubprocess.emit("node_ready", nodeClusterWsEndpoint) + } + + regResult = chunk.match(/\[node handshake finished\] (wss?:\/\/.+\/)/) + if (regResult) { + const nodeClusterWsEndpoint = regResult.at(1) + nodeSubprocess.emit(`node_connected[${nodeClusterWsEndpoint}]`, nodeClusterWsEndpoint) + } + + regResult = chunk.match(/\[clean node\] (wss?:\/\/.+\/)/) + if (regResult) { + const nodeClusterWsEndpoint = regResult.at(1) + nodeSubprocess.emit(`node_clean[${nodeClusterWsEndpoint}]`, nodeClusterWsEndpoint) + } + + regResult = chunk.match(/\[Close\]\[(wss?:\/\/.+\/)\] IsWasOpened/) + if (regResult) { + const nodeClusterWsEndpoint = regResult.at(1) + nodeSubprocess.emit(`node_close[${nodeClusterWsEndpoint}]`, nodeClusterWsEndpoint) + } + + regResult = chunk.match(/\[Reconnecting\]\[(wss?:\/\/.+\/)\]\[start\]/) + if (regResult) { + const nodeClusterWsEndpoint = regResult.at(1) + nodeSubprocess.emit(`node_reconnect_start[${nodeClusterWsEndpoint}]`, nodeClusterWsEndpoint) + } + + regResult = chunk.match(/\[Reconnecting\]\[(wss?:\/\/.+\/)\]\[finish\]/) + if (regResult) { + const nodeClusterWsEndpoint = regResult.at(1) + nodeSubprocess.emit(`node_reconnect_finish[${nodeClusterWsEndpoint}]`, nodeClusterWsEndpoint) + } } - - regResult = chunk.match(/\[Reconnecting\]\[(wss?:\/\/.+\/)\]\[start\]/) - if (regResult) { - const nodeClusterWsEndpoint = regResult.at(1) - nodeSubprocess.emit(`node_reconnect_start[${nodeClusterWsEndpoint}]`, nodeClusterWsEndpoint) - } - - regResult = chunk.match(/\[Reconnecting\]\[(wss?:\/\/.+\/)\]\[finish\]/) - if (regResult) { - const nodeClusterWsEndpoint = regResult.at(1) - nodeSubprocess.emit(`node_reconnect_finish[${nodeClusterWsEndpoint}]`, nodeClusterWsEndpoint) - } - } -}) + })() export const spawnNode = async (cmd, env, tag, notWaitReady) => { const nodeSubprocess = spawn(cmd, [], { shell: true, env: env ? { ...process.env, ...env } : void 0, - stdio: ['ignore', 'pipe', 'pipe'] + stdio: ["ignore", "pipe", "pipe"], }) - const nodeSubprocessReadyPromise = notWaitReady ? true : new Promise((resolve, reject) => { - const rejectTimerId = setTimeout(() => reject(new Error('Timeout')), 5_000) - - nodeSubprocess.once('node_ready', (nodeClusterWsEndpoint) => { - nodeSubprocess.clusterEndpoint = nodeClusterWsEndpoint - console.log('[Node][Ready]', tag, nodeSubprocess.pid, nodeSubprocess.clusterEndpoint) - resolve(nodeSubprocess) - clearTimeout(rejectTimerId) - }) - }) + const nodeSubprocessReadyPromise = notWaitReady + ? true + : new Promise((resolve, reject) => { + const rejectTimerId = setTimeout(() => reject(new Error("Timeout")), 5_000) + + nodeSubprocess.once("node_ready", (nodeClusterWsEndpoint) => { + nodeSubprocess.clusterEndpoint = nodeClusterWsEndpoint + console.log("[Node][Ready]", tag, nodeSubprocess.pid, nodeSubprocess.clusterEndpoint) + resolve(nodeSubprocess) + clearTimeout(rejectTimerId) + }) + }) nodeSubprocess.stdout.pipe(createPipeStream(tag, nodeSubprocess)).pipe(process.stdout) nodeSubprocess.stderr.pipe(createPipeStream(tag, nodeSubprocess)).pipe(process.stderr) @@ -93,11 +107,80 @@ export const spawnNode = async (cmd, env, tag, notWaitReady) => { } export const startOrAccessNodeA = async (notWaitReady, env) => { - return (nodeA && !nodeA.killed) ? nodeA : await spawnNode(RUN_NODE_1_CMD, env, 'NODE_A', notWaitReady).then(node => nodeA = node) + return nodeA && !nodeA.killed ? nodeA : await spawnNode(RUN_NODE_1_CMD, env, "NODE_A", notWaitReady).then((node) => (nodeA = node)) } export const startOrAccessNodeB = async (notWaitReady, env) => { - return (nodeB && !nodeB.killed) ? nodeB : await spawnNode(RUN_NODE_2_CMD, env, 'NODE_B', notWaitReady).then(node => nodeB = node) + return nodeB && !nodeB.killed ? nodeB : await spawnNode(RUN_NODE_2_CMD, env, "NODE_B", notWaitReady).then((node) => (nodeB = node)) +} + +export const createOrganizationId = async (endpoint, name) => { + const orgName = `Test-${name ?? faker.company.name()}-${faker.string.alphanumeric(8)}` + const payload = JSON.stringify({ name: orgName }) + + const response = await fetch(`${endpoint}/admin/organization`, { + method: "POST", + headers: { "Content-Type": "application/json", "admin-api-key": process.env.HTTP_ADMIN_API_KEY }, + body: payload, + }).then((response) => response.json()) + + const organizationId = response.organization._id + + return organizationId +} + +export const createDummyData = async (endpointWs, endpointHttp) => { + dummyDataTestConfig.organizationId = await createOrganizationId(endpointHttp) + + const sdkConfig = { + endpoint: { + ws: endpointWs, + http: endpointHttp, + }, + organization_id: dummyDataTestConfig.organizationId, + disableAutoReconnect: true, + } + + const samaSdk = new SAMAClient(sdkConfig) + samaSdk.deviceId = v4() + + await samaSdk.connect() + + dummyDataTestConfig.users.user1.login = `TestUser-${faker.internet.username()}` + const user1 = await samaSdk.userCreate({ + login: dummyDataTestConfig.users.user1.login, + email: faker.internet.email(), + password: userPassword, + }) + dummyDataTestConfig.users.user1.nativeId = user1.native_id + + dummyDataTestConfig.users.user2.login = `TestUser-${faker.internet.username()}` + const user2 = await samaSdk.userCreate({ + login: dummyDataTestConfig.users.user2.login, + email: faker.internet.email(), + password: userPassword, + }) + dummyDataTestConfig.users.user2.nativeId = user2.native_id + + await samaSdk.socketLogin({ user: { login: dummyDataTestConfig.users.user1.login, password: userPassword } }) + + const privateConversation = await samaSdk.conversationCreate({ + name: `Private-${faker.music.songName()}`, + type: "u", + participants: [user1.native_id, user2.native_id], + }) + dummyDataTestConfig.conversations.private.nativeId = privateConversation._id + + const groupConversation = await samaSdk.conversationCreate({ + name: `Group-${faker.music.songName()}`, + type: "g", + participants: [user1.native_id, user2.native_id], + }) + dummyDataTestConfig.conversations.group.nativeId = groupConversation._id + + samaSdk.disconnect() + + await setTimeoutPromise(200) } export const killNodeA = () => nodeA?.kill() From b3f9889f69e8eec1cfb86ee1debb9f7b02ed6188 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Mon, 6 Apr 2026 15:23:35 +0300 Subject: [PATCH 26/70] update spec titles --- test/cluster/cross-node.spec.js | 80 ++++----- test/cluster/multi-devices.spec.js | 262 ++++++++++++++--------------- test/cluster/node-crash.spec.js | 122 +++++++------- test/cluster/same-node.spec.js | 80 ++++----- test/cluster/utils.js | 26 +-- 5 files changed, 285 insertions(+), 285 deletions(-) diff --git a/test/cluster/cross-node.spec.js b/test/cluster/cross-node.spec.js index 70da1b71..905ad340 100644 --- a/test/cluster/cross-node.spec.js +++ b/test/cluster/cross-node.spec.js @@ -59,63 +59,63 @@ describe("Cross-node behavior", () => { }) describe("Connect", () => { - it("connect client A", async () => { + it("connect client userA", async () => { await samaClientA.connect() - const { token } = await samaClientA.socketLogin({ user: { login: dummyDataTestConfig.users.user1.login, password: userPassword } }) + const { token } = await samaClientA.socketLogin({ user: { login: dummyDataTestConfig.users.userA.login, password: userPassword } }) userAToken = token }) - it("subscribe user B last activity", async () => { - const activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.user2.nativeId) - assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) + it("subscribe userB last activity", async () => { + const activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.userB.nativeId) + assert.ok(activity[dummyDataTestConfig.users.userB.nativeId] > 0) }) - it("connect client B and check last activity", (done) => { + it("connect client userB and check last activity", (done) => { samaClientB .connect() .then(() => samaClientB.socketLogin({ - user: { login: dummyDataTestConfig.users.user2.login, password: userPassword }, + user: { login: dummyDataTestConfig.users.userB.login, password: userPassword }, }) ) .then(({ token }) => (userBToken = token)) samaClientA.onUserActivityListener = (activity) => { - assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.userB.nativeId], 0) done() } }) - it("subscribe user A last activity", async () => { - const activity = await samaClientB.subscribeToUserActivity(dummyDataTestConfig.users.user1.nativeId) - assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] === 0) + it("subscribe userA last activity", async () => { + const activity = await samaClientB.subscribeToUserActivity(dummyDataTestConfig.users.userA.nativeId) + assert.ok(activity[dummyDataTestConfig.users.userA.nativeId] === 0) }) }) describe("Base messaging", () => { describe("System", () => { - describe("A -> B", () => { + describe("userA -> userB", () => { it("event", (done) => { const mId = v4() - samaClientA.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.user2.nativeId], x: { one: "1" } }) + samaClientA.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.userB.nativeId], x: { one: "1" } }) samaClientB.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) done() } }) }) - describe("B -> A", () => { + describe("userB -> userA", () => { it("event", (done) => { const mId = v4() - samaClientB.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.user1.nativeId], x: { two: "2" } }) + samaClientB.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.userA.nativeId], x: { two: "2" } }) samaClientA.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) done() } @@ -124,7 +124,7 @@ describe("Cross-node behavior", () => { }) describe("Private", () => { - describe("A -> B", () => { + describe("userA -> userB", () => { let messageId = void 0 it("typing", (done) => { @@ -132,12 +132,12 @@ describe("Cross-node behavior", () => { samaClientB.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userA.nativeId) done() } }) - it("simple message", (done) => { + it("message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientA.messageCreate({ cid: dummyDataTestConfig.conversations.private.nativeId, body: body, mid: mId, x: { two: "2" } }) @@ -145,7 +145,7 @@ describe("Cross-node behavior", () => { samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) assert.equal(message.x["two"], "2") messageId = message._id @@ -160,14 +160,14 @@ describe("Cross-node behavior", () => { samaClientA.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userB.nativeId) done() } }) }) - describe("B -> A", () => { + describe("userB -> userA", () => { let messageId = void 0 it("typing", (done) => { @@ -175,12 +175,12 @@ describe("Cross-node behavior", () => { samaClientA.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userB.nativeId) done() } }) - it("simple message", (done) => { + it("message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientB.messageCreate({ cid: dummyDataTestConfig.conversations.private.nativeId, body: body, mid: mId, x: { two: "22" } }) @@ -188,7 +188,7 @@ describe("Cross-node behavior", () => { samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) assert.equal(message.x["two"], "22") messageId = message._id @@ -203,7 +203,7 @@ describe("Cross-node behavior", () => { samaClientB.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userA.nativeId) done() } @@ -212,7 +212,7 @@ describe("Cross-node behavior", () => { }) describe("Group", () => { - describe("A -> B", () => { + describe("userA -> userB", () => { let messageId = void 0 it("typing", (done) => { @@ -220,12 +220,12 @@ describe("Cross-node behavior", () => { samaClientB.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userA.nativeId) done() } }) - it("simple message", (done) => { + it("message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientA.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { one: "1" } }) @@ -233,7 +233,7 @@ describe("Cross-node behavior", () => { samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) assert.equal(message.x["one"], "1") messageId = message._id @@ -248,14 +248,14 @@ describe("Cross-node behavior", () => { samaClientA.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userB.nativeId) done() } }) }) - describe("B -> A", () => { + describe("userB -> userA", () => { let messageId = void 0 it("typing", (done) => { @@ -263,12 +263,12 @@ describe("Cross-node behavior", () => { samaClientA.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userB.nativeId) done() } }) - it("simple message", (done) => { + it("message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientB.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { one: "11" } }) @@ -276,7 +276,7 @@ describe("Cross-node behavior", () => { samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) assert.equal(message.x["one"], "11") messageId = message._id @@ -291,7 +291,7 @@ describe("Cross-node behavior", () => { samaClientB.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userA.nativeId) done() } @@ -301,16 +301,16 @@ describe("Cross-node behavior", () => { }) describe("Activity on disconnect", () => { - it("user A logout", (done) => { + it("userA logout", (done) => { samaClientA.disconnect() samaClientB.onUserActivityListener = (activity) => { - assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] > 0) + assert.ok(activity[dummyDataTestConfig.users.userA.nativeId] > 0) done() } }) - it("disconnect B", async () => { + it("disconnect userB", async () => { samaClientB.disconnect() await setTimeoutPromise(200) }) diff --git a/test/cluster/multi-devices.spec.js b/test/cluster/multi-devices.spec.js index a18fff04..4cab6be6 100644 --- a/test/cluster/multi-devices.spec.js +++ b/test/cluster/multi-devices.spec.js @@ -61,67 +61,67 @@ describe("Cross-node behavior", () => { await setTimeoutPromise(500) }) - describe("Node-A (U_A_D1) - Node-B (U_B_D1,U_B_D2)", () => { + describe("Node-A(userA_Device1) - Node-B(userB_Device1,userB_Device2)", () => { describe("Connect", () => { - it("connect U_A_D1", async () => { + it("connect userA_Device1", async () => { await samaClientA_Device1.connect() const { token } = await samaClientA_Device1.socketLogin({ - user: { login: dummyDataTestConfig.users.user1.login, password: userPassword }, + user: { login: dummyDataTestConfig.users.userA.login, password: userPassword }, }) userAToken = token }) - it("subscribe user B last activity", async () => { - const activity = await samaClientA_Device1.subscribeToUserActivity(dummyDataTestConfig.users.user2.nativeId) - assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) + it("subscribe userB last activity", async () => { + const activity = await samaClientA_Device1.subscribeToUserActivity(dummyDataTestConfig.users.userB.nativeId) + assert.ok(activity[dummyDataTestConfig.users.userB.nativeId] > 0) }) - it("connect U_B_D1 and check last activity", (done) => { + it("connect userB_Device1 and check last activity", (done) => { samaClientB_Device1 .connect() .then(() => samaClientB_Device1.socketLogin({ - user: { login: dummyDataTestConfig.users.user2.login, password: userPassword }, + user: { login: dummyDataTestConfig.users.userB.login, password: userPassword }, }) ) .then(({ token }) => (userBToken = token)) samaClientA_Device1.onUserActivityListener = (activity) => { - assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.userB.nativeId], 0) done() } }) - it("connect U_B_D2 and check last activity", (done) => { + it("connect userB_Device2 and check last activity", (done) => { samaClientB_Device2 .connect() - .then(() => samaClientB_Device2.socketLogin({ user: { login: dummyDataTestConfig.users.user2.login, password: userPassword } })) + .then(() => samaClientB_Device2.socketLogin({ user: { login: dummyDataTestConfig.users.userB.login, password: userPassword } })) samaClientA_Device1.onUserActivityListener = (activity) => { - assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.userB.nativeId], 0) done() } }) - it("subscribe user A last activity", async () => { - let activity = await samaClientB_Device1.subscribeToUserActivity(dummyDataTestConfig.users.user1.nativeId) - assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] === 0) + it("subscribe userA last activity", async () => { + let activity = await samaClientB_Device1.subscribeToUserActivity(dummyDataTestConfig.users.userA.nativeId) + assert.ok(activity[dummyDataTestConfig.users.userA.nativeId] === 0) - activity = await samaClientB_Device2.subscribeToUserActivity(dummyDataTestConfig.users.user1.nativeId) - assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] === 0) + activity = await samaClientB_Device2.subscribeToUserActivity(dummyDataTestConfig.users.userA.nativeId) + assert.ok(activity[dummyDataTestConfig.users.userA.nativeId] === 0) }) }) describe("Base messaging", () => { describe("System", () => { - describe("U_A_D1 -> U_B_D1,U_B_D2", async () => { + describe("userA_Device1 -> userB_Device1,userB_Device2", async () => { it("event", async () => { const mId = v4() - samaClientA_Device1.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.user2.nativeId], x: { one: "1" } }) + samaClientA_Device1.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.userB.nativeId], x: { one: "1" } }) const uA_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) resolve() } @@ -130,7 +130,7 @@ describe("Cross-node behavior", () => { const uA_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) resolve() } @@ -140,31 +140,31 @@ describe("Cross-node behavior", () => { }) }) - describe("U_B_D1 sender", () => { - it("event to U_A_D1", (done) => { + describe("userB_Device1 sender", () => { + it("event to userA_Device1", (done) => { const mId = v4() - samaClientB_Device1.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.user1.nativeId], x: { two: "2" } }) + samaClientB_Device1.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.userA.nativeId], x: { two: "2" } }) samaClientA_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) done() } }) - it("event to U_A_D1,U_B_D2", async () => { + it("event to userA_Device1,userB_Device2", async () => { const mId = v4() samaClientB_Device1.messageSystem({ mid: mId, - uids: [dummyDataTestConfig.users.user1.nativeId, dummyDataTestConfig.users.user2.nativeId], + uids: [dummyDataTestConfig.users.userA.nativeId, dummyDataTestConfig.users.userB.nativeId], x: { two: "22" }, }) const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) resolve() } @@ -173,7 +173,7 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) resolve() } @@ -183,7 +183,7 @@ describe("Cross-node behavior", () => { }) }) - describe("U_B_D2 sender", () => { + describe("userB_Device2 sender", () => { it("event to conversation", async () => { const mId = v4() samaClientB_Device2.messageSystem({ mid: mId, cid: dummyDataTestConfig.conversations.private.nativeId, x: { four: "44" } }) @@ -191,7 +191,7 @@ describe("Cross-node behavior", () => { const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) resolve() } @@ -200,7 +200,7 @@ describe("Cross-node behavior", () => { const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) resolve() } @@ -212,7 +212,7 @@ describe("Cross-node behavior", () => { }) describe("Private", () => { - describe("U_A_D1 -> U_B_D1,U_B_D2", () => { + describe("userA_Device1 -> userB_Device1,userB_Device2", () => { let messageId = void 0 it("typing", async () => { @@ -221,7 +221,7 @@ describe("Cross-node behavior", () => { const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userA.nativeId) resolve() } }) @@ -229,7 +229,7 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userA.nativeId) resolve() } }) @@ -237,7 +237,7 @@ describe("Cross-node behavior", () => { await Promise.all([uB_D1_promise, uB_D2_promise]) }) - it("simple message", async () => { + it("message", async () => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientA_Device1.messageCreate({ @@ -251,7 +251,7 @@ describe("Cross-node behavior", () => { samaClientB_Device1.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) assert.equal(message.x["two"], "2") messageId = message._id @@ -264,7 +264,7 @@ describe("Cross-node behavior", () => { samaClientB_Device2.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) assert.equal(message.x["two"], "2") messageId = message._id @@ -282,14 +282,14 @@ describe("Cross-node behavior", () => { samaClientA_Device1.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userB.nativeId) done() } }) }) - describe("U_B_D1 -> U_A_D1,U_B_D2", () => { + describe("userB_Device1 -> userA_Device1,userB_Device2", () => { let messageId = void 0 it("typing", async () => { @@ -298,7 +298,7 @@ describe("Cross-node behavior", () => { const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userB.nativeId) resolve() } }) @@ -306,7 +306,7 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userB.nativeId) resolve() } }) @@ -314,7 +314,7 @@ describe("Cross-node behavior", () => { await Promise.all([uA_D1_promise, uB_D2_promise]) }) - it("simple message", async () => { + it("message", async () => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientB_Device1.messageCreate({ @@ -328,7 +328,7 @@ describe("Cross-node behavior", () => { samaClientA_Device1.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) assert.equal(message.x["two"], "22") messageId = message._id @@ -341,7 +341,7 @@ describe("Cross-node behavior", () => { samaClientB_Device2.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) assert.equal(message.x["two"], "22") messageId = message._id @@ -360,7 +360,7 @@ describe("Cross-node behavior", () => { samaClientB_Device1.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userA.nativeId) resolve() } @@ -370,7 +370,7 @@ describe("Cross-node behavior", () => { samaClientB_Device2.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userA.nativeId) resolve() } @@ -382,7 +382,7 @@ describe("Cross-node behavior", () => { }) describe("Group", () => { - describe("U_A_D1 -> U_B_D1,U_B_D2", () => { + describe("userA_Device1 -> userB_Device1,userB_Device2", () => { let messageId = void 0 it("typing", async () => { @@ -391,7 +391,7 @@ describe("Cross-node behavior", () => { const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userA.nativeId) resolve() } }) @@ -399,7 +399,7 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userA.nativeId) resolve() } }) @@ -407,7 +407,7 @@ describe("Cross-node behavior", () => { await Promise.all([uB_D1_promise, uB_D2_promise]) }) - it("simple message", async () => { + it("message", async () => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientA_Device1.messageCreate({ @@ -421,7 +421,7 @@ describe("Cross-node behavior", () => { samaClientB_Device1.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) assert.equal(message.x["two"], "2") messageId = message._id @@ -434,7 +434,7 @@ describe("Cross-node behavior", () => { samaClientB_Device2.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) assert.equal(message.x["two"], "2") messageId = message._id @@ -452,14 +452,14 @@ describe("Cross-node behavior", () => { samaClientA_Device1.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userB.nativeId) done() } }) }) - describe("U_B_D1 -> U_A_D1,U_B_D2", () => { + describe("userB_Device1 -> userA_Device1,userB_Device2", () => { let messageId = void 0 it("typing", async () => { @@ -468,7 +468,7 @@ describe("Cross-node behavior", () => { const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userB.nativeId) resolve() } }) @@ -476,7 +476,7 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userB.nativeId) resolve() } }) @@ -484,7 +484,7 @@ describe("Cross-node behavior", () => { await Promise.all([uA_D1_promise, uB_D2_promise]) }) - it("simple message", async () => { + it("message", async () => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientB_Device1.messageCreate({ @@ -498,7 +498,7 @@ describe("Cross-node behavior", () => { samaClientA_Device1.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) assert.equal(message.x["two"], "22") messageId = message._id @@ -511,7 +511,7 @@ describe("Cross-node behavior", () => { samaClientB_Device2.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) assert.equal(message.x["two"], "22") messageId = message._id @@ -530,7 +530,7 @@ describe("Cross-node behavior", () => { samaClientB_Device1.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userA.nativeId) resolve() } @@ -540,7 +540,7 @@ describe("Cross-node behavior", () => { samaClientB_Device2.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userA.nativeId) resolve() } @@ -553,14 +553,14 @@ describe("Cross-node behavior", () => { }) describe("Activity on disconnect", () => { - it("U_B_D1 logout (not last activity)", async () => { + it("userB_Device1 logout (not last activity)", async () => { samaClientB_Device1.disconnect() const promise = new Promise((resolve, reject) => { setTimeout(() => resolve(), 500) samaClientA_Device1.onUserActivityListener = (activity) => { - if (activity[dummyDataTestConfig.users.user2.nativeId] > 0) { + if (activity[dummyDataTestConfig.users.userB.nativeId] > 0) { reject(new Error("Should not be called")) } } @@ -569,11 +569,11 @@ describe("Cross-node behavior", () => { await promise }) - it("U_B_D1 logout (last activity)", (done) => { + it("userB_Device1 logout (last activity)", (done) => { samaClientB_Device2.disconnect() samaClientA_Device1.onUserActivityListener = (activity) => { - assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) + assert.ok(activity[dummyDataTestConfig.users.userB.nativeId] > 0) done() } }) @@ -585,67 +585,67 @@ describe("Cross-node behavior", () => { }) }) - describe("Node-A (U_A_D1, U_B_D2) - Node-B (U_B_D1)", () => { + describe("Node-A (userA_Device1, userB_Device2) - Node-B (userB_Device1)", () => { describe("Connect", () => { - it("connect U_A_D1", async () => { + it("connect userA_Device1", async () => { await samaClientA_Device1.connect() const { token } = await samaClientA_Device1.socketLogin({ - user: { login: dummyDataTestConfig.users.user1.login, password: userPassword }, + user: { login: dummyDataTestConfig.users.userA.login, password: userPassword }, }) userAToken = token }) it("subscribe user B last activity", async () => { - const activity = await samaClientA_Device1.subscribeToUserActivity(dummyDataTestConfig.users.user2.nativeId) - assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) + const activity = await samaClientA_Device1.subscribeToUserActivity(dummyDataTestConfig.users.userB.nativeId) + assert.ok(activity[dummyDataTestConfig.users.userB.nativeId] > 0) }) - it("connect U_B_D1 and check last activity", (done) => { + it("connect userB_Device1 and check last activity", (done) => { samaClientB_Device1 .connect() .then(() => samaClientB_Device1.socketLogin({ - user: { login: dummyDataTestConfig.users.user2.login, password: userPassword }, + user: { login: dummyDataTestConfig.users.userB.login, password: userPassword }, }) ) .then(({ token }) => (userBToken = token)) samaClientA_Device1.onUserActivityListener = (activity) => { - assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.userB.nativeId], 0) done() } }) - it("connect U_B_D2 and check last activity", (done) => { + it("connect userB_Device2 and check last activity", (done) => { samaClientB_Device2 .connect(NODE_1_WS_ENDPOINT, NODE_1_HTTP_ENDPOINT) - .then(() => samaClientB_Device2.socketLogin({ user: { login: dummyDataTestConfig.users.user2.login, password: userPassword } })) + .then(() => samaClientB_Device2.socketLogin({ user: { login: dummyDataTestConfig.users.userB.login, password: userPassword } })) samaClientA_Device1.onUserActivityListener = (activity) => { - assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.userB.nativeId], 0) done() } }) it("subscribe user A last activity", async () => { - let activity = await samaClientB_Device1.subscribeToUserActivity(dummyDataTestConfig.users.user1.nativeId) - assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] === 0) + let activity = await samaClientB_Device1.subscribeToUserActivity(dummyDataTestConfig.users.userA.nativeId) + assert.ok(activity[dummyDataTestConfig.users.userA.nativeId] === 0) - activity = await samaClientB_Device2.subscribeToUserActivity(dummyDataTestConfig.users.user1.nativeId) - assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] === 0) + activity = await samaClientB_Device2.subscribeToUserActivity(dummyDataTestConfig.users.userA.nativeId) + assert.ok(activity[dummyDataTestConfig.users.userA.nativeId] === 0) }) }) describe("Base messaging", () => { describe("System", () => { - describe("U_A_D1 -> U_B_D1,U_B_D2", async () => { + describe("userA_Device1 -> userB_Device1,userB_Device2", async () => { it("event", async () => { const mId = v4() - samaClientA_Device1.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.user2.nativeId], x: { one: "1" } }) + samaClientA_Device1.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.userB.nativeId], x: { one: "1" } }) const uA_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) resolve() } @@ -654,7 +654,7 @@ describe("Cross-node behavior", () => { const uA_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) resolve() } @@ -664,31 +664,31 @@ describe("Cross-node behavior", () => { }) }) - describe("U_B_D1 sender", () => { - it("event to U_A_D1", (done) => { + describe("userB_Device1 sender", () => { + it("event to userA_Device1", (done) => { const mId = v4() - samaClientB_Device1.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.user1.nativeId], x: { two: "2" } }) + samaClientB_Device1.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.userA.nativeId], x: { two: "2" } }) samaClientA_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) done() } }) - it("event to U_A_D1,U_B_D2", async () => { + it("event to userA_Device1,userB_Device2", async () => { const mId = v4() samaClientB_Device1.messageSystem({ mid: mId, - uids: [dummyDataTestConfig.users.user1.nativeId, dummyDataTestConfig.users.user2.nativeId], + uids: [dummyDataTestConfig.users.userA.nativeId, dummyDataTestConfig.users.userB.nativeId], x: { two: "22" }, }) const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) resolve() } @@ -697,7 +697,7 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) resolve() } @@ -707,7 +707,7 @@ describe("Cross-node behavior", () => { }) }) - describe("U_B_D2 sender", () => { + describe("userB_Device2 sender", () => { it("event to conversation", async () => { const mId = v4() samaClientB_Device2.messageSystem({ mid: mId, cid: dummyDataTestConfig.conversations.private.nativeId, x: { four: "44" } }) @@ -715,7 +715,7 @@ describe("Cross-node behavior", () => { const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) resolve() } @@ -724,7 +724,7 @@ describe("Cross-node behavior", () => { const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) resolve() } @@ -736,7 +736,7 @@ describe("Cross-node behavior", () => { }) describe("Private", () => { - describe("U_A_D1 -> U_B_D1,U_B_D2", () => { + describe("userA_Device1 -> userB_Device1,userB_Device2", () => { let messageId = void 0 it("typing", async () => { @@ -745,7 +745,7 @@ describe("Cross-node behavior", () => { const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userA.nativeId) resolve() } }) @@ -753,7 +753,7 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userA.nativeId) resolve() } }) @@ -761,7 +761,7 @@ describe("Cross-node behavior", () => { await Promise.all([uB_D1_promise, uB_D2_promise]) }) - it("simple message", async () => { + it("message", async () => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientA_Device1.messageCreate({ @@ -775,7 +775,7 @@ describe("Cross-node behavior", () => { samaClientB_Device1.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) assert.equal(message.x["two"], "2") messageId = message._id @@ -788,7 +788,7 @@ describe("Cross-node behavior", () => { samaClientB_Device2.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) assert.equal(message.x["two"], "2") messageId = message._id @@ -806,14 +806,14 @@ describe("Cross-node behavior", () => { samaClientA_Device1.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userB.nativeId) done() } }) }) - describe("U_B_D1 -> U_A_D1,U_B_D2", () => { + describe("userB_Device1 -> userA_Device1,userB_Device2", () => { let messageId = void 0 it("typing", async () => { @@ -822,7 +822,7 @@ describe("Cross-node behavior", () => { const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userB.nativeId) resolve() } }) @@ -830,7 +830,7 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userB.nativeId) resolve() } }) @@ -838,7 +838,7 @@ describe("Cross-node behavior", () => { await Promise.all([uA_D1_promise, uB_D2_promise]) }) - it("simple message", async () => { + it("message", async () => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientB_Device1.messageCreate({ @@ -852,7 +852,7 @@ describe("Cross-node behavior", () => { samaClientA_Device1.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) assert.equal(message.x["two"], "22") messageId = message._id @@ -865,7 +865,7 @@ describe("Cross-node behavior", () => { samaClientB_Device2.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) assert.equal(message.x["two"], "22") messageId = message._id @@ -884,7 +884,7 @@ describe("Cross-node behavior", () => { samaClientB_Device1.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userA.nativeId) resolve() } @@ -894,7 +894,7 @@ describe("Cross-node behavior", () => { samaClientB_Device2.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userA.nativeId) resolve() } @@ -906,7 +906,7 @@ describe("Cross-node behavior", () => { }) describe("Group", () => { - describe("U_A_D1 -> U_B_D1,U_B_D2", () => { + describe("userA_Device1 -> userB_Device1,userB_Device2", () => { let messageId = void 0 it("typing", async () => { @@ -915,7 +915,7 @@ describe("Cross-node behavior", () => { const uB_D1_promise = new Promise((resolve, reject) => { samaClientB_Device1.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userA.nativeId) resolve() } }) @@ -923,7 +923,7 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userA.nativeId) resolve() } }) @@ -931,7 +931,7 @@ describe("Cross-node behavior", () => { await Promise.all([uB_D1_promise, uB_D2_promise]) }) - it("simple message", async () => { + it("message", async () => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientA_Device1.messageCreate({ @@ -945,7 +945,7 @@ describe("Cross-node behavior", () => { samaClientB_Device1.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) assert.equal(message.x["two"], "2") messageId = message._id @@ -958,7 +958,7 @@ describe("Cross-node behavior", () => { samaClientB_Device2.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) assert.equal(message.x["two"], "2") messageId = message._id @@ -976,14 +976,14 @@ describe("Cross-node behavior", () => { samaClientA_Device1.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userB.nativeId) done() } }) }) - describe("U_B_D1 -> U_A_D1,U_B_D2", () => { + describe("userB_Device1 -> userA_Device1,userB_Device2", () => { let messageId = void 0 it("typing", async () => { @@ -992,7 +992,7 @@ describe("Cross-node behavior", () => { const uA_D1_promise = new Promise((resolve, reject) => { samaClientA_Device1.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userB.nativeId) resolve() } }) @@ -1000,7 +1000,7 @@ describe("Cross-node behavior", () => { const uB_D2_promise = new Promise((resolve, reject) => { samaClientB_Device2.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userB.nativeId) resolve() } }) @@ -1008,7 +1008,7 @@ describe("Cross-node behavior", () => { await Promise.all([uA_D1_promise, uB_D2_promise]) }) - it("simple message", async () => { + it("message", async () => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientB_Device1.messageCreate({ @@ -1022,7 +1022,7 @@ describe("Cross-node behavior", () => { samaClientA_Device1.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) assert.equal(message.x["two"], "22") messageId = message._id @@ -1035,7 +1035,7 @@ describe("Cross-node behavior", () => { samaClientB_Device2.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) assert.equal(message.x["two"], "22") messageId = message._id @@ -1054,7 +1054,7 @@ describe("Cross-node behavior", () => { samaClientB_Device1.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userA.nativeId) resolve() } @@ -1064,7 +1064,7 @@ describe("Cross-node behavior", () => { samaClientB_Device2.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userA.nativeId) resolve() } @@ -1077,14 +1077,14 @@ describe("Cross-node behavior", () => { }) describe("Activity on disconnect", () => { - it("U_B_D1 logout (not last activity)", async () => { + it("userB_Device1 logout (not last activity)", async () => { samaClientB_Device1.disconnect() const promise = new Promise((resolve, reject) => { setTimeout(() => resolve(), 500) samaClientA_Device1.onUserActivityListener = (activity) => { - if (activity[dummyDataTestConfig.users.user2.nativeId] > 0) { + if (activity[dummyDataTestConfig.users.userB.nativeId] > 0) { reject(new Error("Should not be called")) } } @@ -1093,16 +1093,16 @@ describe("Cross-node behavior", () => { await promise }) - it("U_B_D1 logout (last activity)", (done) => { + it("userB_Device1 logout (last activity)", (done) => { samaClientB_Device2.disconnect() samaClientA_Device1.onUserActivityListener = (activity) => { - assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) + assert.ok(activity[dummyDataTestConfig.users.userB.nativeId] > 0) done() } }) - it("disconnect A", async () => { + it("disconnect userA", async () => { samaClientA_Device1.disconnect() await setTimeoutPromise(200) }) diff --git a/test/cluster/node-crash.spec.js b/test/cluster/node-crash.spec.js index 1dac6b63..87dd0eac 100644 --- a/test/cluster/node-crash.spec.js +++ b/test/cluster/node-crash.spec.js @@ -61,36 +61,36 @@ describe("Crash Node behavior:", () => { await setTimeoutPromise(500) }) - describe("NodeA(U_A) - NodeB(U_B) - NodeB crash (wait fro clean):", () => { - it("connect client A", async () => { + describe("NodeA(userA) - NodeB(userB) - NodeB crash (wait fro clean):", () => { + it("connect client userA", async () => { await samaClientA.connect() - const { token } = await samaClientA.socketLogin({ user: { login: dummyDataTestConfig.users.user1.login, password: userPassword } }) + const { token } = await samaClientA.socketLogin({ user: { login: dummyDataTestConfig.users.userA.login, password: userPassword } }) userAToken = token }) - it("subscribe user B last activity", async () => { - const activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.user2.nativeId) - assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) + it("subscribe userB last activity", async () => { + const activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.userB.nativeId) + assert.ok(activity[dummyDataTestConfig.users.userB.nativeId] > 0) }) - it("connect client B and check last activity", (done) => { + it("connect client userB and check last activity", (done) => { samaClientB .connect() .then(() => samaClientB.socketLogin({ - user: { login: dummyDataTestConfig.users.user2.login, password: userPassword }, + user: { login: dummyDataTestConfig.users.userB.login, password: userPassword }, }) ) .then(({ token }) => (userBToken = token)) samaClientA.onUserActivityListener = (activity) => { - assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.userB.nativeId], 0) done() } }) describe("Messaging:", () => { - it("A -> B", (done) => { + it("userA -> userB", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientA.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { two: "2" } }) @@ -98,14 +98,14 @@ describe("Crash Node behavior:", () => { samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) assert.equal(message.x["two"], "2") done() } }) - it("messaging B -> A", (done) => { + it("userB -> userA", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientB.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { two: "22" } }) @@ -113,7 +113,7 @@ describe("Crash Node behavior:", () => { samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) assert.equal(message.x["two"], "22") done() @@ -122,7 +122,7 @@ describe("Crash Node behavior:", () => { }) describe("NodeB Crash", () => { - it("kill NodeB events with U_B last activity from NodeA", async () => { + it("kill NodeB and wait events userB last activity to NodeA", async () => { const closePromise = new Promise((resolve, reject) => { nodeA.once(`node_close[${nodeB.clusterEndpoint}]`, () => { console.log("[node][close]", nodeB.clusterEndpoint) @@ -146,10 +146,10 @@ describe("Crash Node behavior:", () => { const userBOfflinePromise = new Promise((resolve, reject) => { samaClientA.onUserActivityListener = async (activity) => { - assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) + assert.ok(activity[dummyDataTestConfig.users.userB.nativeId] > 0) - activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.user2.nativeId) - assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) + activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.userB.nativeId) + assert.ok(activity[dummyDataTestConfig.users.userB.nativeId] > 0) resolve() } @@ -161,7 +161,7 @@ describe("Crash Node behavior:", () => { }) }) - describe("Run new NodeB and reconnect U_B", () => { + describe("Run new NodeB and reconnect userB", () => { it("start new NodeB", async () => { nodeB = await startOrAccessNodeB(true) @@ -183,19 +183,19 @@ describe("Crash Node behavior:", () => { await Promise.all([nodeReadyPromise, nodeHandShackedPromise]) }) - it("connect client B and check last activity", (done) => { + it("connect client userB and check last activity", (done) => { samaClientB .connect() .then(() => samaClientB.socketLogin({ token: userBToken })) .then(({ token }) => (userBToken = token)) samaClientA.onUserActivityListener = (activity) => { - assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.userB.nativeId], 0) done() } }) describe("it can send message to each other:", () => { - it("A -> B", (done) => { + it("userA -> userB", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientA.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { two: "2" } }) @@ -203,14 +203,14 @@ describe("Crash Node behavior:", () => { samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) assert.equal(message.x["two"], "2") done() } }) - it("B -> A", (done) => { + it("userB -> userA", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientB.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { two: "22" } }) @@ -218,7 +218,7 @@ describe("Crash Node behavior:", () => { samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) assert.equal(message.x["two"], "22") done() @@ -228,7 +228,7 @@ describe("Crash Node behavior:", () => { }) }) - describe("NodeA(U_A) - NodeB(U_B) - NodeA restarting", () => { + describe("NodeA(userA) - NodeB(userB) - NodeA restarting", () => { it("NodeA crash", async () => { const closePromise = new Promise((resolve, reject) => { nodeB.once(`node_close[${nodeA.clusterEndpoint}]`, () => { @@ -249,12 +249,12 @@ describe("Crash Node behavior:", () => { await Promise.all([closePromise, startReconnectPromise]) }) - it("check last activity", async () => { - const activity = await samaClientB.subscribeToUserActivity(dummyDataTestConfig.users.user1.nativeId) - assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] === 0) + it("check last activity userA", async () => { + const activity = await samaClientB.subscribeToUserActivity(dummyDataTestConfig.users.userA.nativeId) + assert.ok(activity[dummyDataTestConfig.users.userA.nativeId] === 0) }) - it("restart NodeA", async () => { + it("restart NodeA (same cluster port)", async () => { const nodeAClusterEndpoint = nodeA.clusterEndpoint const finishReconnectPromise = new Promise((resolve, reject) => { @@ -272,20 +272,20 @@ describe("Crash Node behavior:", () => { await finishReconnectPromise }) - it("connect U_A to NodeA", (done) => { + it("connect userA to NodeA", (done) => { samaClientA .connect() .then(() => samaClientA.socketLogin({ token: userAToken })) .then(({ token }) => (userAToken = token)) samaClientB.onUserActivityListener = (activity) => { - assert.equal(activity[dummyDataTestConfig.users.user1.nativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.userA.nativeId], 0) done() } }) describe("it can send message to each other:", () => { - it("A -> B", (done) => { + it("userA -> userB", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientA.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { two: "2" } }) @@ -293,14 +293,14 @@ describe("Crash Node behavior:", () => { samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) assert.equal(message.x["two"], "2") done() } }) - it("B -> A", (done) => { + it("userB -> userA", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientB.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { two: "22" } }) @@ -308,7 +308,7 @@ describe("Crash Node behavior:", () => { samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) assert.equal(message.x["two"], "22") done() @@ -317,20 +317,20 @@ describe("Crash Node behavior:", () => { }) }) - describe("NodeA(U_A,U_B_D2) - NodeB(U_B_D1) - NodeB Crush check last activity behavior:", () => { - describe("Connect User B Device 2", () => { - it("subscribe last activity U_B", async () => { - const activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.user2.nativeId) - assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] === 0) + describe("NodeA(userA,userB_Device2) - NodeB(userB_Device1) - NodeB Crush check last activity behavior:", () => { + describe("Connect userB_Device2", () => { + it("subscribe last activity userB", async () => { + const activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.userB.nativeId) + assert.ok(activity[dummyDataTestConfig.users.userB.nativeId] === 0) }) - it("connect U_B_D2 to NodeA", (done) => { + it("connect userB_Device2 to NodeA", (done) => { samaClientB_Device2 .connect() - .then(() => samaClientB_Device2.socketLogin({ user: { login: dummyDataTestConfig.users.user2.login, password: userPassword } })) + .then(() => samaClientB_Device2.socketLogin({ user: { login: dummyDataTestConfig.users.userB.login, password: userPassword } })) samaClientA.onUserActivityListener = (activity) => { - assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.userB.nativeId], 0) done() } }) @@ -363,7 +363,7 @@ describe("Crash Node behavior:", () => { cleanNodeBPromise.then(() => setTimeoutPromise(1_000)).then(() => resolve()) samaClientA.onUserActivityListener = async (activity) => { - if (activity[dummyDataTestConfig.users.user2.nativeId] > 0) { + if (activity[dummyDataTestConfig.users.userB.nativeId] > 0) { reject(new Error("Should not be called")) } } @@ -374,16 +374,16 @@ describe("Crash Node behavior:", () => { await Promise.all([closePromise, startReconnectPromise, cleanNodeBPromise, userBNoOfflinePromise]) }) - it("check user B activity", async () => { - const activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.user2.nativeId) - assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] === 0) + it("check userB activity", async () => { + const activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.userB.nativeId) + assert.ok(activity[dummyDataTestConfig.users.userB.nativeId] === 0) }) - it("close last U_B connection on NodeA", (done) => { + it("close last userB connection on NodeA", (done) => { samaClientB_Device2.disconnect() samaClientA.onUserActivityListener = (activity) => { - assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) + assert.ok(activity[dummyDataTestConfig.users.userB.nativeId] > 0) done() } }) @@ -396,17 +396,17 @@ describe("Crash Node behavior:", () => { await setTimeoutPromise(500) }) - it("connect U_B", (done) => { + it("connect userB", (done) => { samaClientB.connect().then(() => samaClientB.socketLogin({ token: userBToken })) samaClientA.onUserActivityListener = (activity) => { - assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.userB.nativeId], 0) done() } }) }) }) - describe("NodeA(U_A) - NodeB(U_B) - NodeA crush - connect to NodeB:", () => { + describe("NodeA(userA) - NodeB(userB) - NodeA crush - userA connect to NodeB:", () => { it("crash NodeA", async () => { const closePromise = new Promise((resolve, reject) => { nodeB.once(`node_close[${nodeA.clusterEndpoint}]`, () => { @@ -427,21 +427,21 @@ describe("Crash Node behavior:", () => { await Promise.all([closePromise, startReconnectPromise]) }) - it("subscribe last activity U_B -> U_A", async () => { - const activity = await samaClientB.subscribeToUserActivity(dummyDataTestConfig.users.user1.nativeId) - assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] === 0) + it("subscribe last activity userB to userA", async () => { + const activity = await samaClientB.subscribeToUserActivity(dummyDataTestConfig.users.userA.nativeId) + assert.ok(activity[dummyDataTestConfig.users.userA.nativeId] === 0) }) - it("connect U_A to NodeB", (done) => { + it("connect userA to NodeB", (done) => { samaClientA.connect(NODE_2_WS_ENDPOINT, NODE_2_HTTP_ENDPOINT).then(() => samaClientA.socketLogin({ token: userAToken })) samaClientB.onUserActivityListener = (activity) => { - assert.equal(activity[dummyDataTestConfig.users.user1.nativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.userA.nativeId], 0) done() } }) describe("it can send message to each other:", () => { - it("A -> B", (done) => { + it("userA -> userB", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientA.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { two: "2" } }) @@ -449,14 +449,14 @@ describe("Crash Node behavior:", () => { samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) assert.equal(message.x["two"], "2") done() } }) - it("B -> A", (done) => { + it("userB -> userA", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientB.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { two: "22" } }) @@ -464,7 +464,7 @@ describe("Crash Node behavior:", () => { samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) assert.equal(message.x["two"], "22") done() diff --git a/test/cluster/same-node.spec.js b/test/cluster/same-node.spec.js index bbe0d5c2..02146585 100644 --- a/test/cluster/same-node.spec.js +++ b/test/cluster/same-node.spec.js @@ -40,63 +40,63 @@ describe("Same-node behavior", () => { }) describe("Connect", () => { - it("connect client A", async () => { + it("connect client userA", async () => { await samaClientA.connect() - const { token } = await samaClientA.socketLogin({ user: { login: dummyDataTestConfig.users.user1.login, password: userPassword } }) + const { token } = await samaClientA.socketLogin({ user: { login: dummyDataTestConfig.users.userA.login, password: userPassword } }) userAToken = token }) - it("subscribe user B last activity", async () => { - const activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.user2.nativeId) - assert.ok(activity[dummyDataTestConfig.users.user2.nativeId] > 0) + it("subscribe userB last activity", async () => { + const activity = await samaClientA.subscribeToUserActivity(dummyDataTestConfig.users.userB.nativeId) + assert.ok(activity[dummyDataTestConfig.users.userB.nativeId] > 0) }) - it("connect client B and check last activity", (done) => { + it("connect client userB and check last activity", (done) => { samaClientB .connect() .then(() => samaClientB.socketLogin({ - user: { login: dummyDataTestConfig.users.user2.login, password: userPassword }, + user: { login: dummyDataTestConfig.users.userB.login, password: userPassword }, }) ) .then(({ token }) => (userBToken = token)) samaClientA.onUserActivityListener = (activity) => { - assert.equal(activity[dummyDataTestConfig.users.user2.nativeId], 0) + assert.equal(activity[dummyDataTestConfig.users.userB.nativeId], 0) done() } }) - it("subscribe user A last activity", async () => { - const activity = await samaClientB.subscribeToUserActivity(dummyDataTestConfig.users.user1.nativeId) - assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] === 0) + it("subscribe userA last activity", async () => { + const activity = await samaClientB.subscribeToUserActivity(dummyDataTestConfig.users.userA.nativeId) + assert.ok(activity[dummyDataTestConfig.users.userA.nativeId] === 0) }) }) describe("Base messaging", () => { describe("System", () => { - describe("A -> B", () => { + describe("userA -> userB", () => { it("event", (done) => { const mId = v4() - samaClientA.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.user2.nativeId], x: { one: "1" } }) + samaClientA.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.userB.nativeId], x: { one: "1" } }) samaClientB.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) done() } }) }) - describe("B -> A", () => { + describe("userB -> userA", () => { it("event", (done) => { const mId = v4() - samaClientB.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.user1.nativeId], x: { two: "2" } }) + samaClientB.messageSystem({ mid: mId, uids: [dummyDataTestConfig.users.userA.nativeId], x: { two: "2" } }) samaClientA.onSystemMessageEvent = (message) => { assert.equal(message._id, mId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) done() } @@ -105,7 +105,7 @@ describe("Same-node behavior", () => { }) describe("Private", () => { - describe("A -> B", () => { + describe("userA -> userB", () => { let messageId = void 0 it("typing", (done) => { @@ -113,12 +113,12 @@ describe("Same-node behavior", () => { samaClientB.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userA.nativeId) done() } }) - it("simple message", (done) => { + it("message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientA.messageCreate({ cid: dummyDataTestConfig.conversations.private.nativeId, body: body, mid: mId, x: { two: "2" } }) @@ -126,7 +126,7 @@ describe("Same-node behavior", () => { samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) assert.equal(message.x["two"], "2") messageId = message._id @@ -141,14 +141,14 @@ describe("Same-node behavior", () => { samaClientA.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userB.nativeId) done() } }) }) - describe("B -> A", () => { + describe("userB -> userA", () => { let messageId = void 0 it("typing", (done) => { @@ -156,12 +156,12 @@ describe("Same-node behavior", () => { samaClientA.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userB.nativeId) done() } }) - it("simple message", (done) => { + it("message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientB.messageCreate({ cid: dummyDataTestConfig.conversations.private.nativeId, body: body, mid: mId, x: { two: "22" } }) @@ -169,7 +169,7 @@ describe("Same-node behavior", () => { samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) assert.equal(message.x["two"], "22") messageId = message._id @@ -184,7 +184,7 @@ describe("Same-node behavior", () => { samaClientB.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.private.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userA.nativeId) done() } @@ -193,7 +193,7 @@ describe("Same-node behavior", () => { }) describe("Group", () => { - describe("A -> B", () => { + describe("userA -> userB", () => { let messageId = void 0 it("typing", (done) => { @@ -201,12 +201,12 @@ describe("Same-node behavior", () => { samaClientB.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userA.nativeId) done() } }) - it("simple message", (done) => { + it("message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientA.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { one: "1" } }) @@ -214,7 +214,7 @@ describe("Same-node behavior", () => { samaClientB.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userA.nativeId) assert.equal(message.x["one"], "1") messageId = message._id @@ -229,14 +229,14 @@ describe("Same-node behavior", () => { samaClientA.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userB.nativeId) done() } }) }) - describe("B -> A", () => { + describe("userB -> userA", () => { let messageId = void 0 it("typing", (done) => { @@ -244,12 +244,12 @@ describe("Same-node behavior", () => { samaClientA.onUserTypingListener = (typing) => { assert.equal(typing.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(typing.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(typing.from, dummyDataTestConfig.users.userB.nativeId) done() } }) - it("simple message", (done) => { + it("message", (done) => { const mId = v4() const body = `Cluster Test Hello m: ${mId}` samaClientB.messageCreate({ cid: dummyDataTestConfig.conversations.group.nativeId, body: body, mid: mId, x: { one: "11" } }) @@ -257,7 +257,7 @@ describe("Same-node behavior", () => { samaClientA.onMessageEvent = (message) => { assert.equal(message.body, body) assert.equal(message.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(message.from, dummyDataTestConfig.users.user2.nativeId) + assert.equal(message.from, dummyDataTestConfig.users.userB.nativeId) assert.equal(message.x["one"], "11") messageId = message._id @@ -272,7 +272,7 @@ describe("Same-node behavior", () => { samaClientB.onMessageStatusListener = (status) => { assert.equal(status.ids.at(0), messageId) assert.equal(status.cid, dummyDataTestConfig.conversations.group.nativeId) - assert.equal(status.from, dummyDataTestConfig.users.user1.nativeId) + assert.equal(status.from, dummyDataTestConfig.users.userA.nativeId) done() } @@ -282,16 +282,16 @@ describe("Same-node behavior", () => { }) describe("Activity on disconnect", () => { - it("user A logout", (done) => { + it("userA logout", (done) => { samaClientA.disconnect() samaClientB.onUserActivityListener = (activity) => { - assert.ok(activity[dummyDataTestConfig.users.user1.nativeId] > 0) + assert.ok(activity[dummyDataTestConfig.users.userA.nativeId] > 0) done() } }) - it("disconnect B", async () => { + it("disconnect client with userB", async () => { samaClientB.disconnect() await setTimeoutPromise(200) }) diff --git a/test/cluster/utils.js b/test/cluster/utils.js index cfa9824f..2e20972d 100644 --- a/test/cluster/utils.js +++ b/test/cluster/utils.js @@ -19,8 +19,8 @@ export const userPassword = "12345678" export const dummyDataTestConfig = { organizationId: void 0, users: { - user1: { nativeId: void 0, login: void 0, password: userPassword }, - user2: { nativeId: void 0, login: void 0, password: userPassword }, + userA: { nativeId: void 0, login: void 0, password: userPassword }, + userB: { nativeId: void 0, login: void 0, password: userPassword }, }, conversations: { private: { nativeId: void 0 }, @@ -146,35 +146,35 @@ export const createDummyData = async (endpointWs, endpointHttp) => { await samaSdk.connect() - dummyDataTestConfig.users.user1.login = `TestUser-${faker.internet.username()}` - const user1 = await samaSdk.userCreate({ - login: dummyDataTestConfig.users.user1.login, + dummyDataTestConfig.users.userA.login = `TestUser-${faker.internet.username()}` + const userA = await samaSdk.userCreate({ + login: dummyDataTestConfig.users.userA.login, email: faker.internet.email(), password: userPassword, }) - dummyDataTestConfig.users.user1.nativeId = user1.native_id + dummyDataTestConfig.users.userA.nativeId = userA.native_id - dummyDataTestConfig.users.user2.login = `TestUser-${faker.internet.username()}` - const user2 = await samaSdk.userCreate({ - login: dummyDataTestConfig.users.user2.login, + dummyDataTestConfig.users.userB.login = `TestUser-${faker.internet.username()}` + const userB = await samaSdk.userCreate({ + login: dummyDataTestConfig.users.userB.login, email: faker.internet.email(), password: userPassword, }) - dummyDataTestConfig.users.user2.nativeId = user2.native_id + dummyDataTestConfig.users.userB.nativeId = userB.native_id - await samaSdk.socketLogin({ user: { login: dummyDataTestConfig.users.user1.login, password: userPassword } }) + await samaSdk.socketLogin({ user: { login: dummyDataTestConfig.users.userA.login, password: userPassword } }) const privateConversation = await samaSdk.conversationCreate({ name: `Private-${faker.music.songName()}`, type: "u", - participants: [user1.native_id, user2.native_id], + participants: [userA.native_id, userB.native_id], }) dummyDataTestConfig.conversations.private.nativeId = privateConversation._id const groupConversation = await samaSdk.conversationCreate({ name: `Group-${faker.music.songName()}`, type: "g", - participants: [user1.native_id, user2.native_id], + participants: [userA.native_id, userB.native_id], }) dummyDataTestConfig.conversations.group.nativeId = groupConversation._id From d51dbdaa07403f823daffdcacf07e0b1203c46ce Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Tue, 7 Apr 2026 16:06:15 +0300 Subject: [PATCH 27/70] update install docker --- .env.example | 11 +++++++++++ Dockerfile | 2 +- Dockerfile.local | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index e51f056b..e8a88dfd 100644 --- a/.env.example +++ b/.env.example @@ -90,3 +90,14 @@ GOOGLE_GENERATIVE_AI_API_KEY= SERVICE_OTP_TOKEN_EXPIRES_IN=300000 RESEND_SENDER="SAMASupport " RESEND_API_KEY= + + +# Testing +RUN_NODE_1_CMD='APP_PORT=9001 APP_TCP_PORT=8001 CORS_ORIGIN=http://localhost:3001 npm start' +RUN_NODE_2_CMD='APP_PORT=9002 APP_TCP_PORT=8002 CORS_ORIGIN=http://localhost:3002 npm start' + +NODE_1_WS_ENDPOINT=ws://localhost:9001 +NODE_1_HTTP_ENDPOINT=http://localhost:9001 + +NODE_2_WS_ENDPOINT=ws://localhost:9002 +NODE_2_HTTP_ENDPOINT=http://localhost:9002 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 0dc79b73..19685472 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app COPY package*.json ./ -RUN npm install +RUN npm install --omit=dev COPY . . diff --git a/Dockerfile.local b/Dockerfile.local index 5f8aa2f6..c2bc5a3a 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -4,7 +4,7 @@ WORKDIR /app COPY package*.json ./ -RUN npm install +RUN npm install --omit=dev COPY . . From 317d59fed7ca91ec675fbb598a9a5fddd5d2c9e8 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Tue, 7 Apr 2026 18:10:36 +0300 Subject: [PATCH 28/70] add hostname to stats --- app/providers/services/stats/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/providers/services/stats/index.js b/app/providers/services/stats/index.js index fd20f857..06c54a0c 100644 --- a/app/providers/services/stats/index.js +++ b/app/providers/services/stats/index.js @@ -142,7 +142,7 @@ class StatsService { } collectStats(format, date = new Date()) { - const stats = {} + const stats = { hostname: this.config.get("app.hostName") } const serverStats = this.collectServerStats(format, date) const usersStats = this.collectUsersStats(format, date) From 46ecc0a8522001a40edbe6a735f6c305811b70e7 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Wed, 8 Apr 2026 15:24:10 +0300 Subject: [PATCH 29/70] update cors header --- app/networking/protocol_processors/http.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/networking/protocol_processors/http.js b/app/networking/protocol_processors/http.js index 0d821928..674ec185 100644 --- a/app/networking/protocol_processors/http.js +++ b/app/networking/protocol_processors/http.js @@ -49,7 +49,7 @@ const parseBaseParamsMiddleware = async (res, req) => { } const addCorsHeaders = (res, req) => { - res.writeHeader("Access-Control-Allow-Origin", config.get("http.corsOrigin") ?? "*") + res.writeHeader("Access-Control-Allow-Origin", config.get("http.corsOrigin") ?? res.parsedHeaders["origin"] ?? "*") res.writeHeader("Access-Control-Allow-Credentials", "true") res.writeHeader("Access-Control-Allow-Methods", "POST, PUT, DELETE") res.writeHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, API-Key") From cdd60e8bfafd469d130ff5a93bc00c79ff9f04e8 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 9 Apr 2026 15:21:08 +0300 Subject: [PATCH 30/70] add logs --- app/cluster/cluster_manager.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index 5a5912ff..b988abc1 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -16,6 +16,7 @@ import packetManager from "../networking/packet_manager.js" import { promiseQueueWithJittering, CancelQueueError } from "../utils/promise-queue-with-jittering.js" const logger = mainLogger.child("[ClusterManager]") +const loggerSync = logger.child("[Sync]") const loggerSender = logger.child("[Sender]") const loggerReceiver = logger.child("[Receiver]") @@ -111,13 +112,17 @@ class ClusterManager extends BaseProtocolProcessor { async #connectToExistingClusterNodes() { const destroyedNodes = new Set() + loggerSender.debug("[Start]") + loggerSync.debug("[connections] %j %s", Array.from(this.clusterNodesConnections.keys()), this.clusterNodesConnections.size) + loggerSync.debug("[reconnecting] %j %s", Array.from(this.reconnectingNodesConnections.keys()), this.reconnectingNodesConnections.size) + const clusterNodeService = ServiceLocatorContainer.use("ClusterNodeService") const activeNodes = await clusterNodeService.retrieveActive() - loggerSender.debug("[active] %j %s", Array.from(activeNodes), activeNodes.size) + loggerSync.debug("[active] %j %s", Array.from(activeNodes), activeNodes.size) const storedNodes = await clusterNodeService.retrieveStored() - loggerSender.debug("[stored] %j %s", Array.from(storedNodes), storedNodes.size) + loggerSync.debug("[stored] %j %s", Array.from(storedNodes), storedNodes.size) // check node-users stored and no active storedNodes.forEach((storedNode) => { @@ -152,9 +157,10 @@ class ClusterManager extends BaseProtocolProcessor { } try { + loggerSync.debug("[try connect node] %s", nodeEndpoint) await this.createOrRetrieveConnectionWithNode(nodeEndpoint, true) } catch (error) { - loggerSender.error(error, "[connect node][failed] %s", nodeEndpoint) + loggerSync.error(error, "[connect node][failed] %s", nodeEndpoint) this.startNodeReconnecting(nodeEndpoint, false) } } @@ -193,6 +199,8 @@ class ClusterManager extends BaseProtocolProcessor { this.cancelNodeReconnecting(nodeEndpoint) + loggerSender.debug("[connect node][success] %s", nodeEndpoint) + if (this.clusterNodesConnections.get(nodeEndpoint)) { ws.close(DUPLICATE_CLOSE_WS_CODE, DUPLICATE_CLOSE_WS_REASON) return this.clusterNodesConnections.get(nodeEndpoint) @@ -322,7 +330,7 @@ class ClusterManager extends BaseProtocolProcessor { close: (ws, code, reason) => { reason = decoder.write(Buffer.from(reason)) - logger.debug("[Close][%s] Code: %s Reason: %s", ws.nodeEndpoint, code, reason) + loggerReceiver.debug("[Close][%s] Code: %s Reason: %s", ws.nodeEndpoint, code, reason) this.onCloseNode(ws.nodeEndpoint, +code, reason) }, }) From d88c9b467f894ddb6fa20711a8eaa9917b512625 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 9 Apr 2026 16:19:18 +0300 Subject: [PATCH 31/70] add log --- app/cluster/cluster_manager.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index b988abc1..f36c6cac 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -159,6 +159,7 @@ class ClusterManager extends BaseProtocolProcessor { try { loggerSync.debug("[try connect node] %s", nodeEndpoint) await this.createOrRetrieveConnectionWithNode(nodeEndpoint, true) + loggerSync.debug("[connected] %s", nodeEndpoint) } catch (error) { loggerSync.error(error, "[connect node][failed] %s", nodeEndpoint) this.startNodeReconnecting(nodeEndpoint, false) From 3fc066f34edf8b65ec0855fe7d328983fdb7012f Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 9 Apr 2026 16:26:25 +0300 Subject: [PATCH 32/70] add logs --- app/cluster/cluster_manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index f36c6cac..9bae1c2c 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -157,7 +157,7 @@ class ClusterManager extends BaseProtocolProcessor { } try { - loggerSync.debug("[try connect node] %s", nodeEndpoint) + loggerSync.debug("[establish connect with node] %s", nodeEndpoint) await this.createOrRetrieveConnectionWithNode(nodeEndpoint, true) loggerSync.debug("[connected] %s", nodeEndpoint) } catch (error) { From 51a5c6e4762b0634b67436b0302ea9a2eb75d641 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Mon, 13 Apr 2026 16:04:11 +0300 Subject: [PATCH 33/70] close cluster socket on delete --- app/cluster/cluster_manager.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/cluster/cluster_manager.js b/app/cluster/cluster_manager.js index 9bae1c2c..459d984c 100644 --- a/app/cluster/cluster_manager.js +++ b/app/cluster/cluster_manager.js @@ -366,14 +366,20 @@ class ClusterManager extends BaseProtocolProcessor { return } - this.clusterNodesConnections.delete(nodeEndpoint) + this.deleteClusterConnection(nodeEndpoint) this.startNodeReconnecting(nodeEndpoint, true) } + deleteClusterConnection(nodeEndpoint) { + const connections = this.clusterNodesConnections.get(nodeEndpoint) + this.clusterNodesConnections.delete(nodeEndpoint) + connections?.close?.() + } + async cleanDestroyedNodeData(nodeEndpoint) { logger.debug("[clean node] %s", nodeEndpoint) - this.clusterNodesConnections.delete(nodeEndpoint) + this.deleteClusterConnection(nodeEndpoint) this.cancelNodeReconnecting(nodeEndpoint) const sessionService = ServiceLocatorContainer.use("SessionService") From 5e869a8273be9908649c04c34571d7486d3d4e5b Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Mon, 13 Apr 2026 16:27:43 +0300 Subject: [PATCH 34/70] fix config --- app/config/index.js | 2 -- index.js | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/index.js b/app/config/index.js index 27ee5b24..b8ea27ee 100644 --- a/app/config/index.js +++ b/app/config/index.js @@ -3,7 +3,6 @@ import ip from "ip" import CONFIG_VALUES from "./default.js" import Config from "./config.js" -import { buildWsEndpoint } from "../utils/build_ws_endpoint.js" const config = new Config(CONFIG_VALUES) @@ -18,7 +17,6 @@ config.set("tcp.options.isTls", !!isTls) if (config.get("app.isStandAloneNode")) { config.set("ws.cluster.port", 1000) - config.set("ws.cluster.endpoint", buildWsEndpoint(config.get("app.ip"), config.get("ws.cluster.port"))) } export default config diff --git a/index.js b/index.js index 3139eccf..5348bd3d 100644 --- a/index.js +++ b/index.js @@ -174,6 +174,8 @@ if (!config.get("app.isStandAloneNode")) { // Start Cluster Sync logger.debug("[Start sync]") await clusterManager.startSyncingClusterNodes(true) +} else { + config.set("ws.cluster.endpoint", buildWsEndpoint(config.get("app.ip"), config.get("ws.cluster.port"))) } logger.debug("[Config][Merged] %s", JSON.stringify(config.toObject(), null, 5)) From 2ce353a5302f6015a1c33a39985e3bb813ab9cd0 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Wed, 15 Apr 2026 16:15:55 +0300 Subject: [PATCH 35/70] remove session with old node endpoint --- app/providers/services/session/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/providers/services/session/index.js b/app/providers/services/session/index.js index 5a8de978..86bbbcaa 100644 --- a/app/providers/services/session/index.js +++ b/app/providers/services/session/index.js @@ -206,8 +206,11 @@ class SessionService { const userDeviceIds = await this.listUserDevice(organizationId, userId) if (userDeviceIds.includes(deviceId)) { + const extraParams = await this.retrieveUserExtraParams(userId, deviceId) + const oldNodeEndpointKey = extraParams?.[CONSTANTS.SESSION_NODE_KEY] || nodeEndpoint + await this.removeUserData(organizationId, userId, deviceId) - await this.removeUserDeviceFromNode(nodeEndpoint, organizationId, userId, deviceId) + await this.removeUserDeviceFromNode(oldNodeEndpointKey, organizationId, userId, deviceId) } await this.addUserDevice(organizationId, userId, deviceId) From 330f2d4c407254ad024f8c6002ea29d4ebc43385 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Tue, 21 Apr 2026 22:07:26 +0300 Subject: [PATCH 36/70] add client cluster test --- .cluster-client-mocharc.json | 17 ++++ .cluster-mocharc.json | 13 ++- test/cluster/client-clustering.spec.js | 109 +++++++++++++++++++++++++ test/cluster/utils.js | 76 ++++++++++++----- 4 files changed, 191 insertions(+), 24 deletions(-) create mode 100644 .cluster-client-mocharc.json create mode 100644 test/cluster/client-clustering.spec.js diff --git a/.cluster-client-mocharc.json b/.cluster-client-mocharc.json new file mode 100644 index 00000000..9d9e9a4b --- /dev/null +++ b/.cluster-client-mocharc.json @@ -0,0 +1,17 @@ +{ + "recursive": true, + "sort": false, + "color": true, + "allow-uncaught": true, + "parallel": false, + "reporter": "list", + "require": [ + "dotenv/config" + ], + "exit": true, + "spec": [ + "./test/cluster/client-clustering.spec.js" + ], + "exclude": "test/**/*.spec.js", + "timeout": 120000 +} diff --git a/.cluster-mocharc.json b/.cluster-mocharc.json index d1600834..531129d7 100644 --- a/.cluster-mocharc.json +++ b/.cluster-mocharc.json @@ -5,9 +5,18 @@ "allow-uncaught": true, "parallel": false, "reporter": "list", - "require": ["dotenv/config", "./test/cluster/utils.js", "./test/cluster/fixtures.js"], + "require": [ + "dotenv/config", + "./test/cluster/utils.js", + "./test/cluster/fixtures.js" + ], "exit": true, - "spec": ["./test/cluster/same-node.spec.js", "./test/cluster/cross-node.spec.js", "./test/cluster/multi-devices.spec.js", "./test/cluster/node-crash.spec.js"], + "spec": [ + "./test/cluster/same-node.spec.js", + "./test/cluster/cross-node.spec.js", + "./test/cluster/multi-devices.spec.js", + "./test/cluster/node-crash.spec.js" + ], "exclude": "test/**/*.spec.js", "timeout": 120000 } diff --git a/test/cluster/client-clustering.spec.js b/test/cluster/client-clustering.spec.js new file mode 100644 index 00000000..f3d22606 --- /dev/null +++ b/test/cluster/client-clustering.spec.js @@ -0,0 +1,109 @@ +import assert from "node:assert" +import { setTimeout as setTimeoutPromise } from "node:timers/promises" + +import { v4 } from "uuid" + +import { + initSdkWithUser, + createGroupConversation +} from "./utils.js" + +const orgId = process.env.CLIENT_TEST_ORG_ID +const wsEndpoint = process.env.CLIENT_WS_ENDPOINT +const httpEndpoint = process.env.CLIENT_HTTP_ENDPOINT + +const clientsCount = 10 +const clientSdkDeviceIds = new Array(clientsCount).fill(0).map((_, i) => `dv-${i}`) +const samaSdks = [] +const users = [] +let groupConv = void 0 + +const sendMessageWithMarkAble = async (samaSdk, convId) => { + const mId = v4() + const mBody = `TestM-${mId}-original` + const mBodyResponse = `${mBody}-Response` + + const responseMessagesUserIds = new Set() + + const waitPromise = new Promise((resolve, reject) => { + samaSdk.onMessageEvent = (message) => { + if ((message.cid === convId) && (message.body === mBodyResponse)) { + responseMessagesUserIds.add(message.from) + } + + if (responseMessagesUserIds.size === (clientsCount - 1)) { + samaSdk.onMessageEvent = void 0 + resolve() + } + } + }) + + const otherSdks = samaSdks.filter(sdk => sdk !== samaSdk) + + for (const sdk of otherSdks) { + sdk.onMessageEvent = (message) => { + if ((message.cid === convId) && (message.body === mBody)) { + sdk.messageCreate({ + cid: convId, + body: mBodyResponse, + mid: v4(), + }) + } + } + } + + await samaSdk.messageCreate({ + cid: convId, + body: mBody, + mid: mId, + }) + + await waitPromise +} + +describe("Multiple Clients", () => { + describe("Init", () => { + it("sdk array", async () => { + for (let i = 0; i < clientsCount; ++i) { + const { samaSdk, user } = await initSdkWithUser( + orgId, true, `TestUser-${i}`, clientSdkDeviceIds.at(i), + wsEndpoint, httpEndpoint + ) + + assert.ok(samaSdk) + assert.ok(user) + + samaSdks.push(samaSdk) + users.push(user) + } + }) + + it("create group conversation", async () => { + const userIds = users.map(user => user.native_id) + const groupConversation = await createGroupConversation(samaSdks.at(0), userIds) + + assert.ok(groupConversation) + + groupConv = groupConversation + }) + }) + + describe("Send messages", () => { + const resetSdkListeners = () => { + for (const sdk of samaSdks) { + sdk.onMessageEvent = void 0 + } + } + + beforeEach(resetSdkListeners) + + clientSdkDeviceIds.forEach((deviceId, i) => { + it(`${deviceId} sender`, async () => { + await sendMessageWithMarkAble(samaSdks.at(i), groupConv._id) + await setTimeoutPromise(100) + }) + }) + + afterEach(resetSdkListeners) + }) +}) \ No newline at end of file diff --git a/test/cluster/utils.js b/test/cluster/utils.js index 2e20972d..7d0637ef 100644 --- a/test/cluster/utils.js +++ b/test/cluster/utils.js @@ -129,53 +129,85 @@ export const createOrganizationId = async (endpoint, name) => { return organizationId } -export const createDummyData = async (endpointWs, endpointHttp) => { - dummyDataTestConfig.organizationId = await createOrganizationId(endpointHttp) - +export const initSdkWithUser = async ( + organizationId, needLogin, + loginPrefix, deviceIdPrefix = 'dv', + endpointWs, endpointHttp +) => { const sdkConfig = { endpoint: { ws: endpointWs, http: endpointHttp, }, - organization_id: dummyDataTestConfig.organizationId, + organization_id: organizationId, disableAutoReconnect: true, } const samaSdk = new SAMAClient(sdkConfig) - samaSdk.deviceId = v4() + samaSdk.deviceId = `${deviceIdPrefix}-${v4()}` await samaSdk.connect() - dummyDataTestConfig.users.userA.login = `TestUser-${faker.internet.username()}` - const userA = await samaSdk.userCreate({ - login: dummyDataTestConfig.users.userA.login, - email: faker.internet.email(), - password: userPassword, - }) - dummyDataTestConfig.users.userA.nativeId = userA.native_id + const user = await createUser(samaSdk, loginPrefix) - dummyDataTestConfig.users.userB.login = `TestUser-${faker.internet.username()}` - const userB = await samaSdk.userCreate({ - login: dummyDataTestConfig.users.userB.login, + if (needLogin) { + await samaSdk.socketLogin({ user: { login: user.login, password: userPassword } }) + } + + return { samaSdk, user } +} + +export const createUser = async (samaSdk, loginPrefix = 'TestUser') => { + const login = `${loginPrefix}-${faker.internet.username()}` + + const user = await samaSdk.userCreate({ + login: login, email: faker.internet.email(), password: userPassword, }) - dummyDataTestConfig.users.userB.nativeId = userB.native_id - await samaSdk.socketLogin({ user: { login: dummyDataTestConfig.users.userA.login, password: userPassword } }) + return user +} +export const createPrivateConversation = async (samaSdk, userAId, userBId) => { const privateConversation = await samaSdk.conversationCreate({ - name: `Private-${faker.music.songName()}`, + name: `Private-${faker.music.songName()}-${faker.string.alphanumeric(8)}`, type: "u", - participants: [userA.native_id, userB.native_id], + participants: [userAId, userBId], }) - dummyDataTestConfig.conversations.private.nativeId = privateConversation._id + return privateConversation +} + +export const createGroupConversation = async (samaSdk, userIds) => { const groupConversation = await samaSdk.conversationCreate({ - name: `Group-${faker.music.songName()}`, + name: `Group-${faker.music.songName()}-${faker.string.alphanumeric(8)}`, type: "g", - participants: [userA.native_id, userB.native_id], + participants: userIds, }) + + return groupConversation +} + +export const createDummyData = async (endpointWs, endpointHttp) => { + dummyDataTestConfig.organizationId = await createOrganizationId(endpointHttp) + + const { samaSdk, user: userA } = await initSdkWithUser( + dummyDataTestConfig.organizationId, true, + void 0, void 0, + endpointWs, endpointHttp, + ) + dummyDataTestConfig.users.userA.login = userA.login + dummyDataTestConfig.users.userA.nativeId = userA.native_id + + const userB = await createUser(samaSdk) + dummyDataTestConfig.users.userB.login = userB.login + dummyDataTestConfig.users.userB.nativeId = userB.native_id + + const privateConversation = await createPrivateConversation(samaSdk, userA.native_id, userB.native_id) + dummyDataTestConfig.conversations.private.nativeId = privateConversation._id + + const groupConversation = await createGroupConversation(samaSdk, [userA.native_id, userB.native_id]) dummyDataTestConfig.conversations.group.nativeId = groupConversation._id samaSdk.disconnect() From 93c464f0085ba6af6b02090f19c29f7661bcb120 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Wed, 22 Apr 2026 16:24:38 +0300 Subject: [PATCH 37/70] update testing clients cluster --- ...harc.json => .cluster-clients-mocharc.json | 2 +- .env.example | 7 +- package.json | 1 + test/cluster/client-clustering.spec.js | 109 --------- test/cluster/cluster-clients.spec.js | 210 ++++++++++++++++++ 5 files changed, 218 insertions(+), 111 deletions(-) rename .cluster-client-mocharc.json => .cluster-clients-mocharc.json (84%) delete mode 100644 test/cluster/client-clustering.spec.js create mode 100644 test/cluster/cluster-clients.spec.js diff --git a/.cluster-client-mocharc.json b/.cluster-clients-mocharc.json similarity index 84% rename from .cluster-client-mocharc.json rename to .cluster-clients-mocharc.json index 9d9e9a4b..0c07ea87 100644 --- a/.cluster-client-mocharc.json +++ b/.cluster-clients-mocharc.json @@ -10,7 +10,7 @@ ], "exit": true, "spec": [ - "./test/cluster/client-clustering.spec.js" + "./test/cluster/cluster-clients.spec.js" ], "exclude": "test/**/*.spec.js", "timeout": 120000 diff --git a/.env.example b/.env.example index e8a88dfd..769c99f8 100644 --- a/.env.example +++ b/.env.example @@ -100,4 +100,9 @@ NODE_1_WS_ENDPOINT=ws://localhost:9001 NODE_1_HTTP_ENDPOINT=http://localhost:9001 NODE_2_WS_ENDPOINT=ws://localhost:9002 -NODE_2_HTTP_ENDPOINT=http://localhost:9002 \ No newline at end of file +NODE_2_HTTP_ENDPOINT=http://localhost:9002 + +TEST_CLIENTS_COUNT=10 +TEST_CLIENT_ORG_ID=683db99d3874471b4dd36c69 +TEST_CLIENT_WS_ENDPOINT=ws://localhost:9001 +TEST_CLIENT_HTTP_ENDPOINT=http://localhost:9001 diff --git a/package.json b/package.json index 6a5410a2..455fb5ef 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "start:dev": "nodemon --experimental-loader ./sama-loader.mjs --import dotenv/config index.js", "test": "mocha", "test:cluster": "mocha --config ./.cluster-mocharc.json", + "test:cluster:clients": "mocha --config ./.cluster-clients-mocharc.json", "test:cover": "c8 npm run test" }, "repository": { diff --git a/test/cluster/client-clustering.spec.js b/test/cluster/client-clustering.spec.js deleted file mode 100644 index f3d22606..00000000 --- a/test/cluster/client-clustering.spec.js +++ /dev/null @@ -1,109 +0,0 @@ -import assert from "node:assert" -import { setTimeout as setTimeoutPromise } from "node:timers/promises" - -import { v4 } from "uuid" - -import { - initSdkWithUser, - createGroupConversation -} from "./utils.js" - -const orgId = process.env.CLIENT_TEST_ORG_ID -const wsEndpoint = process.env.CLIENT_WS_ENDPOINT -const httpEndpoint = process.env.CLIENT_HTTP_ENDPOINT - -const clientsCount = 10 -const clientSdkDeviceIds = new Array(clientsCount).fill(0).map((_, i) => `dv-${i}`) -const samaSdks = [] -const users = [] -let groupConv = void 0 - -const sendMessageWithMarkAble = async (samaSdk, convId) => { - const mId = v4() - const mBody = `TestM-${mId}-original` - const mBodyResponse = `${mBody}-Response` - - const responseMessagesUserIds = new Set() - - const waitPromise = new Promise((resolve, reject) => { - samaSdk.onMessageEvent = (message) => { - if ((message.cid === convId) && (message.body === mBodyResponse)) { - responseMessagesUserIds.add(message.from) - } - - if (responseMessagesUserIds.size === (clientsCount - 1)) { - samaSdk.onMessageEvent = void 0 - resolve() - } - } - }) - - const otherSdks = samaSdks.filter(sdk => sdk !== samaSdk) - - for (const sdk of otherSdks) { - sdk.onMessageEvent = (message) => { - if ((message.cid === convId) && (message.body === mBody)) { - sdk.messageCreate({ - cid: convId, - body: mBodyResponse, - mid: v4(), - }) - } - } - } - - await samaSdk.messageCreate({ - cid: convId, - body: mBody, - mid: mId, - }) - - await waitPromise -} - -describe("Multiple Clients", () => { - describe("Init", () => { - it("sdk array", async () => { - for (let i = 0; i < clientsCount; ++i) { - const { samaSdk, user } = await initSdkWithUser( - orgId, true, `TestUser-${i}`, clientSdkDeviceIds.at(i), - wsEndpoint, httpEndpoint - ) - - assert.ok(samaSdk) - assert.ok(user) - - samaSdks.push(samaSdk) - users.push(user) - } - }) - - it("create group conversation", async () => { - const userIds = users.map(user => user.native_id) - const groupConversation = await createGroupConversation(samaSdks.at(0), userIds) - - assert.ok(groupConversation) - - groupConv = groupConversation - }) - }) - - describe("Send messages", () => { - const resetSdkListeners = () => { - for (const sdk of samaSdks) { - sdk.onMessageEvent = void 0 - } - } - - beforeEach(resetSdkListeners) - - clientSdkDeviceIds.forEach((deviceId, i) => { - it(`${deviceId} sender`, async () => { - await sendMessageWithMarkAble(samaSdks.at(i), groupConv._id) - await setTimeoutPromise(100) - }) - }) - - afterEach(resetSdkListeners) - }) -}) \ No newline at end of file diff --git a/test/cluster/cluster-clients.spec.js b/test/cluster/cluster-clients.spec.js new file mode 100644 index 00000000..6536ab89 --- /dev/null +++ b/test/cluster/cluster-clients.spec.js @@ -0,0 +1,210 @@ +import assert from "node:assert" +import { setTimeout as setTimeoutPromise } from "node:timers/promises" + +import { v4 } from "uuid" + +import { + initSdkWithUser, + createGroupConversation, + createPrivateConversation, +} from "./utils.js" + +const orgId = process.env.TEST_CLIENT_ORG_ID +const wsEndpoint = process.env.TEST_CLIENT_WS_ENDPOINT +const httpEndpoint = process.env.TEST_CLIENT_HTTP_ENDPOINT + +const clientsCount = +process.env.TEST_CLIENTS_COUNT || 10 +const clientSdkDeviceIds = new Array(clientsCount).fill(0).map((_, i) => `dv-${i + 1}`) + +const clientIdUserPairs = new Map() +const userSdkPairs = new Map() + +let groupConv = void 0 +const privateConversationsByPair = new Map() + +const getPairKey = (userAId, userBId) => [userAId, userBId].sort().join(":") + +const sendGroupMessageWithMarkAble = async (senderSdk, convId) => { + const mId = v4() + const mBody = `TestM-${mId}-original` + const mBodyResponse = `${mBody}-Response` + + const responseMessagesUserIds = new Set() + + const waitPromise = new Promise((resolve, reject) => { + senderSdk.onMessageEvent = (message) => { + if ((message.cid === convId) && (message.body === mBodyResponse)) { + responseMessagesUserIds.add(message.from) + } + + if (responseMessagesUserIds.size === (clientsCount - 1)) { + senderSdk.onMessageEvent = void 0 + resolve() + } + } + }) + + const recipientsSdks = Array.from(userSdkPairs.values()).filter(sdk => sdk !== senderSdk) + + for (const sdk of recipientsSdks) { + sdk.onMessageEvent = (message) => { + if ((message.cid === convId) && (message.body === mBody)) { + sdk.messageCreate({ + cid: convId, + body: mBodyResponse, + mid: v4(), + }) + } + } + } + + await senderSdk.messageCreate({ + cid: convId, + body: mBody, + mid: mId, + }) + + await waitPromise +} + +const sendPrivateMessagesWithMarkAble = async (senderSdk, senderUser) => { + const recipients = Array.from(userSdkPairs.keys()).filter(user => user !== senderUser) + + for (const recipientUser of recipients) { + const recipientSdk = userSdkPairs.get(recipientUser) + + const pairKey = getPairKey(senderUser.native_id, recipientUser.native_id) + const privateConversation = privateConversationsByPair.get(pairKey) + + const responseMid = v4() + const mId = v4() + const mBody = `TestPM-${mId}-original` + const mBodyResponse = `${mBody}-Response` + + const waitPromise = new Promise((resolve) => { + senderSdk.onMessageEvent = (message) => { + if ( + (message.cid === privateConversation._id) && + (message.body === mBodyResponse) && + (message.from === recipientUser.native_id) + ) { + resolve() + } + } + }) + + recipientSdk.onMessageEvent = (message) => { + if ( + (message.cid === privateConversation._id) && + (message.body === mBody) && + (message.from === senderUser.native_id) + ) { + recipientSdk.messageCreate({ + cid: privateConversation._id, + body: mBodyResponse, + mid: responseMid, + }) + } + } + + + await senderSdk.messageCreate({ + cid: privateConversation._id, + body: mBody, + mid: mId, + }) + + await waitPromise + } +} + +describe("Multiple Clients", () => { + describe("Init", () => { + it("sdk array", async () => { + for (let i = 0; i < clientsCount; ++i) { + const deviceId = clientSdkDeviceIds.at(i) + + const { samaSdk, user } = await initSdkWithUser( + orgId, true, `TestUser-${i}`, deviceId, + wsEndpoint, httpEndpoint + ) + + assert.ok(samaSdk) + assert.ok(user) + + clientIdUserPairs.set(deviceId, user) + userSdkPairs.set(user, samaSdk) + } + }) + + it("create group conversation", async () => { + const users = Array.from(userSdkPairs.keys()) + const userIds = users.map(user => user.native_id) + const ownerSdk = userSdkPairs.get(users.at(0)) + const groupConversation = await createGroupConversation(ownerSdk, userIds) + + assert.ok(groupConversation) + + groupConv = groupConversation + }) + + it("create private conversations for each pair", async () => { + for (const user of userSdkPairs.keys()) { + const userAId = user.native_id + const samaSdk = userSdkPairs.get(user) + const otherUsers = Array.from(userSdkPairs.keys()).filter(user => user.native_id !== userAId) + + for (const user of otherUsers) { + const userBId = user.native_id + const pairKey = getPairKey(userAId, userBId) + const privateConversation = await createPrivateConversation(samaSdk, userAId, userBId) + + assert.ok(privateConversation) + privateConversationsByPair.set(pairKey, privateConversation) + } + } + }) + }) + + describe("Send messages", () => { + const resetSdkListeners = () => { + for (const sdk of userSdkPairs.values()) { + sdk.onMessageEvent = void 0 + } + } + + beforeEach(resetSdkListeners) + + clientSdkDeviceIds.forEach((deviceId, i) => { + it(`${deviceId} sender`, async () => { + const senderUser = clientIdUserPairs.get(deviceId) + const senderSdk = userSdkPairs.get(senderUser) + await sendGroupMessageWithMarkAble(senderSdk, groupConv._id) + await setTimeoutPromise(50) + }) + }) + + afterEach(resetSdkListeners) + }) + + describe("Send private messages", () => { + const resetSdkListeners = () => { + for (const sdk of userSdkPairs.values()) { + sdk.onMessageEvent = void 0 + } + } + + beforeEach(resetSdkListeners) + + clientSdkDeviceIds.forEach((deviceId, i) => { + it(`${deviceId} sender to private conversations`, async () => { + const senderUser = clientIdUserPairs.get(deviceId) + const senderSdk = userSdkPairs.get(senderUser) + await sendPrivateMessagesWithMarkAble(senderSdk, senderUser) + await setTimeoutPromise(50) + }) + }) + + afterEach(resetSdkListeners) + }) +}) \ No newline at end of file From 1b8f452868e174139fec9d5923575f084f8bd80e Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Wed, 22 Apr 2026 17:51:07 +0300 Subject: [PATCH 38/70] add check last activity --- test/cluster/cluster-clients.spec.js | 51 ++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/test/cluster/cluster-clients.spec.js b/test/cluster/cluster-clients.spec.js index 6536ab89..ef6c3a6f 100644 --- a/test/cluster/cluster-clients.spec.js +++ b/test/cluster/cluster-clients.spec.js @@ -118,6 +118,17 @@ const sendPrivateMessagesWithMarkAble = async (senderSdk, senderUser) => { } } +const retrieveAllLastActivity = async (senderUser) => { + const senderSdk = userSdkPairs.get(senderUser) + const userIds = Array.from(userSdkPairs.keys()).map(user => user.native_id) + + const lastActivity = await senderSdk.getUserActivity(userIds) + + for (const [userId, seconds] of Object.entries(lastActivity)) { + assert.ok(seconds === 0) + } +} + describe("Multiple Clients", () => { describe("Init", () => { it("sdk array", async () => { @@ -166,6 +177,26 @@ describe("Multiple Clients", () => { }) }) + describe("check last activity", () => { + const resetSdkListeners = () => { + for (const sdk of userSdkPairs.values()) { + sdk.onMessageEvent = void 0 + } + } + + beforeEach(resetSdkListeners) + + clientSdkDeviceIds.forEach((deviceId, i) => { + it(`${deviceId} check`, async () => { + const senderUser = clientIdUserPairs.get(deviceId) + await retrieveAllLastActivity(senderUser) + await setTimeoutPromise(50) + }) + }) + + afterEach(resetSdkListeners) + }) + describe("Send messages", () => { const resetSdkListeners = () => { for (const sdk of userSdkPairs.values()) { @@ -207,4 +238,24 @@ describe("Multiple Clients", () => { afterEach(resetSdkListeners) }) + + describe("check last activity", () => { + const resetSdkListeners = () => { + for (const sdk of userSdkPairs.values()) { + sdk.onMessageEvent = void 0 + } + } + + beforeEach(resetSdkListeners) + + clientSdkDeviceIds.forEach((deviceId, i) => { + it(`${deviceId} check`, async () => { + const senderUser = clientIdUserPairs.get(deviceId) + await retrieveAllLastActivity(senderUser) + await setTimeoutPromise(50) + }) + }) + + afterEach(resetSdkListeners) + }) }) \ No newline at end of file From a17c79406d2ccb2e28db8cd9bb436aff35fb1092 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 23 Apr 2026 17:56:29 +0300 Subject: [PATCH 39/70] add repl services --- .env.example | 10 +++ app/config/default.js | 13 ++++ app/constants/constants.js | 1 + app/lib/repl-tools.js | 140 +++++++++++++++++++++++++++++++++++++ index.js | 8 +++ 5 files changed, 172 insertions(+) create mode 100644 app/lib/repl-tools.js diff --git a/.env.example b/.env.example index 769c99f8..d23878aa 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,16 @@ MONGODB_URL=mongodb://127.0.0.1/samadb REDIS_URL=redis://127.0.0.1:6379 +# REPL +# http +APP_REPL_HTTP_PORT=5010 +APP_REPL_HTTP_ACCESS_KEY=ReplUwUKey +# socket +APP_REPL_SOCKET_HANDLER=/tmp/net-repl.socket +# file +APP_REPL_FILE_IN=/tmp/pipe-repl.in +APP_REPL_FILE_IN=/tmp/pipe-repl.out + STORAGE_DRIVER=minio # If you set STORAGE_DRIVER=s3, then fill the below envs diff --git a/app/config/default.js b/app/config/default.js index 5ea61b78..ad715ba8 100644 --- a/app/config/default.js +++ b/app/config/default.js @@ -102,6 +102,19 @@ const CONFIG = { apiKey: process.env.HTTP_ADMIN_API_KEY, }, }, + repl: { + http: { + port: +(process.env.APP_REPL_HTTP_PORT ?? 5010), + accessKey: process.env.APP_REPL_HTTP_ACCESS_KEY + }, + socket: { + handler: process.env.APP_REPL_SOCKET_HANDLER + }, + file: { + in: process.env.APP_REPL_FILE_IN, + out: process.env.APP_REPL_FILE_OUT + } + }, conversation: { disableChannelsLogic: process.env.CONVERSATION_DISABLE_CHANNELS_LOGIC === CONSTANTS.ENV_TRUE, isEventsEnabled: process.env.CONVERSATION_NOTIFICATIONS_ENABLED === CONSTANTS.ENV_TRUE, diff --git a/app/constants/constants.js b/app/constants/constants.js index 518b46a1..d16baa0e 100644 --- a/app/constants/constants.js +++ b/app/constants/constants.js @@ -18,6 +18,7 @@ export const CONSTANTS = { SESSION_DEVICE_ID_KEY: "DEVICE_ID", HTTP_DEVICE_ID: "SAMA_HTTP_CLIENT_DEVICE_ID", HTTP_ADMIN_API_KEY_HEADER: "admin-api-key", + HTTP_REPL_ACCESS_KEY_HEADER: "repl-access-key", CHAT_SUMMARY_FILTERS: { LAST_7_DAYS: "last-7-days", LAST_DAY: "last-day", diff --git a/app/lib/repl-tools.js b/app/lib/repl-tools.js new file mode 100644 index 00000000..09c9ea25 --- /dev/null +++ b/app/lib/repl-tools.js @@ -0,0 +1,140 @@ +import fs from "node:fs" +import fsPromises from "node:fs/promises" +import vm from "node:vm" +import { exec } from "node:child_process" +import repl from "node:repl" +import net from "node:net" +import http from "node:http" + +import { CONSTANTS } from "../constants/constants.js" + + +const httpReplService = async (replOptions, httpOptions) => { + const { ctx } = replOptions + const { accessKey, port } = httpOptions + + const server = http.createServer((req, res) => { + if (req.headers[CONSTANTS.HTTP_REPL_ACCESS_KEY_HEADER] !== accessKey) { + return res.end("Invalid api-key") + } + + res.setHeader('content-type', 'multipart/octet-stream') + + const replService = repl.start({ + prompt: 'curl repl> ', + input: req, + output: res, + terminal: false, + useColors: true, + useGlobal: false, + }) + + const context = vm.createContext({ ...ctx }) + + replService.context = context + + req.on("error", error => replService.close()) + req.on("end", () => replService.close()) + + res.on("error", error => replService.close()) + res.on("end", () => replService.close()) + + replService.on("close", () => !res.closed && res.end("REPL closed")) + replService.on("error", error => { + replService.close() + !res.closed && res.end("REPL closed") + !req.closed && req.destroy() + }) + }) + + return new Promise((resolve, reject) => { + server.listen(port, () => resolve(port)) + }) +} + +const fileReplService = async (replOptions, fileOptions) => { + const { ctx } = replOptions + const { fileIn, fileOut } = fileOptions + + await fsPromises.rm(fileIn, { force: true }).catch(error => {}) + await fsPromises.rm(fileOut, { force: true }).catch(error => {}) + + await new Promise((resolve, reject) => { + exec(`mkfifo ${fileIn}`, (error, out, outErr) => { + if (error) { + return reject(error) + } + resolve() + }) + }) + + const cmdInputPipe = fs.createReadStream(fileIn, { encoding: 'utf8' }) + const cmdOutPipe = fs.createWriteStream(fileOut, { encoding: 'utf8' }) + + const replService = repl.start({ + prompt: 'file repl> ', + input: cmdInputPipe, + output: cmdOutPipe, + terminal: false, + useColors: true, + useGlobal: false, + }) + + const context = vm.createContext({ ...ctx }) + + replService.context = context + + replService.on("error", error => { + replService.close() + cmdInputPipe.close() + }) + + cmdInputPipe.on("end", () => { + replService.close() + fileReplService(replOptions, fileOptions) + }) +} + +const netReplService = async (replOptions, fileOptions) => { + const { ctx } = replOptions + const { socketHandler } = fileOptions + + await fsPromises.rm(socketHandler, { force: true }).catch(error => {}) + + const server = net.createServer((socket) => { + console.log('[Socket]', ctx) + + const replService = repl.start({ + prompt: 'socket repl> ', + input: socket, + output: socket, + terminal: false, + useColors: true, + useGlobal: false, + }) + + const context = vm.createContext({ ...ctx }) + + replService.context = context + + socket.on("end", () => replService.close()) + socket.on("error", error => replService.close()) + replService.on("close", () => !socket.closed && socket.end("REPL closed")) + }) + + return new Promise((resolve, reject) => { + server.listen(socketHandler, () => resolve(socketHandler)) + }) +} + +export const startReplServices = async (replOptions, httpOptions, netOptions, fileOptions) => { + if (httpOptions.accessKey) { + await httpReplService(replOptions, httpOptions) + } + if (netOptions.socketHandler) { + await netReplService(replOptions, netOptions) + } + if (fileOptions.fileIn && fileOptions.fileOut) { + await fileReplService(replOptions, fileOptions) + } +} diff --git a/index.js b/index.js index 5348bd3d..0999dbf8 100644 --- a/index.js +++ b/index.js @@ -19,6 +19,7 @@ import HttpProtocol from "./app/networking/protocol_processors/http.js" import { connectToDBPromise } from "./app/lib/db.js" import RedisClient from "./app/lib/redis.js" import OTPSender from "./app/lib/otp_sender.js" +import { startReplServices } from "./app/lib/repl-tools.js" import { APIs } from "./app/networking/APIs.js" @@ -195,6 +196,13 @@ if (config.get("tcp.isEnabled")) { await tcpProtocolImp.listen(tcpOptions) } +await startReplServices( + { ctx: { slc: ServiceLocatorContainer } }, + { accessKey: config.get("repl.http.accessKey"), port: config.get("repl.http.port") }, + { socketHandler: config.get("repl.socket.handler") }, + { fileIn: config.get("repl.file.in"), fileOut: config.get("repl.file.out") } +) + logger.debug("[Ready] cluster-ws: %s", config.get("ws.cluster.endpoint")) // https://dev.to/mattkrick/replacing-express-with-uwebsockets-48ph From aff9f98b05006f9e5cf877a33640b722aa929589 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 23 Apr 2026 17:57:40 +0300 Subject: [PATCH 40/70] fix typo in env --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index d23878aa..46355aed 100644 --- a/.env.example +++ b/.env.example @@ -19,7 +19,7 @@ APP_REPL_HTTP_ACCESS_KEY=ReplUwUKey APP_REPL_SOCKET_HANDLER=/tmp/net-repl.socket # file APP_REPL_FILE_IN=/tmp/pipe-repl.in -APP_REPL_FILE_IN=/tmp/pipe-repl.out +APP_REPL_FILE_OUT=/tmp/pipe-repl.out STORAGE_DRIVER=minio From 8182ad364e505424a234206220c9b36e1df825fd Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 23 Apr 2026 18:22:32 +0300 Subject: [PATCH 41/70] fix env val --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 46355aed..29399100 100644 --- a/.env.example +++ b/.env.example @@ -14,7 +14,7 @@ REDIS_URL=redis://127.0.0.1:6379 # REPL # http APP_REPL_HTTP_PORT=5010 -APP_REPL_HTTP_ACCESS_KEY=ReplUwUKey +APP_REPL_HTTP_ACCESS_KEY=repl-wow-key # socket APP_REPL_SOCKET_HANDLER=/tmp/net-repl.socket # file From 58fa564c089da1701b0188993db4628fef7f25b7 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Fri, 24 Apr 2026 00:03:20 +0300 Subject: [PATCH 42/70] update --- app/config/default.js | 8 ++-- app/lib/repl-tools.js | 46 ++++++++++++----------- app/utils/promise-queue-with-jittering.js | 8 +++- test/cluster/cluster-clients.spec.js | 42 +++++++-------------- test/cluster/utils.js | 14 ++----- 5 files changed, 51 insertions(+), 67 deletions(-) diff --git a/app/config/default.js b/app/config/default.js index ad715ba8..319c7f70 100644 --- a/app/config/default.js +++ b/app/config/default.js @@ -105,15 +105,15 @@ const CONFIG = { repl: { http: { port: +(process.env.APP_REPL_HTTP_PORT ?? 5010), - accessKey: process.env.APP_REPL_HTTP_ACCESS_KEY + accessKey: process.env.APP_REPL_HTTP_ACCESS_KEY, }, socket: { - handler: process.env.APP_REPL_SOCKET_HANDLER + handler: process.env.APP_REPL_SOCKET_HANDLER, }, file: { in: process.env.APP_REPL_FILE_IN, - out: process.env.APP_REPL_FILE_OUT - } + out: process.env.APP_REPL_FILE_OUT, + }, }, conversation: { disableChannelsLogic: process.env.CONVERSATION_DISABLE_CHANNELS_LOGIC === CONSTANTS.ENV_TRUE, diff --git a/app/lib/repl-tools.js b/app/lib/repl-tools.js index 09c9ea25..707b3420 100644 --- a/app/lib/repl-tools.js +++ b/app/lib/repl-tools.js @@ -8,6 +8,7 @@ import http from "node:http" import { CONSTANTS } from "../constants/constants.js" +// example: curl --no-progress-meter -sSNT. -H "api-key: ****" localhost:5010 const httpReplService = async (replOptions, httpOptions) => { const { ctx } = replOptions @@ -15,13 +16,13 @@ const httpReplService = async (replOptions, httpOptions) => { const server = http.createServer((req, res) => { if (req.headers[CONSTANTS.HTTP_REPL_ACCESS_KEY_HEADER] !== accessKey) { - return res.end("Invalid api-key") + return res.end("Invalid access-key") } - res.setHeader('content-type', 'multipart/octet-stream') - + res.setHeader("content-type", "multipart/octet-stream") + const replService = repl.start({ - prompt: 'curl repl> ', + prompt: "curl repl> ", input: req, output: res, terminal: false, @@ -33,31 +34,34 @@ const httpReplService = async (replOptions, httpOptions) => { replService.context = context - req.on("error", error => replService.close()) + req.on("error", (error) => replService.close()) req.on("end", () => replService.close()) - res.on("error", error => replService.close()) + res.on("error", (error) => replService.close()) res.on("end", () => replService.close()) replService.on("close", () => !res.closed && res.end("REPL closed")) - replService.on("error", error => { + replService.on("error", (error) => { replService.close() !res.closed && res.end("REPL closed") !req.closed && req.destroy() }) }) - + return new Promise((resolve, reject) => { server.listen(port, () => resolve(port)) }) } +// example write: cat > ./pipe-repl.in +// example read: tail -f ./pipe-repl.out + const fileReplService = async (replOptions, fileOptions) => { const { ctx } = replOptions const { fileIn, fileOut } = fileOptions - await fsPromises.rm(fileIn, { force: true }).catch(error => {}) - await fsPromises.rm(fileOut, { force: true }).catch(error => {}) + await fsPromises.rm(fileIn, { force: true }).catch((error) => {}) + await fsPromises.rm(fileOut, { force: true }).catch((error) => {}) await new Promise((resolve, reject) => { exec(`mkfifo ${fileIn}`, (error, out, outErr) => { @@ -65,14 +69,14 @@ const fileReplService = async (replOptions, fileOptions) => { return reject(error) } resolve() - }) + }) }) - const cmdInputPipe = fs.createReadStream(fileIn, { encoding: 'utf8' }) - const cmdOutPipe = fs.createWriteStream(fileOut, { encoding: 'utf8' }) + const cmdInputPipe = fs.createReadStream(fileIn, { encoding: "utf8" }) + const cmdOutPipe = fs.createWriteStream(fileOut, { encoding: "utf8" }) const replService = repl.start({ - prompt: 'file repl> ', + prompt: "file repl> ", input: cmdInputPipe, output: cmdOutPipe, terminal: false, @@ -84,7 +88,7 @@ const fileReplService = async (replOptions, fileOptions) => { replService.context = context - replService.on("error", error => { + replService.on("error", (error) => { replService.close() cmdInputPipe.close() }) @@ -95,30 +99,30 @@ const fileReplService = async (replOptions, fileOptions) => { }) } +// example: nc -U ./net-repl.socket + const netReplService = async (replOptions, fileOptions) => { const { ctx } = replOptions const { socketHandler } = fileOptions - await fsPromises.rm(socketHandler, { force: true }).catch(error => {}) + await fsPromises.rm(socketHandler, { force: true }).catch((error) => {}) const server = net.createServer((socket) => { - console.log('[Socket]', ctx) - const replService = repl.start({ - prompt: 'socket repl> ', + prompt: "socket repl> ", input: socket, output: socket, terminal: false, useColors: true, useGlobal: false, }) - + const context = vm.createContext({ ...ctx }) replService.context = context socket.on("end", () => replService.close()) - socket.on("error", error => replService.close()) + socket.on("error", (error) => replService.close()) replService.on("close", () => !socket.closed && socket.end("REPL closed")) }) diff --git a/app/utils/promise-queue-with-jittering.js b/app/utils/promise-queue-with-jittering.js index b1dc71d3..748610f0 100644 --- a/app/utils/promise-queue-with-jittering.js +++ b/app/utils/promise-queue-with-jittering.js @@ -1,5 +1,9 @@ import { setTimeout } from "node:timers/promises" +import mainLogger from "../logger/index.js" + +const reconnectLogger = mainLogger.child("[Reconnect]") + class CancelQueueError extends Error {} const promiseQueueWithJittering = (executablePromise, tryCount, delay) => { @@ -21,11 +25,11 @@ const promiseQueueWithJittering = (executablePromise, tryCount, delay) => { let lastError = void 0 - console.log("[tryDelays]", tryDelays, isCanceled) + reconnectLogger.debug("[tryDelays] %j %s", tryDelays, isCanceled) for (const tryDelay of tryDelays) { try { - console.log("[try]", tryDelay, isCanceled) + reconnectLogger.debug("[try] %s %s", tryDelay, isCanceled) if (isCanceled) { throw new CancelQueueError("Canceled") diff --git a/test/cluster/cluster-clients.spec.js b/test/cluster/cluster-clients.spec.js index ef6c3a6f..d6e0f620 100644 --- a/test/cluster/cluster-clients.spec.js +++ b/test/cluster/cluster-clients.spec.js @@ -3,11 +3,7 @@ import { setTimeout as setTimeoutPromise } from "node:timers/promises" import { v4 } from "uuid" -import { - initSdkWithUser, - createGroupConversation, - createPrivateConversation, -} from "./utils.js" +import { initSdkWithUser, createGroupConversation, createPrivateConversation } from "./utils.js" const orgId = process.env.TEST_CLIENT_ORG_ID const wsEndpoint = process.env.TEST_CLIENT_WS_ENDPOINT @@ -33,22 +29,22 @@ const sendGroupMessageWithMarkAble = async (senderSdk, convId) => { const waitPromise = new Promise((resolve, reject) => { senderSdk.onMessageEvent = (message) => { - if ((message.cid === convId) && (message.body === mBodyResponse)) { + if (message.cid === convId && message.body === mBodyResponse) { responseMessagesUserIds.add(message.from) } - if (responseMessagesUserIds.size === (clientsCount - 1)) { + if (responseMessagesUserIds.size === clientsCount - 1) { senderSdk.onMessageEvent = void 0 resolve() } } }) - const recipientsSdks = Array.from(userSdkPairs.values()).filter(sdk => sdk !== senderSdk) + const recipientsSdks = Array.from(userSdkPairs.values()).filter((sdk) => sdk !== senderSdk) for (const sdk of recipientsSdks) { sdk.onMessageEvent = (message) => { - if ((message.cid === convId) && (message.body === mBody)) { + if (message.cid === convId && message.body === mBody) { sdk.messageCreate({ cid: convId, body: mBodyResponse, @@ -68,7 +64,7 @@ const sendGroupMessageWithMarkAble = async (senderSdk, convId) => { } const sendPrivateMessagesWithMarkAble = async (senderSdk, senderUser) => { - const recipients = Array.from(userSdkPairs.keys()).filter(user => user !== senderUser) + const recipients = Array.from(userSdkPairs.keys()).filter((user) => user !== senderUser) for (const recipientUser of recipients) { const recipientSdk = userSdkPairs.get(recipientUser) @@ -83,22 +79,14 @@ const sendPrivateMessagesWithMarkAble = async (senderSdk, senderUser) => { const waitPromise = new Promise((resolve) => { senderSdk.onMessageEvent = (message) => { - if ( - (message.cid === privateConversation._id) && - (message.body === mBodyResponse) && - (message.from === recipientUser.native_id) - ) { + if (message.cid === privateConversation._id && message.body === mBodyResponse && message.from === recipientUser.native_id) { resolve() } } }) recipientSdk.onMessageEvent = (message) => { - if ( - (message.cid === privateConversation._id) && - (message.body === mBody) && - (message.from === senderUser.native_id) - ) { + if (message.cid === privateConversation._id && message.body === mBody && message.from === senderUser.native_id) { recipientSdk.messageCreate({ cid: privateConversation._id, body: mBodyResponse, @@ -107,7 +95,6 @@ const sendPrivateMessagesWithMarkAble = async (senderSdk, senderUser) => { } } - await senderSdk.messageCreate({ cid: privateConversation._id, body: mBody, @@ -120,7 +107,7 @@ const sendPrivateMessagesWithMarkAble = async (senderSdk, senderUser) => { const retrieveAllLastActivity = async (senderUser) => { const senderSdk = userSdkPairs.get(senderUser) - const userIds = Array.from(userSdkPairs.keys()).map(user => user.native_id) + const userIds = Array.from(userSdkPairs.keys()).map((user) => user.native_id) const lastActivity = await senderSdk.getUserActivity(userIds) @@ -135,10 +122,7 @@ describe("Multiple Clients", () => { for (let i = 0; i < clientsCount; ++i) { const deviceId = clientSdkDeviceIds.at(i) - const { samaSdk, user } = await initSdkWithUser( - orgId, true, `TestUser-${i}`, deviceId, - wsEndpoint, httpEndpoint - ) + const { samaSdk, user } = await initSdkWithUser(orgId, true, `TestUser-${i}`, deviceId, wsEndpoint, httpEndpoint) assert.ok(samaSdk) assert.ok(user) @@ -150,7 +134,7 @@ describe("Multiple Clients", () => { it("create group conversation", async () => { const users = Array.from(userSdkPairs.keys()) - const userIds = users.map(user => user.native_id) + const userIds = users.map((user) => user.native_id) const ownerSdk = userSdkPairs.get(users.at(0)) const groupConversation = await createGroupConversation(ownerSdk, userIds) @@ -163,7 +147,7 @@ describe("Multiple Clients", () => { for (const user of userSdkPairs.keys()) { const userAId = user.native_id const samaSdk = userSdkPairs.get(user) - const otherUsers = Array.from(userSdkPairs.keys()).filter(user => user.native_id !== userAId) + const otherUsers = Array.from(userSdkPairs.keys()).filter((user) => user.native_id !== userAId) for (const user of otherUsers) { const userBId = user.native_id @@ -258,4 +242,4 @@ describe("Multiple Clients", () => { afterEach(resetSdkListeners) }) -}) \ No newline at end of file +}) diff --git a/test/cluster/utils.js b/test/cluster/utils.js index 7d0637ef..cc1283b2 100644 --- a/test/cluster/utils.js +++ b/test/cluster/utils.js @@ -129,11 +129,7 @@ export const createOrganizationId = async (endpoint, name) => { return organizationId } -export const initSdkWithUser = async ( - organizationId, needLogin, - loginPrefix, deviceIdPrefix = 'dv', - endpointWs, endpointHttp -) => { +export const initSdkWithUser = async (organizationId, needLogin, loginPrefix, deviceIdPrefix = "dv", endpointWs, endpointHttp) => { const sdkConfig = { endpoint: { ws: endpointWs, @@ -157,7 +153,7 @@ export const initSdkWithUser = async ( return { samaSdk, user } } -export const createUser = async (samaSdk, loginPrefix = 'TestUser') => { +export const createUser = async (samaSdk, loginPrefix = "TestUser") => { const login = `${loginPrefix}-${faker.internet.username()}` const user = await samaSdk.userCreate({ @@ -192,11 +188,7 @@ export const createGroupConversation = async (samaSdk, userIds) => { export const createDummyData = async (endpointWs, endpointHttp) => { dummyDataTestConfig.organizationId = await createOrganizationId(endpointHttp) - const { samaSdk, user: userA } = await initSdkWithUser( - dummyDataTestConfig.organizationId, true, - void 0, void 0, - endpointWs, endpointHttp, - ) + const { samaSdk, user: userA } = await initSdkWithUser(dummyDataTestConfig.organizationId, true, void 0, void 0, endpointWs, endpointHttp) dummyDataTestConfig.users.userA.login = userA.login dummyDataTestConfig.users.userA.nativeId = userA.native_id From 20b12969a65aa784678bbf8a939c319bbc6ce0f5 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Fri, 24 Apr 2026 13:28:27 +0300 Subject: [PATCH 43/70] add netcat install to dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 19685472..ca0a01e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ COPY . . FROM node:22-slim RUN apt-get update && apt-get install -y --no-install-recommends \ - curl \ + curl netcat-openbsd \ && rm -rf /var/lib/apt/lists/* WORKDIR /app From d79e6fcf66fb83ca78180fa10741a8fc84229c11 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Mon, 11 May 2026 15:13:09 +0300 Subject: [PATCH 44/70] update addUserDeviceConnection --- APIs/XMPP | 2 +- .../http/activity/online_list/index.js | 2 +- .../operations/http/message/create/index.js | 2 +- .../operations/http/message/delete/index.js | 2 +- .../operations/http/message/edit/index.js | 2 +- .../operations/http/message/reaction/index.js | 2 +- .../operations/http/message/read/index.js | 2 +- .../operations/http/message/system/index.js | 2 +- app/providers/operations/user/auth/index.js | 2 +- .../operations/user/connect/index.js | 2 +- .../activity_manager_cluster/index.js | 4 +- .../activity_manager_standalone/index.js | 4 +- app/providers/services/session/index.js | 62 +++++++++---------- 13 files changed, 44 insertions(+), 46 deletions(-) diff --git a/APIs/XMPP b/APIs/XMPP index 0210f07b..43c78c11 160000 --- a/APIs/XMPP +++ b/APIs/XMPP @@ -1 +1 @@ -Subproject commit 0210f07bf51005d72279c7278e3b196f069d13a4 +Subproject commit 43c78c1118315944c6a684e3aa808953dc83ea86 diff --git a/app/providers/operations/http/activity/online_list/index.js b/app/providers/operations/http/activity/online_list/index.js index 8e451227..01a8b632 100644 --- a/app/providers/operations/http/activity/online_list/index.js +++ b/app/providers/operations/http/activity/online_list/index.js @@ -9,7 +9,7 @@ class HttpActivityOnlineListOperation { async perform(fakeWsSessionKey, payload) { const { organizationId, userId, ...requestData } = payload - this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, userId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + await this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, userId, MAIN_CONSTANTS.HTTP_DEVICE_ID) const operationResponse = await this.onlineListOperation.perform(fakeWsSessionKey, requestData) diff --git a/app/providers/operations/http/message/create/index.js b/app/providers/operations/http/message/create/index.js index fc3e6125..c2fd3bc1 100644 --- a/app/providers/operations/http/message/create/index.js +++ b/app/providers/operations/http/message/create/index.js @@ -9,7 +9,7 @@ class HttpMessageCreateOperation { async perform(fakeWsSessionKey, payload) { const { organizationId, senderId, message: messageParams } = payload - this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + await this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) const operationResponse = await this.messageCreateOperation.perform(fakeWsSessionKey, messageParams) diff --git a/app/providers/operations/http/message/delete/index.js b/app/providers/operations/http/message/delete/index.js index e10d6da7..2b44cdca 100644 --- a/app/providers/operations/http/message/delete/index.js +++ b/app/providers/operations/http/message/delete/index.js @@ -9,7 +9,7 @@ class HttpMessageDeleteOperation { async perform(fakeWsSessionKey, payload) { const { organizationId, senderId, messageDelete: messageDeleteParams } = payload - this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + await this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) const operationResponse = await this.messageDeleteOperation.perform(fakeWsSessionKey, messageDeleteParams) diff --git a/app/providers/operations/http/message/edit/index.js b/app/providers/operations/http/message/edit/index.js index c3197c97..b60618ff 100644 --- a/app/providers/operations/http/message/edit/index.js +++ b/app/providers/operations/http/message/edit/index.js @@ -9,7 +9,7 @@ class HttpMessageEditOperation { async perform(fakeWsSessionKey, payload) { const { organizationId, senderId, messageEdit: messageEditParams } = payload - this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + await this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) const operationResponse = await this.messageEditOperation.perform(fakeWsSessionKey, messageEditParams) diff --git a/app/providers/operations/http/message/reaction/index.js b/app/providers/operations/http/message/reaction/index.js index 355fd7d4..f819def2 100644 --- a/app/providers/operations/http/message/reaction/index.js +++ b/app/providers/operations/http/message/reaction/index.js @@ -9,7 +9,7 @@ class HttpMessageReactionOperation { async perform(fakeWsSessionKey, payload) { const { organizationId, senderId, messageReaction: messageReaction } = payload - this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + await this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) const operationResponse = await this.messageReactionsUpdateOperation.perform(fakeWsSessionKey, messageReaction) diff --git a/app/providers/operations/http/message/read/index.js b/app/providers/operations/http/message/read/index.js index d779834f..aafc414b 100644 --- a/app/providers/operations/http/message/read/index.js +++ b/app/providers/operations/http/message/read/index.js @@ -9,7 +9,7 @@ class HttpMessageReadOperation { async perform(fakeWsSessionKey, payload) { const { organizationId, senderId, messageRead: messageReadParams } = payload - this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + await this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) const operationResponse = await this.messageReadOperation.perform(fakeWsSessionKey, messageReadParams) diff --git a/app/providers/operations/http/message/system/index.js b/app/providers/operations/http/message/system/index.js index 91180926..5b3c4219 100644 --- a/app/providers/operations/http/message/system/index.js +++ b/app/providers/operations/http/message/system/index.js @@ -9,7 +9,7 @@ class HttpMessageSendSystemOperation { async perform(fakeWsSessionKey, payload) { const { organizationId, senderId, messageSystem: systemMessageParams } = payload - this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + await this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) const operationResponse = await this.messageSendSystemOperation.perform(fakeWsSessionKey, systemMessageParams) diff --git a/app/providers/operations/user/auth/index.js b/app/providers/operations/user/auth/index.js index 0650d9f0..a71a6ef5 100644 --- a/app/providers/operations/user/auth/index.js +++ b/app/providers/operations/user/auth/index.js @@ -20,7 +20,7 @@ class UserAuthOperation { // TODO: close connections if (!omitDeviceConnection) { - const wsToClose = this.sessionService.addUserDeviceConnection(ws, user.organization_id, user.native_id, deviceId) + await this.sessionService.addUserDeviceConnection(ws, user.organization_id, user.native_id, deviceId) } const jwtAccessToken = this.#generateToken( diff --git a/app/providers/operations/user/connect/index.js b/app/providers/operations/user/connect/index.js index 75e8525f..8044cbb4 100644 --- a/app/providers/operations/user/connect/index.js +++ b/app/providers/operations/user/connect/index.js @@ -21,7 +21,7 @@ class UserConnectSocketOperation { const user = await this.userService.userRepo.findById(token.user_id) // TODO: close connections - this.sessionService.addUserDeviceConnection(ws, user.organization_id, user.native_id, deviceId) + await this.sessionService.addUserDeviceConnection(ws, user.organization_id, user.native_id, deviceId) await this.sessionService.storeUserNodeData(ws, user.organization_id, user.native_id, deviceId) diff --git a/app/providers/services/activity_manager/activity_manager_cluster/index.js b/app/providers/services/activity_manager/activity_manager_cluster/index.js index efac9f12..b370b782 100644 --- a/app/providers/services/activity_manager/activity_manager_cluster/index.js +++ b/app/providers/services/activity_manager/activity_manager_cluster/index.js @@ -102,9 +102,9 @@ class ActivityManagerClusterService { async updateUserActivity(userId, status) { const currentTime = Math.round(new Date() / 1000) - if (status !== MAIN_CONSTANTS.LAST_ACTIVITY_STATUS.ONLINE) { - await this.userService.updateActivity(userId, currentTime) + await this.userService.updateActivity(userId, currentTime) + if (status !== MAIN_CONSTANTS.LAST_ACTIVITY_STATUS.ONLINE) { await this.unsubscribeObserver(userId) } diff --git a/app/providers/services/activity_manager/activity_manager_standalone/index.js b/app/providers/services/activity_manager/activity_manager_standalone/index.js index 6c762bd4..c8ee3775 100644 --- a/app/providers/services/activity_manager/activity_manager_standalone/index.js +++ b/app/providers/services/activity_manager/activity_manager_standalone/index.js @@ -63,9 +63,9 @@ class ActivityManagerStandaloneService { async updateUserActivity(userId, status) { const currentTime = Math.round(new Date() / 1000) - if (status !== MAIN_CONSTANTS.LAST_ACTIVITY_STATUS.ONLINE) { - await this.userService.updateActivity(userId, currentTime) + await this.userService.updateActivity(userId, currentTime) + if (status !== MAIN_CONSTANTS.LAST_ACTIVITY_STATUS.ONLINE) { this.unsubscribeObserver(userId) } diff --git a/app/providers/services/session/index.js b/app/providers/services/session/index.js index 86bbbcaa..0fa5853d 100644 --- a/app/providers/services/session/index.js +++ b/app/providers/services/session/index.js @@ -18,29 +18,28 @@ class SessionService { return this.activeSessions.SESSIONS.size } - addUserDeviceConnection(socket, organizationId, userId, deviceId) { - const activeConnections = this.activeSessions.DEVICES[userId] - const socketsToClose = [] + async addUserDeviceConnection(socket, organizationId, userId, deviceId) { + const activeConnections = this.getUserDevices(userId) + + const sameSocketConnection = activeConnections.find(connection => connection.socket === socket) + const sameDeviceConnection = activeConnections.find(connection => connection.deviceId === deviceId) + + const otherDeviceConnections = activeConnections.filter(connection => + (connection !== sameSocketConnection) && + (connection !== sameDeviceConnection) + ) const connection = { socket: socket, deviceId, organizationId } - if (activeConnections) { - const devices = activeConnections.filter((connection) => { - if (connection.deviceId !== deviceId) { - return true - } else { - socketsToClose.push(connection.socket) - return false - } - }) - this.activeSessions.DEVICES[userId] = [...devices, connection] - } else { - this.activeSessions.DEVICES[userId] = [connection] - } + this.activeSessions.DEVICES[userId] = [...otherDeviceConnections, connection] this.setSessionUserId(socket, organizationId, userId, { [CONSTANTS.SESSION_DEVICE_ID_KEY]: deviceId }) + + if (sameSocketConnection) { + await this.removeAllUserDeviceData(organizationId, userId, sameSocketConnection.deviceId) + } - return socketsToClose + return sameDeviceConnection } #nodesSetCacheKey(nodeEndpoint) { @@ -168,6 +167,18 @@ class SessionService { await this.deleteUserDevices(organizationId, userId) } + async removeAllUserDeviceData(organizationId, userId, deviceId) { + const isLastConnection = await this.removeUserDevice(organizationId, userId, deviceId) + const extraParams = await this.retrieveUserExtraParams(userId, deviceId) + await this.deleteUserExtraParams(userId, deviceId) + + const nodeEndpoint = extraParams?.[CONSTANTS.SESSION_NODE_KEY] ?? this.config.get("ws.cluster.endpoint") + + await this.removeUserDeviceFromNode(nodeEndpoint, organizationId, userId, deviceId) + + return isLastConnection + } + async listUserData(organizationId, userId) { const userData = {} @@ -206,11 +217,7 @@ class SessionService { const userDeviceIds = await this.listUserDevice(organizationId, userId) if (userDeviceIds.includes(deviceId)) { - const extraParams = await this.retrieveUserExtraParams(userId, deviceId) - const oldNodeEndpointKey = extraParams?.[CONSTANTS.SESSION_NODE_KEY] || nodeEndpoint - - await this.removeUserData(organizationId, userId, deviceId) - await this.removeUserDeviceFromNode(oldNodeEndpointKey, organizationId, userId, deviceId) + await this.removeAllUserDeviceData(organizationId, userId, deviceId) } await this.addUserDevice(organizationId, userId, deviceId) @@ -348,16 +355,7 @@ class SessionService { if (this.config.get("app.isStandAloneNode")) return isLastConnection - isLastConnection = await this.removeUserDevice(organizationId, userId, deviceId) - const extraParams = await this.retrieveUserExtraParams(userId, deviceId) - await this.deleteUserExtraParams(userId, deviceId) - - const nodeEndpoint = extraParams?.[CONSTANTS.SESSION_NODE_KEY] - if (!nodeEndpoint) { - return isLastConnection - } - - await this.removeUserDeviceFromNode(nodeEndpoint, organizationId, userId, deviceId) + isLastConnection = await this.removeAllUserDeviceData(organizationId, userId, deviceId) return isLastConnection } From abb0a39e7c6620497355779da3d6ff12d8edd2ef Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Tue, 12 May 2026 15:39:53 +0300 Subject: [PATCH 45/70] add keep alive --- app/networking/protocol_processors/tcp.js | 5 ++++- app/utils/sockets-utils.js | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/networking/protocol_processors/tcp.js b/app/networking/protocol_processors/tcp.js index b1fa8fef..59ca5e44 100644 --- a/app/networking/protocol_processors/tcp.js +++ b/app/networking/protocol_processors/tcp.js @@ -176,7 +176,10 @@ class TcpProtocol extends BaseProtocolProcessor { this.prepareSocketsListeners() return new Promise((resolve) => { - this.tcpSocketServer = net.createServer((socket) => { + this.tcpSocketServer = net.createServer({ + keepAlive: true, + keepAliveInitialDelay: 5_000 + }, (socket) => { asyncLoggerContextStore.run(this.requestCreateStoreContext(socket), () => { this.onOpen(socket) }) diff --git a/app/utils/sockets-utils.js b/app/utils/sockets-utils.js index 50d654b3..d8f50ad0 100644 --- a/app/utils/sockets-utils.js +++ b/app/utils/sockets-utils.js @@ -6,7 +6,7 @@ export async function tcpSafeSend(socket, data) { socket.write(data, (error) => (error ? reject(error) : resolve())) }) } catch (error) { - logger.error(error) + logger.error(error, "[tcp write]") } } @@ -14,7 +14,7 @@ export function wsSafeSend(ws, data) { try { ws.send(data) } catch (error) { - logger.error(error) + logger.error(error, "[ws send]") } } From 44a6798c84d69b7702db5bee3d5751f45710170a Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Tue, 12 May 2026 17:06:24 +0300 Subject: [PATCH 46/70] fix typo --- app/providers/services/session/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/providers/services/session/index.js b/app/providers/services/session/index.js index 6b2eed17..e8b95035 100644 --- a/app/providers/services/session/index.js +++ b/app/providers/services/session/index.js @@ -345,7 +345,7 @@ class SessionService { deviceId = deviceId ?? this.getDeviceId(socket, userId) const organizationId = this.getSession(socket)?.organizationId - this.logger.debug("[removeUserSession][vars]: %o [session]: %o [device]: %s", { orgId, userId, deviceId }, this.getSession(socket), this.getDeviceId(socket, userId)) + this.logger.debug("[removeUserSession][vars]: %o [session]: %o [device]: %s", { organizationId, userId, deviceId }, this.getSession(socket), this.getDeviceId(socket, userId)) const devicesBefore = this.getUserDevices(userId).map((connection) => { const { socket, ...connectionData } = connection From b8042ac6d050f932402bb6100718623282d06ae3 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 14 May 2026 12:35:04 +0300 Subject: [PATCH 47/70] update submodule --- APIs/XMPP | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/APIs/XMPP b/APIs/XMPP index 54347655..36969f43 160000 --- a/APIs/XMPP +++ b/APIs/XMPP @@ -1 +1 @@ -Subproject commit 54347655167dfe5bf5b7f5a28d7c9d486a92e356 +Subproject commit 36969f431c618092858eb9716632a2cdec4a09a6 From 3eab3591b63187acf147d2ffb03d45cf3351f2e3 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Mon, 25 May 2026 14:09:49 +0300 Subject: [PATCH 48/70] use res instead of res.fakeWsSessionKey --- APIs/JSON/controllers/http/activity.js | 2 +- APIs/JSON/controllers/http/auth.js | 4 ++-- APIs/JSON/controllers/http/message.js | 15 ++++++--------- APIs/JSON/controllers/http/organization.js | 2 +- APIs/JSON/controllers/http/stats.js | 2 +- app/networking/protocol_processors/http.js | 14 ++++++-------- .../operations/http/activity/online_list/index.js | 6 +++--- .../operations/http/message/create/index.js | 6 +++--- .../operations/http/message/delete/index.js | 6 +++--- .../operations/http/message/edit/index.js | 6 +++--- .../operations/http/message/reaction/index.js | 6 +++--- .../operations/http/message/read/index.js | 6 +++--- .../operations/http/message/system/index.js | 6 +++--- .../operations/http/organization/create/index.js | 2 +- .../operations/http/stats/collect/index.js | 2 +- app/providers/operations/http/user/auth/index.js | 4 ++-- .../operations/http/user/logout/index.js | 2 +- app/utils/watchdog-ping-socket.js | 3 ++- 18 files changed, 45 insertions(+), 49 deletions(-) diff --git a/APIs/JSON/controllers/http/activity.js b/APIs/JSON/controllers/http/activity.js index be5589b2..9a8c166c 100644 --- a/APIs/JSON/controllers/http/activity.js +++ b/APIs/JSON/controllers/http/activity.js @@ -13,7 +13,7 @@ class HttpActivityController extends BaseHttpController { const response = new Response() const HttpActivityOnlineListOperation = ServiceLocatorContainer.use("HttpActivityOnlineListOperation") - const onlineListResponse = await HttpActivityOnlineListOperation.perform(res.fakeWsSessionKey, payload) + const onlineListResponse = await HttpActivityOnlineListOperation.perform(res, payload) const responsePayload = isCountRequest ? { count: onlineListResponse } : { users: onlineListResponse } diff --git a/APIs/JSON/controllers/http/auth.js b/APIs/JSON/controllers/http/auth.js index 552043f5..f132c929 100644 --- a/APIs/JSON/controllers/http/auth.js +++ b/APIs/JSON/controllers/http/auth.js @@ -16,7 +16,7 @@ class HttpAuthController extends BaseHttpController { const httpAuthOperation = ServiceLocatorContainer.use("HttpUserAuthOperation") const { user, newAccessToken, accessTokenExpiredAt, newRefreshToken } = await httpAuthOperation.perform( - res.fakeWsSessionKey, + res, res.parsedHeaders, res.parsedSignedCookies, payload @@ -44,7 +44,7 @@ class HttpAuthController extends BaseHttpController { const httpLogoutOperation = ServiceLocatorContainer.use("HttpUserLogoutOperation") const { refreshTokenRecord, isWasLastUserSession } = await httpLogoutOperation.perform( - res.fakeWsSessionKey, + res, res.parsedHeaders, res.parsedSignedCookies ) diff --git a/APIs/JSON/controllers/http/message.js b/APIs/JSON/controllers/http/message.js index a6e74f25..e4ecb56e 100644 --- a/APIs/JSON/controllers/http/message.js +++ b/APIs/JSON/controllers/http/message.js @@ -20,7 +20,7 @@ class HttpMessageController extends BaseHttpController { const response = new Response() const httpMessageCreateOperation = ServiceLocatorContainer.use("HttpMessageCreateOperation") - const createMessageResponse = await httpMessageCreateOperation.perform(res.fakeWsSessionKey, payload) + const createMessageResponse = await httpMessageCreateOperation.perform(res, payload) const { organizationId, messageId, message, deliverMessages, cId, participantIds, modifiedFields, botMessage } = createMessageResponse deliverMessages.forEach((event) => { @@ -48,7 +48,7 @@ class HttpMessageController extends BaseHttpController { const payload = res.parsedBody const messageSendSystemOperation = ServiceLocatorContainer.use("HttpMessageSendSystemOperation") - const { organizationId, cId, recipientsIds, systemMessage } = await messageSendSystemOperation.perform(res.fakeWsSessionKey, payload) + const { organizationId, cId, recipientsIds, systemMessage } = await messageSendSystemOperation.perform(res, payload) const deliverMessage = new DeliverMessage(organizationId, new SystemMessageResponse(systemMessage), true) .setConversationDestination(cId) @@ -63,7 +63,7 @@ class HttpMessageController extends BaseHttpController { const payload = res.parsedBody const messageReadOperation = ServiceLocatorContainer.use("HttpMessageReadOperation") - const { organizationId, readMessagesGroups } = await messageReadOperation.perform(res.fakeWsSessionKey, payload) + const { organizationId, readMessagesGroups } = await messageReadOperation.perform(res, payload) const response = new Response() @@ -79,7 +79,7 @@ class HttpMessageController extends BaseHttpController { const payload = res.parsedBody const messageEditOperation = ServiceLocatorContainer.use("HttpMessageEditOperation") - const { organizationId, cId, participantsIds, editedMessage } = await messageEditOperation.perform(res.fakeWsSessionKey, payload) + const { organizationId, cId, participantsIds, editedMessage } = await messageEditOperation.perform(res, payload) return new Response() .setHttpResponse(new HttpResponse(200, {}, { success: true })) @@ -94,10 +94,7 @@ class HttpMessageController extends BaseHttpController { const payload = res.parsedBody const messageReactionOperation = ServiceLocatorContainer.use("HttpMessageReactionOperation") - const { organizationId, cId, participantsIds, messageReactionsUpdate } = await messageReactionOperation.perform( - res.fakeWsSessionKey, - payload - ) + const { organizationId, cId, participantsIds, messageReactionsUpdate } = await messageReactionOperation.perform(res, payload) return new Response() .setHttpResponse(new HttpResponse(200, {}, { success: true })) @@ -112,7 +109,7 @@ class HttpMessageController extends BaseHttpController { const payload = res.parsedBody const messageDeleteOperation = ServiceLocatorContainer.use("HttpMessageDeleteOperation") - const { organizationId, cId, participantsIds, deletedMessages } = await messageDeleteOperation.perform(res.fakeWsSessionKey, payload) + const { organizationId, cId, participantsIds, deletedMessages } = await messageDeleteOperation.perform(res, payload) const response = new Response() diff --git a/APIs/JSON/controllers/http/organization.js b/APIs/JSON/controllers/http/organization.js index b401fe77..8a00cb71 100644 --- a/APIs/JSON/controllers/http/organization.js +++ b/APIs/JSON/controllers/http/organization.js @@ -12,7 +12,7 @@ class HttpOrganizationController extends BaseHttpController { const response = new Response() const httpOrganizationCreateOperation = ServiceLocatorContainer.use("HttpOrganizationCreateOperation") - const organization = await httpOrganizationCreateOperation.perform(res.fakeWsSessionKey, payload) + const organization = await httpOrganizationCreateOperation.perform(res, payload) return response.setHttpResponse(new HttpResponse(200, {}, { organization: organization.visibleParams() })) } diff --git a/APIs/JSON/controllers/http/stats.js b/APIs/JSON/controllers/http/stats.js index 162b3331..5c781c57 100644 --- a/APIs/JSON/controllers/http/stats.js +++ b/APIs/JSON/controllers/http/stats.js @@ -12,7 +12,7 @@ class HttpServerStatsController extends BaseHttpController { const response = new Response() const httpOrganizationCreateOperation = ServiceLocatorContainer.use("HttpStatsCollectOperation") - const stats = await httpOrganizationCreateOperation.perform(res.fakeWsSessionKey, payload) + const stats = await httpOrganizationCreateOperation.perform(res, payload) return response.setHttpResponse(new HttpResponse(200, {}, stats)) } diff --git a/app/networking/protocol_processors/http.js b/app/networking/protocol_processors/http.js index 674ec185..061fc955 100644 --- a/app/networking/protocol_processors/http.js +++ b/app/networking/protocol_processors/http.js @@ -21,8 +21,6 @@ import { asyncLoggerContextStore, createStore, updateStoreContext } from "@sama/ const logger = mainLogger.child("[Http]") const parseBaseParamsMiddleware = async (res, req) => { - res.fakeWsSessionKey = Symbol("Http ws fake session") - res.parsedHeaders = {} req.forEach((headerName, value) => { @@ -131,13 +129,13 @@ class HttpProtocol extends BaseProtocolProcessor { } } - async unbindSession(wsKey) { - const session = this.sessionService.getSession(wsKey) + async unbindSession(res) { + const session = this.sessionService.getSession(res) if (!session?.userId) { return } - await this.sessionService.removeUserSession(wsKey, session.userId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + await this.sessionService.removeUserSession(res, session.userId, MAIN_CONSTANTS.HTTP_DEVICE_ID) } async processHttpResponseMiddleware(res, req, handlerResponse) { @@ -176,7 +174,7 @@ class HttpProtocol extends BaseProtocolProcessor { emptyBody ? res.endWithoutBody() : res.end(bodyStr) }) - this.processAPIResponse(res.fakeWsSessionKey, handlerResponse, true) + this.processAPIResponse(res, handlerResponse, true) .then(() => logger.trace("[processAPIResponse][success]")) .catch((error) => logger.error(error, "[processAPIResponse][Error]")) } @@ -219,13 +217,13 @@ class HttpProtocol extends BaseProtocolProcessor { res.end(error.message ?? ERROR_STATUES.INTERNAL_SERVER.message) }) } finally { - await this.unbindSession(res.fakeWsSessionKey) + await this.unbindSession(res) } } onHttpRequestHandler(preMiddleware = [], handler) { return (res, req) => { - asyncLoggerContextStore.run(this.requestCreateStoreContext(res), () => { + asyncLoggerContextStore.run(this.requestCreateStoreContext(this.extendSocket(res)), () => { return this.requestHandler(req, res, preMiddleware, handler) }) } diff --git a/app/providers/operations/http/activity/online_list/index.js b/app/providers/operations/http/activity/online_list/index.js index 01a8b632..e79c306a 100644 --- a/app/providers/operations/http/activity/online_list/index.js +++ b/app/providers/operations/http/activity/online_list/index.js @@ -6,12 +6,12 @@ class HttpActivityOnlineListOperation { this.onlineListOperation = onlineListOperation } - async perform(fakeWsSessionKey, payload) { + async perform(res, payload) { const { organizationId, userId, ...requestData } = payload - await this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, userId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + await this.sessionService.addUserDeviceConnection(res, organizationId, userId, MAIN_CONSTANTS.HTTP_DEVICE_ID) - const operationResponse = await this.onlineListOperation.perform(fakeWsSessionKey, requestData) + const operationResponse = await this.onlineListOperation.perform(res, requestData) return operationResponse } diff --git a/app/providers/operations/http/message/create/index.js b/app/providers/operations/http/message/create/index.js index c2fd3bc1..2c55efd0 100644 --- a/app/providers/operations/http/message/create/index.js +++ b/app/providers/operations/http/message/create/index.js @@ -6,12 +6,12 @@ class HttpMessageCreateOperation { this.messageCreateOperation = messageCreateOperation } - async perform(fakeWsSessionKey, payload) { + async perform(res, payload) { const { organizationId, senderId, message: messageParams } = payload - await this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + await this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) - const operationResponse = await this.messageCreateOperation.perform(fakeWsSessionKey, messageParams) + const operationResponse = await this.messageCreateOperation.perform(res, messageParams) return operationResponse } diff --git a/app/providers/operations/http/message/delete/index.js b/app/providers/operations/http/message/delete/index.js index 2b44cdca..da3e5a14 100644 --- a/app/providers/operations/http/message/delete/index.js +++ b/app/providers/operations/http/message/delete/index.js @@ -6,12 +6,12 @@ class HttpMessageDeleteOperation { this.messageDeleteOperation = messageDeleteOperation } - async perform(fakeWsSessionKey, payload) { + async perform(res, payload) { const { organizationId, senderId, messageDelete: messageDeleteParams } = payload - await this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + await this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) - const operationResponse = await this.messageDeleteOperation.perform(fakeWsSessionKey, messageDeleteParams) + const operationResponse = await this.messageDeleteOperation.perform(res, messageDeleteParams) return operationResponse } diff --git a/app/providers/operations/http/message/edit/index.js b/app/providers/operations/http/message/edit/index.js index b60618ff..3546bc7e 100644 --- a/app/providers/operations/http/message/edit/index.js +++ b/app/providers/operations/http/message/edit/index.js @@ -6,12 +6,12 @@ class HttpMessageEditOperation { this.messageEditOperation = messageEditOperation } - async perform(fakeWsSessionKey, payload) { + async perform(res, payload) { const { organizationId, senderId, messageEdit: messageEditParams } = payload - await this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + await this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) - const operationResponse = await this.messageEditOperation.perform(fakeWsSessionKey, messageEditParams) + const operationResponse = await this.messageEditOperation.perform(res, messageEditParams) return operationResponse } diff --git a/app/providers/operations/http/message/reaction/index.js b/app/providers/operations/http/message/reaction/index.js index f819def2..1b1fb93e 100644 --- a/app/providers/operations/http/message/reaction/index.js +++ b/app/providers/operations/http/message/reaction/index.js @@ -6,12 +6,12 @@ class HttpMessageReactionOperation { this.messageReactionsUpdateOperation = messageReactionsUpdateOperation } - async perform(fakeWsSessionKey, payload) { + async perform(res, payload) { const { organizationId, senderId, messageReaction: messageReaction } = payload - await this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + await this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) - const operationResponse = await this.messageReactionsUpdateOperation.perform(fakeWsSessionKey, messageReaction) + const operationResponse = await this.messageReactionsUpdateOperation.perform(res, messageReaction) return operationResponse } diff --git a/app/providers/operations/http/message/read/index.js b/app/providers/operations/http/message/read/index.js index aafc414b..dc16c2fb 100644 --- a/app/providers/operations/http/message/read/index.js +++ b/app/providers/operations/http/message/read/index.js @@ -6,12 +6,12 @@ class HttpMessageReadOperation { this.messageReadOperation = messageReadOperation } - async perform(fakeWsSessionKey, payload) { + async perform(res, payload) { const { organizationId, senderId, messageRead: messageReadParams } = payload - await this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + await this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) - const operationResponse = await this.messageReadOperation.perform(fakeWsSessionKey, messageReadParams) + const operationResponse = await this.messageReadOperation.perform(res, messageReadParams) return operationResponse } diff --git a/app/providers/operations/http/message/system/index.js b/app/providers/operations/http/message/system/index.js index 5b3c4219..e2dcce3c 100644 --- a/app/providers/operations/http/message/system/index.js +++ b/app/providers/operations/http/message/system/index.js @@ -6,12 +6,12 @@ class HttpMessageSendSystemOperation { this.messageSendSystemOperation = messageSendSystemOperation } - async perform(fakeWsSessionKey, payload) { + async perform(res, payload) { const { organizationId, senderId, messageSystem: systemMessageParams } = payload - await this.sessionService.addUserDeviceConnection(fakeWsSessionKey, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + await this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) - const operationResponse = await this.messageSendSystemOperation.perform(fakeWsSessionKey, systemMessageParams) + const operationResponse = await this.messageSendSystemOperation.perform(res, systemMessageParams) return operationResponse } diff --git a/app/providers/operations/http/organization/create/index.js b/app/providers/operations/http/organization/create/index.js index 66e9d9fd..3ff4c106 100644 --- a/app/providers/operations/http/organization/create/index.js +++ b/app/providers/operations/http/organization/create/index.js @@ -5,7 +5,7 @@ class HttpOrganizationCreateOperation { this.organizationService = organizationService } - async perform(fakeWsSessionKey, payload) { + async perform(res, payload) { const organizationCreateDTO = new RequestOrganizationCreateDTO(payload) const organization = await this.organizationService.create(organizationCreateDTO) diff --git a/app/providers/operations/http/stats/collect/index.js b/app/providers/operations/http/stats/collect/index.js index a205ce1b..e16800e3 100644 --- a/app/providers/operations/http/stats/collect/index.js +++ b/app/providers/operations/http/stats/collect/index.js @@ -3,7 +3,7 @@ class HttpStatsCollectOperation { this.statsService = statsService } - async perform(fakeWsSessionKey, payload) { + async perform(res, payload) { const needFormat = payload?.get("format") const stats = await this.statsService.collectStats(needFormat) diff --git a/app/providers/operations/http/user/auth/index.js b/app/providers/operations/http/user/auth/index.js index d19c9cdb..17a8e586 100644 --- a/app/providers/operations/http/user/auth/index.js +++ b/app/providers/operations/http/user/auth/index.js @@ -7,7 +7,7 @@ class HttpUserAuthOperation { this.userAuthOperation = userAuthOperation } - async perform(fakeWsSessionKey, headers, cookies, payload) { + async perform(res, headers, cookies, payload) { const refreshToken = cookies["refresh_token"] const accessToken = this.helpers.extractAccessTokenFromAuthHeader(headers["authorization"]) @@ -30,7 +30,7 @@ class HttpUserAuthOperation { }) } - const { user, token: newAccessToken } = await this.userAuthOperation.perform(fakeWsSessionKey, userInfo, true) + const { user, token: newAccessToken } = await this.userAuthOperation.perform(res, userInfo, true) const newRefreshToken = await this.userAuthOperation.createRefreshToken(user, device_id) const accessTokenExpiredAt = new Date(newAccessToken.updated_at).getTime() + this.config.get("jwt.access.expiresIn") * 1000 diff --git a/app/providers/operations/http/user/logout/index.js b/app/providers/operations/http/user/logout/index.js index db07e470..77009368 100644 --- a/app/providers/operations/http/user/logout/index.js +++ b/app/providers/operations/http/user/logout/index.js @@ -8,7 +8,7 @@ class HttpUserLogoutOperation { this.userLogoutOperation = userLogoutOperation } - async perform(fakeWsSessionKey, headers, cookies) { + async perform(res, headers, cookies) { const refreshToken = cookies["refresh_token"] const accessToken = this.helpers.extractAccessTokenFromAuthHeader(headers["authorization"]) diff --git a/app/utils/watchdog-ping-socket.js b/app/utils/watchdog-ping-socket.js index fa082064..dd818794 100644 --- a/app/utils/watchdog-ping-socket.js +++ b/app/utils/watchdog-ping-socket.js @@ -1,5 +1,6 @@ import net from "node:net" import { APIs, BASE_API } from "../networking/APIs.js" +import { CONSTANTS } from "../constants/constants.js" export const watchdogPingSocket = async (logger, sessionService, onWsCloseCb, onTcpCloseCb) => { const users = Object.keys(sessionService.activeSessions.DEVICES) @@ -9,7 +10,7 @@ export const watchdogPingSocket = async (logger, sessionService, onWsCloseCb, on for (const userId of users) { const connections = sessionService.activeSessions.DEVICES[userId] ?? [] for (const connection of connections) { - if (!connection?.socket) { + if (!connection?.socket /*|| connection?.deviceId !== CONSTANTS.HTTP_DEVICE_ID*/) { continue } From c300b85ff6739a87b1317ada2c51e081868b801d Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Mon, 25 May 2026 17:20:03 +0300 Subject: [PATCH 49/70] watchdogPingSocket ignore HTTP_DEVICE_ID --- app/utils/watchdog-ping-socket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/utils/watchdog-ping-socket.js b/app/utils/watchdog-ping-socket.js index dd818794..3334ff89 100644 --- a/app/utils/watchdog-ping-socket.js +++ b/app/utils/watchdog-ping-socket.js @@ -10,7 +10,7 @@ export const watchdogPingSocket = async (logger, sessionService, onWsCloseCb, on for (const userId of users) { const connections = sessionService.activeSessions.DEVICES[userId] ?? [] for (const connection of connections) { - if (!connection?.socket /*|| connection?.deviceId !== CONSTANTS.HTTP_DEVICE_ID*/) { + if (!connection?.socket || (connection?.deviceId === CONSTANTS.HTTP_DEVICE_ID)) { continue } From c2939d65f3739a80068c487a07f66415bd58b163 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Tue, 26 May 2026 17:39:03 +0300 Subject: [PATCH 50/70] update --- app/providers/services/session/index.js | 6 ++---- index.js | 16 ++++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/providers/services/session/index.js b/app/providers/services/session/index.js index e8b95035..a1e6dab5 100644 --- a/app/providers/services/session/index.js +++ b/app/providers/services/session/index.js @@ -415,9 +415,7 @@ class SessionService { onlineUsersListLocal(organizationId, offset, limit) { const userIds = this.retrieveLocalActiveSessionUserIds(organizationId) - userIds.slice(offset, offset + limit) - - return userIds + return userIds.slice(offset, offset + limit) } onlineUsersCountLocal(organizationId) { @@ -430,7 +428,7 @@ class SessionService { const userIds = Array.from(this.activeSessions.SESSIONS.values()) .filter( (session) => - session?.organizationId === organizationId && + session?.organizationId?.toString() === organizationId?.toString() && session?.extraParams[CONSTANTS.SESSION_DEVICE_ID_KEY] !== CONSTANTS.HTTP_DEVICE_ID && session?.userId && this.listUserDeviceLocal(session?.userId)?.length ) diff --git a/index.js b/index.js index 7fd469a8..6cb9abef 100644 --- a/index.js +++ b/index.js @@ -212,9 +212,13 @@ if (config.get("app.watchdogPingSocketInterval")) { }, config.get("app.watchdogPingSocketInterval")) } -await startReplServices( - { ctx: { slc: ServiceLocatorContainer } }, - { accessKey: config.get("repl.http.accessKey"), port: config.get("repl.http.port") }, - { socketHandler: config.get("repl.socket.handler") }, - { fileIn: config.get("repl.file.in"), fileOut: config.get("repl.file.out") } -) +if (config.get("app.env") !== CONSTANTS.ENVS.TESTING) { + await startReplServices( + { ctx: { slc: ServiceLocatorContainer } }, + { accessKey: config.get("repl.http.accessKey"), port: config.get("repl.http.port") }, + { socketHandler: config.get("repl.socket.handler") }, + { fileIn: config.get("repl.file.in"), fileOut: config.get("repl.file.out") } + ) +} + +logger.debug("[Ready] cluster-ws: %s", config.get("ws.cluster.endpoint")) \ No newline at end of file From 9d440d3ba602856f2cd6661097f37c54c395ec1a Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Mon, 1 Jun 2026 16:41:55 +0300 Subject: [PATCH 51/70] try fix listUserDevice --- app/providers/services/session/index.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/providers/services/session/index.js b/app/providers/services/session/index.js index a1e6dab5..5cd6184b 100644 --- a/app/providers/services/session/index.js +++ b/app/providers/services/session/index.js @@ -113,8 +113,18 @@ class SessionService { const userKey = this.#usersSetCacheKey(organizationId, userId) - const deviceIds = await this.redisConnection.client.sMembers(userKey) - return deviceIds ?? [] + let deviceIds = await this.redisConnection.client.sMembers(userKey) + deviceIds = deviceIds ?? [] + + for (const deviceId of deviceIds) { + const deviceHash = await this.retrieveUserExtraParams(userId, deviceId) + if (!deviceHash[CONSTANTS.SESSION_NODE_KEY]) { + this.logger.debug("[listUserDevice][not existed deviceId]: %s %o", deviceId, deviceIds) + deviceIds.splice(deviceIds.indexOf(deviceId), 1) + } + } + + return deviceIds } listUserDeviceLocal(userId) { @@ -365,7 +375,6 @@ class SessionService { this.activeSessions.SESSIONS.delete(socket) - const devicesAfter = this.getUserDevices(userId).map((connection) => { const { socket, ...connectionData } = connection return { ...connectionData, socket: socket?.clientId } @@ -381,6 +390,8 @@ class SessionService { isLastConnection = await this.removeAllUserDeviceData(organizationId, userId, deviceId) + this.logger.debug("[removeUserSession][devices][donne]: %s %s", deviceId, isLastConnection) + return isLastConnection } From 3a569a2ee30bb3a2265b81ee27bd1fa7c9877750 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Mon, 1 Jun 2026 16:44:43 +0300 Subject: [PATCH 52/70] try fix listUserDevice --- app/providers/services/session/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/providers/services/session/index.js b/app/providers/services/session/index.js index 5cd6184b..e1297d93 100644 --- a/app/providers/services/session/index.js +++ b/app/providers/services/session/index.js @@ -121,6 +121,7 @@ class SessionService { if (!deviceHash[CONSTANTS.SESSION_NODE_KEY]) { this.logger.debug("[listUserDevice][not existed deviceId]: %s %o", deviceId, deviceIds) deviceIds.splice(deviceIds.indexOf(deviceId), 1) + await this.removeUserDevice(organizationId, userId, deviceId) } } From 8b61d5901e1d20a3f15b556d74a136c4c2902bb1 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Tue, 2 Jun 2026 10:46:21 +0300 Subject: [PATCH 53/70] update listUserDevice --- APIs/XMPP | 2 +- .../operations/activity/retrieve/index.js | 2 +- .../operations/activity/subscribe/index.js | 2 +- .../operations/http/user/logout/index.js | 2 +- app/providers/services/session/index.js | 40 ++++++++++++------- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/APIs/XMPP b/APIs/XMPP index 36969f43..e8f85db0 160000 --- a/APIs/XMPP +++ b/APIs/XMPP @@ -1 +1 @@ -Subproject commit 36969f431c618092858eb9716632a2cdec4a09a6 +Subproject commit e8f85db002f701df46b5759d0e7a99040e5f8581 diff --git a/app/providers/operations/activity/retrieve/index.js b/app/providers/operations/activity/retrieve/index.js index 27a7c167..e2d53fb2 100644 --- a/app/providers/operations/activity/retrieve/index.js +++ b/app/providers/operations/activity/retrieve/index.js @@ -15,7 +15,7 @@ class ActivityUserRetrieveOperation { for (const targetUser of targetUsers) { const userId = targetUser.native_id - const devicesList = await this.sessionService.listUserDevice(organizationId, userId) + const devicesList = await this.sessionService.listUserDevice(organizationId, userId, true) const isUserOnline = !!devicesList?.length diff --git a/app/providers/operations/activity/subscribe/index.js b/app/providers/operations/activity/subscribe/index.js index 7b0d49ab..b8b43a48 100644 --- a/app/providers/operations/activity/subscribe/index.js +++ b/app/providers/operations/activity/subscribe/index.js @@ -22,7 +22,7 @@ class ActivityUserSubscribeOperation { await this.activityManagerService.subscribeObserverToTarget(currentUserId, targetUserId) let targetUserActivityStatus = null - const devicesList = await this.sessionService.listUserDevice(organizationId, targetUserId) + const devicesList = await this.sessionService.listUserDevice(organizationId, targetUserId, true) const isUserOnline = !!devicesList?.length if (isUserOnline) { diff --git a/app/providers/operations/http/user/logout/index.js b/app/providers/operations/http/user/logout/index.js index 77009368..8f8fda61 100644 --- a/app/providers/operations/http/user/logout/index.js +++ b/app/providers/operations/http/user/logout/index.js @@ -29,7 +29,7 @@ class HttpUserLogoutOperation { const { user_id: userId, device_id: deviceId } = refreshTokenRecord const socket = this.sessionService.getUserDevices(userId).find((el) => el.deviceId === deviceId)?.socket - const userDevices = await this.sessionService.listUserDevice(refreshTokenRecord.organization_id, refreshTokenRecord.user_id) + const userDevices = await this.sessionService.listUserDevice(refreshTokenRecord.organization_id, refreshTokenRecord.user_id, true) let isWasLastUserSession = !userDevices?.length if (socket) { diff --git a/app/providers/services/session/index.js b/app/providers/services/session/index.js index e1297d93..2c910570 100644 --- a/app/providers/services/session/index.js +++ b/app/providers/services/session/index.js @@ -90,12 +90,18 @@ class SessionService { async addUserDevice(organizationId, userId, deviceId) { const userKey = this.#usersSetCacheKey(organizationId, userId) await this.redisConnection.client.sAdd(userKey, deviceId) + this.logger.debug("[addUserDevice]: %o %s", { organizationId, userId, deviceId }, userKey) } - async removeUserDevice(organizationId, userId, deviceId) { + async removeUserDevice(organizationId, userId, deviceId, returnIsLast) { const userKey = this.#usersSetCacheKey(organizationId, userId) - await this.redisConnection.client.sRem(userKey, deviceId) + this.logger.debug("[removeUserDevice][keys]: %s %s", userKey, deviceId) + await this.redisConnection.client.sRem(userKey, deviceId).then(result => this.logger.debug("[removeUserDevice][keys][removed]: %s %s %s", userKey, deviceId, result)) + + if (!returnIsLast) { + return + } const leftUserDevices = await this.listUserDevice(organizationId, userId) @@ -106,7 +112,7 @@ class SessionService { } } - async listUserDevice(organizationId, userId) { + async listUserDevice(organizationId, userId, needFilter) { if (this.config.get("app.isStandAloneNode")) { return this.listUserDeviceLocal(userId) } @@ -116,12 +122,14 @@ class SessionService { let deviceIds = await this.redisConnection.client.sMembers(userKey) deviceIds = deviceIds ?? [] - for (const deviceId of deviceIds) { - const deviceHash = await this.retrieveUserExtraParams(userId, deviceId) - if (!deviceHash[CONSTANTS.SESSION_NODE_KEY]) { - this.logger.debug("[listUserDevice][not existed deviceId]: %s %o", deviceId, deviceIds) - deviceIds.splice(deviceIds.indexOf(deviceId), 1) - await this.removeUserDevice(organizationId, userId, deviceId) + if (needFilter) { + for (const deviceId of deviceIds) { + const deviceHash = await this.retrieveUserExtraParams(userId, deviceId) + if (!deviceHash[CONSTANTS.SESSION_NODE_KEY]) { + this.logger.debug("[listUserDevice][not existed deviceId]: %s %o %s %s", deviceId, deviceIds, organizationId, userId) + deviceIds.splice(deviceIds.indexOf(deviceId), 1) + await this.removeUserDevice(organizationId, userId, deviceId) + } } } @@ -166,7 +174,7 @@ class SessionService { } async removeUserData(organizationId, userId, deviceId) { - const isWasLastUserSession = await this.removeUserDevice(organizationId, userId, deviceId) + const isWasLastUserSession = await this.removeUserDevice(organizationId, userId, deviceId, true) await this.deleteUserExtraParams(userId, deviceId) @@ -184,7 +192,9 @@ class SessionService { } async removeAllUserDeviceData(organizationId, userId, deviceId) { - const isLastConnection = await this.removeUserDevice(organizationId, userId, deviceId) + this.logger.debug("[removeUserSession][devices][remove redis][before]: %o", { organizationId, userId, deviceId }) + const isLastConnection = await this.removeUserDevice(organizationId, userId, deviceId, true) + this.logger.debug("[removeUserSession][devices][remove redis][after]: %s %o", isLastConnection, await this.listUserDevice(organizationId, userId)) const extraParams = await this.retrieveUserExtraParams(userId, deviceId) await this.deleteUserExtraParams(userId, deviceId) @@ -210,7 +220,7 @@ class SessionService { return userData } - const userDevices = await this.listUserDevice(organizationId, userId) + const userDevices = await this.listUserDevice(organizationId, userId, true) for (const deviceId of userDevices) { const extraParams = await this.retrieveUserExtraParams(userId, deviceId) @@ -230,14 +240,14 @@ class SessionService { if (this.config.get("app.isStandAloneNode")) return - const userDeviceIds = await this.listUserDevice(organizationId, userId) + const userDeviceIds = await this.listUserDevice(organizationId, userId, true) if (userDeviceIds.includes(deviceId)) { await this.removeAllUserDeviceData(organizationId, userId, deviceId) } - await this.addUserDevice(organizationId, userId, deviceId) await this.addUserExtraParams(userId, deviceId, { [CONSTANTS.SESSION_NODE_KEY]: nodeEndpoint }) + await this.addUserDevice(organizationId, userId, deviceId) await this.addUserDeviceToNode(nodeEndpoint, organizationId, userId, deviceId) } @@ -391,7 +401,7 @@ class SessionService { isLastConnection = await this.removeAllUserDeviceData(organizationId, userId, deviceId) - this.logger.debug("[removeUserSession][devices][donne]: %s %s", deviceId, isLastConnection) + this.logger.debug("[removeUserSession][devices][done]: %s %s", deviceId, isLastConnection) return isLastConnection } From 0ea9038cfed30ed2f8439b9ebc27904a79967836 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Wed, 3 Jun 2026 13:15:45 +0300 Subject: [PATCH 54/70] add to session service filterActualDevices --- app/providers/services/session/index.js | 65 ++++++++++--------------- 1 file changed, 25 insertions(+), 40 deletions(-) diff --git a/app/providers/services/session/index.js b/app/providers/services/session/index.js index 2c910570..4e6d5d4d 100644 --- a/app/providers/services/session/index.js +++ b/app/providers/services/session/index.js @@ -90,14 +90,12 @@ class SessionService { async addUserDevice(organizationId, userId, deviceId) { const userKey = this.#usersSetCacheKey(organizationId, userId) await this.redisConnection.client.sAdd(userKey, deviceId) - this.logger.debug("[addUserDevice]: %o %s", { organizationId, userId, deviceId }, userKey) } async removeUserDevice(organizationId, userId, deviceId, returnIsLast) { const userKey = this.#usersSetCacheKey(organizationId, userId) - this.logger.debug("[removeUserDevice][keys]: %s %s", userKey, deviceId) - await this.redisConnection.client.sRem(userKey, deviceId).then(result => this.logger.debug("[removeUserDevice][keys][removed]: %s %s %s", userKey, deviceId, result)) + await this.redisConnection.client.sRem(userKey, deviceId) if (!returnIsLast) { return @@ -105,14 +103,10 @@ class SessionService { const leftUserDevices = await this.listUserDevice(organizationId, userId) - if (!leftUserDevices?.length) { - await this.deleteUserDevices(organizationId, userId) - - return true - } + return !leftUserDevices?.length } - async listUserDevice(organizationId, userId, needFilter) { + async listUserDevice(organizationId, userId, filterActualDevices) { if (this.config.get("app.isStandAloneNode")) { return this.listUserDeviceLocal(userId) } @@ -121,19 +115,20 @@ class SessionService { let deviceIds = await this.redisConnection.client.sMembers(userKey) deviceIds = deviceIds ?? [] + const actualDevices = new Set(deviceIds) - if (needFilter) { + if (filterActualDevices) { for (const deviceId of deviceIds) { const deviceHash = await this.retrieveUserExtraParams(userId, deviceId) if (!deviceHash[CONSTANTS.SESSION_NODE_KEY]) { - this.logger.debug("[listUserDevice][not existed deviceId]: %s %o %s %s", deviceId, deviceIds, organizationId, userId) - deviceIds.splice(deviceIds.indexOf(deviceId), 1) + actualDevices.delete(deviceId) await this.removeUserDevice(organizationId, userId, deviceId) + this.logger.debug("[listUserDevice][not actual deviceId]: %s %s %s", deviceId, userId, organizationId) } } } - return deviceIds + return Array.from(actualDevices) } listUserDeviceLocal(userId) { @@ -192,9 +187,7 @@ class SessionService { } async removeAllUserDeviceData(organizationId, userId, deviceId) { - this.logger.debug("[removeUserSession][devices][remove redis][before]: %o", { organizationId, userId, deviceId }) - const isLastConnection = await this.removeUserDevice(organizationId, userId, deviceId, true) - this.logger.debug("[removeUserSession][devices][remove redis][after]: %s %o", isLastConnection, await this.listUserDevice(organizationId, userId)) + await this.removeUserDevice(organizationId, userId, deviceId) const extraParams = await this.retrieveUserExtraParams(userId, deviceId) await this.deleteUserExtraParams(userId, deviceId) @@ -202,7 +195,9 @@ class SessionService { await this.removeUserDeviceFromNode(nodeEndpoint, organizationId, userId, deviceId) - return isLastConnection + const leftDevices = await this.listUserDevice(organizationId, userId, true) + + return !leftDevices?.length } async listUserData(organizationId, userId) { @@ -220,7 +215,7 @@ class SessionService { return userData } - const userDevices = await this.listUserDevice(organizationId, userId, true) + const userDevices = await this.listUserDevice(organizationId, userId) for (const deviceId of userDevices) { const extraParams = await this.retrieveUserExtraParams(userId, deviceId) @@ -240,7 +235,7 @@ class SessionService { if (this.config.get("app.isStandAloneNode")) return - const userDeviceIds = await this.listUserDevice(organizationId, userId, true) + const userDeviceIds = await this.listUserDevice(organizationId, userId) if (userDeviceIds.includes(deviceId)) { await this.removeAllUserDeviceData(organizationId, userId, deviceId) @@ -360,21 +355,10 @@ class SessionService { } async removeUserSession(socket, userId, deviceId) { - this.logger.debug("[removeUserSession][args]: %o", { socket: socket?.isAlive, userId, deviceId }) - userId = userId ?? this.getSessionUserId(socket) deviceId = deviceId ?? this.getDeviceId(socket, userId) const organizationId = this.getSession(socket)?.organizationId - this.logger.debug("[removeUserSession][vars]: %o [session]: %o [device]: %s", { organizationId, userId, deviceId }, this.getSession(socket), this.getDeviceId(socket, userId)) - - const devicesBefore = this.getUserDevices(userId).map((connection) => { - const { socket, ...connectionData } = connection - return { ...connectionData, socket: socket?.clientId } - }) - - this.logger.debug("[removeUserSession][devices][before]: %o %s", devicesBefore, devicesBefore?.length) - const leftActiveConnections = this.getUserDevices(userId).filter(({ deviceId: activeDeviceId }) => activeDeviceId !== deviceId) let isLastConnection = !leftActiveConnections?.length @@ -386,13 +370,6 @@ class SessionService { this.activeSessions.SESSIONS.delete(socket) - const devicesAfter = this.getUserDevices(userId).map((connection) => { - const { socket, ...connectionData } = connection - return { ...connectionData, socket: socket?.clientId } - }) - - this.logger.debug("[removeUserSession][devices][after]: %o %s", devicesAfter, devicesAfter?.length) - if (!deviceId) { return isLastConnection } @@ -401,8 +378,6 @@ class SessionService { isLastConnection = await this.removeAllUserDeviceData(organizationId, userId, deviceId) - this.logger.debug("[removeUserSession][devices][done]: %s %s", deviceId, isLastConnection) - return isLastConnection } @@ -423,7 +398,17 @@ class SessionService { const userKeys = await this.redisConnection.scanWithPagination("set", matchPattern, offset, limit) - return userKeys.map((userKey) => userKey.split(":").at(-1)) + const userIds = userKeys.map((userKey) => userKey.split(":").at(-1)) + const actualUserIds = new Set(userIds) + + for (const userId of userIds) { + const actualDevices = await this.listUserDevice(organizationId, userId, true) + if (!actualDevices?.length) { + actualUserIds.delete(userId) + } + } + + return Array.from(actualUserIds) } async onlineUsersCountWithNodes(organizationId) { From 95820d90a96625894dd48d3e80349d3406747413 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 4 Jun 2026 16:07:11 +0300 Subject: [PATCH 55/70] update addUserDeviceConnection and storeUserNodeData --- .../http/activity/online_list/index.js | 2 +- .../operations/http/message/create/index.js | 2 +- .../operations/http/message/delete/index.js | 2 +- .../operations/http/message/edit/index.js | 2 +- .../operations/http/message/reaction/index.js | 2 +- .../operations/http/message/read/index.js | 2 +- .../operations/http/message/system/index.js | 2 +- app/providers/operations/user/auth/index.js | 8 +- .../operations/user/connect/index.js | 6 +- app/providers/services/session/index.js | 80 +++++++++++++++---- 10 files changed, 79 insertions(+), 29 deletions(-) diff --git a/app/providers/operations/http/activity/online_list/index.js b/app/providers/operations/http/activity/online_list/index.js index e79c306a..706f248c 100644 --- a/app/providers/operations/http/activity/online_list/index.js +++ b/app/providers/operations/http/activity/online_list/index.js @@ -9,7 +9,7 @@ class HttpActivityOnlineListOperation { async perform(res, payload) { const { organizationId, userId, ...requestData } = payload - await this.sessionService.addUserDeviceConnection(res, organizationId, userId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + this.sessionService.addUserDeviceConnection(res, organizationId, userId, MAIN_CONSTANTS.HTTP_DEVICE_ID) const operationResponse = await this.onlineListOperation.perform(res, requestData) diff --git a/app/providers/operations/http/message/create/index.js b/app/providers/operations/http/message/create/index.js index 2c55efd0..5a9df243 100644 --- a/app/providers/operations/http/message/create/index.js +++ b/app/providers/operations/http/message/create/index.js @@ -9,7 +9,7 @@ class HttpMessageCreateOperation { async perform(res, payload) { const { organizationId, senderId, message: messageParams } = payload - await this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) const operationResponse = await this.messageCreateOperation.perform(res, messageParams) diff --git a/app/providers/operations/http/message/delete/index.js b/app/providers/operations/http/message/delete/index.js index da3e5a14..1817378f 100644 --- a/app/providers/operations/http/message/delete/index.js +++ b/app/providers/operations/http/message/delete/index.js @@ -9,7 +9,7 @@ class HttpMessageDeleteOperation { async perform(res, payload) { const { organizationId, senderId, messageDelete: messageDeleteParams } = payload - await this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) const operationResponse = await this.messageDeleteOperation.perform(res, messageDeleteParams) diff --git a/app/providers/operations/http/message/edit/index.js b/app/providers/operations/http/message/edit/index.js index 3546bc7e..255f242a 100644 --- a/app/providers/operations/http/message/edit/index.js +++ b/app/providers/operations/http/message/edit/index.js @@ -9,7 +9,7 @@ class HttpMessageEditOperation { async perform(res, payload) { const { organizationId, senderId, messageEdit: messageEditParams } = payload - await this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) const operationResponse = await this.messageEditOperation.perform(res, messageEditParams) diff --git a/app/providers/operations/http/message/reaction/index.js b/app/providers/operations/http/message/reaction/index.js index 1b1fb93e..0e330dbe 100644 --- a/app/providers/operations/http/message/reaction/index.js +++ b/app/providers/operations/http/message/reaction/index.js @@ -9,7 +9,7 @@ class HttpMessageReactionOperation { async perform(res, payload) { const { organizationId, senderId, messageReaction: messageReaction } = payload - await this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) const operationResponse = await this.messageReactionsUpdateOperation.perform(res, messageReaction) diff --git a/app/providers/operations/http/message/read/index.js b/app/providers/operations/http/message/read/index.js index dc16c2fb..92b91205 100644 --- a/app/providers/operations/http/message/read/index.js +++ b/app/providers/operations/http/message/read/index.js @@ -9,7 +9,7 @@ class HttpMessageReadOperation { async perform(res, payload) { const { organizationId, senderId, messageRead: messageReadParams } = payload - await this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) const operationResponse = await this.messageReadOperation.perform(res, messageReadParams) diff --git a/app/providers/operations/http/message/system/index.js b/app/providers/operations/http/message/system/index.js index e2dcce3c..b2e0ab8f 100644 --- a/app/providers/operations/http/message/system/index.js +++ b/app/providers/operations/http/message/system/index.js @@ -9,7 +9,7 @@ class HttpMessageSendSystemOperation { async perform(res, payload) { const { organizationId, senderId, messageSystem: systemMessageParams } = payload - await this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) + this.sessionService.addUserDeviceConnection(res, organizationId, senderId, MAIN_CONSTANTS.HTTP_DEVICE_ID) const operationResponse = await this.messageSendSystemOperation.perform(res, systemMessageParams) diff --git a/app/providers/operations/user/auth/index.js b/app/providers/operations/user/auth/index.js index a71a6ef5..ac2b9a02 100644 --- a/app/providers/operations/user/auth/index.js +++ b/app/providers/operations/user/auth/index.js @@ -20,7 +20,11 @@ class UserAuthOperation { // TODO: close connections if (!omitDeviceConnection) { - await this.sessionService.addUserDeviceConnection(ws, user.organization_id, user.native_id, deviceId) + const { sameDeviceConnection, sameSocketConnection } = this.sessionService.addUserDeviceConnection(ws, user.organization_id, user.native_id, deviceId) + await this.sessionService.storeUserNodeData(ws, user.organization_id, user.native_id, deviceId) + if (sameSocketConnection) { + await this.sessionService.removeAllUserDeviceData(user.organization_id, user.native_id, sameSocketConnection.deviceId) + } } const jwtAccessToken = this.#generateToken( @@ -32,8 +36,6 @@ class UserAuthOperation { const updatedToken = await this.userTokenRepo.updateToken(token, organizationId, user.native_id, deviceId, jwtAccessToken, "access") - await this.sessionService.storeUserNodeData(ws, user.organization_id, user.native_id, deviceId) - const userWithAvatarUrl = (await this.userService.addAvatarUrl([user])).at(0) return { user: userWithAvatarUrl, token: updatedToken } diff --git a/app/providers/operations/user/connect/index.js b/app/providers/operations/user/connect/index.js index 8044cbb4..caee107d 100644 --- a/app/providers/operations/user/connect/index.js +++ b/app/providers/operations/user/connect/index.js @@ -21,9 +21,11 @@ class UserConnectSocketOperation { const user = await this.userService.userRepo.findById(token.user_id) // TODO: close connections - await this.sessionService.addUserDeviceConnection(ws, user.organization_id, user.native_id, deviceId) - + const { sameDeviceConnection, sameSocketConnection } = this.sessionService.addUserDeviceConnection(ws, user.organization_id, user.native_id, deviceId) await this.sessionService.storeUserNodeData(ws, user.organization_id, user.native_id, deviceId) + if (sameSocketConnection) { + await this.sessionService.removeAllUserDeviceData(user.organization_id, user.native_id, sameSocketConnection.deviceId) + } return { user } } diff --git a/app/providers/services/session/index.js b/app/providers/services/session/index.js index 4e6d5d4d..abdac61c 100644 --- a/app/providers/services/session/index.js +++ b/app/providers/services/session/index.js @@ -19,7 +19,7 @@ class SessionService { return this.activeSessions.SESSIONS.size } - async addUserDeviceConnection(socket, organizationId, userId, deviceId) { + addUserDeviceConnection(socket, organizationId, userId, deviceId) { const activeConnections = this.getUserDevices(userId) const sameSocketConnection = activeConnections.find(connection => connection.socket === socket) @@ -35,12 +35,8 @@ class SessionService { this.activeSessions.DEVICES[userId] = [...otherDeviceConnections, connection] this.setSessionUserId(socket, organizationId, userId, { [CONSTANTS.SESSION_DEVICE_ID_KEY]: deviceId }) - - if (sameSocketConnection) { - await this.removeAllUserDeviceData(organizationId, userId, sameSocketConnection.deviceId) - } - return sameDeviceConnection + return { sameDeviceConnection, sameSocketConnection } } #nodesSetCacheKey(nodeEndpoint) { @@ -115,16 +111,29 @@ class SessionService { let deviceIds = await this.redisConnection.client.sMembers(userKey) deviceIds = deviceIds ?? [] - const actualDevices = new Set(deviceIds) if (filterActualDevices) { - for (const deviceId of deviceIds) { - const deviceHash = await this.retrieveUserExtraParams(userId, deviceId) - if (!deviceHash[CONSTANTS.SESSION_NODE_KEY]) { - actualDevices.delete(deviceId) - await this.removeUserDevice(organizationId, userId, deviceId) - this.logger.debug("[listUserDevice][not actual deviceId]: %s %s %s", deviceId, userId, organizationId) - } + deviceIds = await this.filterActualDeviceIds(organizationId, userId, deviceIds) + } + + return deviceIds + } + + async filterActualDeviceIds(organizationId, userId, deviceIds) { + const actualDevices = new Set(deviceIds) + + for (const deviceId of deviceIds) { + const deviceExt = await this.retrieveUserExtraParams(userId, deviceId) + + const noEndpointKey = !deviceExt[CONSTANTS.SESSION_NODE_KEY] + const isSameEndpoint = (deviceExt[CONSTANTS.SESSION_NODE_KEY] === this.config.get("ws.cluster.endpoint")) + const hasDeviceConnection = this.getUserDevices(userId).find(connection => connection.deviceId === deviceId) + const isHasExtButNoConnection = isSameEndpoint && !hasDeviceConnection + + if (noEndpointKey || isHasExtButNoConnection) { + actualDevices.delete(deviceId) + await this.removeAllUserDeviceDataTransaction(this.config.get("ws.cluster.endpoint"), organizationId, userId, deviceId) + this.logger.debug("[listUserDevice][not actual deviceId]: %s %s %s", deviceId, userId, organizationId) } } @@ -186,6 +195,25 @@ class SessionService { await this.deleteUserDevices(organizationId, userId) } + async removeAllUserDeviceDataTransaction(nodeEndpoint, organizationId, userId, deviceId) { + const userKey = this.#usersSetCacheKey(organizationId, userId) + const userHashKey = this.#usersHashCacheKey(userId, deviceId) + const nodeKey = this.#nodesSetCacheKey(nodeEndpoint ?? this.config.get("ws.cluster.endpoint")) + const userConnectionMember = `${organizationId}:${userId}:${deviceId}` + + const tx = this.redisConnection.client.multi() + + tx.sRem(userKey, deviceId) + tx.del(userHashKey) + tx.sRem(nodeKey, userConnectionMember) + + const multiResult = await tx.exec() + + this.logger.debug("[removeAllUserDeviceDataTransaction][result]: %o %o", { organizationId, userId, deviceId }, multiResult) + + return multiResult + } + async removeAllUserDeviceData(organizationId, userId, deviceId) { await this.removeUserDevice(organizationId, userId, deviceId) const extraParams = await this.retrieveUserExtraParams(userId, deviceId) @@ -225,6 +253,26 @@ class SessionService { return userData } + async addUserDeviceDataTransaction(nodeEndpoint, organizationId, userId, deviceId, extParams) { + const userKey = this.#usersSetCacheKey(organizationId, userId) + const userHashKey = this.#usersHashCacheKey(userId, deviceId) + const keyValuePairs = Object.entries(extParams).flat().map((val) => `${val}`) + const nodeKey = this.#nodesSetCacheKey(nodeEndpoint ?? this.config.get("ws.cluster.endpoint")) + const userConnectionMember = `${organizationId}:${userId}:${deviceId}` + + const tx = this.redisConnection.client.multi() + + tx.sAdd(userKey, deviceId) + tx.hSet(userHashKey, keyValuePairs) + tx.sAdd(nodeKey, userConnectionMember) + + const multiResult = await tx.exec() + + this.logger.debug("[addUserDeviceDataTransaction][result]: %o %o", { organizationId, userId, deviceId }, multiResult) + + return multiResult + } + async storeUserNodeData(socket, organizationId, userId, deviceId) { const nodeEndpoint = this.config.get("ws.cluster.endpoint") @@ -241,9 +289,7 @@ class SessionService { await this.removeAllUserDeviceData(organizationId, userId, deviceId) } - await this.addUserExtraParams(userId, deviceId, { [CONSTANTS.SESSION_NODE_KEY]: nodeEndpoint }) - await this.addUserDevice(organizationId, userId, deviceId) - await this.addUserDeviceToNode(nodeEndpoint, organizationId, userId, deviceId) + await this.addUserDeviceDataTransaction(nodeEndpoint, organizationId, userId, deviceId, { [CONSTANTS.SESSION_NODE_KEY]: nodeEndpoint }) } async clearNodeUsersSession(nodeEndpoint) { From 16888cb69c0d94a9db2e40e2e03afbe21f303764 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 4 Jun 2026 16:07:22 +0300 Subject: [PATCH 56/70] update package --- APIs/XMPP | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/APIs/XMPP b/APIs/XMPP index e8f85db0..70d7a9d6 160000 --- a/APIs/XMPP +++ b/APIs/XMPP @@ -1 +1 @@ -Subproject commit e8f85db002f701df46b5759d0e7a99040e5f8581 +Subproject commit 70d7a9d6844954fc3493219b4b4c69cb0c6eb07b From b9b1dd482f006292dbd6ec34146d661af8a6567f Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Tue, 9 Jun 2026 16:55:17 +0300 Subject: [PATCH 57/70] add logs --- app/providers/services/session/index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/providers/services/session/index.js b/app/providers/services/session/index.js index abdac61c..3a65bf6b 100644 --- a/app/providers/services/session/index.js +++ b/app/providers/services/session/index.js @@ -133,7 +133,7 @@ class SessionService { if (noEndpointKey || isHasExtButNoConnection) { actualDevices.delete(deviceId) await this.removeAllUserDeviceDataTransaction(this.config.get("ws.cluster.endpoint"), organizationId, userId, deviceId) - this.logger.debug("[listUserDevice][not actual deviceId]: %s %s %s", deviceId, userId, organizationId) + this.logger.debug("[listUserDevice][not actual deviceId]: %o %o", { organizationId, userId, deviceId }, { noEndpointKey, isSameEndpoint, hasDeviceConnection }) } } @@ -405,6 +405,8 @@ class SessionService { deviceId = deviceId ?? this.getDeviceId(socket, userId) const organizationId = this.getSession(socket)?.organizationId + this.logger.debug("[removeUserSession][vars] %o", { organizationId, userId, deviceId }) + const leftActiveConnections = this.getUserDevices(userId).filter(({ deviceId: activeDeviceId }) => activeDeviceId !== deviceId) let isLastConnection = !leftActiveConnections?.length @@ -422,8 +424,12 @@ class SessionService { if (this.config.get("app.isStandAloneNode")) return isLastConnection + this.logger.debug("[removeUserSession][left connections] %o", leftActiveConnections?.map(con => con.deviceId)) + isLastConnection = await this.removeAllUserDeviceData(organizationId, userId, deviceId) + this.logger.debug("[removeUserSession][end] %o %s", { organizationId, userId, deviceId }, isLastConnection) + return isLastConnection } From 5e8f7450a0d79219a035e59f6a20cbc04ef35937 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Fri, 12 Jun 2026 14:47:58 +0300 Subject: [PATCH 58/70] add log --- app/providers/services/session/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/providers/services/session/index.js b/app/providers/services/session/index.js index 3a65bf6b..76bda30e 100644 --- a/app/providers/services/session/index.js +++ b/app/providers/services/session/index.js @@ -223,6 +223,8 @@ class SessionService { await this.removeUserDeviceFromNode(nodeEndpoint, organizationId, userId, deviceId) + this.logger.debug("[removeAllUserDeviceData][removed] %o", { organizationId, userId, deviceId }) + const leftDevices = await this.listUserDevice(organizationId, userId, true) return !leftDevices?.length From 3483dca2c43d45ba8696111a0dc703f7758b8172 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Wed, 17 Jun 2026 17:10:46 +0300 Subject: [PATCH 59/70] it can block create_user/auth if account was blocked --- APIs/XMPP | 2 +- app/constants/errors.js | 4 ++ app/models/organization.js | 2 +- .../operations/user/auth/Provider.js | 3 +- app/providers/operations/user/auth/index.js | 15 ++++- .../operations/user/create/Provider.js | 4 +- app/providers/operations/user/create/index.js | 14 +++- app/providers/services/organization/index.js | 6 ++ test/users.spec.js | 66 +++++++++++++++++++ 9 files changed, 107 insertions(+), 9 deletions(-) diff --git a/APIs/XMPP b/APIs/XMPP index 70d7a9d6..26483372 160000 --- a/APIs/XMPP +++ b/APIs/XMPP @@ -1 +1 @@ -Subproject commit 70d7a9d6844954fc3493219b4b4c69cb0c6eb07b +Subproject commit 264833720a3b9fd11c4ab4a09b76df198528f3f2 diff --git a/app/constants/errors.js b/app/constants/errors.js index 219a6eca..c846007d 100644 --- a/app/constants/errors.js +++ b/app/constants/errors.js @@ -52,6 +52,10 @@ export const ERROR_STATUES = { status: 401, message: "Incorrect username or password.", }, + ORGANIZATION_BLOCKED: { + status: 403, + message: "Organization was blocked.", + }, INCORRECT_OTP_TOKEN: { status: 403, message: "Incorrect token for password recovery." }, DEVICE_ID_MISSED: { status: 422, message: `'device_id' is required.` }, // Contacts --> diff --git a/app/models/organization.js b/app/models/organization.js index b17f0d75..e9819571 100644 --- a/app/models/organization.js +++ b/app/models/organization.js @@ -6,6 +6,6 @@ export default class Organization extends BaseModel { } static get visibleFields() { - return ["_id", "name", "created_at", "updated_at"] + return ["_id", "name", "is_blocked", "block_reason", "created_at", "updated_at"] } } diff --git a/app/providers/operations/user/auth/Provider.js b/app/providers/operations/user/auth/Provider.js index f5c35f3a..c1abd472 100644 --- a/app/providers/operations/user/auth/Provider.js +++ b/app/providers/operations/user/auth/Provider.js @@ -9,8 +9,9 @@ class UserAuthOperationRegisterProvider extends RegisterProvider { const sessionService = slc.use("SessionService") const userService = slc.use("UserService") const userTokenRepo = slc.use("UserTokenRepository") + const organizationService = slc.use("OrganizationService") - return new UserAuthOperation(config, sessionService, userService, userTokenRepo) + return new UserAuthOperation(config, sessionService, userService, userTokenRepo, organizationService) } } diff --git a/app/providers/operations/user/auth/index.js b/app/providers/operations/user/auth/index.js index ac2b9a02..e653b54c 100644 --- a/app/providers/operations/user/auth/index.js +++ b/app/providers/operations/user/auth/index.js @@ -3,11 +3,12 @@ import jwt from "jsonwebtoken" import { ERROR_STATUES } from "../../../../constants/errors.js" class UserAuthOperation { - constructor(config, sessionService, userService, userTokenRepo) { + constructor(config, sessionService, userService, userTokenRepo, organizationService) { this.config = config this.sessionService = sessionService this.userService = userService this.userTokenRepo = userTokenRepo + this.organizationService = organizationService } async perform(ws, userInfo, omitDeviceConnection) { @@ -17,6 +18,8 @@ class UserAuthOperation { const { user, token } = userInfo.token ? await this.#authByToken(userInfo.token, deviceId) : await this.#authByUserInfo(organizationId, userInfo, deviceId) + + await this.#validateOrganization(user) // TODO: close connections if (!omitDeviceConnection) { @@ -83,6 +86,16 @@ class UserAuthOperation { return { user, token } } + async #validateOrganization(user) { + const blockStatus = await this.organizationService.isUserFromBlocked(user) + + if (blockStatus.isBlocked) { + throw new Error(ERROR_STATUES.ORGANIZATION_BLOCKED.message, { + cause: Object.assign({}, blockStatus, ERROR_STATUES.ORGANIZATION_BLOCKED), + }) + } + } + async createRefreshToken(user, deviceId) { const jwtToken = this.#generateToken(user, "refresh", this.config.get("jwt.refresh.secret"), +this.config.get("jwt.refresh.expiresIn")) diff --git a/app/providers/operations/user/create/Provider.js b/app/providers/operations/user/create/Provider.js index 32e4c220..323d9eac 100644 --- a/app/providers/operations/user/create/Provider.js +++ b/app/providers/operations/user/create/Provider.js @@ -5,11 +5,11 @@ const name = "UserCreateOperation" class UserCreateOperationRegisterProvider extends RegisterProvider { register(slc) { - const orgService = slc.use("OrganizationService") + const organizationService = slc.use("OrganizationService") const userService = slc.use("UserService") const contactService = slc.use("ContactService") - return new UserCreateOperation(orgService, userService, contactService) + return new UserCreateOperation(organizationService, userService, contactService) } } diff --git a/app/providers/operations/user/create/index.js b/app/providers/operations/user/create/index.js index a5789939..b76dcb10 100644 --- a/app/providers/operations/user/create/index.js +++ b/app/providers/operations/user/create/index.js @@ -1,14 +1,14 @@ import { ERROR_STATUES } from "../../../../constants/errors.js" class UserCreateOperation { - constructor(orgService, userService, contactsMatchRepository) { - this.orgService = orgService + constructor(organizationService, userService, contactsMatchRepository) { + this.organizationService = organizationService this.userService = userService this.contactsMatchRepository = contactsMatchRepository } async perform(createUserParams) { - const isOrgExist = await this.orgService.isExist(createUserParams.organization_id) + const isOrgExist = await this.organizationService.isExist(createUserParams.organization_id) if (!isOrgExist) { throw new Error(ERROR_STATUES.ORG_NOT_FOUND.message, { @@ -16,6 +16,14 @@ class UserCreateOperation { }) } + const blockStatus = await this.organizationService.isUserFromBlocked(createUserParams) + + if (blockStatus.isBlocked) { + throw new Error(ERROR_STATUES.ORGANIZATION_BLOCKED.message, { + cause: Object.assign({}, blockStatus, ERROR_STATUES.ORGANIZATION_BLOCKED), + }) + } + const existingUser = await this.userService.userRepo.findRegistered( createUserParams.organization_id, createUserParams.login, diff --git a/app/providers/services/organization/index.js b/app/providers/services/organization/index.js index c6390b38..2805140e 100644 --- a/app/providers/services/organization/index.js +++ b/app/providers/services/organization/index.js @@ -14,6 +14,12 @@ class OrganizationService { return !!organization } + + async isUserFromBlocked(user) { + const organization = await this.organizationRepo.findById(user.organization_id) + + return { isBlocked: organization?.is_blocked, reason: organization?.block_reason } + } } export default OrganizationService diff --git a/test/users.spec.js b/test/users.spec.js index 0ab47e3e..d8d8f056 100644 --- a/test/users.spec.js +++ b/test/users.spec.js @@ -7,6 +7,7 @@ import { generateNewOrganizationId } from "./tools/utils.js" import packetJsonProcessor from "../APIs/JSON/routes/packet_processor.js" const userRepo = ServiceLocatorContainer.use("UserRepository") +const orgRepo = ServiceLocatorContainer.use("OrganizationRepository") let orgId = void 0 let userLogin = [...Array(30)].map(() => Math.random().toString(36)[2]).join("") @@ -266,6 +267,71 @@ describe("User cycle", async () => { }) }) + describe("Create/Login in blocked organization", () => { + before(async () => { + await orgRepo.updateOne({ _id: orgId }, { $set: { is_blocked: true, block_reason: "Out of limits" } }) + }) + + it("create in blocked organization" , async () => { + const requestData = { + request: { + user_create: { + organization_id: orgId, + login: userLogin, + email: "email_1_1", + phone: "phone_1_1", + password: "user_paswword_1_1", + device_id: "deveice11", + }, + id: "1_1_blocked", + }, + } + + let responseData = await packetJsonProcessor.processMessageOrError("test", JSON.stringify(requestData)) + responseData = responseData.backMessages.at(0) + + assert.strictEqual(requestData.request.id, responseData.response.id) + assert.deepEqual(responseData.response.error, { + isBlocked: true, + reason: "Out of limits", + status: 403, + message: "Organization was blocked.", + }) + assert.equal(responseData.response.user, void 0) + }) + + it("login in blocked organization" , async () => { + const requestData = { + request: { + user_login: { + organization_id: orgId, + device_id: "PC_Home", + login: userLogin, + password: "user_paswword_1", + }, + id: "2_3_blocked", + }, + } + + let responseData = await packetJsonProcessor.processMessageOrError("test", JSON.stringify(requestData)) + + responseData = responseData.backMessages.at(0) + + assert.strictEqual(requestData.request.id, responseData.response.id) + assert.deepEqual(responseData.response.error, { + isBlocked: true, + reason: "Out of limits", + status: 403, + message: "Organization was blocked.", + }) + assert.equal(responseData.response.user, void 0) + }) + + after(async () => { + await orgRepo.updateOne({ _id: orgId }, { $unset: { is_blocked: 1, blocked_reason: 1} }) + }) + }) + describe("Get Users By IDs", async () => { it(`should fail ids array is empty`, async () => { const requestData = { From f125643bc5108b4e8caec1ee57eb9d1d1bf2453e Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Wed, 17 Jun 2026 17:20:37 +0300 Subject: [PATCH 60/70] update env --- .env.example | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index a965cd96..034f3751 100644 --- a/.env.example +++ b/.env.example @@ -14,13 +14,13 @@ REDIS_URL=redis://127.0.0.1:6379 # REPL # http -APP_REPL_HTTP_PORT=5010 -APP_REPL_HTTP_ACCESS_KEY=repl-wow-key +# APP_REPL_HTTP_PORT=5010 +# APP_REPL_HTTP_ACCESS_KEY=repl-wow-key # socket APP_REPL_SOCKET_HANDLER=/tmp/net-repl.socket # file -APP_REPL_FILE_IN=/tmp/pipe-repl.in -APP_REPL_FILE_OUT=/tmp/pipe-repl.out +# APP_REPL_FILE_IN=/tmp/pipe-repl.in +# APP_REPL_FILE_OUT=/tmp/pipe-repl.out STORAGE_DRIVER=minio From f2f134e4bbcead5e441265687314f9bae3a118f3 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Wed, 17 Jun 2026 18:44:01 +0300 Subject: [PATCH 61/70] update @sama-communications/sdk --- package-lock.json | 914 +----------------- package.json | 2 +- .../sama-communications-sdk-0.16.2.tgz | Bin 24080 -> 0 bytes .../sama-communications-sdk-0.17.1.tgz | Bin 0 -> 13450 bytes 4 files changed, 6 insertions(+), 910 deletions(-) delete mode 100644 test/cluster/sama-communications-sdk-0.16.2.tgz create mode 100644 test/cluster/sama-communications-sdk-0.17.1.tgz diff --git a/package-lock.json b/package-lock.json index 296b5ab1..b1981d66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ }, "devDependencies": { "@faker-js/faker": "^10.4.0", - "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.16.2.tgz", + "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.17.1.tgz", "c8": "^10.1.3", "kill-port": "^2.0.1", "mocha": "^10.0.0", @@ -1014,42 +1014,6 @@ "node": ">=14.0.0" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/runtime": { "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz", @@ -1061,20 +1025,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -1328,134 +1278,6 @@ "tslib": "2" } }, - "node_modules/@microsoft/api-extractor": { - "version": "7.58.0", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.58.0.tgz", - "integrity": "sha512-XcHlDylX5GblbGbs1eBoexjVyvdJMioTPuBCgCorE2rqijzTYxi6eudXyez3xACRxtH9aDtahoL9fYM4XTvQmg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@microsoft/api-extractor-model": "7.33.5", - "@microsoft/tsdoc": "~0.16.0", - "@microsoft/tsdoc-config": "~0.18.1", - "@rushstack/node-core-library": "5.21.0", - "@rushstack/rig-package": "0.7.2", - "@rushstack/terminal": "0.22.4", - "@rushstack/ts-command-line": "5.3.4", - "diff": "~8.0.2", - "lodash": "~4.17.23", - "minimatch": "10.2.3", - "resolve": "~1.22.1", - "semver": "~7.5.4", - "source-map": "~0.6.1", - "typescript": "5.9.3" - }, - "bin": { - "api-extractor": "bin/api-extractor" - } - }, - "node_modules/@microsoft/api-extractor-model": { - "version": "7.33.5", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.33.5.tgz", - "integrity": "sha512-Xh4dXuusndVQqVz4nEN9xOp0DyzsKxeD2FFJkSPg4arAjDSKPcy6cAc7CaeBPA7kF2wV1fuDlo2p/bNMpVr8yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@microsoft/tsdoc": "~0.16.0", - "@microsoft/tsdoc-config": "~0.18.1", - "@rushstack/node-core-library": "5.21.0" - } - }, - "node_modules/@microsoft/api-extractor/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@microsoft/api-extractor/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@microsoft/api-extractor/node_modules/diff": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", - "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/@microsoft/api-extractor/node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@microsoft/api-extractor/node_modules/minimatch": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz", - "integrity": "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@microsoft/api-extractor/node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/@microsoft/tsdoc": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz", - "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@microsoft/tsdoc-config": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.18.1.tgz", - "integrity": "sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@microsoft/tsdoc": "0.16.0", - "ajv": "~8.18.0", - "jju": "~1.4.0", - "resolve": "~1.22.2" - } - }, "node_modules/@mongodb-js/saslprep": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz", @@ -1614,159 +1436,13 @@ "@redis/client": "^1.0.0" } }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@rushstack/node-core-library": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.21.0.tgz", - "integrity": "sha512-LFzN+1lyWROit/P8Md6yxAth7lLYKn37oCKJHirEE2TQB25NDUM7bALf0ar+JAtwFfRCH+D+DGOA7DAzIi2r+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "~8.18.0", - "ajv-draft-04": "~1.0.0", - "ajv-formats": "~3.0.1", - "fs-extra": "~11.3.0", - "import-lazy": "~4.0.0", - "jju": "~1.4.0", - "resolve": "~1.22.1", - "semver": "~7.5.4" - }, - "peerDependencies": { - "@types/node": "*" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@rushstack/problem-matcher": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@rushstack/problem-matcher/-/problem-matcher-0.2.1.tgz", - "integrity": "sha512-gulfhBs6n+I5b7DvjKRfhMGyUejtSgOHTclF/eONr8hcgF1APEDjhxIsfdUYYMzC3rvLwGluqLjbwCFZ8nxrog==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/node": "*" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@rushstack/rig-package": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.7.2.tgz", - "integrity": "sha512-9XbFWuqMYcHUso4mnETfhGVUSaADBRj6HUAAEYk50nMPn8WRICmBuCphycQGNB3duIR6EEZX3Xj3SYc2XiP+9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "~1.22.1", - "strip-json-comments": "~3.1.1" - } - }, - "node_modules/@rushstack/terminal": { - "version": "0.22.4", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.22.4.tgz", - "integrity": "sha512-fhtLjnXCc/4WleVbVl6aoc7jcWnU6yqjS1S8WoaNREG3ycu/viZ9R/9QM7Y/b4CDvcXoiDyMNIay7JMwBptM3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rushstack/node-core-library": "5.21.0", - "@rushstack/problem-matcher": "0.2.1", - "supports-color": "~8.1.1" - }, - "peerDependencies": { - "@types/node": "*" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@rushstack/ts-command-line": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.3.4.tgz", - "integrity": "sha512-MLkVKVEN6/2clKTrjN2B2KqKCuPxRwnNsWY7a+FCAq2EMdkj10cM8YgiBSMeGFfzM0mDMzargpHNnNzaBi9Whg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rushstack/terminal": "0.22.4", - "@types/argparse": "1.0.38", - "argparse": "~1.0.9", - "string-argv": "~0.3.1" - } - }, - "node_modules/@rushstack/ts-command-line/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, "node_modules/@sama-communications/sdk": { - "version": "0.16.2", - "resolved": "file:test/cluster/sama-communications-sdk-0.16.2.tgz", - "integrity": "sha512-BewG6wzTxtFx/tXdAgkDS2psEHSU40FFz3QxAKvriMes2W2Epuunl/07hi+EwIh+QLwpNDj4pK9lAYyM8U6CrQ==", + "version": "0.17.1", + "resolved": "file:test/cluster/sama-communications-sdk-0.17.1.tgz", + "integrity": "sha512-Pgs4KHhZHmBB3JD3ukW51N7HYRcw45Yi6fqtlgR1+XgxSecAeWZSwsJOJYRkVJyZ3CVP/UKlsmipgMeQ2B+a5Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "vite-plugin-dts": "^4.5.4", "ws": "8.18.3" } }, @@ -2441,20 +2117,6 @@ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "license": "MIT" }, - "node_modules/@types/argparse": { - "version": "1.0.38", - "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", - "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2476,138 +2138,12 @@ "@types/webidl-conversions": "*" } }, - "node_modules/@volar/language-core": { - "version": "2.4.28", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", - "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/source-map": "2.4.28" - } - }, - "node_modules/@volar/source-map": { - "version": "2.4.28", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", - "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@volar/typescript": { - "version": "2.4.28", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", - "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/language-core": "2.4.28", - "path-browserify": "^1.0.1", - "vscode-uri": "^3.0.8" - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", - "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.2", - "@vue/shared": "3.5.32", - "entities": "^7.0.1", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", - "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.32", - "@vue/shared": "3.5.32" - } - }, - "node_modules/@vue/compiler-vue2": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", - "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", - "dev": true, - "license": "MIT", - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, - "node_modules/@vue/language-core": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.0.tgz", - "integrity": "sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/language-core": "~2.4.11", - "@vue/compiler-dom": "^3.5.0", - "@vue/compiler-vue2": "^2.7.16", - "@vue/shared": "^3.5.0", - "alien-signals": "^0.4.9", - "minimatch": "^9.0.3", - "muggle-string": "^0.4.1", - "path-browserify": "^1.0.1" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@vue/language-core/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vue/shared": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", - "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", - "dev": true, - "license": "MIT" - }, "node_modules/@zxing/text-encoding": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", "optional": true }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/ai": { "version": "5.0.51", "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.51.tgz", @@ -2626,63 +2162,6 @@ "zod": "^3.25.76 || ^4" } }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-draft-04": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", - "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^8.5.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/alien-signals": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-0.4.14.tgz", - "integrity": "sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==", - "dev": true, - "license": "MIT" - }, "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -3166,26 +2645,12 @@ "node": "^12.20.0 || >=14" } }, - "node_modules/compare-versions": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", - "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", - "dev": true, - "license": "MIT" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/confbox": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", - "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -3277,13 +2742,6 @@ "node": "*" } }, - "node_modules/de-indent": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", - "dev": true, - "license": "MIT" - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3377,19 +2835,6 @@ "once": "^1.4.0" } }, - "node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -3411,13 +2856,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, "node_modules/eventsource-parser": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", @@ -3427,25 +2865,11 @@ "node": ">=18.0.0" } }, - "node_modules/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-copy": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==" }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-redact": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", @@ -3460,23 +2884,6 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "license": "MIT" }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fast-xml-parser": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", @@ -3824,16 +3231,6 @@ "dev": true, "license": "ISC" }, - "node_modules/import-lazy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3925,22 +3322,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4112,13 +3493,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jju": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", - "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", - "dev": true, - "license": "MIT" - }, "node_modules/joi": { "version": "17.11.0", "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", @@ -4158,13 +3532,6 @@ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "license": "(AFL-2.1 OR BSD-3-Clause)" }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/json-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-stream/-/json-stream-1.0.0.tgz", @@ -4243,31 +3610,6 @@ "kill-port": "cli.js" } }, - "node_modules/kolorist": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", - "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/local-pkg": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", - "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mlly": "^1.7.4", - "pkg-types": "^2.3.0", - "quansync": "^0.2.11" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4394,16 +3736,6 @@ "node": ">=12" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, "node_modules/memfs": { "version": "4.17.0", "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.0.tgz", @@ -4531,38 +3863,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mlly": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", - "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" - } - }, - "node_modules/mlly/node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/mlly/node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, "node_modules/mocha": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", @@ -4707,13 +4007,6 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" } }, - "node_modules/muggle-string": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", - "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", - "dev": true, - "license": "MIT" - }, "node_modules/nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -4930,13 +4223,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, - "license": "MIT" - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4964,13 +4250,6 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -4993,13 +4272,6 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -5073,18 +4345,6 @@ "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", "license": "MIT" }, - "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" - } - }, "node_modules/prettier": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", @@ -5156,23 +4416,6 @@ "node": ">=6" } }, - "node_modules/quansync": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", - "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/antfu" - }, - { - "type": "individual", - "url": "https://github.com/sponsors/sxzz" - } - ], - "license": "MIT" - }, "node_modules/query-string": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", @@ -5300,16 +4543,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resend": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/resend/-/resend-6.0.3.tgz", @@ -5327,27 +4560,6 @@ } } }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5526,26 +4738,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -5572,13 +4764,6 @@ "node": ">= 10.x" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -5600,16 +4785,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5693,19 +4868,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/thingies": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", @@ -5794,8 +4956,8 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, "peer": true, "bin": { "tsc": "bin/tsc", @@ -5805,13 +4967,6 @@ "node": ">=14.17" } }, - "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", - "dev": true, - "license": "MIT" - }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -5896,65 +5051,6 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, - "node_modules/vite-plugin-dts": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-4.5.4.tgz", - "integrity": "sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@microsoft/api-extractor": "^7.50.1", - "@rollup/pluginutils": "^5.1.4", - "@volar/typescript": "^2.4.11", - "@vue/language-core": "2.2.0", - "compare-versions": "^6.1.1", - "debug": "^4.4.0", - "kolorist": "^1.8.0", - "local-pkg": "^1.0.0", - "magic-string": "^0.30.17" - }, - "peerDependencies": { - "typescript": "*", - "vite": "*" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/vite-plugin-dts/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/vite-plugin-dts/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "dev": true, - "license": "MIT" - }, "node_modules/web-encoding": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", diff --git a/package.json b/package.json index 8fd16342..243cbcc4 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ }, "devDependencies": { "@faker-js/faker": "^10.4.0", - "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.16.2.tgz", + "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.17.1.tgz", "c8": "^10.1.3", "kill-port": "^2.0.1", "mocha": "^10.0.0", diff --git a/test/cluster/sama-communications-sdk-0.16.2.tgz b/test/cluster/sama-communications-sdk-0.16.2.tgz deleted file mode 100644 index 89fb0a853e1995fef550d045617f777c036e2696..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24080 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�r|FgIV{PPzD&u<92UwI#dqZ@bB$Ti6Hcmf?Q){lLkkKf;lyb)K}#NDxSd)H~qVUQiRZ1SWv<+ z+(sjm{J4^BB809#6Q&qsi$_qg0I#AG%IQ0+K6DWj#t3caq9! zl8wGBv?a=qu1^(x%F@pOO$)=qGb98i241h9PA$VGr&UuD1X`+%#+N3N;Si3s2Wb=mchI%4Ye#L4D?Ju(4 z2o*imlyY0skkqW)YOPR_J6#QI$>A(@bDW|2)F+$Y+2CA9SVC1*rD&Pz2&Sq3FsoCT{T;xB_8ZrH7Trkp+(=`0}O2<`e^HrC)(mn$-e)xUDNUjsd z$|JxQYO?&tQS5P9)~gT_P{dVWVC;?0JN|6q{a};>&vbTGFecFsplJfDnuJm>^^E{0?$U&mj1Dk;RR5Per zCc=r5VqKq_h4eJgq`E`A=mZFdx^Ng@CmGUjCP2os=KdR->{U%^?Mg3|vdX4k3Mrnd zq4mv-24W*g87|Qy_%F)YLz?MI@UUY$4JSmE8S2sXkVt9t^!_#)odyfpP&uSiZBioZ z&L_K59n(RzZLY?0tQB#mqEFA{LcL_q=KPrCIyuWFk=^R5*HiHdQFS_}g2-AG7Md+h z*b)`~J>e;p(eXvKEQN>$xy*W+MkU3(4dwoHZ?8&i6?TCm8l!MG2;pL5(MqdFp9#-~ zxg^-py_R*`Pzlzu)~gz5Cf?JqNQJn`P!9pGYzyOAIlPI|nx?w}W$Zmbxn+=)@iVfk zrVAprNsd#`-2ACDRx?^97*-vX>IqGudG&fEqwTy+GYJ+;8OuDViVnR|uX@UitUldR z_mCg9Uf4M=Mp+UF8A6v~8d3nL`wfP5O}1G zGgef2UKdu@yR4N4qeUy!S~5HCmyKz&WFrkQ@i3KK%HlUBY}Ma;j4vIHBD0=&T;6Lz z~;F-q_ zRH9#bQS}H>JGvh&B7@& zy3sBgpc=coXgb|idpXpM1cVlMXE~)wh8l^*Qj7Y9Lk+9RcgjsNRZecr7wIEYH0M*b zNm&(%c06&8Xa~Df?rh4ANnVMcI@Qq{@ccClV^1E#UMfr@uQM1mjPp;h;yC7ASt=vC zt#xya5dk%a8p%I&rr`f8;M3N8|H4)UGX3!mt+TIvYLzId*13xY$FQ)gUapv2G}>_c z!S?)$8lh2|Oebt;q*UR7K7OG<*ovr?3jMQLFvHe31s163!%!-kIi^hibEQp%lr9_) z=uX!U?$)$@Se2&TXi`Ulr4qNS#k5_{0X_bh5V6(v;r}adzEqQ58#h0t#a&C#T2qSu4|*l(o`cR*KXX z=+?&8R71rMT999_YGkgZ3dIq&j^y`z)8mkFj)it~oH z4o|nv0o+6$!hGsH#pE1Wy2z9LZv`X-`pLM@UCoOwaUW(?$=8%lk_-imtB{E7+Uyx2 zdq{j&Q}H&ZDYVwvIi18Kq)U;OZkV!NUb1T|%0YH~OVb(FHl?Ma%icXj*lZ$2ew{t0 ze1H~3fEDeRp_G@e6d(uUAnb9$LijpZ^5{hiy$cilDJMtcC3ZTC50i0AH+Dkac^y23 z>O+PVmxb|oXIgC?sqbD#>m`Lu+I4X9D1i^4!6iL$Wpix@83p^4U7|J6{RIW+>P=kA zoWOSlpIBzQtA|`p%2R>H<%c_|qKM{GpUfetbO?<^K6h!ORBs9F-WIOuA}7sWM{^B0 z>R*=9wGCOWhq6;3es|UAvYuUSDN6e{&+W04_c+h?wAqBa5~;ln!~I770M&M&)M$+a zQ5z!Y10GEz!s~g*h_=lZ?4MXkUs(;Gv+=kS#nxKiqtkr;cf)o5+KIlcu03F@*R=-| zl+3kKcP)JB)p=~I53gMXQlifG2n!do3vTHpXGEZ_W2n5$c6+cLX=@_U<=^PFuiZqn zA5q^0ZbvZ>F6a;anBlNr=dC?N`Ae_h-S&8;hnQ`>tV^#GrnfAbjKFQb>_E}-%}&dy zkxLv_3$AGw*0!^P1?Owi+&3Av)gP>h*_pb+?#({=aUCbT(N= z`oUqh&by^3y_xaIGdR=JTRYsaHMlAJ-C}p_3&oZB;g}EeXGBqtFst*Ire%01bK6DT z2C81C;-yOsi9_xbNn6Q=tF3Rg%q|OF3GGUpgRkl)W0ie%s+a@$v$>T~g)3m;v9U3^ zc^UmF7uOE6EEf~A2Pn!U>dI+v8=xexT)>^3?oCCC|1ObZgSvfKe(>+x=R;_@6RuAd z^zvUrN9==t9&xWv)%RDm&yt%ZuV*adob`+3HJslYN9p(S>9^%7_~q&M{#1S3#l!d6 z+1Uv2^BeP2$lP)2Ku(}@acFEYD6RI4KcbMBrE`J2cEF3(Ut_+{{wWgfE3B59JLked z@xpEcuKH09U4dPQ?qYVtFL>+m4Y5NhH}y&R7@4 z8wVxy!PxOuexN{DAqHrk*yH2@n;?}EWha8pddX1;#)ql8( zQ)`W<+rEsaeJ>sN7W~rLf1oI1uxRzh}$hoEA9#$v5hW*UId-N1P?N(9E~dm)eQRqK(?I z4(e%gWmD1}r0Cyq6T5f+4v*_W4!H*oY15rHe%@`kw8>i82X$@S)?csBwpqi@Jiaf_ z*X!rYZ<{w=%bB?MJAYp+9sd4N_CI*~D`)Mu=5% Date: Thu, 18 Jun 2026 14:51:26 +0300 Subject: [PATCH 62/70] update requestTrackId in error response --- APIs/JSON/routes/packet_processor.js | 8 +++++--- package-lock.json | 2 +- .../sama-communications-sdk-0.17.1.tgz | Bin 13450 -> 13659 bytes 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/APIs/JSON/routes/packet_processor.js b/APIs/JSON/routes/packet_processor.js index ca93bd78..573e6021 100644 --- a/APIs/JSON/routes/packet_processor.js +++ b/APIs/JSON/routes/packet_processor.js @@ -33,11 +33,13 @@ class PacketJsonProcessor extends BasePacketProcessor { async processMessageOrError(ws, message) { let responseData let json + let requestTrackId try { json = this.#parseMessage(message) + requestTrackId = json?.request?.id ?? json?.message?.id ?? json?.system_message?.id ?? json?.id updateStoreContext( MAIN_CONSTANTS.LOGGER_BINDINGS_NAMES.REQUEST_ID, - json?.request?.id ?? json?.message?.id ?? json?.system_message?.id ?? json?.id ?? MAIN_CONSTANTS.LOGGER_BINDINGS_NAMES.NO_REQUEST_ID + requestTrackId ?? MAIN_CONSTANTS.LOGGER_BINDINGS_NAMES.NO_REQUEST_ID ) responseData = await this.#processMessage(ws, json) if (!responseData) { @@ -49,7 +51,7 @@ class PacketJsonProcessor extends BasePacketProcessor { if (json?.request) { errorBackMessage = { response: { - id: json.request?.id, + id: json.request?.id ?? requestTrackId, error: error.cause || error.message, }, } @@ -57,7 +59,7 @@ class PacketJsonProcessor extends BasePacketProcessor { const topLevelElement = json ? Object.keys(json)[0] : void 0 errorBackMessage = { [topLevelElement]: { - id: json?.[topLevelElement]?.id, + id: json?.[topLevelElement]?.id ?? requestTrackId, error: error.cause || error.message, }, } diff --git a/package-lock.json b/package-lock.json index b1981d66..77b183f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1439,7 +1439,7 @@ "node_modules/@sama-communications/sdk": { "version": "0.17.1", "resolved": "file:test/cluster/sama-communications-sdk-0.17.1.tgz", - "integrity": "sha512-Pgs4KHhZHmBB3JD3ukW51N7HYRcw45Yi6fqtlgR1+XgxSecAeWZSwsJOJYRkVJyZ3CVP/UKlsmipgMeQ2B+a5Q==", + "integrity": "sha512-h0Tgykm6Q8MZ3kI2pU2bdVBIkvbzmmEBF+GjlB2GKjmxBxr+MUBrXY8g8uMI+BCfHSx8jPkHztO/sy2bEOiYYA==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/test/cluster/sama-communications-sdk-0.17.1.tgz b/test/cluster/sama-communications-sdk-0.17.1.tgz index 574d1eb213b0388c32e5ac6e285e81f6e2ffbed9..8ecec99a458e5de8e1333724f2a84a53d3ea3956 100644 GIT binary patch delta 13634 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�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 Date: Thu, 18 Jun 2026 14:53:09 +0300 Subject: [PATCH 63/70] add cluster env CLUSTER_ADVERTISE_IP --- app/config/default.js | 2 +- app/config/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/default.js b/app/config/default.js index c73ac88e..03f264e0 100644 --- a/app/config/default.js +++ b/app/config/default.js @@ -3,7 +3,7 @@ import { CONSTANTS } from "../constants/constants.js" const CONFIG = { app: { env: process.env.NODE_ENV ?? CONSTANTS.ENVS.DEV, - ip: void 0, + ip: process.env.CLUSTER_ADVERTISE_IP, name: process.env.APP_NAME ?? "SAMA", hostName: process.env.HOSTNAME, isStandAloneNode: process.env.STANDALONE_NODE === CONSTANTS.ENV_TRUE, diff --git a/app/config/index.js b/app/config/index.js index b8ea27ee..c317df56 100644 --- a/app/config/index.js +++ b/app/config/index.js @@ -7,7 +7,7 @@ import Config from "./config.js" const config = new Config(CONFIG_VALUES) config.set("app.hostName", os.hostname(), true) -config.set("app.ip", ip.address()) +config.set("app.ip", ip.address(), true) const isSecureWs = config.get("ws.options.ssl.key") && config.get("ws.options.ssl.cert") config.set("ws.options.isSecure", !!isSecureWs) From 44b63503662a10ba559734f2119fa6ef8b1453ea Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 18 Jun 2026 16:17:11 +0300 Subject: [PATCH 64/70] update workflows --- .env.docker | 20 +++++++++++++++++--- .env.example | 8 ++++---- .github/workflows/tests.yml | 7 ++++++- docker-compose-full.yml | 2 +- index.js | 14 ++++++-------- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/.env.docker b/.env.docker index a04cdf2d..ee5acf0c 100644 --- a/.env.docker +++ b/.env.docker @@ -1,4 +1,4 @@ -NODE_ENV=development +NODE_ENV=testing APP_PORT=9001 @@ -19,18 +19,23 @@ CONVERSATION_MAX_PARTICIPANTS=50 CONVERSATION_PRELOAD_COUNT=30 MESSAGE_PRELOAD_COUNT=30 SEARCH_PRELOAD_COUNT=30 + JWT_ACCESS_SECRET=4c09be6a35bec6d4089cde4be5ca57a6ee85da5739579cbecb0b8253f268eca2 JWT_ACCESS_TOKEN_EXPIRES_IN=10800 JWT_REFRESH_TOKEN_EXPIRES_IN=1209600 JWT_REFRESH_SECRET=4c09be6a35bec6d4089cde4be5ca57a6ee85da5739579cbecb0b8253f268eca2 + ENCRYPTION_MESSAGE_EXPIRED_IN=2592000 # 30 days ENCRYPTION_DEVICE_TOKEN_EXPIRES_IN=2592000 # 30 days +ENCRYPTION_MESSAGE_EXPIRED_IN=2592000 +ENCRYPTION_DEVICE_TOKEN_EXPIRES_IN=2592000 + OPERATIONS_LOG_EXPIRES_IN=1209600 NODE_CLUSTER_DATA_EXPIRES_IN=30000 + FILE_UPLOAD_URL_EXPIRES_IN=3600 FILE_DOWNLOAD_URL_EXPIRES_IN=604800 -ENCRYPTION_MESSAGE_EXPIRED_IN=2592000 -ENCRYPTION_DEVICE_TOKEN_EXPIRES_IN=2592000 + SSL_KEY_FILE_NAME= SSL_CERT_FILE_NAME= @@ -39,3 +44,12 @@ HOSTNAME=NodeName CORS_ORIGIN=http://localhost:10000 COOKIE_SECRET=4c09be6a35bec6d4089cde4be5ca57a6ee85da5739579cbecb0b8253f268eca2 +# Testing +RUN_NODE_1_CMD='APP_PORT=9001 APP_TCP_PORT=8001 CORS_ORIGIN=http://localhost:3001 npm start' +RUN_NODE_2_CMD='APP_PORT=9002 APP_TCP_PORT=8002 CORS_ORIGIN=http://localhost:3002 npm start' + +NODE_1_WS_ENDPOINT=ws://localhost:9001 +NODE_1_HTTP_ENDPOINT=http://localhost:9001 + +NODE_2_WS_ENDPOINT=ws://localhost:9002 +NODE_2_HTTP_ENDPOINT=http://localhost:9002 \ No newline at end of file diff --git a/.env.example b/.env.example index 034f3751..a965cd96 100644 --- a/.env.example +++ b/.env.example @@ -14,13 +14,13 @@ REDIS_URL=redis://127.0.0.1:6379 # REPL # http -# APP_REPL_HTTP_PORT=5010 -# APP_REPL_HTTP_ACCESS_KEY=repl-wow-key +APP_REPL_HTTP_PORT=5010 +APP_REPL_HTTP_ACCESS_KEY=repl-wow-key # socket APP_REPL_SOCKET_HANDLER=/tmp/net-repl.socket # file -# APP_REPL_FILE_IN=/tmp/pipe-repl.in -# APP_REPL_FILE_OUT=/tmp/pipe-repl.out +APP_REPL_FILE_IN=/tmp/pipe-repl.in +APP_REPL_FILE_OUT=/tmp/pipe-repl.out STORAGE_DRIVER=minio diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 67cf79ca..d333f10c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,14 +11,19 @@ jobs: steps: - uses: actions/checkout@v3 - name: Build the SAMA resources + env: + NODE_ENV: testing run: docker compose -f docker-compose-full.yml up -d --build - name: Run migrations run: docker compose exec sama-server sh -c "MONGODB_URL=mongodb://172.25.0.4/samatests npm run migrate-mongo-up" - - name: Run tests + - name: Run tests single node run: docker compose exec sama-server sh -c "MONGODB_URL=mongodb://172.25.0.4/samatests npm run test" + - name: Run tests cluster nodes + run: docker compose exec sama-server sh -c "MONGODB_URL=mongodb://172.25.0.4/samatests npm run test:cluster" + - name: Stop the SAMA resources if: always() run: docker compose -f docker-compose-full.yml down -v --rmi all diff --git a/docker-compose-full.yml b/docker-compose-full.yml index e6c5fdee..464f5867 100644 --- a/docker-compose-full.yml +++ b/docker-compose-full.yml @@ -51,7 +51,7 @@ services: mongo: condition: service_healthy entrypoint: > - sh -c "npm run start" + sh -c '[ "$NODE_ENV" = "testing" ] && sleep 1200 || npm run start' networks: sama_network: ipv4_address: 172.25.0.7 diff --git a/index.js b/index.js index 6cb9abef..a5dd2cb5 100644 --- a/index.js +++ b/index.js @@ -212,13 +212,11 @@ if (config.get("app.watchdogPingSocketInterval")) { }, config.get("app.watchdogPingSocketInterval")) } -if (config.get("app.env") !== CONSTANTS.ENVS.TESTING) { - await startReplServices( - { ctx: { slc: ServiceLocatorContainer } }, - { accessKey: config.get("repl.http.accessKey"), port: config.get("repl.http.port") }, - { socketHandler: config.get("repl.socket.handler") }, - { fileIn: config.get("repl.file.in"), fileOut: config.get("repl.file.out") } - ) -} +await startReplServices( + { ctx: { slc: ServiceLocatorContainer } }, + { accessKey: config.get("repl.http.accessKey"), port: config.get("repl.http.port") }, + { socketHandler: config.get("repl.socket.handler") }, + { fileIn: config.get("repl.file.in"), fileOut: config.get("repl.file.out") } +) logger.debug("[Ready] cluster-ws: %s", config.get("ws.cluster.endpoint")) \ No newline at end of file From e667eda7e026062de8c408267c508c7f608e2fab Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 18 Jun 2026 16:19:52 +0300 Subject: [PATCH 65/70] add workflow_dispatch --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d333f10c..5242e118 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,6 +5,8 @@ on: branches: - main + workflow_dispatch: + jobs: run_tests: runs-on: self-hosted From a80011c439688b159ce49ad800c30efc4481ac7b Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 18 Jun 2026 16:32:48 +0300 Subject: [PATCH 66/70] update workflows tests --- docker-compose-full.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-full.yml b/docker-compose-full.yml index 464f5867..a54b29ef 100644 --- a/docker-compose-full.yml +++ b/docker-compose-full.yml @@ -51,7 +51,7 @@ services: mongo: condition: service_healthy entrypoint: > - sh -c '[ "$NODE_ENV" = "testing" ] && sleep 1200 || npm run start' + sh -c 'if [ "$NODE_ENV" = "testing" ]; then tail -f /dev/null; else npm run start; fi' networks: sama_network: ipv4_address: 172.25.0.7 From 2775fdc4f304ebfa4d25ebb946253435c10482c7 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Thu, 18 Jun 2026 17:54:49 +0300 Subject: [PATCH 67/70] update env docker --- .env.docker | 3 ++- .github/workflows/tests.yml | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.env.docker b/.env.docker index ee5acf0c..efbf2d0c 100644 --- a/.env.docker +++ b/.env.docker @@ -31,7 +31,7 @@ ENCRYPTION_MESSAGE_EXPIRED_IN=2592000 ENCRYPTION_DEVICE_TOKEN_EXPIRES_IN=2592000 OPERATIONS_LOG_EXPIRES_IN=1209600 -NODE_CLUSTER_DATA_EXPIRES_IN=30000 +NODE_CLUSTER_DATA_EXPIRES_IN=10000 FILE_UPLOAD_URL_EXPIRES_IN=3600 FILE_DOWNLOAD_URL_EXPIRES_IN=604800 @@ -43,6 +43,7 @@ SSL_CERT_FILE_NAME= HOSTNAME=NodeName CORS_ORIGIN=http://localhost:10000 COOKIE_SECRET=4c09be6a35bec6d4089cde4be5ca57a6ee85da5739579cbecb0b8253f268eca2 +HTTP_ADMIN_API_KEY=docker-secret-key # Testing RUN_NODE_1_CMD='APP_PORT=9001 APP_TCP_PORT=8001 CORS_ORIGIN=http://localhost:3001 npm start' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5242e118..09028fa5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,9 +23,6 @@ jobs: - name: Run tests single node run: docker compose exec sama-server sh -c "MONGODB_URL=mongodb://172.25.0.4/samatests npm run test" - - name: Run tests cluster nodes - run: docker compose exec sama-server sh -c "MONGODB_URL=mongodb://172.25.0.4/samatests npm run test:cluster" - - name: Stop the SAMA resources if: always() run: docker compose -f docker-compose-full.yml down -v --rmi all From 93ec44fa1fa6ec9c0dc8993293755b8943f7f114 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Fri, 19 Jun 2026 13:20:25 +0300 Subject: [PATCH 68/70] formating --- APIs/JSON/controllers/http/auth.js | 6 +--- APIs/XMPP | 2 +- Dockerfile.local | 2 +- app/networking/protocol_processors/tcp.js | 19 ++++++----- app/providers/operations/user/auth/index.js | 9 +++-- .../operations/user/connect/index.js | 7 +++- app/providers/services/session/index.js | 31 +++++++++++------- app/utils/watchdog-ping-socket.js | 14 ++++---- index.js | 2 +- package-lock.json | 6 ++-- package.json | 2 +- .../sama-communications-sdk-0.17.1.tgz | Bin 13659 -> 0 bytes test/users.spec.js | 6 ++-- 13 files changed, 63 insertions(+), 43 deletions(-) delete mode 100644 test/cluster/sama-communications-sdk-0.17.1.tgz diff --git a/APIs/JSON/controllers/http/auth.js b/APIs/JSON/controllers/http/auth.js index f132c929..1bffe660 100644 --- a/APIs/JSON/controllers/http/auth.js +++ b/APIs/JSON/controllers/http/auth.js @@ -43,11 +43,7 @@ class HttpAuthController extends BaseHttpController { async logout(res, req) { const httpLogoutOperation = ServiceLocatorContainer.use("HttpUserLogoutOperation") - const { refreshTokenRecord, isWasLastUserSession } = await httpLogoutOperation.perform( - res, - res.parsedHeaders, - res.parsedSignedCookies - ) + const { refreshTokenRecord, isWasLastUserSession } = await httpLogoutOperation.perform(res, res.parsedHeaders, res.parsedSignedCookies) const httpResponse = new HttpResponse(200, {}, { success: true }).addCookie("refresh_token", refreshTokenRecord.token, { maxAge: 0, diff --git a/APIs/XMPP b/APIs/XMPP index 26483372..3958b00b 160000 --- a/APIs/XMPP +++ b/APIs/XMPP @@ -1 +1 @@ -Subproject commit 264833720a3b9fd11c4ab4a09b76df198528f3f2 +Subproject commit 3958b00bdb0d97cfc76ecbdd4eb96257f08ec0ad diff --git a/Dockerfile.local b/Dockerfile.local index c2bc5a3a..5151c4e3 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -4,7 +4,7 @@ WORKDIR /app COPY package*.json ./ -RUN npm install --omit=dev +RUN npm install COPY . . diff --git a/app/networking/protocol_processors/tcp.js b/app/networking/protocol_processors/tcp.js index 59ca5e44..ca846aaa 100644 --- a/app/networking/protocol_processors/tcp.js +++ b/app/networking/protocol_processors/tcp.js @@ -176,14 +176,17 @@ class TcpProtocol extends BaseProtocolProcessor { this.prepareSocketsListeners() return new Promise((resolve) => { - this.tcpSocketServer = net.createServer({ - keepAlive: true, - keepAliveInitialDelay: 5_000 - }, (socket) => { - asyncLoggerContextStore.run(this.requestCreateStoreContext(socket), () => { - this.onOpen(socket) - }) - }) + this.tcpSocketServer = net.createServer( + { + keepAlive: true, + keepAliveInitialDelay: 5_000, + }, + (socket) => { + asyncLoggerContextStore.run(this.requestCreateStoreContext(socket), () => { + this.onOpen(socket) + }) + } + ) this.tcpSocketServer.listen(tcpOptions.port, () => { logger.debug("[TCP] listening on port %s", tcpOptions.port) diff --git a/app/providers/operations/user/auth/index.js b/app/providers/operations/user/auth/index.js index e653b54c..21b561e3 100644 --- a/app/providers/operations/user/auth/index.js +++ b/app/providers/operations/user/auth/index.js @@ -18,12 +18,17 @@ class UserAuthOperation { const { user, token } = userInfo.token ? await this.#authByToken(userInfo.token, deviceId) : await this.#authByUserInfo(organizationId, userInfo, deviceId) - + await this.#validateOrganization(user) // TODO: close connections if (!omitDeviceConnection) { - const { sameDeviceConnection, sameSocketConnection } = this.sessionService.addUserDeviceConnection(ws, user.organization_id, user.native_id, deviceId) + const { sameDeviceConnection, sameSocketConnection } = this.sessionService.addUserDeviceConnection( + ws, + user.organization_id, + user.native_id, + deviceId + ) await this.sessionService.storeUserNodeData(ws, user.organization_id, user.native_id, deviceId) if (sameSocketConnection) { await this.sessionService.removeAllUserDeviceData(user.organization_id, user.native_id, sameSocketConnection.deviceId) diff --git a/app/providers/operations/user/connect/index.js b/app/providers/operations/user/connect/index.js index caee107d..c030cb42 100644 --- a/app/providers/operations/user/connect/index.js +++ b/app/providers/operations/user/connect/index.js @@ -21,7 +21,12 @@ class UserConnectSocketOperation { const user = await this.userService.userRepo.findById(token.user_id) // TODO: close connections - const { sameDeviceConnection, sameSocketConnection } = this.sessionService.addUserDeviceConnection(ws, user.organization_id, user.native_id, deviceId) + const { sameDeviceConnection, sameSocketConnection } = this.sessionService.addUserDeviceConnection( + ws, + user.organization_id, + user.native_id, + deviceId + ) await this.sessionService.storeUserNodeData(ws, user.organization_id, user.native_id, deviceId) if (sameSocketConnection) { await this.sessionService.removeAllUserDeviceData(user.organization_id, user.native_id, sameSocketConnection.deviceId) diff --git a/app/providers/services/session/index.js b/app/providers/services/session/index.js index 76bda30e..522c060f 100644 --- a/app/providers/services/session/index.js +++ b/app/providers/services/session/index.js @@ -22,12 +22,11 @@ class SessionService { addUserDeviceConnection(socket, organizationId, userId, deviceId) { const activeConnections = this.getUserDevices(userId) - const sameSocketConnection = activeConnections.find(connection => connection.socket === socket) - const sameDeviceConnection = activeConnections.find(connection => connection.deviceId === deviceId) + const sameSocketConnection = activeConnections.find((connection) => connection.socket === socket) + const sameDeviceConnection = activeConnections.find((connection) => connection.deviceId === deviceId) - const otherDeviceConnections = activeConnections.filter(connection => - (connection !== sameSocketConnection) && - (connection !== sameDeviceConnection) + const otherDeviceConnections = activeConnections.filter( + (connection) => connection !== sameSocketConnection && connection !== sameDeviceConnection ) const connection = { socket: socket, deviceId, organizationId } @@ -126,14 +125,18 @@ class SessionService { const deviceExt = await this.retrieveUserExtraParams(userId, deviceId) const noEndpointKey = !deviceExt[CONSTANTS.SESSION_NODE_KEY] - const isSameEndpoint = (deviceExt[CONSTANTS.SESSION_NODE_KEY] === this.config.get("ws.cluster.endpoint")) - const hasDeviceConnection = this.getUserDevices(userId).find(connection => connection.deviceId === deviceId) + const isSameEndpoint = deviceExt[CONSTANTS.SESSION_NODE_KEY] === this.config.get("ws.cluster.endpoint") + const hasDeviceConnection = this.getUserDevices(userId).find((connection) => connection.deviceId === deviceId) const isHasExtButNoConnection = isSameEndpoint && !hasDeviceConnection if (noEndpointKey || isHasExtButNoConnection) { actualDevices.delete(deviceId) await this.removeAllUserDeviceDataTransaction(this.config.get("ws.cluster.endpoint"), organizationId, userId, deviceId) - this.logger.debug("[listUserDevice][not actual deviceId]: %o %o", { organizationId, userId, deviceId }, { noEndpointKey, isSameEndpoint, hasDeviceConnection }) + this.logger.debug( + "[listUserDevice][not actual deviceId]: %o %o", + { organizationId, userId, deviceId }, + { noEndpointKey, isSameEndpoint, hasDeviceConnection } + ) } } @@ -258,7 +261,9 @@ class SessionService { async addUserDeviceDataTransaction(nodeEndpoint, organizationId, userId, deviceId, extParams) { const userKey = this.#usersSetCacheKey(organizationId, userId) const userHashKey = this.#usersHashCacheKey(userId, deviceId) - const keyValuePairs = Object.entries(extParams).flat().map((val) => `${val}`) + const keyValuePairs = Object.entries(extParams) + .flat() + .map((val) => `${val}`) const nodeKey = this.#nodesSetCacheKey(nodeEndpoint ?? this.config.get("ws.cluster.endpoint")) const userConnectionMember = `${organizationId}:${userId}:${deviceId}` @@ -426,7 +431,10 @@ class SessionService { if (this.config.get("app.isStandAloneNode")) return isLastConnection - this.logger.debug("[removeUserSession][left connections] %o", leftActiveConnections?.map(con => con.deviceId)) + this.logger.debug( + "[removeUserSession][left connections] %o", + leftActiveConnections?.map((con) => con.deviceId) + ) isLastConnection = await this.removeAllUserDeviceData(organizationId, userId, deviceId) @@ -491,7 +499,8 @@ class SessionService { (session) => session?.organizationId?.toString() === organizationId?.toString() && session?.extraParams[CONSTANTS.SESSION_DEVICE_ID_KEY] !== CONSTANTS.HTTP_DEVICE_ID && - session?.userId && this.listUserDeviceLocal(session?.userId)?.length + session?.userId && + this.listUserDeviceLocal(session?.userId)?.length ) .map((session) => session.userId) .sort((userIdA, userIdB) => userIdA - userIdB) diff --git a/app/utils/watchdog-ping-socket.js b/app/utils/watchdog-ping-socket.js index 3334ff89..ab9daa8d 100644 --- a/app/utils/watchdog-ping-socket.js +++ b/app/utils/watchdog-ping-socket.js @@ -10,7 +10,7 @@ export const watchdogPingSocket = async (logger, sessionService, onWsCloseCb, on for (const userId of users) { const connections = sessionService.activeSessions.DEVICES[userId] ?? [] for (const connection of connections) { - if (!connection?.socket || (connection?.deviceId === CONSTANTS.HTTP_DEVICE_ID)) { + if (!connection?.socket || connection?.deviceId === CONSTANTS.HTTP_DEVICE_ID) { continue } @@ -20,7 +20,7 @@ export const watchdogPingSocket = async (logger, sessionService, onWsCloseCb, on try { if (isTCP) { await new Promise((resolve, reject) => { - connection.socket?.write(pingPackage, (error) => error ? reject(error) : resolve()) + connection.socket?.write(pingPackage, (error) => (error ? reject(error) : resolve())) }) } else { connection.socket?.send(pingPackage) @@ -30,16 +30,18 @@ export const watchdogPingSocket = async (logger, sessionService, onWsCloseCb, on if (isTCP) { await onTcpCloseCb(connection?.socket) .then(() => logger.debug("[close tcp done] %s", userId)) - .catch(error => logger.error(error, "[close tcp error]")) + .catch((error) => logger.error(error, "[close tcp error]")) } else { await onWsCloseCb(connection?.socket, 10) .then(() => logger.debug("[close ws done] %s", userId)) - .catch(error => logger.error(error, "[close ws error]")) + .catch((error) => logger.error(error, "[close ws error]")) } - await sessionService.removeUserSession(connection?.socket, userId, connection?.deviceId).catch(error => logger.error(error, "[remove]")) + await sessionService + .removeUserSession(connection?.socket, userId, connection?.deviceId) + .catch((error) => logger.error(error, "[remove]")) } } } logger.debug("[finish]") -} \ No newline at end of file +} diff --git a/index.js b/index.js index a5dd2cb5..2188f1e4 100644 --- a/index.js +++ b/index.js @@ -219,4 +219,4 @@ await startReplServices( { fileIn: config.get("repl.file.in"), fileOut: config.get("repl.file.out") } ) -logger.debug("[Ready] cluster-ws: %s", config.get("ws.cluster.endpoint")) \ No newline at end of file +logger.debug("[Ready] cluster-ws: %s", config.get("ws.cluster.endpoint")) diff --git a/package-lock.json b/package-lock.json index 77b183f9..03a5efd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ }, "devDependencies": { "@faker-js/faker": "^10.4.0", - "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.17.1.tgz", + "@sama-communications/sdk": "0.17.1", "c8": "^10.1.3", "kill-port": "^2.0.1", "mocha": "^10.0.0", @@ -1438,8 +1438,8 @@ }, "node_modules/@sama-communications/sdk": { "version": "0.17.1", - "resolved": "file:test/cluster/sama-communications-sdk-0.17.1.tgz", - "integrity": "sha512-h0Tgykm6Q8MZ3kI2pU2bdVBIkvbzmmEBF+GjlB2GKjmxBxr+MUBrXY8g8uMI+BCfHSx8jPkHztO/sy2bEOiYYA==", + "resolved": "https://registry.npmjs.org/@sama-communications/sdk/-/sdk-0.17.1.tgz", + "integrity": "sha512-4BhHKh+Tafv2I94E46f52rZpyPOiihC+7XTRYWDAraXZrcActWmQo4/ng8DK6NcLyXWwq0EmOfCfnD2HSS0Zww==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 243cbcc4..ba0ce9c4 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ }, "devDependencies": { "@faker-js/faker": "^10.4.0", - "@sama-communications/sdk": "file:./test/cluster/sama-communications-sdk-0.17.1.tgz", + "@sama-communications/sdk": "0.17.1", "c8": "^10.1.3", "kill-port": "^2.0.1", "mocha": "^10.0.0", diff --git a/test/cluster/sama-communications-sdk-0.17.1.tgz b/test/cluster/sama-communications-sdk-0.17.1.tgz deleted file mode 100644 index 8ecec99a458e5de8e1333724f2a84a53d3ea3956..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13659 zcmV-hHKfWPiwFP!00002|LuM4cH=g(;QZE8RC8t>@(xXZ^hYxCsC7MVC-I&BI<|YW zbG%-MA|Z)mic|>FvaVYD5c>lA0{d|L2z#mkkN`=2xYOw*v-UYj+ayp`C=?1+g<@gq z2e#S+{Us=YZG=W2J z--ZA1lfuvDaJaw!Y-Oi$o=>}bd$ZZh^9hZYW~04WpyGR-&gJR(n+rI8`wC9py?u3d zdG_w@1$_JN96nr}x^RB_{`}pm4=4E8rB<)bE-uf{{`LW%bUFtfz$-S4k|>YTgnJz# z#F9g?fRFt+h6(c%$j1zFmQ6VLNeDrjgrW}&(+mpET*%loOT!|-$1VZFmSM#6EE*K} z6nqXLqGKTp=5WCR0pbAiah4XNG5iJ6A>`wTLzo7|geAF2Q<^o13exF3i$>!d(pkbX zNHbtb9_4fJi+r4B(SH&)g3{JLANx5(97dU+5OGAgi*$V90`aL@w*Fo3V~m6 zhV2N3ctIo2VB#nKh~ac0PF@6KNt6q-F{6PG=7MscAXgljMJPLI26hxVVoLZpnj)NG zG|cBPWm$lL?dSXd#UW&A#w5E1m?G!7pM=PFKK3)l6}-rS0ZZ603L-x?VQVz1N&ZJ# zSYW3a{gzpdKF@wayts?P0wHCfO9XXi_bdw{jzmmZHiKq#<+56-HnPhfSt=>#S$ z82d>S_+laCnV)cM=PU7{C$ao<2tEk55HQyq8bPQHFGwfT2$xMtgvsHJSi&+Nn-~MF zG*gf!cS0d^gd*1GgoTk0`FvUp_lGpQZ7lj(n%xquq+?L3Dp8LTH2^8-f?;wv6F+3& z-}zDO4`Q{xHJ#(4mPBC-d?`4-T=z? zpG;$h-KJT37s+ne?sz(7Nf_P3fW_&|QKNswvgppwqdNx32yWG99O1W*U5=aJh_Ne@ z2R_H?N@%%-hykTMOD94PAtIV0l=;~>3dZGPXJM4587}9H-9WR2#k?3yqFgEmWjBy45zR!J6na8Ky18K(h|PDt9Ho-JAWir* z3W~Jg@f;?8c8iKFt85!#EHNI964K{ULi0lGY?V7yM%LRj2Osn`=~?xKTC;`A0jrg^ z!C=wZhPrQ3LkMG^!+@~_G8Qn>(1W>wMJ+bYekxd!$A~gWvuP@}T{KX&W#)A{haSLp zXb>UFlXBR~CW4DXY?M+mTiuOvIqBM#`N0@8#sDgb!CY8hw7dT!ExV5n2mLn(WeC^XQKL+zrNlHbS>1o%?Y<-yLR*xe#R;yGsL9(6wDzI%SAVc|y!Z zBW{W^+t6c5Nl%MG90loAEbGLdjWtrGEIX#iaf$mdP=6I=N$*^r;YuT1bdw8AB zQ+-n2`>0HR*E3?X`+!YzTt+-sOOA-+!eMb>Dn_Zz*u=kOE{y#KzI7Is_ z1(=^M)WRxpi8n)7!BQ+w%ghLneqA>c?X^c)_n#6O#ThvUFtRVKI372il zVGZU2gwE4aBPJ|hocmcsYjBuF$w>JeED}4JzO-%bfFGv`llxncP6km@+TYZv-a!mS z_?&Xz%u{KcnS@CMGn@dmb9rz!#2J^q3eTe)rMH}jJQ@k~{>aDIq`;&TZC4wRw1l&i z^Ic*S4j@Piw0*?iQ35{1{)`t7 z^x7JO^GVy+ZT%zK3I(o#hD(G?Dw7vI9Q2|^C?!K-$~6z7VPLd!u$6lxI(`l@^EtXOtnzL3H8zdW&Fm`Dd_`Bq&59GDQc&yg z9_i}V5E-!&%>WKW!%83EHa4pEr6%Mwb9FxY%Ffc_L(&d!;l{9(a@xf2SSBVSA4gfZ zi$j{1d`~c<6~}Yj+?k(wa5)w}4(b=3k=iWM<}Do4G9c#1nmdU0fg(Y!2+}5VBT8MW za6b(3_Y6G=UA9!zj0lyS*hY?B!CxMQtlTuw{zF7835$s`3=IiTi(hzBYN{$-Lo8Ho zzaO_R0*dLtfQg+qD=JA63}}zkw=$x#(a9jBFtyljYhj_1)d)h;trLpDstD~t!+LD7 ziB-s}PSH3`o>K&L^*zlEPy*;w3z4@t~8r-@?s(>MwubQ}qub|4^Q0rR?rriFU6 zFwe@+j}W7iO$>@`3D@$3?hnfA`@M zemFiqKYn|8c6tHt&hnic_!{iWc=*$4mQJ$>&0iYm5DJPLlC)KtREr&o_#5ZNgsgv|gd%QO-In{biVO0u&Ee{U0z52Ad2bu_Y8MWV0S2|NP~coq!3f+FCViso0lO8by}0B zS+2X-2^+=Hh$R7Y-15ZMHA0#)I8tX`qZ@#LJ-`EL74 z5PH$r5-@lnrYVjHsq~>~GX04^GNaenR~=_p2ittg@N~F5MU6rs{ zSuitJR6{z5Wt4{Ojy+T>XsL>Wg^1IpBfXo5YX=+5gYVNBdL+VWD;Y&x)WB9Fqs)sW zE~EdYeJqawD3~c9ql&B)B+*KiN_A?W^mG+X(WYHSM$vnThC=V+q84kKcruhc3E7Y( zp=du&}M0wG|Ch%xu=$Fk!96skc^)CoU;skA{#zHPjQsHFG@i>tcRhCu-w_crab6;F zz%IA!;L4+a(_5E725A(tY#O5u7Cx3+dCkL!#UTfl1aZoRDj(qK8_PLZSJ#$=kumc8 zw*0$O};A6yCo3Br+`0kfV^aK8~Hdb;t&n;k`8IfYn!kHLfRM@;XYuV>&b)-B$N3(l4r}HIH%4O%OFSps_NwqeMD|r0z<| zDNPflWYPfzn|ve6rKHu*l}v9+Y?R5iyNBL>r?ZU>y~=8knNJ-pBGpW}!3eqDpw&qJ znFg{lj=jzWV+L|1y<~|DqX1$*85RDB!6?0BSyE3a!yrXvP4nh(yv`pUe?HsKROWx9 zfb9J{_b2|YkqW>)9^QJvKlv}1|NZ=+p8x&)`HTI>{O>REW1&7o-4Nu~(Vd^cNlzS2 zc_NkheU@T~eNOLQ_3Y4PE_Zq_MZ}?hLJt-!DJG2ay1GyH-7>jS{*C%d{eI`i1?%#q z(+#iKb&nI=w1WG}4qePo#Jry;J&VsLgEY4K?9Z0f#TG}c)YI`Cr*U3ovS!9|MZ zN5B8!NbLS^7Ba{jY7gI^uEOBBT2n7g(kV-NHiAGUlhL|QW9G%_$hNLjvnfli zEyuO99`!ca>GXO@5yu}tW?taOF=2Mx#CDc7Tob{|UM8`P+#7f5-nm2f)*ZM%x##Y^ zd*RO9*Y10%fAZdcT)cbhP5q2BJM%(6_no6?Xtz=+!CzZ$?zp*^lLR^od!&eN%4JFV zs3N`UDs{^;8rr!hau9DMl8+zln_h|1c|u@zc5Z6Tl#cX!bpZXoZBI1_f{?yYVclsR zj=KmnIll>L*@WHLH69nVOv3KOe0&=Pw=C=y%bx9Gu~)^p6KSFF*J zWp^6OVx3nLXJHT90GX?}1T@qDO>{tov#@X50GaE&1azwbDoQ~1K!Iqk=(6iT>*|{m zANxGN@uj|V7WPlNIMwxF3GgRFpiCGBcsav2m>l3N>~jUZRo$21&kgVfj8^EFH190z z`x20^8uzpnA5!HU;ytZ=GdJv_Lt3@qnKPe3$GoShxpeqWa( zV==vNMPqW<=eIg1eSYgrqHyitc^cm_+0_-|0xR1qWCBId%ga`@e^j^wFADL|6JI>T zZH4%12Q~gukA)Keq9o^&Psg z9XLmu=5bj!BB(!^5UoZ4y}q{G%yA<-YnUJuR@`XsP%G2mshNB2Hji#|g%M@$9v69f z&Sck*ALTfeS?Da6&Qklcbg78K#J|HBewuk{@;XY`&QAGBEPnVTLeIECF!Yh=QOqpO zd=x<4S(Jq7%*)s$y%S4SX$IR$lQ>G4G{v z1%1s{?ZgQaJS=CaxGa;6ONh%#)J^q3JPaQc#bE5oQ(P2V(BJ5ix&Z=U0Z76 zqTV7Cjk=2~cHQM}6n2ko<}8=WQ57gz5-JluauXeidaQd4Z5BVOES>yCWLv_k#aJdr zapkC4=vk!>(^msxv&p{Zr0=8Q+-8pBwk`>5d`;7^VyQAuj@*mF>t3lQ6Qw32O-*tX z`)bJF=lN6{wwpM$wT+&S0)K6}C6ALbP!aVO^jQo`^4-h%lv!O%28eN!-}@&|6Km;a zy*Lg0_##g;-0emzKg-#~wm74}DmmlX&W;^<$MTCV>fZbDZ_LkF27g*)%O9Q6H(5+r zK2F0fbAyb9;*6MgEqP2}xnt&sEaTm1d6eL)^ag1-@6|Y-I0|6U^JjjP!;s~{*#7ZP zi(IzWfpWoRRDlczA?jlXvta@X}icKd7I9xur=~DkV8`+|kF6f!u~J*?lfa zx#^Ain&&lgx{FePTc<`ygP0rfd-rDPxHqO7EaVfzEA_>omX;-3TJ9oGZ&}i19{uZz zaLG*+cA2L_Cw-wt)Ss115t`TOC`#7iDmcq-Vr*u)i!>YgiJWjmqcgwu&hzOZSTKTCPQZTh~oJCE2tE8)Q>=l#1aYY^_Ad zlxDTw_|A^d9yHn>n$&8%y~J_F@-I25>r(6eyNgT9wWNsWHBP35CS%)yL3wj=o76#F zuq1qU`Ti2cxttr|pM?vim^C+1Q*iLfO`16UaO#%L~*>=i_T+l{4i9#1msV*an$_?mg5QXl2mwETD zNPg1P%+JDl%eKeM<*J}Xq{W9ciDaL>59AC!+6W*`J3iK5Xg;ouL6jBuOogIDc z|Kci4i4fLBARQ(jG#1Q7F~Jn}wQEuVL^xc&a!2RsgCF}P1sgg@)TY2 zg}w&y5p8=3YeV2?w|W5on4dF0+zJ=Qw2k42Ihx=k)#%@7UFxxH()3MbQimyb+HIi9 zjkKLsI+CbImGpIKk|+A=T;kRaoUEkkpbLIj<&-gw3k{Q$RA9NMC;X`FjvctB$v1R%hm0i z?!phluFZSs4pxtAF*c5oJXe^jT{~IdIFWYk(v2`b{6@rXPliuC3`3)A;lJC@?<-6(O^B1B1Ry!5~h9 zTT+Z`;G&wjp_VMXk^btU;UrT7^1jy+NzHCVB(yYM>SPtkme6FwP4LP?f3-J?!-j2a zP$OBuau*X^FfQ(LTpe<)&-^|<`c>nma|;STu$%e_|5T2j0H@3Z;l?5 zzJJa9@1K?ay($0u`PYs5-_M^NK7Y*r{t`d7N_)0Lw{R9J^;#^!Ue6M?r?s;q-%Kcr z`ma`D^@}p=+DN2!y0WQA(eCvug~Hm|5zjn-686Qf_LU`}ST1(Ib_=I#hdl|S%4{Z- z)t}w)<41He-NFuaO|e+Ip*Q1i(-2R|9g(PGj*{Ss5t3wP}FUKS$d{gfU& zIOTmzDeuK~Z(OCk3%Hk6%6p-uycd}AjxDOp^s#;Svoq84tuoW|?K0EvnwjZ$T4wr( z6`AS9V`lneX8IS&OwYH?OwZS5rsvybrhoW7WTsEtxt5bYb1yI_-92+p+*f79;FB`b zx#RLGGkvl}X8J@+rsOS|>AablKJW2HX8L?(W;$=lOuyA2n30OxEi=)!+e{>3TA^!K)iReS3?{bUlgtMh6t@nc7=qrk}Uw%2(;wYckXCS7oO2MrQhK ztIYJ7Ay6wb(|J8J{bIY!^a}(0s?2m=&rE;4O=kM*R@n8-bl%KNf4^O3`ukS+&CGPZ zE;IdX>&*1CRjgW>na+*ObiOJxoquX(`l$DE^fNNk`KrwHlTDfFCvB{7s$aLxOn

`taIury?^wmYM0(-peVv8plq5WFI5mO_}L&Ke&po`y(6wS>K^Mddy6J z%uN4%W~S#`W~S$unLhr+%=G+UHZwh6mzkdbPBPQ^Lo?I)XU$B{mp`JPGx<50=@ZAD zv}UHydlNk~eex-p>GP%I&L1<=A2ZW`>CE)}H

sKW}DwE;7@(%uLT+{)w6C{EKF$ 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 diff --git a/test/users.spec.js b/test/users.spec.js index d8d8f056..44cad1d9 100644 --- a/test/users.spec.js +++ b/test/users.spec.js @@ -272,7 +272,7 @@ describe("User cycle", async () => { await orgRepo.updateOne({ _id: orgId }, { $set: { is_blocked: true, block_reason: "Out of limits" } }) }) - it("create in blocked organization" , async () => { + it("create in blocked organization", async () => { const requestData = { request: { user_create: { @@ -300,7 +300,7 @@ describe("User cycle", async () => { assert.equal(responseData.response.user, void 0) }) - it("login in blocked organization" , async () => { + it("login in blocked organization", async () => { const requestData = { request: { user_login: { @@ -328,7 +328,7 @@ describe("User cycle", async () => { }) after(async () => { - await orgRepo.updateOne({ _id: orgId }, { $unset: { is_blocked: 1, blocked_reason: 1} }) + await orgRepo.updateOne({ _id: orgId }, { $unset: { is_blocked: 1, blocked_reason: 1 } }) }) }) From 1aaca5789e0376b571c23eee80ea665230009e95 Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Tue, 7 Jul 2026 16:07:09 +0300 Subject: [PATCH 69/70] update submodule --- APIs/XMPP | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/APIs/XMPP b/APIs/XMPP index 3958b00b..3a20ecbb 160000 --- a/APIs/XMPP +++ b/APIs/XMPP @@ -1 +1 @@ -Subproject commit 3958b00bdb0d97cfc76ecbdd4eb96257f08ec0ad +Subproject commit 3a20ecbb85511231b48a86b58e6b96a175439ddd From 31cce61b7d10d644617fda84fadca57e97b3c93b Mon Sep 17 00:00:00 2001 From: AntonSelezev Date: Tue, 7 Jul 2026 16:12:49 +0300 Subject: [PATCH 70/70] update submodule to main branch --- APIs/XMPP | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/APIs/XMPP b/APIs/XMPP index 3a20ecbb..53e1ab5b 160000 --- a/APIs/XMPP +++ b/APIs/XMPP @@ -1 +1 @@ -Subproject commit 3a20ecbb85511231b48a86b58e6b96a175439ddd +Subproject commit 53e1ab5ba00fe910e48450431d2c6aff8bebc26e