From f3d1ac662cac2ee33968dab12390efa79f5e992f Mon Sep 17 00:00:00 2001 From: shash-hq Date: Sun, 18 Jan 2026 03:30:21 +0530 Subject: [PATCH 1/4] fix(presence): batch presence updates to prevent broadcast storm #21182 - Implemented buffering in Presence service to batch updates every 500ms - Added 'presence.status.batch' event to Events definition - Updated ListenersModule to handle batched updates - Updated AppsEngineService to handle batched updates - Updated OmnichannelService to handle batched updates - Added unit tests for batching logic --- .../modules/listeners/listeners.module.ts | 33 ++- .../server/services/apps-engine/service.ts | 10 + .../server/services/omnichannel/service.ts | 13 ++ ee/packages/presence/src/Presence.spec.ts | 101 +++++++++ ee/packages/presence/src/Presence.ts | 32 ++- packages/core-services/src/events/Events.ts | 192 +++++++++--------- 6 files changed, 281 insertions(+), 100 deletions(-) create mode 100644 ee/packages/presence/src/Presence.spec.ts diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index 8b04d04b869ba..4b6269973cf68 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -54,9 +54,9 @@ export class ListenersModule { if (!isMessageParserDisabled && message.msg) { const customDomains = settings.get('Message_CustomDomain_AutoLink') ? settings - .get('Message_CustomDomain_AutoLink') - .split(',') - .map((domain) => domain.trim()) + .get('Message_CustomDomain_AutoLink') + .split(',') + .map((domain) => domain.trim()) : []; message.md = parse(message.msg, { @@ -181,6 +181,33 @@ export class ListenersModule { } }); + service.onEvent('presence.status.batch', (batch) => { + batch.forEach(({ user }) => { + const { _id, username, name, status, statusText, roles } = user; + if (!status || !username) { + return; + } + + notifications.notifyUserInThisInstance(_id, 'userData', { + type: 'updated', + id: _id, + diff: { + status, + ...(statusText && { statusText }), + }, + unset: { + ...(!statusText && { statusText: 1 }), + }, + }); + + notifications.notifyLoggedInThisInstance('user-status', [_id, username, STATUS_MAP[status], statusText, name, roles]); + + if (_id) { + notifications.sendPresence(_id, username, STATUS_MAP[status], statusText); + } + }); + }); + service.onEvent('user.updateCustomStatus', (userStatus) => { notifications.notifyLoggedInThisInstance('updateCustomUserStatus', { userStatusData: userStatus, diff --git a/apps/meteor/server/services/apps-engine/service.ts b/apps/meteor/server/services/apps-engine/service.ts index 0be60a579908d..e9984524e128a 100644 --- a/apps/meteor/server/services/apps-engine/service.ts +++ b/apps/meteor/server/services/apps-engine/service.ts @@ -30,6 +30,16 @@ export class AppsEngineService extends ServiceClassInternal implements IAppsEngi }); }); + this.onEvent('presence.status.batch', async (batch): Promise => { + for (const { user, previousStatus } of batch) { + await Apps.self?.triggerEvent(AppEvents.IPostUserStatusChanged, { + user, + currentStatus: user.status, + previousStatus, + }); + } + }); + this.onEvent('apps.added', async (appId: string): Promise => { Apps.self?.getRocketChatLogger().debug({ msg: '"apps.added" event received for app', diff --git a/apps/meteor/server/services/omnichannel/service.ts b/apps/meteor/server/services/omnichannel/service.ts index 067cd9474ce7d..40b5d75adaedd 100644 --- a/apps/meteor/server/services/omnichannel/service.ts +++ b/apps/meteor/server/services/omnichannel/service.ts @@ -30,6 +30,19 @@ export class OmnichannelService extends ServiceClassInternal implements IOmnicha await notifyAgentStatusChanged(user._id, user.status); } }); + + this.onEvent('presence.status.batch', async (batch): Promise => { + for (const { user } of batch) { + if (!user?._id) { + continue; + } + const hasRole = user.roles.some((role) => ['livechat-manager', 'livechat-monitor', 'livechat-agent'].includes(role)); + if (hasRole) { + // TODO change `Livechat.notifyAgentStatusChanged` to a service call + await notifyAgentStatusChanged(user._id, user.status); + } + } + }); } override async started() { diff --git a/ee/packages/presence/src/Presence.spec.ts b/ee/packages/presence/src/Presence.spec.ts new file mode 100644 index 0000000000000..46674cec412d1 --- /dev/null +++ b/ee/packages/presence/src/Presence.spec.ts @@ -0,0 +1,101 @@ +import { Presence } from './Presence'; + +jest.mock('@rocket.chat/core-services', () => ({ + ServiceClass: class { + api = { + broadcast: jest.fn(), + }; + onEvent() { } + }, + License: { + hasModule: jest.fn().mockResolvedValue(true), + }, +})); + +jest.mock('@rocket.chat/models', () => ({ + Settings: { updateValueById: jest.fn() }, + Users: {}, + UsersSessions: {}, + registerServiceModels: jest.fn(), +})); + +jest.mock('./lib/PresenceReaper', () => ({ + PresenceReaper: class { + start() { } + stop() { } + }, +})); + +describe('Presence Batching', () => { + let presence: Presence; + + beforeEach(() => { + jest.useFakeTimers(); + presence = new Presence(); + (presence as any).broadcastEnabled = true; + (presence as any).hasLicense = true; + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should buffer broadcast events', () => { + const user = { _id: 'u1', username: 'user1', status: 'online' } as any; + (presence as any).broadcast(user, 'offline'); + + // Check buffer + expect((presence as any).presenceBatch.size).toBe(1); + + // Assert API not called yet + expect((presence as any).api.broadcast).not.toHaveBeenCalled(); + + // Advance timers + jest.advanceTimersByTime(500); + + // Assert API called + expect((presence as any).api.broadcast).toHaveBeenCalledWith('presence.status.batch', expect.any(Array)); + expect((presence as any).api.broadcast).toHaveBeenCalledTimes(1); + + const batch = (presence as any).api.broadcast.mock.calls[0][1]; + expect(batch).toHaveLength(1); + expect(batch[0].user._id).toBe('u1'); + }); + + it('should batch multiple updates', () => { + const u1 = { _id: 'u1', username: 'user1', status: 'online' } as any; + const u2 = { _id: 'u2', username: 'user2', status: 'busy' } as any; + + (presence as any).broadcast(u1, 'offline'); + (presence as any).broadcast(u2, 'online'); + + expect((presence as any).presenceBatch.size).toBe(2); + + jest.advanceTimersByTime(500); + + expect((presence as any).api.broadcast).toHaveBeenCalledWith('presence.status.batch', expect.any(Array)); + const batch = (presence as any).api.broadcast.mock.calls[0][1]; + expect(batch).toHaveLength(2); + const ids = batch.map((item: any) => item.user._id).sort(); + expect(ids).toEqual(['u1', 'u2']); + }); + + it('should debounce updates for same user', () => { + const u1_v1 = { _id: 'u1', username: 'user1', status: 'online' } as any; + const u1_v2 = { _id: 'u1', username: 'user1', status: 'offline' } as any; + + (presence as any).broadcast(u1_v1, 'busy'); + (presence as any).broadcast(u1_v2, 'online'); + + // Should update the existing entry in map + expect((presence as any).presenceBatch.size).toBe(1); + const stored = (presence as any).presenceBatch.get('u1'); + expect(stored.user.status).toBe('offline'); + + jest.advanceTimersByTime(500); + expect((presence as any).api.broadcast).toHaveBeenCalledTimes(1); + const batch = (presence as any).api.broadcast.mock.calls[0][1]; + expect(batch).toHaveLength(1); + expect(batch[0].user.status).toBe('offline'); + }); +}); diff --git a/ee/packages/presence/src/Presence.ts b/ee/packages/presence/src/Presence.ts index 0fe78b9e47dcd..4790b57a87283 100755 --- a/ee/packages/presence/src/Presence.ts +++ b/ee/packages/presence/src/Presence.ts @@ -22,6 +22,16 @@ export class Presence extends ServiceClass implements IPresence { private lostConTimeout?: NodeJS.Timeout; + private presenceBatch = new Map< + string, + { + user: Pick; + previousStatus: UserStatus | undefined; + } + >(); + + private batchTimeout?: NodeJS.Timeout; + private connsPerInstance = new Map(); private peakConnections = 0; @@ -298,10 +308,24 @@ export class Presence extends ServiceClass implements IPresence { if (!this.broadcastEnabled) { return; } - this.api?.broadcast('presence.status', { - user, - previousStatus, - }); + + this.presenceBatch.set(user._id, { user, previousStatus }); + + if (this.batchTimeout) { + return; + } + + this.batchTimeout = setTimeout(() => { + this.batchTimeout = undefined; + if (this.presenceBatch.size === 0) { + return; + } + + const batch = Array.from(this.presenceBatch.values()); + this.presenceBatch.clear(); + + this.api?.broadcast('presence.status.batch', batch); + }, 500); } private async validateAvailability(): Promise { diff --git a/packages/core-services/src/events/Events.ts b/packages/core-services/src/events/Events.ts index b80b6a7d90061..4aa8bce8929bb 100644 --- a/packages/core-services/src/events/Events.ts +++ b/packages/core-services/src/events/Events.ts @@ -43,15 +43,15 @@ type ClientAction = 'inserted' | 'updated' | 'removed' | 'changed'; type LoginServiceConfigurationEvent = { id: string; } & ( - | { + | { clientAction: 'removed'; data?: never; - } - | { + } + | { clientAction: Omit; data: Omit, 'secret'> & { secret?: never }; - } -); + } + ); export type EventSignatures = { 'room.video-conference': (params: { rid: string; callId: string }) => void; @@ -83,13 +83,13 @@ export type EventSignatures = { uid: string, data: | { - type: 'changed'; - account: Partial; - } + type: 'changed'; + account: Partial; + } | { - type: 'removed'; - account: { _id: IWebdavAccount['_id'] }; - }, + type: 'removed'; + account: { _id: IWebdavAccount['_id'] }; + }, ): void; 'notify.e2e.keyRequest'(rid: string, data: IRoom['e2eKeyId']): void; 'notify.deleteMessage'(rid: string, data: { _id: string }): void; @@ -104,14 +104,14 @@ export type EventSignatures = { ids?: string[]; // message ids have priority over ts showDeletedStatus?: boolean; } & ( - | { + | { filesOnly: true; replaceFileAttachmentsWith?: MessageAttachment; - } - | { + } + | { filesOnly?: false; - } - ), + } + ), ): void; 'notify.deleteCustomSound'(data: { soundData: ICustomSound }): void; 'notify.updateCustomSound'(data: { soundData: ICustomSound }): void; @@ -129,12 +129,12 @@ export type EventSignatures = { user: Pick, data: | { - messageErasureType: 'Delete'; - } + messageErasureType: 'Delete'; + } | { - messageErasureType: 'Unlink'; - replaceByUser: { _id: IUser['_id']; username: IUser['username']; alias: string }; - }, + messageErasureType: 'Unlink'; + replaceByUser: { _id: IUser['_id']; username: IUser['username']; alias: string }; + }, ): void; 'user.deleteCustomStatus'(userStatus: Omit): void; 'user.forceLogout': (uid: string) => void; @@ -162,83 +162,89 @@ export type EventSignatures = { user: Pick; previousStatus: UserStatus | undefined; }): void; + 'presence.status.batch'( + data: { + user: Pick; + previousStatus: UserStatus | undefined; + }[], + ): void; 'watch.messages'(data: { message: IMessage }): void; 'watch.roles'( data: | { clientAction: Exclude; role: IRole } | { - clientAction: 'removed'; - role: { - _id: string; - name: string; - }; - }, + clientAction: 'removed'; + role: { + _id: string; + name: string; + }; + }, ): void; 'watch.rooms'(data: { clientAction: ClientAction; room: Pick | IRoom }): void; 'watch.subscriptions'( data: | { - clientAction: 'updated' | 'inserted'; - subscription: Pick< - ISubscription, - | 't' - | 'ts' - | 'ls' - | 'lr' - | 'name' - | 'fname' - | 'rid' - | 'code' - | 'f' - | 'u' - | 'open' - | 'alert' - | 'roles' - | 'unread' - | 'prid' - | 'userMentions' - | 'groupMentions' - | 'archived' - | 'audioNotificationValue' - | 'desktopNotifications' - | 'mobilePushNotifications' - | 'emailNotifications' - | 'desktopPrefOrigin' - | 'mobilePrefOrigin' - | 'emailPrefOrigin' - | 'unreadAlert' - | '_updatedAt' - | 'blocked' - | 'blocker' - | 'autoTranslate' - | 'autoTranslateLanguage' - | 'disableNotifications' - | 'hideUnreadStatus' - | 'hideMentionStatus' - | 'muteGroupMentions' - | 'ignored' - | 'E2EKey' - | 'E2ESuggestedKey' - | 'oldRoomKeys' - | 'tunread' - | 'tunreadGroup' - | 'tunreadUser' + clientAction: 'updated' | 'inserted'; + subscription: Pick< + ISubscription, + | 't' + | 'ts' + | 'ls' + | 'lr' + | 'name' + | 'fname' + | 'rid' + | 'code' + | 'f' + | 'u' + | 'open' + | 'alert' + | 'roles' + | 'unread' + | 'prid' + | 'userMentions' + | 'groupMentions' + | 'archived' + | 'audioNotificationValue' + | 'desktopNotifications' + | 'mobilePushNotifications' + | 'emailNotifications' + | 'desktopPrefOrigin' + | 'mobilePrefOrigin' + | 'emailPrefOrigin' + | 'unreadAlert' + | '_updatedAt' + | 'blocked' + | 'blocker' + | 'autoTranslate' + | 'autoTranslateLanguage' + | 'disableNotifications' + | 'hideUnreadStatus' + | 'hideMentionStatus' + | 'muteGroupMentions' + | 'ignored' + | 'E2EKey' + | 'E2ESuggestedKey' + | 'oldRoomKeys' + | 'tunread' + | 'tunreadGroup' + | 'tunreadUser' - // Omnichannel fields - | 'department' - | 'v' - | 'onHold' - >; - } + // Omnichannel fields + | 'department' + | 'v' + | 'onHold' + >; + } | { - clientAction: 'removed'; - subscription: { - _id: string; - u?: Pick; - rid?: string; - t?: string; - }; - }, + clientAction: 'removed'; + subscription: { + _id: string; + u?: Pick; + rid?: string; + t?: string; + }; + }, ): void; 'watch.inquiries'(data: { clientAction: ClientAction; inquiry: ILivechatInquiryRecord; diff?: undefined | Record }): void; 'watch.settings'(data: { clientAction: ClientAction; setting: ISetting }): void; @@ -246,19 +252,19 @@ export type EventSignatures = { data: { id: string; } & ( - | { + | { clientAction: 'inserted'; data: IUser; - } - | { + } + | { clientAction: 'removed'; - } - | { + } + | { clientAction: 'updated'; diff: Record; unset: Record; - } - ), + } + ), ): void; 'watch.loginServiceConfiguration'(data: LoginServiceConfigurationEvent): void; 'watch.instanceStatus'(data: { From 8392a4db8cc7ac258741d3e00ac08691b1c92afd Mon Sep 17 00:00:00 2001 From: shash-hq Date: Sun, 18 Jan 2026 03:43:34 +0530 Subject: [PATCH 2/4] chore: add changeset for presence fix --- .changeset/breezy-dolphins-sing.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/breezy-dolphins-sing.md diff --git a/.changeset/breezy-dolphins-sing.md b/.changeset/breezy-dolphins-sing.md new file mode 100644 index 0000000000000..ea09c16c0c772 --- /dev/null +++ b/.changeset/breezy-dolphins-sing.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/core-services": patch +"@rocket.chat/meteor": patch +--- + +Fix: Implement batching for presence status updates to prevent broadcast storms during mass user reconnections. From db69ed4a4119374388d63f5406ad6e7e3ebd6199 Mon Sep 17 00:00:00 2001 From: Shashank Date: Sun, 18 Jan 2026 04:21:57 +0530 Subject: [PATCH 3/4] Update ee/packages/presence/src/Presence.ts Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- ee/packages/presence/src/Presence.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ee/packages/presence/src/Presence.ts b/ee/packages/presence/src/Presence.ts index 4790b57a87283..8c3cfa0fc4c76 100755 --- a/ee/packages/presence/src/Presence.ts +++ b/ee/packages/presence/src/Presence.ts @@ -324,6 +324,10 @@ export class Presence extends ServiceClass implements IPresence { const batch = Array.from(this.presenceBatch.values()); this.presenceBatch.clear(); + if (!this.broadcastEnabled) { + return; + } + this.api?.broadcast('presence.status.batch', batch); }, 500); } From 71a77ff3f687d0629993a935d6edf150e9e58161 Mon Sep 17 00:00:00 2001 From: shash-hq Date: Sun, 18 Jan 2026 04:28:49 +0530 Subject: [PATCH 4/4] Address PR feedback and improve presence batching resilience --- .../server/services/apps-engine/service.ts | 14 +- .../server/services/omnichannel/service.ts | 31 ++- ee/packages/presence/src/Presence.spec.ts | 11 ++ ee/packages/presence/src/Presence.ts | 30 ++- packages/core-services/src/events/Events.ts | 186 +++++++++--------- 5 files changed, 147 insertions(+), 125 deletions(-) diff --git a/apps/meteor/server/services/apps-engine/service.ts b/apps/meteor/server/services/apps-engine/service.ts index e9984524e128a..e7d5f7642146c 100644 --- a/apps/meteor/server/services/apps-engine/service.ts +++ b/apps/meteor/server/services/apps-engine/service.ts @@ -32,11 +32,15 @@ export class AppsEngineService extends ServiceClassInternal implements IAppsEngi this.onEvent('presence.status.batch', async (batch): Promise => { for (const { user, previousStatus } of batch) { - await Apps.self?.triggerEvent(AppEvents.IPostUserStatusChanged, { - user, - currentStatus: user.status, - previousStatus, - }); + try { + await Apps.self?.triggerEvent(AppEvents.IPostUserStatusChanged, { + user, + currentStatus: user.status, + previousStatus, + }); + } catch (error) { + SystemLogger.error({ msg: 'Error triggering IPostUserStatusChanged event', error }); + } } }); diff --git a/apps/meteor/server/services/omnichannel/service.ts b/apps/meteor/server/services/omnichannel/service.ts index 40b5d75adaedd..5a5c39f132fea 100644 --- a/apps/meteor/server/services/omnichannel/service.ts +++ b/apps/meteor/server/services/omnichannel/service.ts @@ -1,6 +1,6 @@ import { ServiceClassInternal } from '@rocket.chat/core-services'; import type { IOmnichannelService } from '@rocket.chat/core-services'; -import type { AtLeast, IOmnichannelQueue, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { AtLeast, IOmnichannelQueue, IOmnichannelRoom, IUser, UserStatus } from '@rocket.chat/core-typings'; import { License } from '@rocket.chat/license'; import moment from 'moment'; @@ -21,30 +21,27 @@ export class OmnichannelService extends ServiceClassInternal implements IOmnicha override async created() { this.onEvent('presence.status', async ({ user }): Promise => { - if (!user?._id) { - return; - } - const hasRole = user.roles.some((role) => ['livechat-manager', 'livechat-monitor', 'livechat-agent'].includes(role)); - if (hasRole) { - // TODO change `Livechat.notifyAgentStatusChanged` to a service call - await notifyAgentStatusChanged(user._id, user.status); - } + await this.handlePresenceUpdate(user); }); this.onEvent('presence.status.batch', async (batch): Promise => { for (const { user } of batch) { - if (!user?._id) { - continue; - } - const hasRole = user.roles.some((role) => ['livechat-manager', 'livechat-monitor', 'livechat-agent'].includes(role)); - if (hasRole) { - // TODO change `Livechat.notifyAgentStatusChanged` to a service call - await notifyAgentStatusChanged(user._id, user.status); - } + await this.handlePresenceUpdate(user); } }); } + private async handlePresenceUpdate(user: Pick): Promise { + if (!user?._id) { + return; + } + const hasRole = user.roles.some((role) => ['livechat-manager', 'livechat-monitor', 'livechat-agent'].includes(role)); + if (hasRole) { + // TODO change `Livechat.notifyAgentStatusChanged` to a service call + await notifyAgentStatusChanged(user._id, user.status as UserStatus); + } + } + override async started() { settings.watchMultiple(['Livechat_enabled', 'Livechat_Routing_Method'], () => { this.queueWorker.shouldStart(); diff --git a/ee/packages/presence/src/Presence.spec.ts b/ee/packages/presence/src/Presence.spec.ts index 46674cec412d1..0b97340ae6fa7 100644 --- a/ee/packages/presence/src/Presence.spec.ts +++ b/ee/packages/presence/src/Presence.spec.ts @@ -98,4 +98,15 @@ describe('Presence Batching', () => { expect(batch).toHaveLength(1); expect(batch[0].user.status).toBe('offline'); }); + + it('should not broadcast if broadcastEnabled is false', () => { + (presence as any).broadcastEnabled = false; + const user = { _id: 'u1', username: 'user1', status: 'online' } as any; + + (presence as any).broadcast(user, 'offline'); + + expect((presence as any).presenceBatch.size).toBe(0); + jest.advanceTimersByTime(500); + expect((presence as any).api.broadcast).not.toHaveBeenCalled(); + }); }); diff --git a/ee/packages/presence/src/Presence.ts b/ee/packages/presence/src/Presence.ts index 8c3cfa0fc4c76..31ed3caa0d273 100755 --- a/ee/packages/presence/src/Presence.ts +++ b/ee/packages/presence/src/Presence.ts @@ -25,7 +25,7 @@ export class Presence extends ServiceClass implements IPresence { private presenceBatch = new Map< string, { - user: Pick; + user: Pick; previousStatus: UserStatus | undefined; } >(); @@ -128,10 +128,13 @@ export class Presence extends ServiceClass implements IPresence { override async stopped(): Promise { this.reaper.stop(); - if (!this.lostConTimeout) { - return; + if (this.lostConTimeout) { + clearTimeout(this.lostConTimeout); + } + if (this.batchTimeout) { + clearTimeout(this.batchTimeout); + this.batchTimeout = undefined; } - clearTimeout(this.lostConTimeout); } async toggleBroadcast(enabled: boolean): Promise { @@ -243,8 +246,8 @@ export class Presence extends ServiceClass implements IPresence { async setStatus(uid: string, statusDefault: UserStatus, statusText?: string): Promise { const userSessions = (await UsersSessions.findOneById(uid)) || { connections: [] }; - const user = await Users.findOneById>(uid, { - projection: { username: 1, roles: 1, status: 1 }, + const user = await Users.findOneById>(uid, { + projection: { username: 1, name: 1, roles: 1, status: 1 }, }); const { status, statusConnection } = processPresenceAndStatus(userSessions.connections, statusDefault); @@ -257,7 +260,10 @@ export class Presence extends ServiceClass implements IPresence { }); if (result.modifiedCount > 0) { - this.broadcast({ _id: uid, username: user?.username, status, statusText, roles: user?.roles || [] }, user?.status); + this.broadcast( + { _id: uid, username: user?.username, status, statusText, name: user?.name, roles: user?.roles || [] }, + user?.status, + ); } return !!result.modifiedCount; @@ -272,9 +278,10 @@ export class Presence extends ServiceClass implements IPresence { } async updateUserPresence(uid: string): Promise { - const user = await Users.findOneById>(uid, { + const user = await Users.findOneById>(uid, { projection: { username: 1, + name: 1, statusDefault: 1, statusText: 1, roles: 1, @@ -297,12 +304,15 @@ export class Presence extends ServiceClass implements IPresence { }); if (result.modifiedCount > 0) { - this.broadcast({ _id: uid, username: user.username, status, statusText: user.statusText, roles: user.roles }, user.status); + this.broadcast( + { _id: uid, username: user.username, status, statusText: user.statusText, name: user.name, roles: user.roles }, + user.status, + ); } } private broadcast( - user: Pick, + user: Pick, previousStatus: UserStatus | undefined, ): void { if (!this.broadcastEnabled) { diff --git a/packages/core-services/src/events/Events.ts b/packages/core-services/src/events/Events.ts index 4aa8bce8929bb..f1856f222bc09 100644 --- a/packages/core-services/src/events/Events.ts +++ b/packages/core-services/src/events/Events.ts @@ -43,15 +43,15 @@ type ClientAction = 'inserted' | 'updated' | 'removed' | 'changed'; type LoginServiceConfigurationEvent = { id: string; } & ( - | { + | { clientAction: 'removed'; data?: never; - } - | { + } + | { clientAction: Omit; data: Omit, 'secret'> & { secret?: never }; - } - ); + } +); export type EventSignatures = { 'room.video-conference': (params: { rid: string; callId: string }) => void; @@ -83,13 +83,13 @@ export type EventSignatures = { uid: string, data: | { - type: 'changed'; - account: Partial; - } + type: 'changed'; + account: Partial; + } | { - type: 'removed'; - account: { _id: IWebdavAccount['_id'] }; - }, + type: 'removed'; + account: { _id: IWebdavAccount['_id'] }; + }, ): void; 'notify.e2e.keyRequest'(rid: string, data: IRoom['e2eKeyId']): void; 'notify.deleteMessage'(rid: string, data: { _id: string }): void; @@ -104,14 +104,14 @@ export type EventSignatures = { ids?: string[]; // message ids have priority over ts showDeletedStatus?: boolean; } & ( - | { + | { filesOnly: true; replaceFileAttachmentsWith?: MessageAttachment; - } - | { + } + | { filesOnly?: false; - } - ), + } + ), ): void; 'notify.deleteCustomSound'(data: { soundData: ICustomSound }): void; 'notify.updateCustomSound'(data: { soundData: ICustomSound }): void; @@ -129,12 +129,12 @@ export type EventSignatures = { user: Pick, data: | { - messageErasureType: 'Delete'; - } + messageErasureType: 'Delete'; + } | { - messageErasureType: 'Unlink'; - replaceByUser: { _id: IUser['_id']; username: IUser['username']; alias: string }; - }, + messageErasureType: 'Unlink'; + replaceByUser: { _id: IUser['_id']; username: IUser['username']; alias: string }; + }, ): void; 'user.deleteCustomStatus'(userStatus: Omit): void; 'user.forceLogout': (uid: string) => void; @@ -173,78 +173,78 @@ export type EventSignatures = { data: | { clientAction: Exclude; role: IRole } | { - clientAction: 'removed'; - role: { - _id: string; - name: string; - }; - }, + clientAction: 'removed'; + role: { + _id: string; + name: string; + }; + }, ): void; 'watch.rooms'(data: { clientAction: ClientAction; room: Pick | IRoom }): void; 'watch.subscriptions'( data: | { - clientAction: 'updated' | 'inserted'; - subscription: Pick< - ISubscription, - | 't' - | 'ts' - | 'ls' - | 'lr' - | 'name' - | 'fname' - | 'rid' - | 'code' - | 'f' - | 'u' - | 'open' - | 'alert' - | 'roles' - | 'unread' - | 'prid' - | 'userMentions' - | 'groupMentions' - | 'archived' - | 'audioNotificationValue' - | 'desktopNotifications' - | 'mobilePushNotifications' - | 'emailNotifications' - | 'desktopPrefOrigin' - | 'mobilePrefOrigin' - | 'emailPrefOrigin' - | 'unreadAlert' - | '_updatedAt' - | 'blocked' - | 'blocker' - | 'autoTranslate' - | 'autoTranslateLanguage' - | 'disableNotifications' - | 'hideUnreadStatus' - | 'hideMentionStatus' - | 'muteGroupMentions' - | 'ignored' - | 'E2EKey' - | 'E2ESuggestedKey' - | 'oldRoomKeys' - | 'tunread' - | 'tunreadGroup' - | 'tunreadUser' + clientAction: 'updated' | 'inserted'; + subscription: Pick< + ISubscription, + | 't' + | 'ts' + | 'ls' + | 'lr' + | 'name' + | 'fname' + | 'rid' + | 'code' + | 'f' + | 'u' + | 'open' + | 'alert' + | 'roles' + | 'unread' + | 'prid' + | 'userMentions' + | 'groupMentions' + | 'archived' + | 'audioNotificationValue' + | 'desktopNotifications' + | 'mobilePushNotifications' + | 'emailNotifications' + | 'desktopPrefOrigin' + | 'mobilePrefOrigin' + | 'emailPrefOrigin' + | 'unreadAlert' + | '_updatedAt' + | 'blocked' + | 'blocker' + | 'autoTranslate' + | 'autoTranslateLanguage' + | 'disableNotifications' + | 'hideUnreadStatus' + | 'hideMentionStatus' + | 'muteGroupMentions' + | 'ignored' + | 'E2EKey' + | 'E2ESuggestedKey' + | 'oldRoomKeys' + | 'tunread' + | 'tunreadGroup' + | 'tunreadUser' - // Omnichannel fields - | 'department' - | 'v' - | 'onHold' - >; - } + // Omnichannel fields + | 'department' + | 'v' + | 'onHold' + >; + } | { - clientAction: 'removed'; - subscription: { - _id: string; - u?: Pick; - rid?: string; - t?: string; - }; - }, + clientAction: 'removed'; + subscription: { + _id: string; + u?: Pick; + rid?: string; + t?: string; + }; + }, ): void; 'watch.inquiries'(data: { clientAction: ClientAction; inquiry: ILivechatInquiryRecord; diff?: undefined | Record }): void; 'watch.settings'(data: { clientAction: ClientAction; setting: ISetting }): void; @@ -252,19 +252,19 @@ export type EventSignatures = { data: { id: string; } & ( - | { + | { clientAction: 'inserted'; data: IUser; - } - | { + } + | { clientAction: 'removed'; - } - | { + } + | { clientAction: 'updated'; diff: Record; unset: Record; - } - ), + } + ), ): void; 'watch.loginServiceConfiguration'(data: LoginServiceConfigurationEvent): void; 'watch.instanceStatus'(data: {