Skip to content

Commit

Permalink
Merge branch 'Agent-Req-From-Agentx' into cc/getBuddyAgents-2
Browse files Browse the repository at this point in the history
  • Loading branch information
bhabalan authored Nov 12, 2024
2 parents ee2ca21 + 2926276 commit b781b05
Show file tree
Hide file tree
Showing 21 changed files with 1,074 additions and 577 deletions.
15 changes: 15 additions & 0 deletions packages/@webex/plugin-cc/__mocks__/workerMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class Worker {
constructor(stringUrl) {
this.url = stringUrl;
this.onmessage = () => {};
}

postMessage(msg) {
this.onmessage(msg);
}

terminate() {}
}

global.Worker = Worker;
global.URL.createObjectURL = jest.fn(() => 'blob:http://localhost:3000/12345');
43 changes: 30 additions & 13 deletions packages/@webex/plugin-cc/src/cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@ import {
StationReLoginResponse,
BuddyAgentsResponse,
BuddyAgents,
SubscribeRequest,
} from './types';
import {READY, CC_FILE, EMPTY_STRING} from './constants';
import HttpRequest from './services/core/HttpRequest';
import WebCallingService from './services/WebCallingService';
import {AGENT, WEB_RTC_PREFIX} from './services/constants';
import {WebSocketManager} from './services/core/WebSocket/WebSocketManager';
import Services from './services';
import LoggerProxy from './logger-proxy';
import {ConnectionService} from './services/core/WebSocket/connection-service';
import {Logout} from './services/agent/types';
import {getErrorDetails} from './services/core/Utils';

Expand All @@ -28,9 +31,10 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
private $config: CCPluginConfig;
private $webex: WebexSDK;
private agentConfig: IAgentProfile;
private registered = false;
private httpRequest: HttpRequest;
private webSocketManager: WebSocketManager;
private webCallingService: WebCallingService;
private connectionService: ConnectionService;
private services: Services;

constructor(...args) {
Expand All @@ -50,7 +54,14 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
webex: this.$webex,
});

this.services = Services.getInstance();
this.webSocketManager = new WebSocketManager({webex: this.$webex});

this.connectionService = new ConnectionService(
this.webSocketManager,
this.getConnectionConfig()
);

this.services = Services.getInstance(this.webSocketManager);

this.webCallingService = new WebCallingService(this.$webex, this.$config.callingClientConfig);

