@webex/webex-core 3.12.0-webex-services-ready.1 → 3.12.0-webex-services-ready.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/dist/lib/batcher.js +1 -1
- package/dist/lib/credentials/credentials.js +1 -1
- package/dist/lib/credentials/token.js +1 -1
- package/dist/lib/services/services.js +121 -37
- package/dist/lib/services/services.js.map +1 -1
- package/dist/lib/services-v2/services-v2.js +118 -36
- package/dist/lib/services-v2/services-v2.js.map +1 -1
- package/dist/plugins/logger.js +1 -1
- package/dist/webex-core.js +2 -2
- package/package.json +3 -3
- package/src/lib/services/services.js +92 -19
- package/src/lib/services-v2/services-v2.ts +90 -18
- package/test/integration/spec/services/services.js +39 -17
- package/test/integration/spec/services-v2/services-v2.js +39 -17
- package/test/unit/spec/services/services.js +402 -322
- package/test/unit/spec/services-v2/services-v2.ts +331 -214
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
1
|
import sha256 from 'crypto-js/sha256';
|
|
3
2
|
|
|
4
3
|
import {union, forEach} from 'lodash';
|
|
@@ -66,8 +65,10 @@ const Services = WebexPlugin.extend({
|
|
|
66
65
|
|
|
67
66
|
session: {
|
|
68
67
|
/**
|
|
69
|
-
* Becomes `true` once
|
|
70
|
-
*
|
|
68
|
+
* Becomes `true` once the initial catalog collection has completed
|
|
69
|
+
* (successfully or otherwise) and any in-flight credentials refresh has
|
|
70
|
+
* settled. Blocks `webex.ready` so consumers can rely on `webex.ready`
|
|
71
|
+
* implying "catalogs populated AND credential state stable".
|
|
71
72
|
* @instance
|
|
72
73
|
* @memberof Services
|
|
73
74
|
* @type {boolean}
|
|
@@ -1414,7 +1415,6 @@ const Services = WebexPlugin.extend({
|
|
|
1414
1415
|
}
|
|
1415
1416
|
|
|
1416
1417
|
this.ready = true;
|
|
1417
|
-
this.trigger('services:initialized');
|
|
1418
1418
|
},
|
|
1419
1419
|
|
|
1420
1420
|
/**
|
|
@@ -1433,14 +1433,90 @@ const Services = WebexPlugin.extend({
|
|
|
1433
1433
|
this.registries.set(this.webex, registry);
|
|
1434
1434
|
this.states.set(this.webex, state);
|
|
1435
1435
|
|
|
1436
|
-
// Listen for configuration changes once.
|
|
1436
|
+
// Listen for configuration changes once. The config is not populated on the
|
|
1437
|
+
// webex instance until the `change:config` event fires, so any decision that
|
|
1438
|
+
// depends on config values (such as the gated-vs-ungated init below) must be
|
|
1439
|
+
// made from within this handler rather than synchronously in `initialize()`.
|
|
1437
1440
|
this.listenToOnce(this.webex, 'change:config', () => {
|
|
1438
1441
|
this.initConfig();
|
|
1442
|
+
|
|
1443
|
+
// Feature flag: when enabled, `webex.ready` is blocked until the initial
|
|
1444
|
+
// catalog collection has settled AND any in-flight credentials refresh has
|
|
1445
|
+
// completed. When disabled (the default), preserves the pre-existing
|
|
1446
|
+
// behavior where `webex.ready` fires as soon as `webex.loaded` does and
|
|
1447
|
+
// the catalog is collected out-of-band.
|
|
1448
|
+
const waitForCatalogInit = this.webex.config?.services?.waitForCatalogInit === true;
|
|
1449
|
+
|
|
1450
|
+
if (waitForCatalogInit) {
|
|
1451
|
+
this._initializeCatalogsGated(catalog);
|
|
1452
|
+
} else {
|
|
1453
|
+
// Not gating - immediately mark ready so we do not block webex.ready.
|
|
1454
|
+
this.ready = true;
|
|
1455
|
+
this._initializeCatalogsUngated(catalog);
|
|
1456
|
+
}
|
|
1439
1457
|
});
|
|
1458
|
+
},
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* Original (pre-verified-ready) initialization path. Runs on `webex.ready`
|
|
1462
|
+
* and collects catalogs opportunistically without blocking anything.
|
|
1463
|
+
*
|
|
1464
|
+
* @private
|
|
1465
|
+
* @param {ServiceCatalog} catalog
|
|
1466
|
+
* @returns {void}
|
|
1467
|
+
*/
|
|
1468
|
+
_initializeCatalogsUngated(catalog) {
|
|
1469
|
+
// wait for webex instance to be ready before attempting
|
|
1470
|
+
// to update the service catalogs
|
|
1471
|
+
// this can cause a race condition because credentials may
|
|
1472
|
+
// not be valid when services is initialized
|
|
1473
|
+
this.listenToOnce(this.webex, 'ready', async () => {
|
|
1474
|
+
const cachedCatalog = await this._loadCatalogFromCache();
|
|
1475
|
+
if (cachedCatalog) {
|
|
1476
|
+
catalog.isReady = true;
|
|
1440
1477
|
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1478
|
+
return; // skip initServiceCatalogs() on reload when cache exists
|
|
1479
|
+
}
|
|
1480
|
+
const {supertoken} = this.webex.credentials;
|
|
1481
|
+
// Validate if the supertoken exists.
|
|
1482
|
+
if (supertoken && supertoken.access_token) {
|
|
1483
|
+
this.initServiceCatalogs()
|
|
1484
|
+
.then(() => {
|
|
1485
|
+
catalog.isReady = true;
|
|
1486
|
+
})
|
|
1487
|
+
.catch((error) => {
|
|
1488
|
+
this.initFailed = true;
|
|
1489
|
+
this.logger.error(
|
|
1490
|
+
`services: failed to init initial services when credentials available, ${error?.message}`
|
|
1491
|
+
);
|
|
1492
|
+
});
|
|
1493
|
+
} else {
|
|
1494
|
+
const {email} = this.webex.config;
|
|
1495
|
+
|
|
1496
|
+
this.collectPreauthCatalog(email ? {email} : undefined).catch((error) => {
|
|
1497
|
+
this.initFailed = true;
|
|
1498
|
+
this.logger.error(
|
|
1499
|
+
`services: failed to init initial services when no credentials available, ${error?.message}`
|
|
1500
|
+
);
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
},
|
|
1505
|
+
|
|
1506
|
+
/**
|
|
1507
|
+
* Verified-ready initialization path. Blocks `webex.ready` until the initial
|
|
1508
|
+
* catalog fetch has settled (or timed out) AND any in-flight credentials
|
|
1509
|
+
* refresh has completed. Also handles the fresh-login case where OAuth
|
|
1510
|
+
* completes after `loaded` fires.
|
|
1511
|
+
*
|
|
1512
|
+
* @private
|
|
1513
|
+
* @param {ServiceCatalog} catalog
|
|
1514
|
+
* @returns {void}
|
|
1515
|
+
*/
|
|
1516
|
+
_initializeCatalogsGated(catalog) {
|
|
1517
|
+
// Wait for storage to be loaded before attempting to update the service
|
|
1518
|
+
// catalogs. We listen for 'loaded' instead of 'ready' because `services.ready`
|
|
1519
|
+
// now blocks `webex.ready` - listening to 'ready' would deadlock.
|
|
1444
1520
|
this.listenToOnce(this.webex, 'loaded', async () => {
|
|
1445
1521
|
const cachedCatalog = await this._loadCatalogFromCache();
|
|
1446
1522
|
if (cachedCatalog) {
|
|
@@ -1485,19 +1561,16 @@ const Services = WebexPlugin.extend({
|
|
|
1485
1561
|
);
|
|
1486
1562
|
})
|
|
1487
1563
|
.finally(() => this._finalizeReady());
|
|
1488
|
-
|
|
1489
|
-
//
|
|
1564
|
+
|
|
1565
|
+
// Handle fresh login: 'loaded' fires before OAuth completes, so listen
|
|
1566
|
+
// for `canAuthorize` flipping true and then collect the postauth catalog.
|
|
1490
1567
|
this.listenToOnce(this.webex, 'change:canAuthorize', () => {
|
|
1491
1568
|
if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
|
|
1492
|
-
this.initServiceCatalogs()
|
|
1493
|
-
.
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
this.logger.error(
|
|
1498
|
-
`services: failed to init service catalogs after auth, ${error?.message}`
|
|
1499
|
-
);
|
|
1500
|
-
});
|
|
1569
|
+
this.initServiceCatalogs().catch((error) => {
|
|
1570
|
+
this.logger.error(
|
|
1571
|
+
`services: failed to init service catalogs after auth, ${error?.message}`
|
|
1572
|
+
);
|
|
1573
|
+
});
|
|
1501
1574
|
}
|
|
1502
1575
|
});
|
|
1503
1576
|
}
|
|
@@ -52,8 +52,10 @@ const Services = WebexPlugin.extend({
|
|
|
52
52
|
|
|
53
53
|
session: {
|
|
54
54
|
/**
|
|
55
|
-
* Becomes `true` once
|
|
56
|
-
*
|
|
55
|
+
* Becomes `true` once the initial catalog collection has completed
|
|
56
|
+
* (successfully or otherwise) and any in-flight credentials refresh has
|
|
57
|
+
* settled. Blocks `webex.ready` so consumers can rely on `webex.ready`
|
|
58
|
+
* implying "catalogs populated AND credential state stable".
|
|
57
59
|
* @instance
|
|
58
60
|
* @memberof Services
|
|
59
61
|
* @type {boolean}
|
|
@@ -1378,7 +1380,6 @@ const Services = WebexPlugin.extend({
|
|
|
1378
1380
|
}
|
|
1379
1381
|
|
|
1380
1382
|
this.ready = true;
|
|
1381
|
-
this.trigger('services:initialized');
|
|
1382
1383
|
},
|
|
1383
1384
|
|
|
1384
1385
|
/**
|
|
@@ -1392,14 +1393,88 @@ const Services = WebexPlugin.extend({
|
|
|
1392
1393
|
const catalog = new ServiceCatalog();
|
|
1393
1394
|
this._catalogs.set(this.webex, catalog);
|
|
1394
1395
|
|
|
1395
|
-
// Listen for configuration changes once.
|
|
1396
|
+
// Listen for configuration changes once. The config is not populated on the
|
|
1397
|
+
// webex instance until the `change:config` event fires, so any decision that
|
|
1398
|
+
// depends on config values (such as the gated-vs-ungated init below) must be
|
|
1399
|
+
// made from within this handler rather than synchronously in `initialize()`.
|
|
1396
1400
|
this.listenToOnce(this.webex, 'change:config', () => {
|
|
1397
1401
|
this.initConfig();
|
|
1402
|
+
|
|
1403
|
+
// Feature flag: when enabled, `webex.ready` is blocked until the initial
|
|
1404
|
+
// catalog collection has settled AND any in-flight credentials refresh has
|
|
1405
|
+
// completed. When disabled (the default), preserves the pre-existing
|
|
1406
|
+
// behavior where `webex.ready` fires as soon as `webex.loaded` does and
|
|
1407
|
+
// the catalog is collected out-of-band.
|
|
1408
|
+
const waitForCatalogInit = this.webex.config?.services?.waitForCatalogInit === true;
|
|
1409
|
+
|
|
1410
|
+
if (waitForCatalogInit) {
|
|
1411
|
+
this._initializeCatalogsGated(catalog);
|
|
1412
|
+
} else {
|
|
1413
|
+
// Not gating - immediately mark ready so we do not block webex.ready.
|
|
1414
|
+
this.ready = true;
|
|
1415
|
+
this._initializeCatalogsUngated(catalog);
|
|
1416
|
+
}
|
|
1398
1417
|
});
|
|
1418
|
+
},
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Original (pre-verified-ready) initialization path. Runs on `webex.ready`
|
|
1422
|
+
* and collects catalogs opportunistically without blocking anything.
|
|
1423
|
+
*
|
|
1424
|
+
* @private
|
|
1425
|
+
* @param {ServiceCatalog} catalog
|
|
1426
|
+
* @returns {void}
|
|
1427
|
+
*/
|
|
1428
|
+
_initializeCatalogsUngated(catalog: ServiceCatalog): void {
|
|
1429
|
+
// wait for webex instance to be ready before attempting
|
|
1430
|
+
// to update the service catalogs
|
|
1431
|
+
this.listenToOnce(this.webex, 'ready', async () => {
|
|
1432
|
+
const warmed = await this._loadCatalogFromCache();
|
|
1433
|
+
if (warmed) {
|
|
1434
|
+
catalog.isReady = true;
|
|
1399
1435
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
const {supertoken} = this.webex.credentials;
|
|
1439
|
+
// Validate if the supertoken exists.
|
|
1440
|
+
if (supertoken && supertoken.access_token) {
|
|
1441
|
+
this.initServiceCatalogs()
|
|
1442
|
+
.then(() => {
|
|
1443
|
+
catalog.isReady = true;
|
|
1444
|
+
})
|
|
1445
|
+
.catch((error) => {
|
|
1446
|
+
this.initFailed = true;
|
|
1447
|
+
this.logger.error(
|
|
1448
|
+
`services: failed to init initial services when credentials available, ${error?.message}`
|
|
1449
|
+
);
|
|
1450
|
+
});
|
|
1451
|
+
} else {
|
|
1452
|
+
const {email} = this.webex.config;
|
|
1453
|
+
|
|
1454
|
+
this.collectPreauthCatalog(email ? {email} : undefined).catch((error) => {
|
|
1455
|
+
this.initFailed = true;
|
|
1456
|
+
this.logger.error(
|
|
1457
|
+
`services: failed to init initial services when no credentials available, ${error?.message}`
|
|
1458
|
+
);
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
},
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Verified-ready initialization path. Blocks `webex.ready` until the initial
|
|
1466
|
+
* catalog fetch has settled (or timed out) AND any in-flight credentials
|
|
1467
|
+
* refresh has completed. Also handles the fresh-login case where OAuth
|
|
1468
|
+
* completes after `loaded` fires.
|
|
1469
|
+
*
|
|
1470
|
+
* @private
|
|
1471
|
+
* @param {ServiceCatalog} catalog
|
|
1472
|
+
* @returns {void}
|
|
1473
|
+
*/
|
|
1474
|
+
_initializeCatalogsGated(catalog: ServiceCatalog): void {
|
|
1475
|
+
// Wait for storage to be loaded before attempting to update the service
|
|
1476
|
+
// catalogs. We listen for 'loaded' instead of 'ready' because `services.ready`
|
|
1477
|
+
// now blocks `webex.ready` - listening to 'ready' would deadlock.
|
|
1403
1478
|
this.listenToOnce(this.webex, 'loaded', async () => {
|
|
1404
1479
|
const warmed = await this._loadCatalogFromCache();
|
|
1405
1480
|
if (warmed) {
|
|
@@ -1444,19 +1519,16 @@ const Services = WebexPlugin.extend({
|
|
|
1444
1519
|
);
|
|
1445
1520
|
})
|
|
1446
1521
|
.finally(() => this._finalizeReady());
|
|
1447
|
-
|
|
1448
|
-
//
|
|
1522
|
+
|
|
1523
|
+
// Handle fresh login: 'loaded' fires before OAuth completes, so listen
|
|
1524
|
+
// for `canAuthorize` flipping true and then collect the postauth catalog.
|
|
1449
1525
|
this.listenToOnce(this.webex, 'change:canAuthorize', () => {
|
|
1450
1526
|
if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
|
|
1451
|
-
this.initServiceCatalogs()
|
|
1452
|
-
.
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
this.logger.error(
|
|
1457
|
-
`services: failed to init service catalogs after auth, ${error?.message}`
|
|
1458
|
-
);
|
|
1459
|
-
});
|
|
1527
|
+
this.initServiceCatalogs().catch((error) => {
|
|
1528
|
+
this.logger.error(
|
|
1529
|
+
`services: failed to init service catalogs after auth, ${error?.message}`
|
|
1530
|
+
);
|
|
1531
|
+
});
|
|
1460
1532
|
}
|
|
1461
1533
|
});
|
|
1462
1534
|
}
|
|
@@ -404,26 +404,15 @@ describe('webex-core', () => {
|
|
|
404
404
|
assert.isTrue(catalog.isReady);
|
|
405
405
|
});
|
|
406
406
|
|
|
407
|
-
it('should call services#initServiceCatalogs() on webex
|
|
407
|
+
it('should call services#initServiceCatalogs() on webex ready', async () => {
|
|
408
|
+
services._loadCatalogFromCache = sinon.stub().resolves(false);
|
|
408
409
|
services.initServiceCatalogs = sinon.stub().resolves();
|
|
409
410
|
services.initialize();
|
|
410
|
-
webex.trigger('
|
|
411
|
-
// Wait for the async
|
|
412
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
411
|
+
webex.trigger('ready');
|
|
412
|
+
// Wait for the async 'ready' handler to complete
|
|
413
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
413
414
|
assert.called(services.initServiceCatalogs);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
it('should set services.ready to true after initialization completes', async () => {
|
|
417
|
-
// services.ready starts as false
|
|
418
|
-
const newWebex = new WebexCore({credentials: {supertoken: webexUser.token}});
|
|
419
|
-
const newServices = newWebex.internal.services;
|
|
420
|
-
|
|
421
|
-
// Wait for initialization to complete
|
|
422
|
-
await new Promise((resolve) => {
|
|
423
|
-
newServices.on('services:initialized', resolve);
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
assert.isTrue(newServices.ready);
|
|
415
|
+
assert.isTrue(catalog.isReady);
|
|
427
416
|
});
|
|
428
417
|
|
|
429
418
|
it('should collect different catalogs based on OrgId region', () =>
|
|
@@ -441,6 +430,39 @@ describe('webex-core', () => {
|
|
|
441
430
|
done();
|
|
442
431
|
}, 2000);
|
|
443
432
|
});
|
|
433
|
+
|
|
434
|
+
it('blocks webex.ready until services.ready flips when waitForCatalogInit is enabled', async () => {
|
|
435
|
+
const gatedWebex = new WebexCore({
|
|
436
|
+
credentials: {supertoken: webexUser.token},
|
|
437
|
+
config: {services: {waitForCatalogInit: true}},
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Before init settles, webex.ready must be false because services.ready
|
|
441
|
+
// is a dependency and starts false in the gated path.
|
|
442
|
+
assert.isFalse(gatedWebex.internal.services.ready, 'services.ready should start false');
|
|
443
|
+
assert.isFalse(gatedWebex.ready, 'webex.ready should not fire while services.ready is false');
|
|
444
|
+
|
|
445
|
+
// Wait up to 30s for services init to complete and flip ready.
|
|
446
|
+
await new Promise((resolve, reject) => {
|
|
447
|
+
if (gatedWebex.internal.services.ready) {
|
|
448
|
+
resolve();
|
|
449
|
+
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const timer = setTimeout(
|
|
453
|
+
() => reject(new Error('timed out waiting for services.ready')),
|
|
454
|
+
30_000
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
gatedWebex.internal.services.once('change:ready', () => {
|
|
458
|
+
clearTimeout(timer);
|
|
459
|
+
resolve();
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
assert.isTrue(gatedWebex.internal.services.ready, 'services.ready should flip true after init settles');
|
|
464
|
+
assert.isTrue(gatedWebex.ready, 'webex.ready should fire once services.ready flips');
|
|
465
|
+
});
|
|
444
466
|
});
|
|
445
467
|
|
|
446
468
|
describe('#initServiceCatalogs()', () => {
|
|
@@ -316,26 +316,15 @@ describe('webex-core', () => {
|
|
|
316
316
|
assert.isTrue(catalog.isReady);
|
|
317
317
|
});
|
|
318
318
|
|
|
319
|
-
it('should call services#initServiceCatalogs() on webex
|
|
319
|
+
it('should call services#initServiceCatalogs() on webex ready', async () => {
|
|
320
|
+
services._loadCatalogFromCache = sinon.stub().resolves(false);
|
|
320
321
|
services.initServiceCatalogs = sinon.stub().resolves();
|
|
321
322
|
services.initialize();
|
|
322
|
-
webex.trigger('
|
|
323
|
-
// Wait for the async
|
|
324
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
323
|
+
webex.trigger('ready');
|
|
324
|
+
// Wait for the async 'ready' handler to complete
|
|
325
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
325
326
|
assert.called(services.initServiceCatalogs);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
it('should set services.ready to true after initialization completes', async () => {
|
|
329
|
-
// services.ready starts as false
|
|
330
|
-
const newWebex = new WebexCore({credentials: {supertoken: webexUser.token}});
|
|
331
|
-
const newServices = newWebex.internal.services;
|
|
332
|
-
|
|
333
|
-
// Wait for initialization to complete
|
|
334
|
-
await new Promise((resolve) => {
|
|
335
|
-
newServices.on('services:initialized', resolve);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
assert.isTrue(newServices.ready);
|
|
327
|
+
assert.isTrue(catalog.isReady);
|
|
339
328
|
});
|
|
340
329
|
|
|
341
330
|
it('should collect different catalogs based on OrgId region', () =>
|
|
@@ -352,6 +341,39 @@ describe('webex-core', () => {
|
|
|
352
341
|
done();
|
|
353
342
|
}, 2000);
|
|
354
343
|
});
|
|
344
|
+
|
|
345
|
+
it('blocks webex.ready until services.ready flips when waitForCatalogInit is enabled', async () => {
|
|
346
|
+
const gatedWebex = new WebexCore({
|
|
347
|
+
credentials: {supertoken: webexUser.token},
|
|
348
|
+
config: {services: {waitForCatalogInit: true}},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Before init settles, webex.ready must be false because services.ready
|
|
352
|
+
// is a dependency and starts false in the gated path.
|
|
353
|
+
assert.isFalse(gatedWebex.internal.services.ready, 'services.ready should start false');
|
|
354
|
+
assert.isFalse(gatedWebex.ready, 'webex.ready should not fire while services.ready is false');
|
|
355
|
+
|
|
356
|
+
// Wait up to 30s for services init to complete and flip ready.
|
|
357
|
+
await new Promise((resolve, reject) => {
|
|
358
|
+
if (gatedWebex.internal.services.ready) {
|
|
359
|
+
resolve();
|
|
360
|
+
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const timer = setTimeout(
|
|
364
|
+
() => reject(new Error('timed out waiting for services.ready')),
|
|
365
|
+
30_000
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
gatedWebex.internal.services.once('change:ready', () => {
|
|
369
|
+
clearTimeout(timer);
|
|
370
|
+
resolve();
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
assert.isTrue(gatedWebex.internal.services.ready, 'services.ready should flip true after init settles');
|
|
375
|
+
assert.isTrue(gatedWebex.ready, 'webex.ready should fire once services.ready flips');
|
|
376
|
+
});
|
|
355
377
|
});
|
|
356
378
|
|
|
357
379
|
describe('#initServiceCatalogs()', () => {
|