-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(cc-sdk): added-health-check-and-keep-alives #2
Changes from 6 commits
0f5c1c8
99fc367
5665ee1
3cd6307
8821963
994fe63
ff494fc
859cfd6
f8923d4
83652ba
51fbfe1
4c303f2
8d7bad7
22fdd15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ import {READY, CC_FILE} from './constants'; | |
import HttpRequest from './services/core/HttpRequest'; | ||
import WebCallingService from './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 * as Agent from './services/agent/types'; | ||
|
@@ -26,8 +27,8 @@ 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Decided to add an instance of the |
||
private webCallingService: WebCallingService; | ||
private services: Services; | ||
|
||
|
@@ -48,7 +49,9 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter | |
webex: this.$webex, | ||
}); | ||
|
||
this.services = Services.getInstance(); | ||
this.webSocketManager = new WebSocketManager({webex: this.$webex}); | ||
|
||
this.services = Services.getInstance(this.webSocketManager); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here we will inject the manager for reasons that will be explained in the Service class implementation. |
||
|
||
this.webCallingService = new WebCallingService(this.$webex, this.$config.callingClientConfig); | ||
|
||
|
@@ -84,8 +87,8 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter | |
}; | ||
|
||
try { | ||
return this.httpRequest | ||
.subscribeNotifications({ | ||
return this.webSocketManager | ||
.initWebSocket({ | ||
body: connectionConfig, | ||
}) | ||
.then(async (data: WelcomeEvent) => { | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,166 @@ | ||||||
/* eslint-disable */ | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could disable the
Suggested change
adhmenon marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
import { Signal } from '../Signal'; | ||||||
import { WebexSDK, SubscribeRequest, HTTP_METHODS, WelcomeResponse } from '../../../types'; | ||||||
import { SUBSCRIBE_API, WCC_API_GATEWAY } from '../../constants'; | ||||||
import { SubscribeResponse } from '../../config/types'; | ||||||
import workerScript from './keepalive.worker'; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note - |
||||||
|
||||||
export class WebSocketManager { | ||||||
readonly onMessage: Signal.WithData<string>; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. RIght now, we will go ahead with a signal implementation, as discussed with Ravi. We will have another ticket to refactor it to using event handlers - wanted to avoid increasing the scope of this ticket as of now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a TODO and a SPARK for this? |
||||||
private readonly onMessageSend: Signal.Send<string>; | ||||||
readonly onSocketClose: Signal.Empty; | ||||||
private readonly onSocketCloseSend: Signal.SendEmpty; | ||||||
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; | ||||||
adhmenon marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
private keepaliveWorker: Worker; | ||||||
|
||||||
constructor(options: {webex: WebexSDK}) { | ||||||
const {webex} = options; | ||||||
this.webex = webex; | ||||||
const { send, signal } = Signal.create.withData<string>(); | ||||||
this.onMessage = signal; | ||||||
this.onMessageSend = send; | ||||||
|
||||||
const socketCloseSignal = Signal.create.empty(); | ||||||
this.onSocketClose = socketCloseSignal.signal; | ||||||
this.onSocketCloseSend = socketCloseSignal.send; | ||||||
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) => { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again as mentioned, this promise is for the registration flow, if the connect fails, we simply reject the promise, it works for both cases. |
||||||
this.welcomePromiseResolve = resolve; | ||||||
this.connect() | ||||||
.catch((error) => { | ||||||
console.error(`[WebSocketStatus] | Error in connecting Websocket`, error); | ||||||
reject(error); | ||||||
}); | ||||||
}); | ||||||
} | ||||||
|
||||||
async reconnect() { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is exposed so that we can reconnect the flow if the network is turned off and gets turned on again. It uses the keep-alive to monitor this and based on the message, it initiates a reconnect logic (found in connection-service.ts, which is taken from agent desktop). |
||||||
await this.connect().catch(() => { | ||||||
console.error(`[WebSocketStatus] | Error in connecting Websocket`); | ||||||
}); | ||||||
} | ||||||
|
||||||
close(shouldReconnect: boolean, reason = 'Unknown') { | ||||||
if (!this.isSocketClosed && this.shouldReconnect) { | ||||||
this.shouldReconnect = shouldReconnect; | ||||||
this.websocket.close(); | ||||||
this.keepaliveWorker.postMessage({ type: 'terminate' }); | ||||||
console.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) { | ||||||
console.error("Register API Failed", "Request to RoutingNotifs websocket registration API failed", e); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, please replace all the console with LoggerProxy |
||||||
} | ||||||
} | ||||||
|
||||||
private async connect() { | ||||||
if (!this.url) { | ||||||
return undefined; | ||||||
} | ||||||
console.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'); | ||||||
console.error('[webSocketTimeout] | event=webSocketTimeout | WebSocket connection closed forcefully'); | ||||||
} | ||||||
}; | ||||||
|
||||||
this.keepaliveWorker.postMessage({ | ||||||
type: 'start', | ||||||
intervalDuration: 4000, // Keepalive interval | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are durations taken from agent desktop, open to changing them. |
||||||
isSocketClosed: this.isSocketClosed, | ||||||
closeSocketTimeout: 16000, // Close socket timeout | ||||||
}); | ||||||
}; | ||||||
|
||||||
this.websocket.onerror = (event: any) => { | ||||||
console.error(`[WebSocketStatus] | event=socketConnectionFailed | WebSocket connection failed`, event); | ||||||
reject(); | ||||||
}; | ||||||
|
||||||
this.websocket.onclose = async (event: any) => { | ||||||
this.webSocketOnCloseHandler(event); | ||||||
}; | ||||||
|
||||||
this.websocket.onmessage = (e: MessageEvent) => { | ||||||
this.onMessageSend(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') { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, all have been taken from agent desktop, so if we do not require this, we can remove them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is okay to keep this as this whole copy-paste effort is keeping the WxCC Desktop use cases in mind |
||||||
this.close(false, 'multiLogin'); | ||||||
console.error('[WebSocketStatus] | event=agentMultiLogin | WebSocket connection closed by agent multiLogin'); | ||||||
} | ||||||
}; | ||||||
}); | ||||||
} | ||||||
|
||||||
private async webSocketOnCloseHandler(event: any) { | ||||||
this.isSocketClosed = true; | ||||||
this.keepaliveWorker.postMessage({ type: 'terminate' }); | ||||||
if (this.shouldReconnect) { | ||||||
this.onSocketCloseSend(); | ||||||
let issueReason; | ||||||
if (this.forceCloseWebSocketOnTimeout) { | ||||||
issueReason = 'WebSocket auto close timed out. Forcefully closed websocket.'; | ||||||
} else { | ||||||
const onlineStatus = navigator.onLine; | ||||||
console.info(`[WebSocketStatus] | desktop online status is ${onlineStatus}`); | ||||||
issueReason = !onlineStatus ? 'network issue' : 'missing keepalive from either desktop or notif service'; | ||||||
} | ||||||
console.error(`[WebSocketStatus] | event=webSocketClose | WebSocket connection closed REASON: ${issueReason}`); | ||||||
this.forceCloseWebSocketOnTimeout = false; | ||||||
} | ||||||
} | ||||||
} |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created this so that we can mock the worker thread in tests. Kept it ouside so it can be reused.