-
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
feat(cc-sdk): added-health-check-and-keep-alives #2
Conversation
…bex-js-sdk into dev/adhmenon-SPARK-574669
@@ -0,0 +1,15 @@ | |||
class Worker { |
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.
private httpRequest: HttpRequest; | ||
private webSocketManager: WebSocketManager; |
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.
Decided to add an instance of the WebSocketManager
here as now we will directly call the initWebSocket
method to establish a connection with the web socket and use that Promise to setup the config (shown below).
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 comment
The 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.webSocket.connectWebSocket({ | ||
webSocketUrl: subscribeResponse.body.webSocketUrl, | ||
}); | ||
}); | ||
} | ||
|
||
public async request(options: { |
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.
We only need to retain this method, the rest of them are now supported via the WebSocketManager
.
Discussed with Ravi and we felt this was the best course of action.
packages/@webex/plugin-cc/src/services/core/WebSocket/WebSocketManager.ts
Outdated
Show resolved
Hide resolved
import workerScript from './keepalive.worker'; | ||
|
||
export class WebSocketManager { | ||
readonly onMessage: Signal.WithData<string>; |
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a TODO and a SPARK for this?
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 comment
The 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.
}); | ||
} | ||
|
||
async reconnect() { |
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.
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).
|
||
this.keepaliveWorker.postMessage({ | ||
type: 'start', | ||
intervalDuration: 4000, // Keepalive interval |
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.
These are durations taken from agent desktop, open to changing them.
} | ||
} | ||
|
||
if (eventData.type === 'AGENT_MULTI_LOGIN') { |
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.
Again, all have been taken from agent desktop, so if we do not require this, we can remove them.
But overall the logic is pretty much the same.
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.
It is okay to keep this as this whole copy-paste effort is keeping the WxCC Desktop use cases in mind
@@ -0,0 +1,89 @@ | |||
/* eslint-disable */ | |||
// TODO: Try to find alternative to using Blob and script here | |||
const workerScript = ` |
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.
As mentioned, this is a workaround, currently we export the template string and make it a Blob and use it as a worker script.
Work is being done to explore a better method for this, however to unblock we can go ahead with this workaround.
Open to any suggestions.
@@ -2,6 +2,7 @@ import {Signal} from './Signal'; // TODO: remove and use Event Emitters after Ad | |||
import {Msg} from './GlobalTypes'; | |||
import * as Err from './Err'; | |||
import {HTTP_METHODS, WebexRequestPayload} from '../../types'; | |||
import {WebSocketManager} from './WebSocket/WebSocketManager'; |
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.
The changes in this file are simply replacing the web socket manager in the same manner in which we have aqm-notfifs
inside of the the agent desktop.
It uses signals as well (logic is basically the same as agent desktop, with minor refactoring).
if (event.type === 'Welcome') { | ||
LoggerProxy.logger.info(`Welcome message from Notifs Websocket${event}`); | ||
|
||
return; | ||
} | ||
|
||
if (event.keepalive) { | ||
LoggerProxy.logger.info(`Keepalive from notifs${event}`); | ||
if (event.keepalive === 'true') { |
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.
To record the keepalive message.
|
||
export default class Services { | ||
public readonly agent: ReturnType<typeof routingAgent>; | ||
private static instance: Services; | ||
|
||
constructor() { | ||
const aqmReq = new AqmReqs(); | ||
constructor(webSocketManager: WebSocketManager) { |
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.
We use DI to inject the websocketmanager from the cc.ts into the sqm-reqs. Preferred this approach over using a singleton system.
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.
Non-blocking Q: What is the consensus behind preferring this approach over singleton?
Would we have another entity in the plugin that would reuse this services with a different webSocketManager?
@@ -1,12 +1,18 @@ | |||
import 'jsdom-global/register'; | |||
import {LoginOption, StationLogoutResponse, WebexSDK} from '../../../src/types'; | |||
import WebSocketManager from '../../../src/services/core/WebSocket/WebSocketManager'; |
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.
Tests here are mostly the same, just include some of the mocks and the blob mocking. All seem to run properly and are passing.
}); | ||
|
||
describe('subscribeNotifications', () => { |
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.
Removed tests that were no longer required.
} | ||
} | ||
|
||
describe('WebSocketManager', () => { |
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.
Here, I have mocked the webSocket
creation in a similar manner to the way it was being done in agentx.
|
||
await webSocketManager.initWebSocket({ body: subscribeRequest }); | ||
|
||
setTimeout(() => { |
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.
We need to set this timeout so that we cna call the onopen
callback and the code flows as per usual.
|
||
jest.mock('../../../../../../src/services/core/WebSocket/WebSocketManager'); | ||
|
||
describe('ConnectionService', () => { |
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.
Mostly taken tests from the agent desktop in this case. All test cases are passing.
@@ -121,13 +120,13 @@ describe('AqmReqs', () => { | |||
req({}), | |||
new Promise<void>((resolve) => { | |||
setTimeout(() => { | |||
aqm['onMessage']({ | |||
webSocketManagerInstance.onMessageSend(JSON.stringify({ |
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.
Had to modify it in order to accommodate the latest changes.
resolve(); | ||
}, 1000); | ||
}), | ||
]); | ||
expect(p).toBeDefined(); | ||
} catch (e) {} | ||
}); | ||
|
||
it('should handle onMessage with Welcome event', () => { |
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.
We do not need these tests anymore, hence removed them.
packages/@webex/plugin-cc/src/services/core/WebSocket/connection-service.ts
Outdated
Show resolved
Hide resolved
lostConnectionRecoveryTimeout: number; | ||
}; | ||
|
||
const LOST_CONNECTION_RECOVERY_TIMEOUT = 20000; // 20 seconds |
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.
can be moved to config?
packages/@webex/plugin-cc/src/services/core/WebSocket/WebSocketManager.ts
Outdated
Show resolved
Hide resolved
}); | ||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
yes, please replace all the console with LoggerProxy
import {SUBSCRIBE_API, WCC_API_GATEWAY} from '../../constants'; | ||
import {SubscribeResponse} from '../../config/types'; | ||
import LoggerProxy from '../../../logger-proxy'; | ||
import workerScript from './keepalive.worker'; |
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.
Note -
This is a workaround, currently I was not able to find any way to import the script the same way we were doing in agent desktop (as that is a node server).
However this workaround can be improved (later) wherein we read from the .js script file rather than exporting the string.
private forceCloseWebSocketOnTimeout: boolean; | ||
private isConnectionLost: boolean; | ||
private webex: WebexSDK; | ||
private welcomePromiseResolve: |
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.
This was a needed change so that when we call the initWebSocket from the cc.ts, we are able to return a promise to resolve the initial registration. This differs from agentx which was calling it from the aqm-reqs and hence it could be handled via signals, however since we call it from cc.ts, I had to add this here for that.
It works fine (as shown in the GIF).
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.
First round of review
@@ -0,0 +1,166 @@ | |||
/* eslint-disable */ |
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.
You could disable the console.error
-specific check alone
/* eslint-disable */ | |
/* eslint no-console: "error" */ |
import workerScript from './keepalive.worker'; | ||
|
||
export class WebSocketManager { | ||
readonly onMessage: Signal.WithData<string>; |
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.
Can we add a TODO and a SPARK for this?
packages/@webex/plugin-cc/src/services/core/WebSocket/keepalive.worker.js
Outdated
Show resolved
Hide resolved
packages/@webex/plugin-cc/src/services/core/WebSocket/connection-service.ts
Outdated
Show resolved
Hide resolved
}; | ||
|
||
this.websocket.onmessage = (e: MessageEvent) => { | ||
this.dispatchEvent(new CustomEvent('message', {detail: e.data})); |
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.
As per discussion with Ravi, I have switched over to using events here as opposed to singals. I felt this was a more straight-forward solution. Now, I am not using custom event handlers here as it was working with events, however I would like someone to point out any edge cases or issues that might arise (most things are working including reconnect logic).
this.isSocketReconnected = false; | ||
this.isKeepAlive = false; | ||
|
||
this.webSocketManager.addEventListener('message', this.onPing); |
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.
As mentioned, using event listeners here over signals, however this was the only change here.
this.dispatchConnectionEvent(true); | ||
} | ||
|
||
this.reconnectingTimer = setTimeout(this.handleConnectionLost, this.wsDisconnectAllowed); |
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.
We have a timer of 8 seconds, which closes the web socket if keep alive is false for 8 seconds (health check gets browser online status).
In that case, when keep alive is true once again, it restores the timer by simply re-registering the web socket connection (mentioned in handleSocketClose).
Logic is the same as agent desktop here.
NOTE |
@@ -1,7 +1,9 @@ | |||
/* eslint-disable @typescript-eslint/no-explicit-any */ | |||
import AqmReqs from '../../../../../src/services/core/aqm-reqs'; | |||
import HttpRequest from '../../../../../src/services/core/HttpRequest'; | |||
import {WebSocketManager} from '../../../../../src/services/core/WebSocket/WebSocketManager'; |
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.
I had to add couple of more tests so coverage went above 85%. Discussed this with Ravi and I got it working... it has made the PR a little huge, however.
jest.mock('../../../../../src/services/core/WebSocket/WebSocketManager'); | ||
|
||
// Mock CustomEvent class | ||
class MockCustomEvent<T> extends Event { |
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.
Simply mocking the event class as we are now using events.
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.
Changes mostly look good to me. Just a few non-blocking questions. Approved
this.services = Services.getInstance(); | ||
this.webSocketManager = new WebSocketManager({webex: this.$webex}); | ||
|
||
this.connectionService = new ConnectionService( |
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.
Q: This is just an instance creation. We do not call any methods of this conneionService directly. Is my understanding correct?
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.
Yes, but since we are setting up the event listeners on init, it will call the internal handlers.
} | ||
} | ||
|
||
if (eventData.type === 'AGENT_MULTI_LOGIN') { |
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.
It is okay to keep this as this whole copy-paste effort is keeping the WxCC Desktop use cases in mind
|
||
export default class Services { | ||
public readonly agent: ReturnType<typeof routingAgent>; | ||
private static instance: Services; | ||
|
||
constructor() { | ||
const aqmReq = new AqmReqs(); | ||
constructor(webSocketManager: WebSocketManager) { |
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.
Non-blocking Q: What is the consensus behind preferring this approach over singleton?
Would we have another entity in the plugin that would reuse this services with a different webSocketManager?
COMPLETES #< SPARK-574669 >
This pull request addresses
WebSocket
handling from agentx to cc-sdk.by making the following changes
WebSocketManager
which handles the web socket connection.GIF
(With Logout/Login)
Change Type
The following scenarios were tested
< ENUMERATE TESTS PERFORMED, WHETHER MANUAL OR AUTOMATED >
I certified that
I have read and followed contributing guidelines
I discussed changes with code owners prior to submitting this pull request
I have not skipped any automated checks
All existing and new tests passed
I have updated the documentation accordingly
Make sure to have followed the contributing guidelines before submitting.