Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions packages/@webex/contact-center/src/cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
METHODS,
} from './constants';
import {AGENT_STATE_AVAILABLE, AGENT_STATE_AVAILABLE_ID} from './services/config/constants';
import {AGENT, WEB_RTC_PREFIX} from './services/constants';
import {AGENT, RTD_SUBSCRIBE_API, SUBSCRIBE_API, WEB_RTC_PREFIX} from './services/constants';
import Services from './services';
import WebexRequest from './services/core/WebexRequest';
import LoggerProxy from './logger-proxy';
Expand Down Expand Up @@ -372,7 +372,8 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
this.apiAIAssistant,
this.services.contact,
this.webCallingService,
this.services.webSocketManager
this.services.webSocketManager,
this.services.rtdWebSocketManager
);
this.incomingTaskListener();

Expand Down Expand Up @@ -577,6 +578,9 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
if (!this.services.webSocketManager.isSocketClosed) {
this.services.webSocketManager.close(false, 'Unregistering the SDK');
}
if (!this.services.rtdWebSocketManager.isSocketClosed) {
this.services.rtdWebSocketManager.close(false, 'Unregistering the SDK');
Comment on lines +581 to +582
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard RTD websocket close when no RTD connection exists

This unconditional RTD close path can throw during deregister() for agents without realtime transcription enabled. rtdWebSocketManager.initWebSocket() is only called when aiFeature.realtimeTranscripts.enable is true, but a fresh WebSocketManager starts with isSocketClosed = false and websocket = {}, so calling close() here leads to this.websocket.close() on a non-WebSocket object and aborts deregistration. Please gate this close call on actual RTD connection initialization/open state (or make the manager default to a closed state until connected).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adrressed

}