Expand Down Expand Up @@ -95,17 +106,10 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
* @private
*/
private async connectWebsocket() {
const connectionConfig = {
force: this.$config?.force ?? true,
isKeepAliveEnabled: this.$config?.isKeepAliveEnabled ?? false,
clientType: this.$config?.clientType ?? 'WebexCCSDK',
allowMultiLogin: this.$config?.allowMultiLogin ?? true,
};

try {
return this.httpRequest
.subscribeNotifications({
body: connectionConfig,
return this.webSocketManager
.initWebSocket({
body: this.getConnectionConfig(),
})
.then(async (data: WelcomeEvent) => {
const agentId = data.agentId;
Expand Down Expand Up @@ -135,7 +139,8 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
try {
const loginResponse = this.services.agent.stationLogin({
data: {
dialNumber: data.dialNumber || this.agentConfig.agentId,
dialNumber:
data.loginOption === LoginOption.BROWSER ? this.agentConfig.agentId : data.dialNumber,
teamId: data.teamId,
deviceType: data.loginOption,
isExtension: data.loginOption === LoginOption.EXTENSION,
Expand Down Expand Up @@ -205,4 +210,16 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter

return WEB_RTC_PREFIX + this.agentConfig.agentId;
}

/**
* This method returns the connection configuration.
*/
private getConnectionConfig(): SubscribeRequest {
return {
force: this.$config?.force ?? true,
isKeepAliveEnabled: this.$config?.isKeepAliveEnabled ?? false,
clientType: this.$config?.clientType ?? 'WebexCCSDK',
allowMultiLogin: this.$config?.allowMultiLogin ?? true,
};
}
}
71 changes: 1 addition & 70 deletions packages/@webex/plugin-cc/src/services/core/HttpRequest.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,7 @@
import {SUBSCRIBE_API, WCC_API_GATEWAY, WEBSOCKET_EVENT_TIMEOUT} from '../constants';
import {
WebexSDK,
HTTP_METHODS,
SubscribeRequest,
IHttpResponse,
WelcomeResponse,
WelcomeEvent,
RequestBody,
} from '../../types';
import IWebSocket from '../WebSocket/types';
import WebSocket from '../WebSocket';
import {CC_EVENTS, SubscribeResponse} from '../config/types';
import {EVENT} from '../../constants';

export type EventHandler = {(data: any): void};
import {WebexSDK, HTTP_METHODS, IHttpResponse, RequestBody} from '../../types';

class HttpRequest {
private webSocket: IWebSocket;
private webex: WebexSDK;
private eventHandlers: Map<string, EventHandler>;
private static instance: HttpRequest;

public static getInstance(options?: {webex: WebexSDK}): HttpRequest {
Expand All @@ -32,58 +15,6 @@ class HttpRequest {
private constructor(options: {webex: WebexSDK}) {
const {webex} = options;
this.webex = webex;
this.webSocket = new WebSocket({
parent: this.webex,
});
this.eventHandlers = new Map();

// Centralized WebSocket event listener
this.webSocket.on(EVENT, (eventData) => {
this.webex.logger.log(`Received event: ${eventData.type}`);
const handler = this.eventHandlers.get(eventData.type);
if (handler) {
handler(eventData.data);
}
});
}

public getWebSocket(): IWebSocket {
return this.webSocket;
}

/* This calls subscribeNotifications and establishes a websocket connection
* It sends the request and then listens for the Welcome event
* If the Welcome event is received, it resolves the promise
* If the Welcome event is not received, it rejects the promise
*/
public async subscribeNotifications(options: {body: SubscribeRequest}): Promise<WelcomeResponse> {
const {body} = options;
const eventType = CC_EVENTS.WELCOME;
const subscribeResponse: SubscribeResponse = await this.webex.request({
service: WCC_API_GATEWAY,
resource: SUBSCRIBE_API,
method: HTTP_METHODS.POST,
body,
});

return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
this.webex.logger.error('Timeout waiting for event');
this.eventHandlers.delete(eventType);
reject(new Error('Timeout waiting for event'));
}, WEBSOCKET_EVENT_TIMEOUT);

// Store the event handler
this.eventHandlers.set(eventType, (data: WelcomeEvent) => {
clearTimeout(timeoutId);
this.eventHandlers.delete(eventType);
resolve(data);
});

this.webSocket.connectWebSocket({
webSocketUrl: subscribeResponse.body.webSocketUrl,
});
});
}

public async request(options: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import {WebexSDK, SubscribeRequest, HTTP_METHODS, WelcomeResponse} from '../../../types';
import {SUBSCRIBE_API, WCC_API_GATEWAY} from '../../constants';
import {SubscribeResponse} from '../../config/types';
import LoggerProxy from '../../../logger-proxy';
import workerScript from './keepalive.worker';
import {KEEPALIVE_WORKER_INTERVAL, CLOSE_SOCKET_TIMEOUT} from '../constants';

export class WebSocketManager extends EventTarget {
private websocket: WebSocket;
shouldReconnect: boolean;
isSocketClosed: boolean;
private isWelcomeReceived: boolean;
private url: string | null = null;
private forceCloseWebSocketOnTimeout: boolean;
private isConnectionLost: boolean;
private webex: WebexSDK;
private welcomePromiseResolve:
| ((value: WelcomeResponse | PromiseLike<WelcomeResponse>) => void)
| null = null;

private keepaliveWorker: Worker;

constructor(options: {webex: WebexSDK}) {
super();
const {webex} = options;
this.webex = webex;
this.shouldReconnect = true;
this.websocket = {} as WebSocket;
this.isSocketClosed = false;
this.isWelcomeReceived = false;
this.forceCloseWebSocketOnTimeout = false;
this.isConnectionLost = false;

const workerScriptBlob = new Blob([workerScript], {type: 'application/javascript'});
this.keepaliveWorker = new Worker(URL.createObjectURL(workerScriptBlob));
}

async initWebSocket(options: {body: SubscribeRequest}): Promise<WelcomeResponse> {
const connectionConfig = options.body;
await this.register(connectionConfig);

return new Promise((resolve, reject) => {
this.welcomePromiseResolve = resolve;
this.connect().catch((error) => {
LoggerProxy.logger.error(`[WebSocketStatus] | Error in connecting Websocket ${error}`);
reject(error);
});
});
}

close(shouldReconnect: boolean, reason = 'Unknown') {
if (!this.isSocketClosed && this.shouldReconnect) {
this.shouldReconnect = shouldReconnect;
this.websocket.close();
this.keepaliveWorker.postMessage({type: 'terminate'});
LoggerProxy.logger.error(
`[WebSocketStatus] | event=webSocketClose | WebSocket connection closed manually REASON: ${reason}`
);
}
}

private async register(connectionConfig: SubscribeRequest) {
try {
const subscribeResponse: SubscribeResponse = await this.webex.request({
service: WCC_API_GATEWAY,
resource: SUBSCRIBE_API,
method: HTTP_METHODS.POST,
body: connectionConfig,
});
this.url = subscribeResponse.body.webSocketUrl;
} catch (e) {
LoggerProxy.logger.error(
`Register API Failed, Request to RoutingNotifs websocket registration API failed ${e}`
);
}
}

private async connect() {
if (!this.url) {
return undefined;
}
LoggerProxy.logger.log(
`[WebSocketStatus] | event=webSocketConnecting | Connecting to WebSocket: ${this.url}`
);
this.websocket = new WebSocket(this.url);

return new Promise((resolve, reject) => {
this.websocket.onopen = () => {
this.isSocketClosed = false;
this.shouldReconnect = true;

this.websocket.send(JSON.stringify({keepalive: 'true'}));
this.keepaliveWorker.onmessage = (keepAliveEvent: {data: any}) => {
if (keepAliveEvent?.data?.type === 'keepalive') {
this.websocket.send(JSON.stringify({keepalive: 'true'}));
}

if (keepAliveEvent?.data?.type === 'closeSocket' && this.isConnectionLost) {
this.forceCloseWebSocketOnTimeout = true;
this.close(true, 'WebSocket did not auto close within 16 secs');
LoggerProxy.logger.error(
'[webSocketTimeout] | event=webSocketTimeout | WebSocket connection closed forcefully'
);
}
};

this.keepaliveWorker.postMessage({
type: 'start',
intervalDuration: KEEPALIVE_WORKER_INTERVAL, // Keepalive interval
isSocketClosed: this.isSocketClosed,
closeSocketTimeout: CLOSE_SOCKET_TIMEOUT, // Close socket timeout
});
};

this.websocket.onerror = (event: any) => {
LoggerProxy.logger.error(
`[WebSocketStatus] | event=socketConnectionFailed | WebSocket connection failed ${event}`
);
reject();
};

this.websocket.onclose = async (event: any) => {
this.webSocketOnCloseHandler(event);
};

this.websocket.onmessage = (e: MessageEvent) => {
this.dispatchEvent(new CustomEvent('message', {detail: e.data}));
const eventData = JSON.parse(e.data);

if (eventData.type === 'Welcome') {
this.isWelcomeReceived = true;
if (this.welcomePromiseResolve) {
this.welcomePromiseResolve(eventData.data as WelcomeResponse);
this.welcomePromiseResolve = null;
}
}

if (eventData.type === 'AGENT_MULTI_LOGIN') {
this.close(false, 'multiLogin');
LoggerProxy.logger.error(
'[WebSocketStatus] | event=agentMultiLogin | WebSocket connection closed by agent multiLogin'
);
}
};
});
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
private async webSocketOnCloseHandler(event: any) {
this.isSocketClosed = true;
this.keepaliveWorker.postMessage({type: 'terminate'});
if (this.shouldReconnect) {
this.dispatchEvent(new Event('socketClose'));
let issueReason;
if (this.forceCloseWebSocketOnTimeout) {
issueReason = 'WebSocket auto close timed out. Forcefully closed websocket.';
} else {
const onlineStatus = navigator.onLine;
LoggerProxy.logger.info(`[WebSocketStatus] | desktop online status is ${onlineStatus}`);
issueReason = !onlineStatus
? 'network issue'
: 'missing keepalive from either desktop or notif service';
}
LoggerProxy.logger.error(
`[WebSocketStatus] | event=webSocketClose | WebSocket connection closed REASON: ${issueReason}`
);
this.forceCloseWebSocketOnTimeout = false;
}
}
}
Loading

0 comments on commit b781b05

Please sign in to comment.