@webex/internal-plugin-mercury 3.11.0 → 3.12.0-llmrefactor.2
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 +54 -0
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/mercury-plugin.js +125 -0
- package/dist/mercury-plugin.js.map +1 -0
- package/dist/mercury.js +76 -33
- package/dist/mercury.js.map +1 -1
- package/dist/socket/constants.js +16 -0
- package/dist/socket/constants.js.map +1 -0
- package/dist/socket/socket-base.js +36 -3
- package/dist/socket/socket-base.js.map +1 -1
- package/package.json +17 -17
- package/src/index.js +3 -2
- package/src/mercury-plugin.ts +79 -0
- package/src/mercury.js +105 -49
- package/src/socket/constants.js +6 -0
- package/src/socket/socket-base.js +40 -3
- package/test/unit/spec/mercury-events.js +26 -2
- package/test/unit/spec/mercury-plugin.js +280 -0
- package/test/unit/spec/mercury.js +374 -136
- package/test/unit/spec/socket.js +61 -0
package/package.json
CHANGED
|
@@ -29,27 +29,27 @@
|
|
|
29
29
|
"@webex/eslint-config-legacy": "0.0.0",
|
|
30
30
|
"@webex/jest-config-legacy": "0.0.0",
|
|
31
31
|
"@webex/legacy-tools": "0.0.0",
|
|
32
|
-
"@webex/test-helper-chai": "3.
|
|
33
|
-
"@webex/test-helper-mocha": "3.
|
|
34
|
-
"@webex/test-helper-mock-webex": "3.
|
|
35
|
-
"@webex/test-helper-test-users": "3.
|
|
32
|
+
"@webex/test-helper-chai": "3.12.0-llmrefactor.1",
|
|
33
|
+
"@webex/test-helper-mocha": "3.12.0-llmrefactor.1",
|
|
34
|
+
"@webex/test-helper-mock-webex": "3.12.0-llmrefactor.0",
|
|
35
|
+
"@webex/test-helper-test-users": "3.12.0-llmrefactor.0",
|
|
36
36
|
"eslint": "^8.24.0",
|
|
37
37
|
"prettier": "^2.7.1",
|
|
38
38
|
"sinon": "^9.2.4"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@webex/common": "3.
|
|
42
|
-
"@webex/common-timers": "3.
|
|
43
|
-
"@webex/internal-plugin-device": "3.
|
|
44
|
-
"@webex/internal-plugin-feature": "3.
|
|
45
|
-
"@webex/internal-plugin-metrics": "3.
|
|
46
|
-
"@webex/test-helper-chai": "3.
|
|
47
|
-
"@webex/test-helper-mocha": "3.
|
|
48
|
-
"@webex/test-helper-mock-web-socket": "3.
|
|
49
|
-
"@webex/test-helper-mock-webex": "3.
|
|
50
|
-
"@webex/test-helper-refresh-callback": "3.
|
|
51
|
-
"@webex/test-helper-test-users": "3.
|
|
52
|
-
"@webex/webex-core": "3.
|
|
41
|
+
"@webex/common": "3.12.0-llmrefactor.1",
|
|
42
|
+
"@webex/common-timers": "3.12.0-llmrefactor.1",
|
|
43
|
+
"@webex/internal-plugin-device": "3.12.0-llmrefactor.1",
|
|
44
|
+
"@webex/internal-plugin-feature": "3.12.0-llmrefactor.1",
|
|
45
|
+
"@webex/internal-plugin-metrics": "3.12.0-llmrefactor.1",
|
|
46
|
+
"@webex/test-helper-chai": "3.12.0-llmrefactor.1",
|
|
47
|
+
"@webex/test-helper-mocha": "3.12.0-llmrefactor.1",
|
|
48
|
+
"@webex/test-helper-mock-web-socket": "3.12.0-llmrefactor.1",
|
|
49
|
+
"@webex/test-helper-mock-webex": "3.12.0-llmrefactor.0",
|
|
50
|
+
"@webex/test-helper-refresh-callback": "3.12.0-llmrefactor.0",
|
|
51
|
+
"@webex/test-helper-test-users": "3.12.0-llmrefactor.0",
|
|
52
|
+
"@webex/webex-core": "3.12.0-llmrefactor.1",
|
|
53
53
|
"backoff": "^2.5.0",
|
|
54
54
|
"lodash": "^4.17.21",
|
|
55
55
|
"uuid": "^3.3.2",
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"test:style": "eslint ./src/**/*.*",
|
|
65
65
|
"test:unit": "webex-legacy-tools test --unit --runner mocha"
|
|
66
66
|
},
|
|
67
|
-
"version": "3.
|
|
67
|
+
"version": "3.12.0-llmrefactor.2"
|
|
68
68
|
}
|
package/src/index.js
CHANGED
|
@@ -8,10 +8,10 @@ import '@webex/internal-plugin-metrics';
|
|
|
8
8
|
|
|
9
9
|
import {registerInternalPlugin} from '@webex/webex-core';
|
|
10
10
|
|
|
11
|
-
import
|
|
11
|
+
import MercuryPlugin from './mercury-plugin';
|
|
12
12
|
import config from './config';
|
|
13
13
|
|
|
14
|
-
registerInternalPlugin('mercury',
|
|
14
|
+
registerInternalPlugin('mercury', MercuryPlugin, {
|
|
15
15
|
config,
|
|
16
16
|
onBeforeLogout() {
|
|
17
17
|
return this.logout();
|
|
@@ -20,6 +20,7 @@ registerInternalPlugin('mercury', Mercury, {
|
|
|
20
20
|
|
|
21
21
|
export {default} from './mercury';
|
|
22
22
|
export {default as Mercury} from './mercury';
|
|
23
|
+
export {MercuryPlugin} from './mercury-plugin';
|
|
23
24
|
export {default as Socket} from './socket';
|
|
24
25
|
export {default as config} from './config';
|
|
25
26
|
export {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/* eslint-disable require-jsdoc */
|
|
2
|
+
/*!
|
|
3
|
+
* Copyright (c) 2015-2024 Cisco Systems, Inc. See LICENSE file.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// @ts-ignore
|
|
7
|
+
import {WebexPlugin} from '@webex/webex-core';
|
|
8
|
+
|
|
9
|
+
import config from './config';
|
|
10
|
+
|
|
11
|
+
// since mercury-plugin.ts is a .ts file, the TS language server tries to parse mercury.js and chokes on the @deprecated decorator.
|
|
12
|
+
// using a require() call instead of import to avoid TS parsing the file:
|
|
13
|
+
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
15
|
+
const Mercury = require('./mercury').default;
|
|
16
|
+
|
|
17
|
+
export class MercuryPlugin extends (WebexPlugin as any) {
|
|
18
|
+
namespace = 'Mercury';
|
|
19
|
+
|
|
20
|
+
private _mercury: any;
|
|
21
|
+
|
|
22
|
+
constructor(...args: any[]) {
|
|
23
|
+
super(...args);
|
|
24
|
+
this._mercury = new (Mercury as any)({parent: (this as any).webex});
|
|
25
|
+
|
|
26
|
+
// Re-emit every event from Mercury on this plugin so listeners attached to
|
|
27
|
+
// `this.webex.internal.mercury` continue to work.
|
|
28
|
+
this._mercury.on('all', (eventName: string, ...rest: any[]) => {
|
|
29
|
+
(this as any).trigger(eventName, ...rest);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** True when the primary Mercury socket is connected. */
|
|
34
|
+
get connected(): boolean {
|
|
35
|
+
return this._mercury?.connected ?? false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The primary socket, if connected. */
|
|
39
|
+
get socket(): any {
|
|
40
|
+
return this._mercury?.socket;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Cluster service URLs received from registration status event. */
|
|
44
|
+
get localClusterServiceUrls(): any {
|
|
45
|
+
return this._mercury?.localClusterServiceUrls;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getLastError(): any {
|
|
49
|
+
return this._mercury?.getLastError();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get hasEverConnected(): boolean {
|
|
53
|
+
return this._mercury?.hasEverConnected ?? false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
connect(webSocketUrl?: string): Promise<void> {
|
|
57
|
+
return this._mercury.connect(webSocketUrl);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
disconnect(options?: any): Promise<void> {
|
|
61
|
+
return this._mercury.disconnect(options);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
logout(): Promise<void> {
|
|
65
|
+
const normalReconnectReasons = ['idle', 'done (forced)', 'pong not received', 'pong mismatch'];
|
|
66
|
+
const reason = this.config.beforeLogoutOptionsCloseReason;
|
|
67
|
+
const options =
|
|
68
|
+
reason && !normalReconnectReasons.includes(reason) ? {code: 3050, reason} : undefined;
|
|
69
|
+
|
|
70
|
+
return this._mercury.disconnect(options);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
processRegistrationStatusEvent(message: any): void {
|
|
74
|
+
return this._mercury.processRegistrationStatusEvent(message);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export {config};
|
|
79
|
+
export default MercuryPlugin;
|
package/src/mercury.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import url from 'url';
|
|
7
7
|
|
|
8
8
|
import {WebexPlugin} from '@webex/webex-core';
|
|
9
|
-
import {deprecated
|
|
9
|
+
import {deprecated} from '@webex/common';
|
|
10
10
|
import {camelCase, get, set} from 'lodash';
|
|
11
11
|
import backoff from 'backoff';
|
|
12
12
|
|
|
@@ -45,6 +45,13 @@ const Mercury = WebexPlugin.extend({
|
|
|
45
45
|
default: undefined,
|
|
46
46
|
type: 'number',
|
|
47
47
|
},
|
|
48
|
+
backoffCall: 'object',
|
|
49
|
+
_connectPromise: 'object',
|
|
50
|
+
_shutdownSwitchoverBackoffCall: 'object',
|
|
51
|
+
_shutdownSwitchoverId: 'string',
|
|
52
|
+
// Store the resolved (pre-proxy) URL so reconnection uses a catalog-valid host
|
|
53
|
+
// instead of the potentially rewritten socket.url from an interceptor.
|
|
54
|
+
resolvedWebSocketUrl: 'string',
|
|
48
55
|
},
|
|
49
56
|
|
|
50
57
|
derived: {
|
|
@@ -116,13 +123,14 @@ const Mercury = WebexPlugin.extend({
|
|
|
116
123
|
* @returns {void}
|
|
117
124
|
*/
|
|
118
125
|
_handleImminentShutdown() {
|
|
126
|
+
const oldSocket = this.socket;
|
|
127
|
+
|
|
119
128
|
try {
|
|
120
|
-
if (this.
|
|
129
|
+
if (this._shutdownSwitchoverBackoffCall) {
|
|
121
130
|
this.logger.info(`${this.namespace}: [shutdown] switchover already in progress`);
|
|
122
131
|
|
|
123
132
|
return;
|
|
124
133
|
}
|
|
125
|
-
this._shutdownSwitchoverInProgress = true;
|
|
126
134
|
this._shutdownSwitchoverId = `${Date.now()}`;
|
|
127
135
|
this.logger.info(
|
|
128
136
|
`${this.namespace}: [shutdown] switchover start, id=${this._shutdownSwitchoverId}`
|
|
@@ -137,7 +145,6 @@ const Mercury = WebexPlugin.extend({
|
|
|
137
145
|
`${this.namespace}: [shutdown] switchover connected, url: ${webSocketUrl}`
|
|
138
146
|
);
|
|
139
147
|
|
|
140
|
-
const oldSocket = this.socket;
|
|
141
148
|
// Atomically switch active socket reference
|
|
142
149
|
this.socket = newSocket;
|
|
143
150
|
this.connected = true; // remain connected throughout
|
|
@@ -165,7 +172,7 @@ const Mercury = WebexPlugin.extend({
|
|
|
165
172
|
});
|
|
166
173
|
} catch (e) {
|
|
167
174
|
this.logger.error(`${this.namespace}: [shutdown] error during switchover`, e);
|
|
168
|
-
this.
|
|
175
|
+
this._shutdownSwitchoverBackoffCall = undefined;
|
|
169
176
|
this._emit('event:mercury_shutdown_switchover_failed', {reason: e});
|
|
170
177
|
}
|
|
171
178
|
},
|
|
@@ -178,7 +185,14 @@ const Mercury = WebexPlugin.extend({
|
|
|
178
185
|
return this.lastError;
|
|
179
186
|
},
|
|
180
187
|
|
|
181
|
-
|
|
188
|
+
/**
|
|
189
|
+
* Get the underlying WebSocket.
|
|
190
|
+
* @returns {any} The socket instance, or undefined if not connected.
|
|
191
|
+
*/
|
|
192
|
+
getSocket() {
|
|
193
|
+
return this.socket;
|
|
194
|
+
},
|
|
195
|
+
|
|
182
196
|
connect(webSocketUrl) {
|
|
183
197
|
if (this.connected) {
|
|
184
198
|
this.logger.info(`${this.namespace}: already connected, will not connect again`);
|
|
@@ -186,40 +200,41 @@ const Mercury = WebexPlugin.extend({
|
|
|
186
200
|
return Promise.resolve();
|
|
187
201
|
}
|
|
188
202
|
|
|
203
|
+
// If already connecting, return the existing promise so callers can await it
|
|
204
|
+
if (this._connectPromise) {
|
|
205
|
+
this.logger.info(
|
|
206
|
+
`${this.namespace}: connection already in progress, returning existing promise`
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return this._connectPromise;
|
|
210
|
+
}
|
|
211
|
+
|
|
189
212
|
this.connecting = true;
|
|
190
213
|
|
|
191
214
|
this.logger.info(`${this.namespace}: starting connection attempt`);
|
|
192
|
-
this.logger.info(
|
|
193
|
-
`${this.namespace}: debug_mercury_logging stack: `,
|
|
194
|
-
new Error('debug_mercury_logging').stack
|
|
195
|
-
);
|
|
196
215
|
|
|
197
|
-
|
|
216
|
+
this._connectPromise = Promise.resolve(
|
|
198
217
|
this.webex.internal.device.registered || this.webex.internal.device.register()
|
|
199
|
-
)
|
|
200
|
-
|
|
218
|
+
)
|
|
219
|
+
.then(() => {
|
|
220
|
+
this.logger.info(`${this.namespace}: connecting`);
|
|
201
221
|
|
|
202
|
-
|
|
203
|
-
|
|
222
|
+
return this._connectWithBackoff(webSocketUrl);
|
|
223
|
+
})
|
|
224
|
+
.finally(() => {
|
|
225
|
+
this._connectPromise = null;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return this._connectPromise;
|
|
204
229
|
},
|
|
205
230
|
|
|
206
|
-
|
|
207
|
-
this.logger.info(`${this.namespace}: logout() called`);
|
|
231
|
+
disconnect(options) {
|
|
208
232
|
this.logger.info(
|
|
209
|
-
`${this.namespace}:
|
|
210
|
-
|
|
233
|
+
`${this.namespace}#disconnect: connecting state: ${this.connecting}, connected state: ${
|
|
234
|
+
this.connected
|
|
235
|
+
}, socket exists: ${!!this.socket}, options: ${JSON.stringify(options)}`
|
|
211
236
|
);
|
|
212
237
|
|
|
213
|
-
return this.disconnect(
|
|
214
|
-
this.config.beforeLogoutOptionsCloseReason &&
|
|
215
|
-
!normalReconnectReasons.includes(this.config.beforeLogoutOptionsCloseReason)
|
|
216
|
-
? {code: 3050, reason: this.config.beforeLogoutOptionsCloseReason}
|
|
217
|
-
: undefined
|
|
218
|
-
);
|
|
219
|
-
},
|
|
220
|
-
|
|
221
|
-
@oneFlight
|
|
222
|
-
disconnect(options) {
|
|
223
238
|
return new Promise((resolve) => {
|
|
224
239
|
if (this.backoffCall) {
|
|
225
240
|
this.logger.info(`${this.namespace}: aborting connection`);
|
|
@@ -234,7 +249,9 @@ const Mercury = WebexPlugin.extend({
|
|
|
234
249
|
if (this.socket) {
|
|
235
250
|
this.socket.removeAllListeners('message');
|
|
236
251
|
this.once('offline', resolve);
|
|
237
|
-
|
|
252
|
+
this.socket.close(options || undefined);
|
|
253
|
+
|
|
254
|
+
return;
|
|
238
255
|
}
|
|
239
256
|
|
|
240
257
|
resolve();
|
|
@@ -330,6 +347,8 @@ const Mercury = WebexPlugin.extend({
|
|
|
330
347
|
|
|
331
348
|
webSocketUrl.query.clientTimestamp = Date.now();
|
|
332
349
|
|
|
350
|
+
delete webSocketUrl.search;
|
|
351
|
+
|
|
333
352
|
return url.format(webSocketUrl);
|
|
334
353
|
});
|
|
335
354
|
},
|
|
@@ -364,6 +383,7 @@ const Mercury = WebexPlugin.extend({
|
|
|
364
383
|
// Call the callback with the error before rejecting
|
|
365
384
|
callback(err);
|
|
366
385
|
|
|
386
|
+
// eslint-disable-next-line no-unreachable
|
|
367
387
|
return Promise.reject(err);
|
|
368
388
|
}
|
|
369
389
|
|
|
@@ -459,12 +479,24 @@ const Mercury = WebexPlugin.extend({
|
|
|
459
479
|
return this.webex.internal.feature
|
|
460
480
|
.getFeature('developer', 'web-high-availability')
|
|
461
481
|
.then((haMessagingEnabled) => {
|
|
482
|
+
const wsUrl = newWSUrl || reason.webSocketUrl;
|
|
483
|
+
|
|
462
484
|
if (haMessagingEnabled) {
|
|
463
485
|
this.logger.info(
|
|
464
|
-
`${this.namespace}: received a generic connection error, will try to connect to another datacenter. failed, action: 'failed', url: ${
|
|
486
|
+
`${this.namespace}: received a generic connection error, will try to connect to another datacenter. failed, action: 'failed', url: ${wsUrl} error: ${reason.message}`
|
|
465
487
|
);
|
|
466
488
|
|
|
467
|
-
|
|
489
|
+
if (wsUrl) {
|
|
490
|
+
this.logger.info(
|
|
491
|
+
`${this.namespace}: marking ${wsUrl} as failed due to connection error`
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
return this.webex.internal.services.markFailedUrl(wsUrl);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
this.logger.info(
|
|
498
|
+
`${this.namespace}: no socket url available to mark as failed due to connection error`
|
|
499
|
+
);
|
|
468
500
|
}
|
|
469
501
|
|
|
470
502
|
return null;
|
|
@@ -505,7 +537,21 @@ const Mercury = WebexPlugin.extend({
|
|
|
505
537
|
|
|
506
538
|
this.logger.info(`${this.namespace}: ${logPrefix} url: ${webSocketUrl}`);
|
|
507
539
|
|
|
508
|
-
|
|
540
|
+
// Store the resolved URL before socket.open(), which may be rewritten by
|
|
541
|
+
// an interceptor (e.g. same-site websocket proxy). On reconnection we need
|
|
542
|
+
// the original catalog-valid URL, not the post-proxy one from socket.url.
|
|
543
|
+
if (!isShutdownSwitchover) {
|
|
544
|
+
this.resolvedWebSocketUrl = webSocketUrl;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return socket
|
|
548
|
+
.open(webSocketUrl, options)
|
|
549
|
+
.then(() => webSocketUrl)
|
|
550
|
+
.catch((err) => {
|
|
551
|
+
err.webSocketUrl = webSocketUrl;
|
|
552
|
+
|
|
553
|
+
return Promise.reject(err);
|
|
554
|
+
});
|
|
509
555
|
}
|
|
510
556
|
);
|
|
511
557
|
},
|
|
@@ -520,7 +566,6 @@ const Mercury = WebexPlugin.extend({
|
|
|
520
566
|
const onComplete = (err) => {
|
|
521
567
|
// Clear state flags based on connection type
|
|
522
568
|
if (isShutdownSwitchover) {
|
|
523
|
-
this._shutdownSwitchoverInProgress = false;
|
|
524
569
|
this._shutdownSwitchoverBackoffCall = undefined;
|
|
525
570
|
} else {
|
|
526
571
|
this.connecting = false;
|
|
@@ -619,16 +664,26 @@ const Mercury = WebexPlugin.extend({
|
|
|
619
664
|
try {
|
|
620
665
|
this.trigger(...args);
|
|
621
666
|
} catch (error) {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
667
|
+
try {
|
|
668
|
+
this.logger.error(
|
|
669
|
+
`${this.namespace}: error occurred in event handler:`,
|
|
670
|
+
error,
|
|
671
|
+
' with args: ',
|
|
672
|
+
args
|
|
673
|
+
);
|
|
674
|
+
} catch (logError) {
|
|
675
|
+
// If even logging fails, just ignore to prevent cascading errors during cleanup
|
|
676
|
+
// eslint-disable-next-line no-console
|
|
677
|
+
console.error('Mercury _emit error handling failed:', logError);
|
|
678
|
+
}
|
|
628
679
|
}
|
|
629
680
|
},
|
|
630
681
|
|
|
631
682
|
_getEventHandlers(eventType) {
|
|
683
|
+
if (!eventType) {
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
|
|
632
687
|
const [namespace, name] = eventType.split('.');
|
|
633
688
|
const handlers = [];
|
|
634
689
|
|
|
@@ -656,14 +711,9 @@ const Mercury = WebexPlugin.extend({
|
|
|
656
711
|
const isActiveSocket = sourceSocket === this.socket;
|
|
657
712
|
const reason = event.reason && event.reason.toLowerCase();
|
|
658
713
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
socketUrl = this.socket.url;
|
|
663
|
-
} else if (sourceSocket) {
|
|
664
|
-
// Old socket closed - get URL from the closed socket
|
|
665
|
-
socketUrl = sourceSocket.url;
|
|
666
|
-
}
|
|
714
|
+
// Use the stored resolved URL for reconnection instead of sourceSocket.url,
|
|
715
|
+
// which may have been rewritten by an interceptor to a non-catalog host.
|
|
716
|
+
const socketUrl = this.resolvedWebSocketUrl || (sourceSocket && sourceSocket.url);
|
|
667
717
|
|
|
668
718
|
if (isActiveSocket) {
|
|
669
719
|
// Only tear down state if the currently active socket closed
|
|
@@ -780,6 +830,12 @@ const Mercury = WebexPlugin.extend({
|
|
|
780
830
|
|
|
781
831
|
const {data} = envelope;
|
|
782
832
|
|
|
833
|
+
if (!data || !data.eventType) {
|
|
834
|
+
this._emit('event', envelope);
|
|
835
|
+
|
|
836
|
+
return Promise.resolve();
|
|
837
|
+
}
|
|
838
|
+
|
|
783
839
|
this._applyOverrides(data);
|
|
784
840
|
|
|
785
841
|
return this._getEventHandlers(data.eventType)
|
|
@@ -801,7 +857,7 @@ const Mercury = WebexPlugin.extend({
|
|
|
801
857
|
)
|
|
802
858
|
.then(() => {
|
|
803
859
|
this._emit('event', event.data);
|
|
804
|
-
const [namespace] = data.eventType.split('.');
|
|
860
|
+
const [namespace] = data.eventType ? data.eventType.split('.') : [];
|
|
805
861
|
|
|
806
862
|
if (namespace === data.eventType) {
|
|
807
863
|
this._emit(`event:${namespace}`, envelope);
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
UnknownResponse,
|
|
18
18
|
// NotFound
|
|
19
19
|
} from '../errors';
|
|
20
|
+
import {SOCKET_READY_STATE} from './constants';
|
|
20
21
|
|
|
21
22
|
const sockets = new WeakMap();
|
|
22
23
|
|
|
@@ -33,6 +34,8 @@ export default class Socket extends EventEmitter {
|
|
|
33
34
|
this._domain = 'unknown-domain';
|
|
34
35
|
this.onmessage = this.onmessage.bind(this);
|
|
35
36
|
this.onclose = this.onclose.bind(this);
|
|
37
|
+
// Increase max listeners to avoid memory leak warning in tests
|
|
38
|
+
this.setMaxListeners(10);
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
/**
|
|
@@ -114,7 +117,10 @@ export default class Socket extends EventEmitter {
|
|
|
114
117
|
// logger is defined once open is called
|
|
115
118
|
this.logger.info(`socket,${this._domain}: closing`);
|
|
116
119
|
|
|
117
|
-
if (
|
|
120
|
+
if (
|
|
121
|
+
socket.readyState === SOCKET_READY_STATE.CLOSING ||
|
|
122
|
+
socket.readyState === SOCKET_READY_STATE.CLOSED
|
|
123
|
+
) {
|
|
118
124
|
this.logger.info(`socket,${this._domain}: already closed`);
|
|
119
125
|
resolve();
|
|
120
126
|
|
|
@@ -161,7 +167,27 @@ export default class Socket extends EventEmitter {
|
|
|
161
167
|
resolve(event);
|
|
162
168
|
};
|
|
163
169
|
|
|
164
|
-
socket
|
|
170
|
+
// If socket is still connecting, manually trigger close handler with desired code
|
|
171
|
+
// because calling close() on a CONNECTING socket may not preserve custom codes
|
|
172
|
+
if (socket.readyState === SOCKET_READY_STATE.CONNECTING) {
|
|
173
|
+
this.logger.info(
|
|
174
|
+
`socket,${this._domain}: socket still connecting, triggering close manually`
|
|
175
|
+
);
|
|
176
|
+
clearTimeout(closeTimer);
|
|
177
|
+
const closeEvent = {code: options.code, reason: options.reason};
|
|
178
|
+
this.onclose(closeEvent);
|
|
179
|
+
resolve(closeEvent);
|
|
180
|
+
try {
|
|
181
|
+
socket.close(options.code, options.reason);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
this.logger.info(
|
|
184
|
+
`socket,${this._domain}: error while closing CONNECTING socket, likely due to browser incompatibility with custom close codes`,
|
|
185
|
+
error
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
socket.close(options.code, options.reason);
|
|
190
|
+
}
|
|
165
191
|
});
|
|
166
192
|
}
|
|
167
193
|
|
|
@@ -328,7 +354,7 @@ export default class Socket extends EventEmitter {
|
|
|
328
354
|
*/
|
|
329
355
|
send(data) {
|
|
330
356
|
return new Promise((resolve, reject) => {
|
|
331
|
-
if (this.readyState !==
|
|
357
|
+
if (this.readyState !== SOCKET_READY_STATE.OPEN) {
|
|
332
358
|
return reject(new Error('INVALID_STATE_ERROR'));
|
|
333
359
|
}
|
|
334
360
|
|
|
@@ -358,9 +384,20 @@ export default class Socket extends EventEmitter {
|
|
|
358
384
|
return Promise.reject(new Error('`event.data.id` is required'));
|
|
359
385
|
}
|
|
360
386
|
|
|
387
|
+
// Don't try to acknowledge if socket is not in open state
|
|
388
|
+
if (this.readyState !== SOCKET_READY_STATE.OPEN) {
|
|
389
|
+
return Promise.resolve(); // Silently ignore acknowledgment for closed sockets
|
|
390
|
+
}
|
|
391
|
+
|
|
361
392
|
return this.send({
|
|
362
393
|
messageId: event.data.id,
|
|
363
394
|
type: 'ack',
|
|
395
|
+
}).catch((error) => {
|
|
396
|
+
// Gracefully handle send errors (like INVALID_STATE_ERROR) to prevent test issues
|
|
397
|
+
if (error.message === 'INVALID_STATE_ERROR') {
|
|
398
|
+
return Promise.resolve(); // Socket was closed, ignore the acknowledgment
|
|
399
|
+
}
|
|
400
|
+
throw error; // Re-throw other errors
|
|
364
401
|
});
|
|
365
402
|
}
|
|
366
403
|
|
|
@@ -44,8 +44,24 @@ describe('plugin-mercury', () => {
|
|
|
44
44
|
clock = FakeTimers.install({now: Date.now()});
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
afterEach(() => {
|
|
47
|
+
afterEach(async () => {
|
|
48
48
|
clock.uninstall();
|
|
49
|
+
// Clean up mercury socket and mockWebSocket
|
|
50
|
+
if (mercury && mercury.socket) {
|
|
51
|
+
try {
|
|
52
|
+
await mercury.socket.close();
|
|
53
|
+
} catch (e) {}
|
|
54
|
+
}
|
|
55
|
+
if (mockWebSocket && typeof mockWebSocket.close === 'function') {
|
|
56
|
+
mockWebSocket.close();
|
|
57
|
+
}
|
|
58
|
+
// Restore stubs
|
|
59
|
+
if (Socket.getWebSocketConstructor.restore) {
|
|
60
|
+
Socket.getWebSocketConstructor.restore();
|
|
61
|
+
}
|
|
62
|
+
if (socketOpenStub && socketOpenStub.restore) {
|
|
63
|
+
socketOpenStub.restore();
|
|
64
|
+
}
|
|
49
65
|
});
|
|
50
66
|
|
|
51
67
|
beforeEach(() => {
|
|
@@ -274,6 +290,8 @@ describe('plugin-mercury', () => {
|
|
|
274
290
|
mercury._reconnect.restore();
|
|
275
291
|
}
|
|
276
292
|
|
|
293
|
+
let expectedReconnectUrl;
|
|
294
|
+
|
|
277
295
|
sinon.spy(mercury, 'connect');
|
|
278
296
|
|
|
279
297
|
const offlineSpy = sinon.spy();
|
|
@@ -295,6 +313,12 @@ describe('plugin-mercury', () => {
|
|
|
295
313
|
// Make sure mercury.connect has a call count of zero
|
|
296
314
|
mercury.connect.resetHistory();
|
|
297
315
|
|
|
316
|
+
// Reconnection re-derives from the resolved URL
|
|
317
|
+
// captured in _prepareAndOpenSocket, not the socket's actual
|
|
318
|
+
// (possibly changed by interceptors) url.
|
|
319
|
+
expectedReconnectUrl =
|
|
320
|
+
mercury.sessionWebSocketUrls.get('mercury-default-session');
|
|
321
|
+
|
|
298
322
|
mockWebSocket.emit('close', {code, reason});
|
|
299
323
|
|
|
300
324
|
return promiseTick(1);
|
|
@@ -324,7 +348,7 @@ describe('plugin-mercury', () => {
|
|
|
324
348
|
assert.isFalse(mercury.connected, 'Mercury is not connected');
|
|
325
349
|
if (action === 'reconnect') {
|
|
326
350
|
assert.called(mercury.connect);
|
|
327
|
-
assert.calledWith(mercury.connect,
|
|
351
|
+
assert.calledWith(mercury.connect, expectedReconnectUrl);
|
|
328
352
|
assert.isTrue(mercury.connecting, 'Mercury is connecting');
|
|
329
353
|
|
|
330
354
|
// Block until reconnect completes so logs don't overlap
|