// Clear any cached agent configuration
this.agentConfig = null;
Expand Down Expand Up @@ -706,6 +710,7 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
return this.services.webSocketManager
.initWebSocket({
body: this.getConnectionConfig(),
resource: SUBSCRIBE_API,
})
.then(async (data: WelcomeEvent) => {
const agentId = data.agentId;
Expand All @@ -729,7 +734,35 @@ export default class ContactCenter extends WebexPlugin implements IContactCenter
this.taskManager.setAgentId(this.agentConfig.agentId);
this.taskManager.setWebRtcEnabled(this.agentConfig.webRtcEnabled);
this.apiAIAssistant.setAIFeatureFlags(this.agentConfig.aiFeature);

/**
* TODO: We need to re-check this condition if this websocket is only for realtime transcripts
* or other AI Assistant features will also use the same.
* If the latter is true, we need to update this condition.
*/
if (this.agentConfig.aiFeature?.realtimeTranscripts?.enable) {
LoggerProxy.info('Connecting to RTD websocket', {
module: CC_FILE,
method: METHODS.CONNECT_WEBSOCKET,
});

this.services.rtdWebSocketManager
.initWebSocket({
body: this.getConnectionConfig(),
resource: RTD_SUBSCRIBE_API,
})
.then(() => {
LoggerProxy.log('RTD websocket connected successfully', {
module: CC_FILE,
method: METHODS.CONNECT_WEBSOCKET,
});
})
.catch((error) => {
LoggerProxy.error(`Error connecting to RTD websocket ${error}`, {
module: CC_FILE,
method: METHODS.CONNECT_WEBSOCKET,
});
});
}
if (
this.agentConfig.webRtcEnabled &&
this.agentConfig.loginVoiceOptions.includes(LoginOption.BROWSER)
Expand Down
7 changes: 7 additions & 0 deletions packages/@webex/contact-center/src/services/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ export const AGENT = 'agent';
* @ignore
*/
export const SUBSCRIBE_API = 'v1/notification/subscribe';
/**
* API path for realtime transcription subscription.
* @type {string}
* @public
* @ignore
*/
export const RTD_SUBSCRIBE_API = 'v1/realtime/subscribe';

/**
* API path for agent login.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import EventEmitter from 'events';
import {WebexSDK, SubscribeRequest, HTTP_METHODS} from '../../../types';
import {SUBSCRIBE_API, WCC_API_GATEWAY} from '../../constants';
import {WCC_API_GATEWAY} from '../../constants';
import {ConnectionLostDetails} from './types';
import {CC_EVENTS, SubscribeResponse, WelcomeResponse} from '../../config/types';
import LoggerProxy from '../../../logger-proxy';
Expand Down Expand Up @@ -44,9 +44,12 @@ export class WebSocketManager extends EventEmitter {
this.keepaliveWorker = new Worker(URL.createObjectURL(workerScriptBlob));
}

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

return new Promise((resolve, reject) => {
this.welcomePromiseResolve = resolve;
Expand Down Expand Up @@ -76,11 +79,11 @@ export class WebSocketManager extends EventEmitter {
this.isConnectionLost = event.isConnectionLost;
}

private async register(connectionConfig: SubscribeRequest) {
private async register(connectionConfig: SubscribeRequest, resource: string) {
try {
const subscribeResponse: SubscribeResponse = await this.webex.request({
service: WCC_API_GATEWAY,
resource: SUBSCRIBE_API,
resource,
method: HTTP_METHODS.POST,
body: connectionConfig,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '../constants';
import {CONNECTION_SERVICE_FILE} from '../../../constants';
import {SubscribeRequest} from '../../../types';
import {SUBSCRIBE_API} from '../../constants';

export class ConnectionService extends EventEmitter {
private connectionProp: ConnectionProp = {
Expand Down Expand Up @@ -124,7 +125,10 @@ export class ConnectionService extends EventEmitter {
});
const onlineStatus = navigator.onLine;
if (onlineStatus) {
await this.webSocketManager.initWebSocket({body: this.subscribeRequest});
await this.webSocketManager.initWebSocket({
body: this.subscribeRequest,
resource: SUBSCRIBE_API,
});
await this.clearTimerOnRestoreFailed();
this.isSocketReconnected = true;
} else {
Expand Down
3 changes: 3 additions & 0 deletions packages/@webex/contact-center/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export default class Services {
public readonly dialer: ReturnType<typeof aqmDialer>;
/** WebSocket manager for handling real-time communications */
public readonly webSocketManager: WebSocketManager;
/** RTD WebSocket manager for handling real-time transcription */
public readonly rtdWebSocketManager: WebSocketManager;
/** Connection service for managing websocket connections */
public readonly connectionService: ConnectionService;
/** Singleton instance of the Services class */
Expand All @@ -39,6 +41,7 @@ export default class Services {
constructor(options: {webex: WebexSDK; connectionConfig: SubscribeRequest}) {
const {webex, connectionConfig} = options;
this.webSocketManager = new WebSocketManager({webex});
this.rtdWebSocketManager = new WebSocketManager({webex});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add reconnect wiring for the RTD websocket manager

The new RTD manager is created but not integrated into reconnection handling: ConnectionService only subscribes to 'message'/'socketClose' on the primary webSocketManager, and WebSocketManager itself does not perform retries. In practice, if the RTD socket drops once (for example, a transient network loss), transcript delivery will stop for the rest of the session unless the SDK is fully re-registered. Please attach equivalent reconnect logic for rtdWebSocketManager.

Useful? React with 👍 / 👎.

const aqmReq = new AqmReqs(this.webSocketManager);
this.config = new AgentConfigService();
this.agent = routingAgent(aqmReq);
Expand Down
52 changes: 40 additions & 12 deletions packages/@webex/contact-center/src/services/task/TaskManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default class TaskManager extends EventEmitter {
private taskCollection: Record<TaskId, ITask>;
private webCallingService: WebCallingService;
private webSocketManager: WebSocketManager;
private rtdWebSocketManager: WebSocketManager;
// eslint-disable-next-line no-use-before-define
private static taskManager: TaskManager;
private configFlags?: ConfigFlags;
Expand All @@ -58,18 +59,51 @@ export default class TaskManager extends EventEmitter {
apiAIAssistant: ApiAIAssistant,
contact: ReturnType<typeof routingContact>,
webCallingService: WebCallingService,
webSocketManager: WebSocketManager
webSocketManager: WebSocketManager,
rtdWebSocketManager: WebSocketManager
) {
super();
this.apiAIAssistant = apiAIAssistant;
this.contact = contact;
this.webCallingService = webCallingService;
this.webSocketManager = webSocketManager;
this.rtdWebSocketManager = rtdWebSocketManager;
this.taskCollection = {};
this.webRtcEnabled = false;

this.registerTaskListeners();
this.registerIncomingCallEvent();
this.registerRealtimeWSListeners();
}

private registerRealtimeWSListeners() {
this.rtdWebSocketManager.on('message', (event: string) => {
try {
const payload = JSON.parse(event);

const interactionId = payload?.data?.data?.conversationId;
if (!interactionId) return;

const task = this.taskCollection[interactionId];
if (!task) {
LoggerProxy.info(`Realtime transcription task not found`, {
module: TASK_MANAGER_FILE,
method: METHODS.REGISTER_REAL_TIME_WS_LISTENERS,
interactionId,
});

return;
}

task.emit(payload.type, payload.data);
} catch (error) {
LoggerProxy.error('Failed to parse RTD WebSocket message', {
module: TASK_MANAGER_FILE,
method: METHODS.REGISTER_TASK_LISTENERS,
error,
});
}
});
}

/**
Expand Down Expand Up @@ -325,11 +359,6 @@ export default class TaskManager extends EventEmitter {
const eventContext = this.prepareEventContext(message);
if (!eventContext) return;

if (eventContext.eventType === CC_EVENTS.REAL_TIME_TRANSCRIPTION) {
eventContext.task?.emit(CC_EVENTS.REAL_TIME_TRANSCRIPTION, eventContext.payload);

return;
}
const actions = this.handleTaskLifecycleEvent(eventContext);

const {task} = actions;
Expand Down Expand Up @@ -395,10 +424,7 @@ export default class TaskManager extends EventEmitter {
return null;
}

const interactionId =
eventType === CC_EVENTS.REAL_TIME_TRANSCRIPTION
? message.data.data.conversationId
: message.data.interactionId;
const interactionId = message.data.interactionId;
const task = this.taskCollection[interactionId];

const wasConsultedTask = Boolean(task?.data?.isConsulted);
Expand Down Expand Up @@ -733,14 +759,16 @@ export default class TaskManager extends EventEmitter {
apiAIAssistant: ApiAIAssistant,
contact: ReturnType<typeof routingContact>,
webCallingService: WebCallingService,
webSocketManager: WebSocketManager
webSocketManager: WebSocketManager,
rtdWebSocketManager?: WebSocketManager
): TaskManager {
if (!TaskManager.taskManager) {
TaskManager.taskManager = new TaskManager(
apiAIAssistant,
contact,
webCallingService,
webSocketManager
webSocketManager,
rtdWebSocketManager
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const METHODS = {
// TaskManager class methods
HANDLE_INCOMING_WEB_CALL: 'handleIncomingWebCall',
REGISTER_TASK_LISTENERS: 'registerTaskListeners',
REGISTER_REAL_TIME_WS_LISTENERS: 'registerRealtimeWSListeners',
REMOVE_TASK_FROM_COLLECTION: 'removeTaskFromCollection',
HANDLE_TASK_CLEANUP: 'handleTaskCleanup',
GET_TASK: 'getTask',
Expand Down
15 changes: 15 additions & 0 deletions packages/@webex/contact-center/test/unit/spec/cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ describe('webex.cc', () => {
clientType: 'WebexCCSDK',
allowMultiLogin: false,
},
resource: 'v1/notification/subscribe',
});

// TODO: https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-626777 Implement the de-register method and close the listener there
Expand Down Expand Up @@ -381,6 +382,7 @@ describe('webex.cc', () => {
clientType: 'WebexCCSDK',
allowMultiLogin: true,
},
resource: 'v1/notification/subscribe',
});
expect(configSpy).toHaveBeenCalled();
expect(LoggerProxy.log).toHaveBeenCalledWith('Agent config is fetched successfully', {
Expand Down Expand Up @@ -460,6 +462,7 @@ describe('webex.cc', () => {
clientType: 'WebexCCSDK',
allowMultiLogin: false,
},
resource: 'v1/notification/subscribe',
});

expect(mockTaskManager.on).toHaveBeenCalledWith(
Expand Down Expand Up @@ -512,6 +515,7 @@ describe('webex.cc', () => {
clientType: 'WebexCCSDK',
allowMultiLogin: false,
},
resource: 'v1/notification/subscribe',
});

expect(configSpy).toHaveBeenCalled();
Expand Down Expand Up @@ -1510,6 +1514,7 @@ describe('webex.cc', () => {

describe('unregister', () => {
let mockWebSocketManager;
let mockRTDWebSocketManager;
let mercuryDisconnectSpy;
let deviceUnregisterSpy;

Expand All @@ -1526,8 +1531,15 @@ describe('webex.cc', () => {
off: jest.fn(),
on: jest.fn(),
};
mockRTDWebSocketManager = {
isSocketClosed: false,
close: jest.fn(),
off: jest.fn(),
on: jest.fn(),
};

webex.cc.services.webSocketManager = mockWebSocketManager;
webex.cc.services.rtdWebSocketManager = mockRTDWebSocketManager;

webex.internal = webex.internal || {};
webex.internal.mercury = {
Expand Down Expand Up @@ -1561,6 +1573,7 @@ describe('webex.cc', () => {
);

expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
expect(mockRTDWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
expect(webex.cc.agentConfig).toBeNull();

expect(webex.internal.mercury.off).toHaveBeenCalledWith('online');
Expand Down Expand Up @@ -1636,6 +1649,7 @@ describe('webex.cc', () => {
expect(webex.internal.mercury.off).not.toHaveBeenCalled();
expect(mercuryDisconnectSpy).not.toHaveBeenCalled();
expect(deviceUnregisterSpy).not.toHaveBeenCalled();
expect(mockRTDWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
});

it('should skip internal mercury cleanup when loginVoiceOptions does not include BROWSER', async () => {
Expand All @@ -1653,6 +1667,7 @@ describe('webex.cc', () => {
expect(deviceUnregisterSpy).not.toHaveBeenCalled();

expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
expect(mockRTDWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
expect(webex.cc.agentConfig).toBeNull();
});

Expand Down
Loading
Loading