diff --git a/src/agent.ts b/src/agent.ts index ff935da..f931978 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -7,9 +7,10 @@ import { config } from './config'; import { rand } from './lib/rand'; import { getClients } from './macros/getClients'; import { populateDatabase, isOnlyUserPopulation } from './populate'; +import { OmnichannelClient } from './client/Omnichannel'; export default (): void => { - let agents: Client[]; + let agents: OmnichannelClient[]; const b = new (class extends BenchmarkRunner { private db: Db | undefined; @@ -117,7 +118,7 @@ export default (): void => { } console.log(this.usernames); - agents = await getClients(config.HOW_MANY_USERS, this.extraPrefix, this.getCurrentFromUsers(this.usernames)); + agents = await getClients(OmnichannelClient, config.HOW_MANY_USERS, this.extraPrefix, this.getCurrentFromUsers(this.usernames)); const settings = this.db.collection('rocketchat_settings'); await settings.updateOne( diff --git a/src/client/Client.ts b/src/client/Client.ts index 6112a16..b735d54 100644 --- a/src/client/Client.ts +++ b/src/client/Client.ts @@ -3,13 +3,16 @@ import { URLSearchParams } from 'url'; import RocketChatClient from '@rocket.chat/sdk/lib/clients/Rocketchat'; import EJSON from 'ejson'; import fetch from 'node-fetch'; +import type Api from '@rocket.chat/sdk/lib/api/api'; +import type { IAPIRequest, ISubscription } from '@rocket.chat/sdk/interfaces'; import { config } from '../config'; -import type { Subscription, Department, Inquiry, Visitor } from '../definifitons'; +import type { Subscription } from '../definifitons'; import { delay } from '../lib/delay'; import { username, email } from '../lib/ids'; import * as prom from '../lib/prom'; import { rand } from '../lib/rand'; +import { action, errorLogger, suppressError } from './decorators'; const logger = { debug: (...args: any) => true || console.log(args), @@ -26,7 +29,37 @@ const { SSL_ENABLED = 'no', LOG_IN = 'yes' } = process.env; const useSsl = typeof SSL_ENABLED !== 'undefined' ? ['yes', 'true'].includes(SSL_ENABLED) : true; export type ClientType = 'web' | 'android' | 'ios'; -export class Client { + +// eslint-disable-next-line @typescript-eslint/naming-convention +interface ClientLoadTest { + joinRoom(rid: string): Promise; + + setStatus(): Promise; + + read(rid: string): Promise; + + login(): Promise; + + sendMessage(msg: string, rid: string): Promise; + + typing(rid: string, typing: boolean): Promise; + + openRoom(rid: string, roomType: string): Promise; + + subscribeRoom(rid: string): Promise; + + beforeLogin(): Promise; + + getRandomSubscription(): Subscription | undefined; + + get: Api['get']; + + post: Api['post']; + + subscribe: RocketChatClient['subscribe']; +} + +export class Client implements ClientLoadTest { host: string; subscriptions: Subscription[] = []; @@ -76,8 +109,21 @@ export class Client { }); this.client = client; + + this.get = prom.promWrapperRest('GET', (...args) => this.client.get(...args)); + this.post = prom.promWrapperRest('POST', (...args) => this.client.post(...args)); + + this.subscribe = prom.promWrapperSubscribe((...args) => this.client.subscribe(...args)); } + get: IAPIRequest; + + post: IAPIRequest; + + subscribe: (topic: string, ...args: any[]) => Promise; + + @suppressError + @action async beforeLogin(): Promise { await this.client.connect({}); @@ -86,11 +132,9 @@ export class Client { switch (this.type) { case 'android': case 'ios': - await Promise.all([this.client.get('settings.public'), this.client.get('settings.oauth')]); + await Promise.all([this.get('settings.public'), this.get('settings.oauth')]); break; } - - // await loginOrRegister(client, credentials, type, current); } getManyPresences(): number { @@ -115,113 +159,66 @@ export class Client { return this.credentials.username; } + @errorLogger protected async loginOrRegister(): Promise { - // if (tryRegister) { - // await register(client, credentials, type); - // } - - try { - if (!['yes', 'true'].includes(LOG_IN)) { - return; - } - await this.login(); - } catch (e) { - console.error('could not login/register for', this.credentials, e); + if (!['yes', 'true'].includes(LOG_IN)) { + return; } + await this.login(); } - async joinRoom(rid = 'GENERAL'): Promise { - if (!this.loggedIn) { - await this.login(); - } - - const end = prom.roomJoin.startTimer(); - const endAction = prom.actions.startTimer({ action: 'joinRoom' }); - try { - await this.client.joinRoom({ rid }); - end({ status: 'success' }); - endAction({ status: 'success' }); - } catch (e) { - console.error('error joining room', { uid: this.client.userId, rid }, e); - end({ status: 'error' }); - endAction({ status: 'error' }); - } + @suppressError + @action + async joinRoom(rid = 'GENERAL') { + await this.client.joinRoom({ rid }); } - async setStatus(): Promise { - if (!this.loggedIn) { - await this.login(); - } - + @suppressError + @action + async setStatus() { const status = rand(['online', 'away', 'offline', 'busy']); - const endAction = prom.actions.startTimer({ action: 'setStatus' }); - try { - await this.client.post('users.setStatus', { status }); - endAction({ status: 'success' }); - } catch (e) { - endAction({ status: 'error' }); - } + await this.post('users.setStatus', { status }); } + @suppressError + @action async read(rid: string): Promise { - if (!this.loggedIn) { - await this.login(); - } - - const endAction = prom.actions.startTimer({ action: 'read' }); - try { - await this.client.post('subscriptions.read', { rid }); - endAction({ status: 'success' }); - } catch (e) { - endAction({ status: 'error' }); - } + await this.post('subscriptions.read', { rid }); } - async login(): Promise { + @suppressError + @action + async login() { await this.beforeLogin(); - const end = prom.login.startTimer(); - const endAction = prom.actions.startTimer({ action: 'login' }); const { credentials } = this; - try { - const user = await this.client.login(credentials); - // do one by one as doing three at same time was hanging - switch (this.type) { - case 'android': - case 'ios': - await Promise.all( - ['rooms-changed', 'subscriptions-changed'].map((stream) => this.client.subscribe('stream-notify-user', `${user.id}/${stream}`)), - ); - - await Promise.all(['userData', 'activeUsers'].map((stream) => this.client.subscribe(stream, ''))); - - await Promise.all([ - this.client.get('me'), - this.client.get('permissions'), - this.client.get('settings.public'), - this.client.get('subscriptions.get'), - this.client.get('rooms.get'), - ]); - break; - } - - await Promise.all(this.getLoginSubs().map(([stream, ...params]) => this.client.subscribe(stream, ...params))); + const user = await this.client.login(credentials); - await Promise.all(this.getLoginMethods().map((params) => this.client.methodCall(...params))); + // do one by one as doing three at same time was hanging + switch (this.type) { + case 'android': + case 'ios': + await Promise.all( + ['rooms-changed', 'subscriptions-changed'].map((stream) => this.subscribe('stream-notify-user', `${user.id}/${stream}`)), + ); + + await Promise.all(['userData', 'activeUsers'].map((stream) => this.subscribe(stream, ''))); + + await Promise.all([ + this.get('me'), + this.get('permissions'), + this.get('settings.public'), + this.get('subscriptions.get'), + this.get('rooms.get'), + ]); + break; + } - // client.loggedInInternal = true; - // client.userCount = userCount; + await Promise.all(this.getLoginSubs().map(([stream, ...params]) => this.subscribe(stream, ...params))); - end({ status: 'success' }); - endAction({ status: 'success' }); - } catch (e) { - console.error('error during login', e, credentials); - end({ status: 'error' }); - endAction({ status: 'error' }); - throw e; - } + await Promise.all(this.getLoginMethods().map((params) => this.client.methodCall(...params))); this.loggedIn = true; } @@ -242,73 +239,51 @@ export class Client { return subs; }; + @suppressError + @action async sendMessage(msg: string, rid: string): Promise { - if (!this.loggedIn) { - await this.login(); - } - await this.typing(rid, true); await delay(1000); - const endAction = prom.actions.startTimer({ action: 'sendMessage' }); - const end = prom.messages.startTimer(); + try { await this.client.sendMessage(msg, rid); - end({ status: 'success' }); - endAction({ status: 'success' }); } catch (e) { - end({ status: 'error' }); - endAction({ status: 'error' }); throw e; } await this.typing(rid, false); } + @suppressError + @action async typing(rid: string, typing: boolean): Promise { - if (!this.loggedIn) { - await this.login(); - } - await this.client.methodCall('stream-notify-room', `${rid}/typing`, this.client.username, typing); } + @suppressError + @action async openRoom(rid = 'GENERAL', roomType = 'groups'): Promise { - if (!this.loggedIn) { - await this.login(); - } - - const endAction = prom.actions.startTimer({ action: 'openRoom' }); - const end = prom.openRoom.startTimer(); try { const calls: Promise[] = [this.subscribeRoom(rid)]; switch (this.type) { case 'android': case 'ios': - calls.push(this.client.get('commands.list')); - calls.push(this.client.get(`${roomType}.members`, { roomId: rid })); - calls.push(this.client.get(`${roomType}.roles`, { roomId: rid })); - calls.push(this.client.get(`${roomType}.history`, { roomId: rid })); + calls.push(this.get('commands.list')); + calls.push(this.get(`${roomType}.members`, { roomId: rid })); + calls.push(this.get(`${roomType}.roles`, { roomId: rid })); + calls.push(this.get(`${roomType}.history`, { roomId: rid })); break; } await Promise.all(calls); - await this.client.post('subscriptions.read', { rid }); - - end({ status: 'success' }); - endAction({ status: 'success' }); + await this.post('subscriptions.read', { rid }); } catch (e) { console.error('error open room', { uid: this.client.userId, rid }, e); - end({ status: 'error' }); - endAction({ status: 'error' }); } } - async openLivechatRoom(_rid: string, _vid: string): Promise { - // do nothing - } - getRandomSubscription(): Subscription { const subscriptions = this.subscriptions.filter( (sub) => config.IGNORE_ROOMS.indexOf(sub.rid) === -1 && config.IGNORE_ROOMS.indexOf(sub.name) === -1, @@ -316,160 +291,15 @@ export class Client { return rand(subscriptions); } - getRandomLivechatSubscription(): Subscription { - const subscriptions = this.subscriptions.filter( - (sub) => config.IGNORE_ROOMS.indexOf(sub.rid) === -1 && config.IGNORE_ROOMS.indexOf(sub.name) === -1 && sub.t === 'l', - ); - return rand(subscriptions); - } - + @suppressError + @action async subscribeRoom(rid: string): Promise { - const end = prom.roomSubscribe.startTimer(); - const endAction = prom.actions.startTimer({ action: 'roomSubscribe' }); - try { - // await this.client.subscribeRoom(rid); - - const topic = 'stream-notify-room'; - await Promise.all([ - this.client.subscribe('stream-room-messages', rid), - this.client.subscribe(topic, `${rid}/typing`), - this.client.subscribe(topic, `${rid}/deleteMessage`), - ]); - - end({ status: 'success' }); - endAction({ status: 'success' }); - } catch (e) { - console.error('error subscribing room', { uid: this.client.userId, rid }, e); - end({ status: 'error' }); - endAction({ status: 'error' }); - } - } - - async getRoutingConfig(): Promise<{ [k: string]: string } | undefined> { - if (!this.loggedIn) { - await this.login(); - } - - const end = prom.actions.startTimer({ action: 'getRoutingConfig' }); - try { - const routingConfig = await this.client.methodCall('livechat:getRoutingConfig'); - - end({ status: 'success' }); - return routingConfig; - } catch (e) { - end({ status: 'error' }); - } - } - - async getAgentDepartments(): Promise<{ departments: Department[] } | undefined> { - if (!this.loggedIn) { - await this.login(); - } - - const end = prom.actions.startTimer({ action: 'getAgentDepartments' }); - try { - const departments = await this.client.get(`livechat/agents/${this.client.userId}/departments?enabledDepartmentsOnly=true`); - - end({ status: 'success' }); - return departments; - } catch (e) { - end({ status: 'error' }); - } - } - - async getQueuedInquiries(): Promise<{ inquiries: Inquiry[] } | undefined> { - if (!this.loggedIn) { - await this.login(); - } - - const end = prom.actions.startTimer({ action: 'getQueuedInquiries' }); - try { - const inquiries = await this.client.get(`livechat/inquiries.queuedForUser`, { userId: this.client.userId }); - - end({ status: 'success' }); - return inquiries; - } catch (e) { - end({ status: 'error' }); - } - } - - async subscribeDeps(deps: string[]): Promise { - if (this.subscribedToLivechat) { - return; - } - - if (!this.loggedIn) { - await this.login(); - } - - try { - const topic = 'livechat-inquiry-queue-observer'; - - await Promise.all([ - this.client.subscribe(topic, 'public'), // always to public - ...deps.map( - (department) => this.client.subscribe(topic, `department/${department}`), // and to deps, if any - ), - ]); - this.subscribedToLivechat = true; - } catch (e) { - console.error('error subscribing to livechat', e); - this.subscribedToLivechat = false; - } - } - - async takeInquiry(id: string): Promise { - if (!this.loggedIn) { - await this.login(); - } - - const end = prom.actions.startTimer({ action: 'takeInquiry' }); - const endInq = prom.inquiryTaken.startTimer(); - try { - await this.client.methodCall('livechat:takeInquiry', id, { - clientAction: true, - }); - end({ status: 'success' }); - endInq({ status: 'success' }); - } catch (e) { - end({ status: 'error' }); - endInq({ status: 'error' }); - } - } - - async getInquiry(id: string): Promise { - if (!this.loggedIn) { - await this.login(); - } - - const end = prom.actions.startTimer({ action: 'getOneInquiry' }); - try { - const inq = await this.client.get(`livechat/inquiries.getOne`, { - roomId: id, - }); - - end({ status: 'success' }); - return inq; - } catch (e) { - end({ status: 'error' }); - } - } - - async getVisitorInfo(vid: string): Promise { - if (!this.loggedIn) { - await this.login(); - } - - const end = prom.actions.startTimer({ action: 'getVisitorInfo' }); - try { - const v = await this.client.get(`livechat/visitors.info`, { - visitorId: vid, - }); - end({ status: 'success' }); - return v; - } catch (e) { - end({ status: 'error' }); - } + const topic = 'stream-notify-room'; + await Promise.all([ + this.subscribe('stream-room-messages', rid), + this.subscribe(topic, `${rid}/typing`), + this.subscribe(topic, `${rid}/deleteMessage`), + ]); } private async methodCallRest({ method, params, anon }: { method: string; params: unknown[]; anon?: boolean }) { @@ -480,7 +310,7 @@ export class Client { params, }); - const result = await this.client.post(`${anon ? 'method.callAnon' : 'method.call'}/${encodeURIComponent(method)}`, { + const result = await this.post(`${anon ? 'method.callAnon' : 'method.call'}/${encodeURIComponent(method)}`, { message, }); diff --git a/src/client/Omnichannel.ts b/src/client/Omnichannel.ts new file mode 100644 index 0000000..53a3529 --- /dev/null +++ b/src/client/Omnichannel.ts @@ -0,0 +1,165 @@ +import type { Department, Inquiry, Subscription, Visitor } from '../definifitons'; +import * as prom from '../lib/prom'; +import { rand } from '../lib/rand'; +import { Client } from './Client'; +import { config } from '../config'; + +export class OmnichannelClient extends Client { + getRandomLivechatSubscription(): Subscription { + const subscriptions = this.subscriptions.filter( + (sub) => config.IGNORE_ROOMS.indexOf(sub.rid) === -1 && config.IGNORE_ROOMS.indexOf(sub.name) === -1 && sub.t === 'l', + ); + return rand(subscriptions); + } + + async getRoutingConfig(): Promise<{ [k: string]: string } | undefined> { + if (!this.loggedIn) { + await this.login(); + } + + const end = prom.actions.startTimer({ action: 'getRoutingConfig' }); + try { + const routingConfig = await this.client.methodCall('livechat:getRoutingConfig'); + + end({ status: 'success' }); + return routingConfig; + } catch (e) { + end({ status: 'error' }); + } + } + + async getAgentDepartments(): Promise<{ departments: Department[] } | undefined> { + if (!this.loggedIn) { + await this.login(); + } + + const end = prom.actions.startTimer({ action: 'getAgentDepartments' }); + try { + const departments = await this.get(`livechat/agents/${this.client.userId}/departments?enabledDepartmentsOnly=true`); + + end({ status: 'success' }); + return departments; + } catch (e) { + end({ status: 'error' }); + } + } + + async getQueuedInquiries(): Promise<{ inquiries: Inquiry[] } | undefined> { + if (!this.loggedIn) { + await this.login(); + } + + const end = prom.actions.startTimer({ action: 'getQueuedInquiries' }); + try { + const inquiries = await this.get(`livechat/inquiries.queuedForUser`, { userId: this.client.userId }); + + end({ status: 'success' }); + return inquiries; + } catch (e) { + end({ status: 'error' }); + } + } + + async subscribeDeps(deps: string[]): Promise { + if (this.subscribedToLivechat) { + return; + } + + if (!this.loggedIn) { + await this.login(); + } + + try { + const topic = 'livechat-inquiry-queue-observer'; + + await Promise.all([ + this.subscribe(topic, 'public'), // always to public + ...deps.map( + (department) => this.subscribe(topic, `department/${department}`), // and to deps, if any + ), + ]); + this.subscribedToLivechat = true; + } catch (e) { + console.error('error subscribing to livechat', e); + this.subscribedToLivechat = false; + } + } + + async takeInquiry(id: string): Promise { + if (!this.loggedIn) { + await this.login(); + } + + const end = prom.actions.startTimer({ action: 'takeInquiry' }); + const endInq = prom.inquiryTaken.startTimer(); + try { + await this.client.methodCall('livechat:takeInquiry', id, { + clientAction: true, + }); + end({ status: 'success' }); + endInq({ status: 'success' }); + } catch (e) { + end({ status: 'error' }); + endInq({ status: 'error' }); + } + } + + async getInquiry(id: string): Promise { + if (!this.loggedIn) { + await this.login(); + } + + const end = prom.actions.startTimer({ action: 'getOneInquiry' }); + try { + const inq = await this.get(`livechat/inquiries.getOne`, { + roomId: id, + }); + + end({ status: 'success' }); + return inq; + } catch (e) { + end({ status: 'error' }); + } + } + + async getVisitorInfo(vid: string): Promise { + if (!this.loggedIn) { + await this.login(); + } + + const end = prom.actions.startTimer({ action: 'getVisitorInfo' }); + try { + const v = await this.get(`livechat/visitors.info`, { + visitorId: vid, + }); + end({ status: 'success' }); + return v; + } catch (e) { + end({ status: 'error' }); + } + } + + async openLivechatRoom(rid: string, vid: string): Promise { + if (!this.loggedIn) { + await this.login(); + } + + const end = prom.openRoom.startTimer(); + const endAction = prom.actions.startTimer({ action: 'openRoom' }); + try { + await Promise.all([ + this.subscribeRoom(rid), + this.methodViaRest('loadHistory', rid, null, 50, new Date()), + this.methodViaRest('getRoomRoles', rid), + this.getVisitorInfo(vid), + ]); + + end({ status: 'success' }); + endAction({ status: 'success' }); + } catch (e) { + console.error('error open room', { uid: this.client.userId, rid }, e); + end({ status: 'error' }); + endAction({ status: 'error' }); + } + } +} diff --git a/src/client/WebClient.ts b/src/client/WebClient.ts index 15fa498..4b7f63c 100644 --- a/src/client/WebClient.ts +++ b/src/client/WebClient.ts @@ -1,6 +1,7 @@ import type { Subscription } from '../definifitons'; import * as prom from '../lib/prom'; import { Client } from './Client'; +import { action, suppressError } from './decorators'; export class WebClient extends Client { loginPromise: Promise | undefined; @@ -15,22 +16,21 @@ export class WebClient extends Client { await this.httpGet('/api/apps/actionButtons'); // this is done to simulate web client - await this.client.subscribe('meteor.loginServiceConfiguration'); - await this.client.subscribe('meteor_autoupdate_clientVersions'); + await this.subscribe('meteor.loginServiceConfiguration'); + await this.subscribe('meteor_autoupdate_clientVersions'); - // await client.subscribeNotifyAll(); - await Promise.all(['public-settings-changed'].map((event) => this.client.subscribe('stream-notify-all', event, false))); + // await subscribeNotifyAll(); + await Promise.all(['public-settings-changed'].map((event) => this.subscribe('stream-notify-all', event, false))); } + @suppressError + @action async login(): Promise { if (this.loginPromise) { return this.loginPromise; } this.loginPromise = new Promise(async (resolve, reject) => { - const end = prom.login.startTimer(); - const endAction = prom.actions.startTimer({ action: 'login' }); - const { credentials } = this; try { @@ -38,7 +38,7 @@ export class WebClient extends Client { const user = await this.client.login(credentials); - // await this.client.subscribeLoggedNotify(); + // await this.subscribeLoggedNotify(); await Promise.all( [ 'deleteCustomSound', @@ -54,10 +54,10 @@ export class WebClient extends Client { 'roles-change', 'voip.statuschanged', 'permissions-changed', - ].map((event) => this.client.subscribe('stream-notify-logged', event, false)), + ].map((event) => this.subscribe('stream-notify-logged', event, false)), ); - // await client.subscribeNotifyUser(); + // await subscribeNotifyUser(); await Promise.all( [ 'uiInteraction', @@ -70,7 +70,7 @@ export class WebClient extends Client { 'rooms-changed', 'webrtc', 'userData', - ].map((event) => this.client.subscribe('stream-notify-user', `${user.id}/${event}`, false)), + ].map((event) => this.subscribe('stream-notify-user', `${user.id}/${event}`, false)), ); await Promise.all( @@ -84,7 +84,7 @@ export class WebClient extends Client { 'command/updated', 'command/removed', 'actions/changed', - ].map((event) => this.client.subscribe('stream-apps', event, false)), + ].map((event) => this.subscribe('stream-apps', event, false)), ); await Promise.all(this.getLoginMethods().map((params) => this.methodViaRest(...params))); @@ -95,14 +95,8 @@ export class WebClient extends Client { this.loggedIn = true; - end({ status: 'success' }); - endAction({ action: 'login', status: 'success' }); - resolve(); } catch (error) { - end({ status: 'error' }); - endAction({ action: 'login', status: 'error' }); - reject({ error, credentials }); } }); @@ -110,33 +104,23 @@ export class WebClient extends Client { return this.loginPromise; } + @suppressError + @action async listenPresence(userIds: string[]): Promise { - if (!this.loggedIn) { - await this.login(); - } - - const endAction = prom.actions.startTimer({ action: 'listenPresence' }); + const newIds = userIds.filter((id) => !this.usersPresence.includes(id)); + const removeIds = this.usersPresence.filter((id) => !userIds.includes(id)); - try { - const newIds = userIds.filter((id) => !this.usersPresence.includes(id)); - const removeIds = this.usersPresence.filter((id) => !userIds.includes(id)); + await this.get(`users.presence?ids[]=${newIds.join('&ids[]=')}&_empty=`); - await this.client.get(`users.presence?ids[]=${newIds.join('&ids[]=')}&_empty=`); + ((await this.client.socket) as any).ddp.subscribe('stream-user-presence', [ + '', + { + ...(newIds && { added: newIds }), + ...(removeIds && { removed: removeIds }), + }, + ]); - ((await this.client.socket) as any).ddp.subscribe('stream-user-presence', [ - '', - { - ...(newIds && { added: newIds }), - ...(removeIds && { removed: removeIds }), - }, - ]); - - this.usersPresence = [...new Set(userIds)]; - - endAction({ action: 'listenPresence', status: 'success' }); - } catch (e) { - endAction({ action: 'listenPresence', status: 'error' }); - } + this.usersPresence = [...new Set(userIds)]; } protected getLoginMethods(): [string, string?][] { @@ -160,87 +144,33 @@ export class WebClient extends Client { return methods; } + @suppressError + @action async typing(rid: string, typing: boolean): Promise { - if (!this.loggedIn) { - await this.login(); - } - await this.client.methodCall('stream-notify-room', `${rid}/user-activity`, this.client.username, typing); } + @suppressError + @action async openRoom(rid = 'GENERAL'): Promise { - if (!this.loggedIn) { - await this.login(); - } - - const end = prom.openRoom.startTimer(); - const endAction = prom.actions.startTimer({ action: 'openRoom' }); - try { - await Promise.all([ - this.subscribeRoom(rid), - this.methodViaRest('loadHistory', rid, null, 50, new Date()), - this.methodViaRest('getRoomRoles', rid), - ]); - - await this.read(rid); - - end({ status: 'success' }); - endAction({ status: 'success' }); - } catch (e) { - console.error('error open room', { uid: this.client.userId, rid }, e); - end({ status: 'error' }); - endAction({ status: 'error' }); - } - } - - async openLivechatRoom(rid: string, vid: string): Promise { - if (!this.loggedIn) { - await this.login(); - } + await Promise.all([ + this.subscribeRoom(rid), + this.methodViaRest('loadHistory', rid, null, 50, new Date()), + this.methodViaRest('getRoomRoles', rid), + ]); - const end = prom.openRoom.startTimer(); - const endAction = prom.actions.startTimer({ action: 'openRoom' }); - try { - await Promise.all([ - this.subscribeRoom(rid), - this.methodViaRest('loadHistory', rid, null, 50, new Date()), - this.methodViaRest('getRoomRoles', rid), - this.getVisitorInfo(vid), - ]); - - end({ status: 'success' }); - endAction({ status: 'success' }); - } catch (e) { - console.error('error open room', { uid: this.client.userId, rid }, e); - end({ status: 'error' }); - endAction({ status: 'error' }); - } + await this.read(rid); } + @suppressError + @action async subscribeRoom(rid: string): Promise { - if (!this.loggedIn) { - await this.login(); - } - - const end = prom.roomSubscribe.startTimer(); - const endAction = prom.actions.startTimer({ action: 'subscribeRoom' }); - try { - // await this.client.subscribeRoom(rid); - - const topic = 'stream-notify-room'; - await Promise.all([ - this.client.subscribe('stream-room-messages', rid), - this.client.subscribe(topic, `${rid}/user-activity`), - this.client.subscribe(topic, `${rid}/deleteMessage`), - this.client.subscribe(topic, `${rid}/deleteMessageBulk`), - ]); - - end({ status: 'success' }); - endAction({ status: 'success' }); - } catch (e) { - console.error('error subscribing room', { uid: this.client.userId, rid }, e); - end({ status: 'error' }); - endAction({ status: 'error' }); - } + const topic = 'stream-notify-room'; + await Promise.all([ + this.subscribe('stream-room-messages', rid), + this.subscribe(topic, `${rid}/user-activity`), + this.subscribe(topic, `${rid}/deleteMessage`), + this.subscribe(topic, `${rid}/deleteMessageBulk`), + ]); } } diff --git a/src/client/decorators/index.ts b/src/client/decorators/index.ts new file mode 100644 index 0000000..967a671 --- /dev/null +++ b/src/client/decorators/index.ts @@ -0,0 +1,45 @@ +import { actions } from '../../lib/prom'; + +export function action(_target: unknown, action: string, descriptor: PropertyDescriptor) { + const childFunction = descriptor.value; + + descriptor.value = async function (...args: any[]) { + const endTimer = actions.startTimer({ action }); + try { + const result = await childFunction.apply(this, args); + endTimer({ status: 'success' }); + return result; + } catch (e) { + endTimer({ status: 'error' }); + throw e; + } + }; + return descriptor; +} + +export function errorLogger(_target: unknown, action: string, descriptor: PropertyDescriptor) { + const childFunction = descriptor.value; + + descriptor.value = async function (...args: any[]) { + try { + return await childFunction.apply(this, args); + } catch (e) { + console.error(`error in ${action}`, e); + throw e; + } + }; + return descriptor; +} + +export function suppressError(_target: unknown, action: string, descriptor: PropertyDescriptor) { + const childFunction = descriptor.value; + + descriptor.value = async function (...args: any[]) { + try { + return await childFunction.apply(this, args); + } catch (e) { + console.error(`error in ${action}`, e); + } + }; + return descriptor; +} diff --git a/src/lib/prom.ts b/src/lib/prom.ts index 75949d2..430973b 100644 --- a/src/lib/prom.ts +++ b/src/lib/prom.ts @@ -40,12 +40,25 @@ export const openRoom = new client.Summary({ help: 'Open room sction', labelNames: ['status'], }); + export const actions = new client.Summary({ name: 'rc_actions', help: 'All performed actions', labelNames: ['action', 'status'], }); +export const rest = new client.Summary({ + name: 'rc_rest_time', + help: 'All performed rest actions', + labelNames: ['endpoint', 'status', 'method'], +}); + +export const subscriptions = new client.Summary({ + name: 'rc_load_subscriptions', + help: 'Subscriptions', + labelNames: ['name', 'status'], +}); + export const roleAdd = new client.Summary({ name: 'rc_load_role_add', help: 'Role added', @@ -61,3 +74,33 @@ export const inquiryTaken = new client.Summary({ export default client; export { client }; + +export const promWrapperRest = Promise>(method: string, fn: F): F => { + return (async (url: string, ...args: any[]) => { + const [endpoint] = url.split('?'); + + const endTimer = rest.startTimer({ endpoint, method }); + try { + const result = await fn(endpoint, ...args); + endTimer({ status: 'success' }); + return result; + } catch (e) { + endTimer({ status: 'error' }); + throw e; + } + }) as unknown as F; +}; + +export const promWrapperSubscribe = Promise>(fn: F): F => { + return (async (...args: any[]) => { + const endTimer = subscriptions.startTimer({ name: args[0] }); + try { + const result = await fn(...args); + endTimer({ status: 'success' }); + return result; + } catch (e) { + endTimer({ status: 'error' }); + throw e; + } + }) as unknown as F; +}; diff --git a/src/macros/getClients.ts b/src/macros/getClients.ts index 21ea8b0..e74da09 100644 --- a/src/macros/getClients.ts +++ b/src/macros/getClients.ts @@ -1,7 +1,6 @@ import PromisePool from '@supercharge/promise-pool'; import type { Client } from '../client/Client'; -import { ClientBase } from '../client/ClientBase'; import { config } from '../config'; const { @@ -13,7 +12,18 @@ const { // MESSAGE_SENDING_RATE = 0.002857142857143, } = process.env; -export const getClients = async (size: number, userPrefix = '', usersCurrent?: number[]): Promise => { +// eslint-disable-next-line @typescript-eslint/naming-convention +interface ConstructorOf { + new (...args: any[]): T; +} + +export const getClients = async >( + // eslint-disable-next-line @typescript-eslint/naming-convention + Kind: T, + size: number, + userPrefix = '', + usersCurrent?: number[], +): Promise => { const users = Array.isArray(usersCurrent) ? usersCurrent : Array.from({ length: size }).map((_, i) => i); console.log('Logging in', size, 'users'); @@ -21,7 +31,7 @@ export const getClients = async (size: number, userPrefix = '', usersCurrent?: n if (config.DYNAMIC_LOGIN) { console.log('Creating a total of dynamic clients:', users.length); - return users.map((index) => ClientBase.getClient(HOST_URL, CLIENT_TYPE as 'web' | 'android' | 'ios', index as number, userPrefix)); + return users.map((index) => new Kind(HOST_URL, CLIENT_TYPE as 'web' | 'android' | 'ios', index as number, userPrefix)); } const { results } = await PromisePool.withConcurrency(config.LOGIN_BATCH) @@ -31,7 +41,7 @@ export const getClients = async (size: number, userPrefix = '', usersCurrent?: n // throw error; }) .process(async (index) => { - const client = ClientBase.getClient(HOST_URL, CLIENT_TYPE as 'web' | 'android' | 'ios', index as number, userPrefix); + const client = new Kind(HOST_URL, CLIENT_TYPE as 'web' | 'android' | 'ios', index as number, userPrefix); await client.login(); diff --git a/src/profile.ts b/src/profile.ts index 4b130cd..658aa30 100644 --- a/src/profile.ts +++ b/src/profile.ts @@ -6,8 +6,8 @@ import { config } from './config'; import { userId } from './lib/ids'; import { getRandomInt, rand } from './lib/rand'; import { getClients } from './macros/getClients'; -// import { joinRooms } from './macros/joinRooms'; import { populateDatabase, isFullPopulation } from './populate'; +import { WebClient } from './client/WebClient'; export default (): void => { let clients: Client[]; @@ -70,7 +70,7 @@ export default (): void => { } async setup() { - clients = await getClients(config.HOW_MANY_USERS); + clients = await getClients(WebClient, config.HOW_MANY_USERS); // if (config.JOIN_ROOM) { // await joinRooms(clients); diff --git a/tsconfig.json b/tsconfig.json index b2f5898..686029e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,28 +1,29 @@ { - "compilerOptions": { - "target": "es2018", - "module": "ESNext", - "lib": ["es2019"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "./dist/esm", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", - "resolveJsonModule": true, - "typeRoots": ["./types"], - "downlevelIteration": true, - }, - "include": ["./src/**/*"], - "files": ["./src/index.ts", "./types/index.d.ts"], - "ts-node": { - "files": true, - "compilerOptions": { - "module": "CommonJS" - } - }, - "exclude": ["node_modules", ".npm"] + "compilerOptions": { + "target": "es2018", + "module": "ESNext", + "lib": ["es2019"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist/esm", + "strict": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "typeRoots": ["./types"], + "downlevelIteration": true + }, + "include": ["./src/**/*"], + "files": ["./src/index.ts", "./types/index.d.ts"], + "ts-node": { + "files": true, + "compilerOptions": { + "module": "CommonJS" + } + }, + "exclude": ["node_modules", ".npm"] }