@webex/contact-center 0.0.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/__mocks__/workerMock.js +15 -0
- package/babel.config.js +15 -0
- package/dist/cc.js +1416 -0
- package/dist/cc.js.map +1 -0
- package/dist/config.js +72 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.js +58 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.js +142 -0
- package/dist/index.js.map +1 -0
- package/dist/logger-proxy.js +115 -0
- package/dist/logger-proxy.js.map +1 -0
- package/dist/metrics/MetricsManager.js +474 -0
- package/dist/metrics/MetricsManager.js.map +1 -0
- package/dist/metrics/behavioral-events.js +322 -0
- package/dist/metrics/behavioral-events.js.map +1 -0
- package/dist/metrics/constants.js +134 -0
- package/dist/metrics/constants.js.map +1 -0
- package/dist/services/WebCallingService.js +323 -0
- package/dist/services/WebCallingService.js.map +1 -0
- package/dist/services/agent/index.js +177 -0
- package/dist/services/agent/index.js.map +1 -0
- package/dist/services/agent/types.js +137 -0
- package/dist/services/agent/types.js.map +1 -0
- package/dist/services/config/Util.js +203 -0
- package/dist/services/config/Util.js.map +1 -0
- package/dist/services/config/constants.js +221 -0
- package/dist/services/config/constants.js.map +1 -0
- package/dist/services/config/index.js +607 -0
- package/dist/services/config/index.js.map +1 -0
- package/dist/services/config/types.js +334 -0
- package/dist/services/config/types.js.map +1 -0
- package/dist/services/constants.js +117 -0
- package/dist/services/constants.js.map +1 -0
- package/dist/services/core/Err.js +43 -0
- package/dist/services/core/Err.js.map +1 -0
- package/dist/services/core/GlobalTypes.js +6 -0
- package/dist/services/core/GlobalTypes.js.map +1 -0
- package/dist/services/core/Utils.js +126 -0
- package/dist/services/core/Utils.js.map +1 -0
- package/dist/services/core/WebexRequest.js +96 -0
- package/dist/services/core/WebexRequest.js.map +1 -0
- package/dist/services/core/aqm-reqs.js +246 -0
- package/dist/services/core/aqm-reqs.js.map +1 -0
- package/dist/services/core/constants.js +109 -0
- package/dist/services/core/constants.js.map +1 -0
- package/dist/services/core/types.js +6 -0
- package/dist/services/core/types.js.map +1 -0
- package/dist/services/core/websocket/WebSocketManager.js +187 -0
- package/dist/services/core/websocket/WebSocketManager.js.map +1 -0
- package/dist/services/core/websocket/connection-service.js +111 -0
- package/dist/services/core/websocket/connection-service.js.map +1 -0
- package/dist/services/core/websocket/keepalive.worker.js +94 -0
- package/dist/services/core/websocket/keepalive.worker.js.map +1 -0
- package/dist/services/core/websocket/types.js +6 -0
- package/dist/services/core/websocket/types.js.map +1 -0
- package/dist/services/index.js +78 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/task/AutoWrapup.js +88 -0
- package/dist/services/task/AutoWrapup.js.map +1 -0
- package/dist/services/task/TaskManager.js +369 -0
- package/dist/services/task/TaskManager.js.map +1 -0
- package/dist/services/task/constants.js +58 -0
- package/dist/services/task/constants.js.map +1 -0
- package/dist/services/task/contact.js +464 -0
- package/dist/services/task/contact.js.map +1 -0
- package/dist/services/task/dialer.js +60 -0
- package/dist/services/task/dialer.js.map +1 -0
- package/dist/services/task/index.js +1188 -0
- package/dist/services/task/index.js.map +1 -0
- package/dist/services/task/types.js +214 -0
- package/dist/services/task/types.js.map +1 -0
- package/dist/types/cc.d.ts +676 -0
- package/dist/types/config.d.ts +66 -0
- package/dist/types/constants.d.ts +45 -0
- package/dist/types/index.d.ts +178 -0
- package/dist/types/logger-proxy.d.ts +71 -0
- package/dist/types/metrics/MetricsManager.d.ts +223 -0
- package/dist/types/metrics/behavioral-events.d.ts +29 -0
- package/dist/types/metrics/constants.d.ts +127 -0
- package/dist/types/services/WebCallingService.d.ts +1 -0
- package/dist/types/services/agent/index.d.ts +46 -0
- package/dist/types/services/agent/types.d.ts +413 -0
- package/dist/types/services/config/Util.d.ts +19 -0
- package/dist/types/services/config/constants.d.ts +203 -0
- package/dist/types/services/config/index.d.ts +171 -0
- package/dist/types/services/config/types.d.ts +1113 -0
- package/dist/types/services/constants.d.ts +97 -0
- package/dist/types/services/core/Err.d.ts +119 -0
- package/dist/types/services/core/GlobalTypes.d.ts +33 -0
- package/dist/types/services/core/Utils.d.ts +36 -0
- package/dist/types/services/core/WebexRequest.d.ts +22 -0
- package/dist/types/services/core/aqm-reqs.d.ts +16 -0
- package/dist/types/services/core/constants.d.ts +85 -0
- package/dist/types/services/core/types.d.ts +47 -0
- package/dist/types/services/core/websocket/WebSocketManager.d.ts +34 -0
- package/dist/types/services/core/websocket/connection-service.d.ts +27 -0
- package/dist/types/services/core/websocket/keepalive.worker.d.ts +2 -0
- package/dist/types/services/core/websocket/types.d.ts +37 -0
- package/dist/types/services/index.d.ts +52 -0
- package/dist/types/services/task/AutoWrapup.d.ts +40 -0
- package/dist/types/services/task/TaskManager.d.ts +1 -0
- package/dist/types/services/task/constants.d.ts +46 -0
- package/dist/types/services/task/contact.d.ts +59 -0
- package/dist/types/services/task/dialer.d.ts +28 -0
- package/dist/types/services/task/index.d.ts +569 -0
- package/dist/types/services/task/types.d.ts +1041 -0
- package/dist/types/types.d.ts +452 -0
- package/dist/types/webex-config.d.ts +53 -0
- package/dist/types/webex.d.ts +7 -0
- package/dist/types.js +292 -0
- package/dist/types.js.map +1 -0
- package/dist/webex-config.js +60 -0
- package/dist/webex-config.js.map +1 -0
- package/dist/webex.js +99 -0
- package/dist/webex.js.map +1 -0
- package/jest.config.js +45 -0
- package/package.json +83 -0
- package/src/cc.ts +1618 -0
- package/src/config.ts +65 -0
- package/src/constants.ts +51 -0
- package/src/index.ts +220 -0
- package/src/logger-proxy.ts +110 -0
- package/src/metrics/MetricsManager.ts +512 -0
- package/src/metrics/behavioral-events.ts +332 -0
- package/src/metrics/constants.ts +135 -0
- package/src/services/WebCallingService.ts +351 -0
- package/src/services/agent/index.ts +149 -0
- package/src/services/agent/types.ts +440 -0
- package/src/services/config/Util.ts +261 -0
- package/src/services/config/constants.ts +249 -0
- package/src/services/config/index.ts +743 -0
- package/src/services/config/types.ts +1117 -0
- package/src/services/constants.ts +111 -0
- package/src/services/core/Err.ts +126 -0
- package/src/services/core/GlobalTypes.ts +34 -0
- package/src/services/core/Utils.ts +132 -0
- package/src/services/core/WebexRequest.ts +103 -0
- package/src/services/core/aqm-reqs.ts +272 -0
- package/src/services/core/constants.ts +106 -0
- package/src/services/core/types.ts +48 -0
- package/src/services/core/websocket/WebSocketManager.ts +196 -0
- package/src/services/core/websocket/connection-service.ts +142 -0
- package/src/services/core/websocket/keepalive.worker.js +88 -0
- package/src/services/core/websocket/types.ts +40 -0
- package/src/services/index.ts +71 -0
- package/src/services/task/AutoWrapup.ts +86 -0
- package/src/services/task/TaskManager.ts +420 -0
- package/src/services/task/constants.ts +52 -0
- package/src/services/task/contact.ts +429 -0
- package/src/services/task/dialer.ts +52 -0
- package/src/services/task/index.ts +1375 -0
- package/src/services/task/types.ts +1113 -0
- package/src/types.ts +639 -0
- package/src/webex-config.ts +54 -0
- package/src/webex.js +96 -0
- package/test/unit/spec/cc.ts +1985 -0
- package/test/unit/spec/metrics/MetricsManager.ts +491 -0
- package/test/unit/spec/metrics/behavioral-events.ts +102 -0
- package/test/unit/spec/services/WebCallingService.ts +416 -0
- package/test/unit/spec/services/agent/index.ts +65 -0
- package/test/unit/spec/services/config/index.ts +1035 -0
- package/test/unit/spec/services/core/Utils.ts +279 -0
- package/test/unit/spec/services/core/WebexRequest.ts +144 -0
- package/test/unit/spec/services/core/aqm-reqs.ts +570 -0
- package/test/unit/spec/services/core/websocket/WebSocketManager.ts +378 -0
- package/test/unit/spec/services/core/websocket/connection-service.ts +178 -0
- package/test/unit/spec/services/task/TaskManager.ts +1351 -0
- package/test/unit/spec/services/task/contact.ts +204 -0
- package/test/unit/spec/services/task/dialer.ts +157 -0
- package/test/unit/spec/services/task/index.ts +1474 -0
- package/tsconfig.json +6 -0
- package/typedoc.json +37 -0
- package/typedoc.md +240 -0
- package/umd/contact-center.min.js +3 -0
- package/umd/contact-center.min.js.map +1 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import {Msg} from './GlobalTypes';
|
|
2
|
+
import * as Err from './Err';
|
|
3
|
+
import {HTTP_METHODS, WebexRequestPayload} from '../../types';
|
|
4
|
+
import LoggerProxy from '../../logger-proxy';
|
|
5
|
+
import {CbRes, Conf, ConfEmpty, Pending, Req, Res, ResEmpty} from './types';
|
|
6
|
+
import {TIMEOUT_REQ, METHODS} from './constants';
|
|
7
|
+
import {AQM_REQS_FILE} from '../../constants';
|
|
8
|
+
import WebexRequest from './WebexRequest';
|
|
9
|
+
import {WebSocketManager} from './websocket/WebSocketManager';
|
|
10
|
+
|
|
11
|
+
export default class AqmReqs {
|
|
12
|
+
private pendingRequests: Record<string, Pending> = {};
|
|
13
|
+
private pendingNotifCancelrequest: Record<string, Pending> = {};
|
|
14
|
+
private webexRequest: WebexRequest;
|
|
15
|
+
private webSocketManager: WebSocketManager;
|
|
16
|
+
|
|
17
|
+
constructor(webSocketManager: WebSocketManager) {
|
|
18
|
+
this.webexRequest = WebexRequest.getInstance();
|
|
19
|
+
this.webSocketManager = webSocketManager;
|
|
20
|
+
this.webSocketManager.on('message', this.onMessage.bind(this));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
req<TRes, TErr, TReq>(c: Conf<TRes, TErr, TReq>): Res<TRes, TReq> {
|
|
24
|
+
return (p: TReq, cbRes?: CbRes<TRes>) => this.makeAPIRequest(c(p), cbRes);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
reqEmpty<TRes, TErr>(c: ConfEmpty<TRes, TErr>): ResEmpty<TRes> {
|
|
28
|
+
return (cbRes?: CbRes<TRes>) => this.makeAPIRequest(c(), cbRes);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private async makeAPIRequest<TRes, TErr>(c: Req<TRes, TErr>, cbRes?: CbRes<TRes>): Promise<TRes> {
|
|
32
|
+
return this.createPromise(c, cbRes);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private createPromise<TRes, TErr>(c: Req<TRes, TErr>, cbRes?: CbRes<TRes>) {
|
|
36
|
+
return new Promise<TRes>((resolve, reject) => {
|
|
37
|
+
const keySuccess = this.bindPrint(c.notifSuccess.bind);
|
|
38
|
+
const keyFail = c.notifFail ? this.bindPrint(c.notifFail.bind) : null;
|
|
39
|
+
const keyCancel = c.notifCancel?.bind ? this.bindPrint(c.notifCancel.bind) : null;
|
|
40
|
+
let k = '';
|
|
41
|
+
if (this.pendingRequests[keySuccess]) {
|
|
42
|
+
k = keySuccess;
|
|
43
|
+
}
|
|
44
|
+
if (keyFail && this.pendingRequests[keyFail]) {
|
|
45
|
+
k += keyFail;
|
|
46
|
+
}
|
|
47
|
+
if (k && c.timeout !== 'disabled') {
|
|
48
|
+
reject(
|
|
49
|
+
new Err.Details('Service.aqm.reqs.Pending', {
|
|
50
|
+
key: k,
|
|
51
|
+
msg: 'The request has been already created, multiple requests are not allowed.',
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let isClear = false;
|
|
59
|
+
const clear = () => {
|
|
60
|
+
delete this.pendingRequests[keySuccess];
|
|
61
|
+
if (keyFail) {
|
|
62
|
+
delete this.pendingRequests[keyFail];
|
|
63
|
+
}
|
|
64
|
+
if (keyCancel) {
|
|
65
|
+
delete this.pendingNotifCancelrequest[keyCancel];
|
|
66
|
+
}
|
|
67
|
+
isClear = true;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
this.pendingRequests[keySuccess] = {
|
|
71
|
+
check: (msg: Msg) => this.bindCheck(c.notifSuccess.bind, msg),
|
|
72
|
+
handle: (msg: Msg) => {
|
|
73
|
+
clear();
|
|
74
|
+
resolve(msg as any);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
if (keyCancel) {
|
|
78
|
+
this.pendingRequests[keySuccess].alternateBind = keyCancel;
|
|
79
|
+
this.pendingNotifCancelrequest[keyCancel] = {
|
|
80
|
+
check: (msg: Msg) => this.bindCheck(c.notifCancel?.bind, msg),
|
|
81
|
+
handle: (msg: Msg) => {
|
|
82
|
+
const alternateBindKey = this.pendingNotifCancelrequest[keyCancel].alternateBind;
|
|
83
|
+
if (alternateBindKey) {
|
|
84
|
+
this.pendingRequests[alternateBindKey].handle(msg);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
alternateBind: keySuccess,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (keyFail) {
|
|
92
|
+
this.pendingRequests[keyFail] = {
|
|
93
|
+
check: (msg: Msg) => this.bindCheck(c.notifFail!.bind, msg),
|
|
94
|
+
handle: (msg: Msg) => {
|
|
95
|
+
clear();
|
|
96
|
+
const notifFail = c.notifFail!;
|
|
97
|
+
if ('errId' in notifFail) {
|
|
98
|
+
LoggerProxy.log(`Routing request failed: ${JSON.stringify(msg)}`, {
|
|
99
|
+
module: AQM_REQS_FILE,
|
|
100
|
+
method: METHODS.CREATE_PROMISE,
|
|
101
|
+
});
|
|
102
|
+
const eerr = new Err.Details(notifFail.errId, msg as any);
|
|
103
|
+
LoggerProxy.log(`Routing request failed: ${eerr}`, {
|
|
104
|
+
module: AQM_REQS_FILE,
|
|
105
|
+
method: METHODS.CREATE_PROMISE,
|
|
106
|
+
});
|
|
107
|
+
reject(eerr);
|
|
108
|
+
} else {
|
|
109
|
+
reject(notifFail.err(msg as any));
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
let response: WebexRequestPayload | null = null;
|
|
115
|
+
this.webexRequest
|
|
116
|
+
.request({
|
|
117
|
+
service: c.host ?? '',
|
|
118
|
+
resource: c.url,
|
|
119
|
+
// eslint-disable-next-line no-nested-ternary
|
|
120
|
+
method: c.method ? c.method : c.data ? HTTP_METHODS.POST : HTTP_METHODS.GET,
|
|
121
|
+
|
|
122
|
+
body: c.data,
|
|
123
|
+
})
|
|
124
|
+
.then((res: any) => {
|
|
125
|
+
response = res;
|
|
126
|
+
if (cbRes) {
|
|
127
|
+
cbRes(res);
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
.catch((error: WebexRequestPayload) => {
|
|
131
|
+
clear();
|
|
132
|
+
if (error?.headers) {
|
|
133
|
+
error.headers.Authorization = '*';
|
|
134
|
+
}
|
|
135
|
+
if (error?.headers) {
|
|
136
|
+
error.headers.Authorization = '*';
|
|
137
|
+
}
|
|
138
|
+
if (typeof c.err === 'function') {
|
|
139
|
+
reject(c.err(error));
|
|
140
|
+
} else if (typeof c.err === 'string') {
|
|
141
|
+
reject(new Err.Message(c.err));
|
|
142
|
+
} else {
|
|
143
|
+
reject(new Err.Message('Service.aqm.reqs.GenericRequestError'));
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (c.timeout !== 'disabled') {
|
|
148
|
+
window.setTimeout(
|
|
149
|
+
() => {
|
|
150
|
+
if (isClear) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
clear();
|
|
154
|
+
if (response?.headers) {
|
|
155
|
+
response.headers.Authorization = '*';
|
|
156
|
+
}
|
|
157
|
+
LoggerProxy.error(`Routing request timeout${keySuccess}${response!}${c.url}`, {
|
|
158
|
+
module: AQM_REQS_FILE,
|
|
159
|
+
method: METHODS.CREATE_PROMISE,
|
|
160
|
+
});
|
|
161
|
+
reject(
|
|
162
|
+
new Err.Details('Service.aqm.reqs.Timeout', {
|
|
163
|
+
key: keySuccess,
|
|
164
|
+
response: response!,
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
c.timeout && c.timeout > 0 ? c.timeout : TIMEOUT_REQ
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private bindPrint(bind: any) {
|
|
175
|
+
let result = '';
|
|
176
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
177
|
+
for (const k in bind) {
|
|
178
|
+
if (Array.isArray(bind[k])) {
|
|
179
|
+
result += `${k}=[${bind[k].join(',')}],`;
|
|
180
|
+
} else if (typeof bind[k] === 'object' && bind[k] !== null) {
|
|
181
|
+
result += `${k}=(${this.bindPrint(bind[k])}),`;
|
|
182
|
+
} else {
|
|
183
|
+
result += `${k}=${bind[k]},`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result ? result.slice(0, -1) : result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private bindCheck(bind: any, msg: any) {
|
|
191
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
192
|
+
for (const k in bind) {
|
|
193
|
+
if (Array.isArray(bind[k])) {
|
|
194
|
+
// Check if the message value matches any of the values in the array
|
|
195
|
+
if (!bind[k].includes(msg[k])) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
} else if (typeof bind[k] === 'object' && bind[k] !== null) {
|
|
199
|
+
if (typeof msg[k] === 'object' && msg[k] !== null) {
|
|
200
|
+
if (!this.bindCheck(bind[k], msg[k])) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
} else if (!msg[k] || msg[k] !== bind[k]) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// must be lambda
|
|
215
|
+
private readonly onMessage = (msg: any) => {
|
|
216
|
+
const event = JSON.parse(msg);
|
|
217
|
+
if (event.type === 'Welcome') {
|
|
218
|
+
LoggerProxy.info(`Welcome message from Notifs Websocket`, {
|
|
219
|
+
module: AQM_REQS_FILE,
|
|
220
|
+
method: METHODS.ON_MESSAGE,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (event.keepalive === 'true') {
|
|
227
|
+
LoggerProxy.info(`Keepalive from web socket`, {
|
|
228
|
+
module: AQM_REQS_FILE,
|
|
229
|
+
method: METHODS.ON_MESSAGE,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (event.type === 'AgentReloginFailed') {
|
|
236
|
+
LoggerProxy.info('Silently handling the agent relogin fail', {
|
|
237
|
+
module: AQM_REQS_FILE,
|
|
238
|
+
method: METHODS.ON_MESSAGE,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let isHandled = false;
|
|
243
|
+
|
|
244
|
+
const kReq = Object.keys(this.pendingRequests);
|
|
245
|
+
for (const thisReq of kReq) {
|
|
246
|
+
const req = this.pendingRequests[thisReq];
|
|
247
|
+
if (req.check(event)) {
|
|
248
|
+
req.handle(event);
|
|
249
|
+
isHandled = true;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// pendingNotifCancelrequest stores the secondary bind key, checks for the secondary bind key and handles the event
|
|
254
|
+
const kReqAlt = Object.keys(this.pendingNotifCancelrequest);
|
|
255
|
+
for (const thisReq of kReqAlt) {
|
|
256
|
+
const req = this.pendingNotifCancelrequest[thisReq];
|
|
257
|
+
if (req.check(event)) {
|
|
258
|
+
req.handle(event);
|
|
259
|
+
isHandled = true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// TODO: add event emitter for unhandled events to replicate event.listen or .on
|
|
264
|
+
|
|
265
|
+
if (!isHandled) {
|
|
266
|
+
LoggerProxy.info(`event=missingEventHandler | [AqmReqs] missing routing message handler`, {
|
|
267
|
+
module: AQM_REQS_FILE,
|
|
268
|
+
method: METHODS.ON_MESSAGE,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interval in milliseconds for sending keepalive pings to the worker.
|
|
3
|
+
* @ignore
|
|
4
|
+
*/
|
|
5
|
+
export const KEEPALIVE_WORKER_INTERVAL = 4000;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Delay in milliseconds before resolving notification handlers.
|
|
9
|
+
* @ignore
|
|
10
|
+
*/
|
|
11
|
+
export const NOTIFS_RESOLVE_DELAY = 1200;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Timeout duration in milliseconds before forcefully closing a WebSocket connection.
|
|
15
|
+
* @ignore
|
|
16
|
+
*/
|
|
17
|
+
export const CLOSE_SOCKET_TIMEOUT_DURATION = 16000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* API endpoint used for connectivity or health checks.
|
|
21
|
+
* @ignore
|
|
22
|
+
*/
|
|
23
|
+
export const PING_API_URL = '/health';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Timeout in milliseconds to wait for a welcome message after socket connection.
|
|
27
|
+
* @ignore
|
|
28
|
+
*/
|
|
29
|
+
export const WELCOME_TIMEOUT = 30000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Event name used for real-time device (RTD) ping status.
|
|
33
|
+
* @ignore
|
|
34
|
+
*/
|
|
35
|
+
export const RTD_PING_EVENT = 'rtd-online-status';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Timeout in milliseconds for individual HTTP requests.
|
|
39
|
+
* @ignore
|
|
40
|
+
*/
|
|
41
|
+
export const TIMEOUT_REQ = 20000;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Duration in milliseconds to wait before attempting lost connection recovery.
|
|
45
|
+
* @ignore
|
|
46
|
+
*/
|
|
47
|
+
export const LOST_CONNECTION_RECOVERY_TIMEOUT = 50000;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Duration in milliseconds after which a WebSocket disconnect is considered allowed or expected.
|
|
51
|
+
* @ignore
|
|
52
|
+
*/
|
|
53
|
+
export const WS_DISCONNECT_ALLOWED = 8000;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Interval in milliseconds to check for connectivity status.
|
|
57
|
+
* @ignore
|
|
58
|
+
*/
|
|
59
|
+
export const CONNECTIVITY_CHECK_INTERVAL = 5000;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Timeout in milliseconds for cleanly closing the WebSocket.
|
|
63
|
+
* @ignore
|
|
64
|
+
*/
|
|
65
|
+
export const CLOSE_SOCKET_TIMEOUT = 16000;
|
|
66
|
+
|
|
67
|
+
// Method names for core services
|
|
68
|
+
export const METHODS = {
|
|
69
|
+
// WebexRequest methods
|
|
70
|
+
REQUEST: 'request',
|
|
71
|
+
UPLOAD_LOGS: 'uploadLogs',
|
|
72
|
+
|
|
73
|
+
// Utils methods
|
|
74
|
+
GET_ERROR_DETAILS: 'getErrorDetails',
|
|
75
|
+
GET_COMMON_ERROR_DETAILS: 'getCommonErrorDetails',
|
|
76
|
+
CREATE_ERR_DETAILS_OBJECT: 'createErrDetailsObject',
|
|
77
|
+
|
|
78
|
+
// AqmReqs methods
|
|
79
|
+
REQ: 'req',
|
|
80
|
+
REQ_EMPTY: 'reqEmpty',
|
|
81
|
+
MAKE_API_REQUEST: 'makeAPIRequest',
|
|
82
|
+
CREATE_PROMISE: 'createPromise',
|
|
83
|
+
BIND_PRINT: 'bindPrint',
|
|
84
|
+
BIND_CHECK: 'bindCheck',
|
|
85
|
+
ON_MESSAGE: 'onMessage',
|
|
86
|
+
|
|
87
|
+
// WebSocketManager methods
|
|
88
|
+
INIT_WEB_SOCKET: 'initWebSocket',
|
|
89
|
+
CLOSE: 'close',
|
|
90
|
+
HANDLE_CONNECTION_LOST: 'handleConnectionLost',
|
|
91
|
+
REGISTER: 'register',
|
|
92
|
+
CONNECT: 'connect',
|
|
93
|
+
WEB_SOCKET_ON_CLOSE_HANDLER: 'webSocketOnCloseHandler',
|
|
94
|
+
|
|
95
|
+
// ConnectionService methods
|
|
96
|
+
SETUP_EVENT_LISTENERS: 'setupEventListeners',
|
|
97
|
+
DISPATCH_CONNECTION_EVENT: 'dispatchConnectionEvent',
|
|
98
|
+
CS_HANDLE_CONNECTION_LOST: 'handleConnectionLost',
|
|
99
|
+
CLEAR_TIMER_ON_RESTORE_FAILED: 'clearTimerOnRestoreFailed',
|
|
100
|
+
HANDLE_RESTORE_FAILED: 'handleRestoreFailed',
|
|
101
|
+
UPDATE_CONNECTION_DATA: 'updateConnectionData',
|
|
102
|
+
SET_CONNECTION_PROP: 'setConnectionProp',
|
|
103
|
+
ON_PING: 'onPing',
|
|
104
|
+
HANDLE_SOCKET_CLOSE: 'handleSocketClose',
|
|
105
|
+
ON_SOCKET_CLOSE: 'onSocketClose',
|
|
106
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {HTTP_METHODS, RequestBody, WebexRequestPayload} from '../../types';
|
|
2
|
+
import * as Err from './Err';
|
|
3
|
+
import {Msg} from './GlobalTypes';
|
|
4
|
+
|
|
5
|
+
export type Pending = {
|
|
6
|
+
check: (msg: Msg) => boolean;
|
|
7
|
+
handle: (msg: Msg) => void;
|
|
8
|
+
alternateBind?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type BindType = string | string[] | {[key: string]: BindType};
|
|
12
|
+
interface Bind {
|
|
13
|
+
type: BindType;
|
|
14
|
+
data?: any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type Timeout = number | 'disabled';
|
|
18
|
+
|
|
19
|
+
export type Req<TRes, TErr> = {
|
|
20
|
+
url: string;
|
|
21
|
+
host?: string;
|
|
22
|
+
method?: HTTP_METHODS;
|
|
23
|
+
err?:
|
|
24
|
+
| ((errObj: WebexRequestPayload) => Err.Details<'Service.reqs.generic.failure'>)
|
|
25
|
+
| Err.IdsMessage
|
|
26
|
+
| ((e: WebexRequestPayload) => Err.Message | Err.Details<Err.IdsDetails>);
|
|
27
|
+
notifSuccess: {bind: Bind; msg: TRes};
|
|
28
|
+
notifFail?:
|
|
29
|
+
| {
|
|
30
|
+
bind: Bind;
|
|
31
|
+
errMsg: TErr;
|
|
32
|
+
err: (e: TErr) => Err.Details<Err.IdsDetails>;
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
bind: Bind;
|
|
36
|
+
errId: Err.IdsDetails;
|
|
37
|
+
};
|
|
38
|
+
data?: RequestBody;
|
|
39
|
+
headers?: Record<string, string>;
|
|
40
|
+
timeout?: Timeout;
|
|
41
|
+
notifCancel?: {bind: Bind; msg: TRes};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type Conf<TRes, TErr, TReq> = (p: TReq) => Req<TRes, TErr>;
|
|
45
|
+
export type ConfEmpty<TRes, TErr> = () => Req<TRes, TErr>;
|
|
46
|
+
export type Res<TRes, TReq> = (p: TReq, cbRes?: CbRes<TRes>) => Promise<TRes>;
|
|
47
|
+
export type ResEmpty<TRes> = (cbRes?: CbRes<TRes>) => Promise<TRes>;
|
|
48
|
+
export type CbRes<TRes> = (res: any) => void | TRes;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import EventEmitter from 'events';
|
|
2
|
+
import {WebexSDK, SubscribeRequest, HTTP_METHODS} from '../../../types';
|
|
3
|
+
import {SUBSCRIBE_API, WCC_API_GATEWAY} from '../../constants';
|
|
4
|
+
import {ConnectionLostDetails} from './types';
|
|
5
|
+
import {CC_EVENTS, SubscribeResponse, WelcomeResponse} from '../../config/types';
|
|
6
|
+
import LoggerProxy from '../../../logger-proxy';
|
|
7
|
+
import workerScript from './keepalive.worker';
|
|
8
|
+
import {KEEPALIVE_WORKER_INTERVAL, CLOSE_SOCKET_TIMEOUT, METHODS} from '../constants';
|
|
9
|
+
import {WEB_SOCKET_MANAGER_FILE} from '../../../constants';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* WebSocketManager handles the WebSocket connection for Contact Center operations.
|
|
13
|
+
* It manages the connection lifecycle, including registration, reconnection, and message handling.
|
|
14
|
+
* It also utilizes a Web Worker to manage keepalive messages and socket closure.
|
|
15
|
+
* @ignore
|
|
16
|
+
*/
|
|
17
|
+
export class WebSocketManager extends EventEmitter {
|
|
18
|
+
private websocket: WebSocket;
|
|
19
|
+
shouldReconnect: boolean;
|
|
20
|
+
isSocketClosed: boolean;
|
|
21
|
+
private isWelcomeReceived: boolean;
|
|
22
|
+
private url: string | null = null;
|
|
23
|
+
private forceCloseWebSocketOnTimeout: boolean;
|
|
24
|
+
private isConnectionLost: boolean;
|
|
25
|
+
private webex: WebexSDK;
|
|
26
|
+
private welcomePromiseResolve:
|
|
27
|
+
| ((value: WelcomeResponse | PromiseLike<WelcomeResponse>) => void)
|
|
28
|
+
| null = null;
|
|
29
|
+
|
|
30
|
+
private keepaliveWorker: Worker;
|
|
31
|
+
|
|
32
|
+
constructor(options: {webex: WebexSDK}) {
|
|
33
|
+
super();
|
|
34
|
+
const {webex} = options;
|
|
35
|
+
this.webex = webex;
|
|
36
|
+
this.shouldReconnect = true;
|
|
37
|
+
this.websocket = {} as WebSocket;
|
|
38
|
+
this.isSocketClosed = false;
|
|
39
|
+
this.isWelcomeReceived = false;
|
|
40
|
+
this.forceCloseWebSocketOnTimeout = false;
|
|
41
|
+
this.isConnectionLost = false;
|
|
42
|
+
|
|
43
|
+
const workerScriptBlob = new Blob([workerScript], {type: 'application/javascript'});
|
|
44
|
+
this.keepaliveWorker = new Worker(URL.createObjectURL(workerScriptBlob));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async initWebSocket(options: {body: SubscribeRequest}): Promise<WelcomeResponse> {
|
|
48
|
+
const connectionConfig = options.body;
|
|
49
|
+
await this.register(connectionConfig);
|
|
50
|
+
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
this.welcomePromiseResolve = resolve;
|
|
53
|
+
this.connect().catch((error) => {
|
|
54
|
+
LoggerProxy.error(`[WebSocketStatus] | Error in connecting Websocket ${error}`, {
|
|
55
|
+
module: WEB_SOCKET_MANAGER_FILE,
|
|
56
|
+
method: METHODS.INIT_WEB_SOCKET,
|
|
57
|
+
});
|
|
58
|
+
reject(error);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
close(shouldReconnect: boolean, reason = 'Unknown') {
|
|
64
|
+
if (!this.isSocketClosed && this.shouldReconnect) {
|
|
65
|
+
this.shouldReconnect = shouldReconnect;
|
|
66
|
+
this.websocket.close();
|
|
67
|
+
this.keepaliveWorker.postMessage({type: 'terminate'});
|
|
68
|
+
LoggerProxy.log(
|
|
69
|
+
`[WebSocketStatus] | event=webSocketClose | WebSocket connection closed manually REASON: ${reason}`,
|
|
70
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: METHODS.CLOSE}
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
handleConnectionLost(event: ConnectionLostDetails) {
|
|
76
|
+
this.isConnectionLost = event.isConnectionLost;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async register(connectionConfig: SubscribeRequest) {
|
|
80
|
+
try {
|
|
81
|
+
const subscribeResponse: SubscribeResponse = await this.webex.request({
|
|
82
|
+
service: WCC_API_GATEWAY,
|
|
83
|
+
resource: SUBSCRIBE_API,
|
|
84
|
+
method: HTTP_METHODS.POST,
|
|
85
|
+
body: connectionConfig,
|
|
86
|
+
});
|
|
87
|
+
this.url = subscribeResponse.body.webSocketUrl;
|
|
88
|
+
} catch (e) {
|
|
89
|
+
LoggerProxy.error(
|
|
90
|
+
`Register API Failed, Request to RoutingNotifs websocket registration API failed ${e}`,
|
|
91
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: METHODS.REGISTER}
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async connect() {
|
|
97
|
+
if (!this.url) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
LoggerProxy.log(
|
|
101
|
+
`[WebSocketStatus] | event=webSocketConnecting | Connecting to WebSocket: ${this.url}`,
|
|
102
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: METHODS.CONNECT}
|
|
103
|
+
);
|
|
104
|
+
this.websocket = new WebSocket(this.url);
|
|
105
|
+
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
this.websocket.onopen = () => {
|
|
108
|
+
this.isSocketClosed = false;
|
|
109
|
+
this.shouldReconnect = true;
|
|
110
|
+
|
|
111
|
+
this.websocket.send(JSON.stringify({keepalive: 'true'}));
|
|
112
|
+
this.keepaliveWorker.onmessage = (keepAliveEvent: {data: any}) => {
|
|
113
|
+
if (keepAliveEvent?.data?.type === 'keepalive') {
|
|
114
|
+
this.websocket.send(JSON.stringify({keepalive: 'true'}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (keepAliveEvent?.data?.type === 'closeSocket' && this.isConnectionLost) {
|
|
118
|
+
this.forceCloseWebSocketOnTimeout = true;
|
|
119
|
+
this.close(true, 'WebSocket did not auto close within 16 secs');
|
|
120
|
+
LoggerProxy.error(
|
|
121
|
+
'[webSocketTimeout] | event=webSocketTimeout | WebSocket connection closed forcefully',
|
|
122
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: METHODS.CONNECT}
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
this.keepaliveWorker.postMessage({
|
|
128
|
+
type: 'start',
|
|
129
|
+
intervalDuration: KEEPALIVE_WORKER_INTERVAL, // Keepalive interval
|
|
130
|
+
isSocketClosed: this.isSocketClosed,
|
|
131
|
+
closeSocketTimeout: CLOSE_SOCKET_TIMEOUT, // Close socket timeout
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
this.websocket.onerror = (event: any) => {
|
|
136
|
+
LoggerProxy.error(
|
|
137
|
+
`[WebSocketStatus] | event=socketConnectionFailed | WebSocket connection failed ${event}`,
|
|
138
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: METHODS.CONNECT}
|
|
139
|
+
);
|
|
140
|
+
reject();
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
this.websocket.onclose = async (event: any) => {
|
|
144
|
+
this.webSocketOnCloseHandler(event);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
this.websocket.onmessage = (e: MessageEvent) => {
|
|
148
|
+
this.emit('message', e.data);
|
|
149
|
+
const eventData = JSON.parse(e.data);
|
|
150
|
+
|
|
151
|
+
if (eventData.type === CC_EVENTS.WELCOME) {
|
|
152
|
+
this.isWelcomeReceived = true;
|
|
153
|
+
if (this.welcomePromiseResolve) {
|
|
154
|
+
this.welcomePromiseResolve(eventData.data as WelcomeResponse);
|
|
155
|
+
this.welcomePromiseResolve = null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (eventData.type === 'AGENT_MULTI_LOGIN') {
|
|
160
|
+
this.close(false, 'multiLogin');
|
|
161
|
+
LoggerProxy.error(
|
|
162
|
+
'[WebSocketStatus] | event=agentMultiLogin | WebSocket connection closed by agent multiLogin',
|
|
163
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: METHODS.CONNECT}
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
171
|
+
private async webSocketOnCloseHandler(event: any) {
|
|
172
|
+
this.isSocketClosed = true;
|
|
173
|
+
this.keepaliveWorker.postMessage({type: 'terminate'});
|
|
174
|
+
if (this.shouldReconnect) {
|
|
175
|
+
this.emit('socketClose');
|
|
176
|
+
let issueReason;
|
|
177
|
+
if (this.forceCloseWebSocketOnTimeout) {
|
|
178
|
+
issueReason = 'WebSocket auto close timed out. Forcefully closed websocket.';
|
|
179
|
+
} else {
|
|
180
|
+
const onlineStatus = navigator.onLine;
|
|
181
|
+
LoggerProxy.info(`[WebSocketStatus] | desktop online status is ${onlineStatus}`, {
|
|
182
|
+
module: WEB_SOCKET_MANAGER_FILE,
|
|
183
|
+
method: METHODS.WEB_SOCKET_ON_CLOSE_HANDLER,
|
|
184
|
+
});
|
|
185
|
+
issueReason = !onlineStatus
|
|
186
|
+
? 'network issue'
|
|
187
|
+
: 'missing keepalive from either desktop or notif service';
|
|
188
|
+
}
|
|
189
|
+
LoggerProxy.error(
|
|
190
|
+
`[WebSocketStatus] | event=webSocketClose | WebSocket connection closed REASON: ${issueReason}`,
|
|
191
|
+
{module: WEB_SOCKET_MANAGER_FILE, method: METHODS.WEB_SOCKET_ON_CLOSE_HANDLER}
|
|
192
|
+
);
|
|
193
|
+
this.forceCloseWebSocketOnTimeout = false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|