@webex/webex-core 3.12.0-task-refactor.1 → 3.12.0-webex-services-ready.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/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/credentials-config.js +12 -0
- package/dist/credentials-config.js.map +1 -1
- package/dist/interceptors/redirect.js +1 -1
- package/dist/interceptors/redirect.js.map +1 -1
- package/dist/lib/batcher.js +23 -7
- package/dist/lib/batcher.js.map +1 -1
- package/dist/lib/credentials/credentials.js +48 -4
- package/dist/lib/credentials/credentials.js.map +1 -1
- package/dist/lib/credentials/token.js +1 -1
- package/dist/lib/services/service-url.js +11 -1
- package/dist/lib/services/service-url.js.map +1 -1
- package/dist/lib/services/services.js +501 -96
- package/dist/lib/services/services.js.map +1 -1
- package/dist/lib/services-v2/services-v2.js +426 -42
- package/dist/lib/services-v2/services-v2.js.map +1 -1
- package/dist/lib/services-v2/types.js.map +1 -1
- package/dist/plugins/logger.js +1 -1
- package/dist/webex-core.js +2 -2
- package/dist/webex-core.js.map +1 -1
- package/package.json +13 -13
- package/src/config.js +7 -0
- package/src/credentials-config.js +13 -0
- package/src/interceptors/redirect.js +4 -1
- package/src/lib/batcher.js +25 -10
- package/src/lib/credentials/credentials.js +50 -3
- package/src/lib/services/service-url.js +9 -1
- package/src/lib/services/services.js +368 -14
- package/src/lib/services-v2/services-v2.ts +353 -10
- package/src/lib/services-v2/types.ts +5 -0
- package/test/integration/spec/services/service-catalog.js +16 -11
- package/test/integration/spec/services/services.js +38 -11
- package/test/integration/spec/services-v2/services-v2.js +29 -8
- package/test/unit/spec/credentials/credentials.js +133 -2
- package/test/unit/spec/lib/batcher.js +56 -0
- package/test/unit/spec/services/service-url.js +110 -0
- package/test/unit/spec/services/services.js +680 -80
- package/test/unit/spec/services-v2/services-v2.ts +484 -62
- package/test/unit/spec/webex-core.js +2 -0
- package/test/unit/spec/webex-internal-core.js +2 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
1
2
|
import sha256 from 'crypto-js/sha256';
|
|
2
3
|
|
|
3
4
|
import {union, forEach} from 'lodash';
|
|
@@ -20,6 +21,14 @@ export const DEFAULT_CLUSTER_SERVICE = 'identityLookup';
|
|
|
20
21
|
const CLUSTER_SERVICE = process.env.WEBEX_CONVERSATION_CLUSTER_SERVICE || DEFAULT_CLUSTER_SERVICE;
|
|
21
22
|
const DEFAULT_CLUSTER_IDENTIFIER =
|
|
22
23
|
process.env.WEBEX_CONVERSATION_DEFAULT_CLUSTER || `${DEFAULT_CLUSTER}:${CLUSTER_SERVICE}`;
|
|
24
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
25
|
+
const CATALOG_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
26
|
+
|
|
27
|
+
// Maximum time we will wait for the initial catalog collection before letting
|
|
28
|
+
// `services.ready` (and therefore `webex.ready`) fire anyway. A hung request
|
|
29
|
+
// must never leave the app on a permanent spinner - past this point downstream
|
|
30
|
+
// consumers must fall through to their normal error/login paths.
|
|
31
|
+
const SERVICES_INIT_TIMEOUT_MS = 15_000;
|
|
23
32
|
|
|
24
33
|
/* eslint-disable no-underscore-dangle */
|
|
25
34
|
/**
|
|
@@ -55,6 +64,20 @@ const Services = WebexPlugin.extend({
|
|
|
55
64
|
initFailed: ['boolean', false, false],
|
|
56
65
|
},
|
|
57
66
|
|
|
67
|
+
session: {
|
|
68
|
+
/**
|
|
69
|
+
* Becomes `true` once service catalog initialization has completed.
|
|
70
|
+
* Blocks `webex.ready` until services are initialized.
|
|
71
|
+
* @instance
|
|
72
|
+
* @memberof Services
|
|
73
|
+
* @type {boolean}
|
|
74
|
+
*/
|
|
75
|
+
ready: {
|
|
76
|
+
default: false,
|
|
77
|
+
type: 'boolean',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
|
|
58
81
|
_catalogs: new WeakMap(),
|
|
59
82
|
|
|
60
83
|
_serviceUrls: null,
|
|
@@ -96,6 +119,49 @@ const Services = WebexPlugin.extend({
|
|
|
96
119
|
return this._catalogs.get(this.webex);
|
|
97
120
|
},
|
|
98
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Safely access localStorage if available; returns the Storage or null.
|
|
124
|
+
* @returns {Storage|null}
|
|
125
|
+
*/
|
|
126
|
+
_getLocalStorageSafe() {
|
|
127
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
128
|
+
return window.localStorage;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return null;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Determine the intended preauth selection based on the current context.
|
|
136
|
+
* @param {string|undefined} currentOrgId
|
|
137
|
+
* @returns {{selectionType: string, selectionValue: string}}
|
|
138
|
+
*/
|
|
139
|
+
getIntendedPreauthSelection(currentOrgId) {
|
|
140
|
+
if (this.webex.credentials?.canAuthorize) {
|
|
141
|
+
if (currentOrgId) {
|
|
142
|
+
return {
|
|
143
|
+
selectionType: 'orgId',
|
|
144
|
+
selectionValue: currentOrgId,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const emailConfig = this.webex.config && this.webex.config.email;
|
|
150
|
+
|
|
151
|
+
if (typeof emailConfig === 'string' && emailConfig.trim()) {
|
|
152
|
+
return {
|
|
153
|
+
selectionType: 'emailhash',
|
|
154
|
+
selectionValue: sha256(emailConfig.toLowerCase()).toString(),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// fall back to proximity mode when no orgId or email available
|
|
159
|
+
return {
|
|
160
|
+
selectionType: 'mode',
|
|
161
|
+
selectionValue: 'DEFAULT_BY_PROXIMITY',
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
|
|
99
165
|
/**
|
|
100
166
|
* Get a service url from the current services list by name
|
|
101
167
|
* from the associated instance catalog.
|
|
@@ -165,9 +231,10 @@ const Services = WebexPlugin.extend({
|
|
|
165
231
|
|
|
166
232
|
/**
|
|
167
233
|
* Get all Mobius cluster host entries from the legacy host catalog.
|
|
168
|
-
* @returns {Array<
|
|
234
|
+
* @returns {Array<{host: string, id: string, ttl: number, priority: number}>}
|
|
169
235
|
*/
|
|
170
236
|
getMobiusClusters() {
|
|
237
|
+
this.logger.info('services: fetching mobius clusters');
|
|
171
238
|
const clusters = [];
|
|
172
239
|
const hostCatalog = this._hostCatalog || {};
|
|
173
240
|
|
|
@@ -197,6 +264,28 @@ const Services = WebexPlugin.extend({
|
|
|
197
264
|
|
|
198
265
|
return !!hostCatalog[host]?.length;
|
|
199
266
|
},
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Checks if the current environment is an integration (INT) environment
|
|
270
|
+
* by examining the u2c discovery URL from webex config.
|
|
271
|
+
* INT environments use discovery URLs containing 'intb' (e.g., u2c-intb.ciscospark.com).
|
|
272
|
+
* @returns {boolean} True if INT environment, false otherwise
|
|
273
|
+
*/
|
|
274
|
+
isIntegrationEnvironment() {
|
|
275
|
+
try {
|
|
276
|
+
const u2cUrl = this.webex?.config?.services?.discovery?.u2c || '';
|
|
277
|
+
const isInt = u2cUrl.includes('intb');
|
|
278
|
+
|
|
279
|
+
this.logger.info(`services: isIntegrationEnvironment: ${isInt}`);
|
|
280
|
+
|
|
281
|
+
return isInt;
|
|
282
|
+
} catch (error) {
|
|
283
|
+
this.logger.error('services: failed to determine integration environment', error);
|
|
284
|
+
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
|
|
200
289
|
/**
|
|
201
290
|
* Merge provided active cluster mappings into current state.
|
|
202
291
|
* @param {Record<string,string>} activeServices
|
|
@@ -237,7 +326,7 @@ const Services = WebexPlugin.extend({
|
|
|
237
326
|
* @param {string} [param.token] - used for signin catalog
|
|
238
327
|
* @returns {Promise<object>}
|
|
239
328
|
*/
|
|
240
|
-
updateServices({from, query, token, forceRefresh} = {}) {
|
|
329
|
+
async updateServices({from, query, token, forceRefresh} = {}) {
|
|
241
330
|
const catalog = this._getCatalog();
|
|
242
331
|
let formattedQuery;
|
|
243
332
|
let serviceGroup;
|
|
@@ -291,7 +380,20 @@ const Services = WebexPlugin.extend({
|
|
|
291
380
|
forceRefresh,
|
|
292
381
|
})
|
|
293
382
|
.then((serviceHostMap) => {
|
|
294
|
-
|
|
383
|
+
const formattedServiceHostMap = this._formatReceivedHostmap(serviceHostMap);
|
|
384
|
+
// Build selection metadata for caching discrimination
|
|
385
|
+
let selectionMeta;
|
|
386
|
+
if (serviceGroup === 'preauth' || serviceGroup === 'signin') {
|
|
387
|
+
const key = formattedQuery && Object.keys(formattedQuery || {})[0];
|
|
388
|
+
if (key) {
|
|
389
|
+
selectionMeta = {
|
|
390
|
+
selectionType: key,
|
|
391
|
+
selectionValue: formattedQuery[key],
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
this._cacheCatalog(serviceGroup, serviceHostMap, selectionMeta);
|
|
396
|
+
catalog.updateServiceUrls(serviceGroup, formattedServiceHostMap);
|
|
295
397
|
this.updateCredentialsConfig();
|
|
296
398
|
catalog.status[serviceGroup].collecting = false;
|
|
297
399
|
})
|
|
@@ -1008,7 +1110,197 @@ const Services = WebexPlugin.extend({
|
|
|
1008
1110
|
|
|
1009
1111
|
return this.webex.internal.newMetrics.callDiagnosticLatencies
|
|
1010
1112
|
.measureLatency(() => this.request(requestObject), 'internal.get.u2c.time')
|
|
1011
|
-
.then(({body}) =>
|
|
1113
|
+
.then(({body}) => body);
|
|
1114
|
+
},
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Cache the catalog in the bounded storage.
|
|
1118
|
+
* @param {string} serviceGroup - preauth, signin, postauth
|
|
1119
|
+
* @param {object} hostMap - The hostmap to cache
|
|
1120
|
+
* @param {object} [meta] - Optional selection metadata used to validate cache reuse
|
|
1121
|
+
* @returns {Promise<void>}
|
|
1122
|
+
*
|
|
1123
|
+
*/
|
|
1124
|
+
async _cacheCatalog(serviceGroup, hostMap, meta) {
|
|
1125
|
+
let current = {};
|
|
1126
|
+
let orgId;
|
|
1127
|
+
try {
|
|
1128
|
+
// Respect calling.cacheU2C toggle; if disabled, skip writing cache
|
|
1129
|
+
if (!this.webex.config?.calling?.cacheU2C) {
|
|
1130
|
+
this.logger.info(`services: skipping cache write for ${serviceGroup} as per the config`);
|
|
1131
|
+
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Persist to localStorage to survive browser refresh
|
|
1136
|
+
try {
|
|
1137
|
+
const ls = this._getLocalStorageSafe();
|
|
1138
|
+
const cachedJson = ls ? ls.getItem(CATALOG_CACHE_KEY_V1) : null;
|
|
1139
|
+
current = cachedJson ? JSON.parse(cachedJson) : {};
|
|
1140
|
+
} catch (e) {
|
|
1141
|
+
current = {};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
try {
|
|
1145
|
+
const {credentials} = this.webex;
|
|
1146
|
+
orgId = credentials.getOrgId();
|
|
1147
|
+
} catch (e) {
|
|
1148
|
+
orgId = current.orgId;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Capture environment fingerprint to invalidate cache across env changes
|
|
1152
|
+
let {env} = current;
|
|
1153
|
+
const fedramp = !!this.webex?.config?.fedramp;
|
|
1154
|
+
const u2cDiscoveryUrl = this.webex?.config?.services?.discovery?.u2c;
|
|
1155
|
+
env = {fedramp, u2cDiscoveryUrl};
|
|
1156
|
+
|
|
1157
|
+
const updated = {
|
|
1158
|
+
...current,
|
|
1159
|
+
orgId: orgId || current.orgId,
|
|
1160
|
+
env: env || current.env,
|
|
1161
|
+
// When selection meta is provided, store as an object; otherwise keep legacy shape
|
|
1162
|
+
[serviceGroup]: meta ? {hostMap, meta} : hostMap,
|
|
1163
|
+
cachedAt: Date.now(),
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
const ls = this._getLocalStorageSafe();
|
|
1167
|
+
if (ls) {
|
|
1168
|
+
ls.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify(updated));
|
|
1169
|
+
}
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
this.logger.warn('services: error caching catalog', error);
|
|
1172
|
+
}
|
|
1173
|
+
},
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* Load the catalog from cache and hydrate the in-memory ServiceCatalog.
|
|
1177
|
+
* @returns {Promise<boolean>} true if cache was loaded, false otherwise
|
|
1178
|
+
*/
|
|
1179
|
+
async _loadCatalogFromCache() {
|
|
1180
|
+
let currentOrgId;
|
|
1181
|
+
try {
|
|
1182
|
+
// Respect calling.cacheU2C toggle; if disabled, skip using cache
|
|
1183
|
+
if (!this.webex.config?.calling?.cacheU2C) {
|
|
1184
|
+
this.logger.info('services: skipping cache warm-up as per the cache config');
|
|
1185
|
+
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
const ls = this._getLocalStorageSafe();
|
|
1190
|
+
if (!ls) {
|
|
1191
|
+
this.logger.info('services: skipping cache warm-up as no localStorage is available');
|
|
1192
|
+
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
const cachedJson = ls.getItem(CATALOG_CACHE_KEY_V1);
|
|
1196
|
+
const cached = cachedJson ? JSON.parse(cachedJson) : undefined;
|
|
1197
|
+
if (!cached) {
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
// TTL enforcement: clear if older than 24 hours
|
|
1201
|
+
const cachedAt = Number(cached.cachedAt) || 0;
|
|
1202
|
+
if (!cachedAt || Date.now() - cachedAt > CATALOG_TTL_MS) {
|
|
1203
|
+
this.clearCatalogCache();
|
|
1204
|
+
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// If authorized, ensure cached org matches
|
|
1209
|
+
try {
|
|
1210
|
+
if (this.webex.credentials?.canAuthorize) {
|
|
1211
|
+
const {credentials} = this.webex;
|
|
1212
|
+
currentOrgId = credentials.getOrgId();
|
|
1213
|
+
if (cached.orgId && cached.orgId !== currentOrgId) {
|
|
1214
|
+
return false;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
} catch (e) {
|
|
1218
|
+
this.logger.warn('services: error checking orgId', e);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Ensure cached environment matches current environment
|
|
1222
|
+
|
|
1223
|
+
const fedramp = !!this.webex.config?.fedramp;
|
|
1224
|
+
const u2cDiscoveryUrl = this.webex.config?.services?.discovery?.u2c;
|
|
1225
|
+
const currentEnv = {fedramp, u2cDiscoveryUrl};
|
|
1226
|
+
if (cached.env) {
|
|
1227
|
+
const sameEnv =
|
|
1228
|
+
cached.env.fedramp === currentEnv.fedramp &&
|
|
1229
|
+
cached.env.u2cDiscoveryUrl === currentEnv.u2cDiscoveryUrl;
|
|
1230
|
+
if (!sameEnv) {
|
|
1231
|
+
this.logger.info('services: skipping cache warm due to environment mismatch');
|
|
1232
|
+
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const catalog = this._getCatalog();
|
|
1238
|
+
|
|
1239
|
+
// Apply any cached groups (with preauth selection validation if available)
|
|
1240
|
+
const groups = ['preauth', 'signin', 'postauth'];
|
|
1241
|
+
groups.forEach((serviceGroup) => {
|
|
1242
|
+
const cachedGroup = cached[serviceGroup];
|
|
1243
|
+
if (!cachedGroup) {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Support legacy (hostMap) and new ({hostMap, meta}) shapes
|
|
1248
|
+
const hostMap = cachedGroup && cachedGroup.hostMap ? cachedGroup.hostMap : cachedGroup;
|
|
1249
|
+
const meta = cachedGroup?.meta;
|
|
1250
|
+
|
|
1251
|
+
if (serviceGroup === 'preauth' && meta) {
|
|
1252
|
+
// For proximity-based selection, always fetch fresh to respect IP/region changes
|
|
1253
|
+
if (meta.selectionType === 'mode') {
|
|
1254
|
+
this.logger.info('services: skipping preauth cache warm for proximity mode');
|
|
1255
|
+
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const intended = this.getIntendedPreauthSelection(currentOrgId);
|
|
1260
|
+
const matches =
|
|
1261
|
+
intended &&
|
|
1262
|
+
intended.selectionType === meta.selectionType &&
|
|
1263
|
+
intended.selectionValue === meta.selectionValue;
|
|
1264
|
+
|
|
1265
|
+
if (!matches) {
|
|
1266
|
+
this.logger.info('services: skipping preauth cache warm due to selection mismatch');
|
|
1267
|
+
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
if (hostMap) {
|
|
1273
|
+
const formatted = this._formatReceivedHostmap(hostMap);
|
|
1274
|
+
catalog.updateServiceUrls(serviceGroup, formatted);
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
// Align credentials against warmed catalog
|
|
1279
|
+
this.updateCredentialsConfig();
|
|
1280
|
+
|
|
1281
|
+
return true;
|
|
1282
|
+
} catch (e) {
|
|
1283
|
+
this.logger.warn('services: error loading catalog from cache', e);
|
|
1284
|
+
|
|
1285
|
+
return false;
|
|
1286
|
+
}
|
|
1287
|
+
},
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* Clear the catalog cache from the bounded storage.
|
|
1291
|
+
* @returns {Promise<void>}
|
|
1292
|
+
*/
|
|
1293
|
+
clearCatalogCache() {
|
|
1294
|
+
try {
|
|
1295
|
+
const ls = this._getLocalStorageSafe();
|
|
1296
|
+
if (ls) {
|
|
1297
|
+
ls.removeItem(CATALOG_CACHE_KEY_V1);
|
|
1298
|
+
}
|
|
1299
|
+
} catch (e) {
|
|
1300
|
+
this.logger.warn('services: error clearing catalog cache', e);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
return Promise.resolve();
|
|
1012
1304
|
},
|
|
1013
1305
|
|
|
1014
1306
|
/**
|
|
@@ -1088,6 +1380,7 @@ const Services = WebexPlugin.extend({
|
|
|
1088
1380
|
// Validate if the token is authorized.
|
|
1089
1381
|
if (credentials.canAuthorize) {
|
|
1090
1382
|
// Attempt to collect the postauth catalog.
|
|
1383
|
+
|
|
1091
1384
|
return this.updateServices().catch(() => {
|
|
1092
1385
|
this.initFailed = true;
|
|
1093
1386
|
this.logger.warn('services: cannot retrieve postauth catalog');
|
|
@@ -1100,6 +1393,30 @@ const Services = WebexPlugin.extend({
|
|
|
1100
1393
|
);
|
|
1101
1394
|
},
|
|
1102
1395
|
|
|
1396
|
+
/**
|
|
1397
|
+
* Await any in-flight credentials refresh, then flip `services.ready` so
|
|
1398
|
+
* `webex.ready` can fire. Closes the parallel-refresh window: if a credential
|
|
1399
|
+
* refresh is in flight when initial catalog collection settles, we must not
|
|
1400
|
+
* signal ready until the refresh has resolved - otherwise downstream
|
|
1401
|
+
* consumers may observe `canAuthorize`/token state that is about to change
|
|
1402
|
+
* under them.
|
|
1403
|
+
*
|
|
1404
|
+
* @private
|
|
1405
|
+
* @returns {Promise<void>}
|
|
1406
|
+
*/
|
|
1407
|
+
async _finalizeReady() {
|
|
1408
|
+
const {credentials} = this.webex;
|
|
1409
|
+
|
|
1410
|
+
if (credentials && credentials.isRefreshing) {
|
|
1411
|
+
await new Promise((resolve) => {
|
|
1412
|
+
credentials.once('change:isRefreshing', resolve);
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
this.ready = true;
|
|
1417
|
+
this.trigger('services:initialized');
|
|
1418
|
+
},
|
|
1419
|
+
|
|
1103
1420
|
/**
|
|
1104
1421
|
* Initializer
|
|
1105
1422
|
*
|
|
@@ -1121,13 +1438,32 @@ const Services = WebexPlugin.extend({
|
|
|
1121
1438
|
this.initConfig();
|
|
1122
1439
|
});
|
|
1123
1440
|
|
|
1124
|
-
//
|
|
1125
|
-
//
|
|
1126
|
-
|
|
1441
|
+
// Wait for storage to be loaded before attempting to update the service catalogs.
|
|
1442
|
+
// We listen for 'loaded' instead of 'ready' because services.ready is a dependency
|
|
1443
|
+
// of webex.ready - listening to 'ready' would cause a deadlock.
|
|
1444
|
+
this.listenToOnce(this.webex, 'loaded', async () => {
|
|
1445
|
+
const cachedCatalog = await this._loadCatalogFromCache();
|
|
1446
|
+
if (cachedCatalog) {
|
|
1447
|
+
catalog.isReady = true;
|
|
1448
|
+
await this._finalizeReady();
|
|
1449
|
+
|
|
1450
|
+
return; // skip initServiceCatalogs() on reload when cache exists
|
|
1451
|
+
}
|
|
1127
1452
|
const {supertoken} = this.webex.credentials;
|
|
1453
|
+
|
|
1454
|
+
// Race init against a hard timeout so a hung request never leaves
|
|
1455
|
+
// `services.ready` false forever - that would stall `webex.ready` and
|
|
1456
|
+
// leave the app on a permanent spinner.
|
|
1457
|
+
const timeout = new Promise((_, reject) => {
|
|
1458
|
+
setTimeout(
|
|
1459
|
+
() => reject(new Error(`services: init timed out after ${SERVICES_INIT_TIMEOUT_MS}ms`)),
|
|
1460
|
+
SERVICES_INIT_TIMEOUT_MS
|
|
1461
|
+
);
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1128
1464
|
// Validate if the supertoken exists.
|
|
1129
1465
|
if (supertoken && supertoken.access_token) {
|
|
1130
|
-
this.initServiceCatalogs()
|
|
1466
|
+
Promise.race([this.initServiceCatalogs(), timeout])
|
|
1131
1467
|
.then(() => {
|
|
1132
1468
|
catalog.isReady = true;
|
|
1133
1469
|
})
|
|
@@ -1136,15 +1472,33 @@ const Services = WebexPlugin.extend({
|
|
|
1136
1472
|
this.logger.error(
|
|
1137
1473
|
`services: failed to init initial services when credentials available, ${error?.message}`
|
|
1138
1474
|
);
|
|
1139
|
-
})
|
|
1475
|
+
})
|
|
1476
|
+
.finally(() => this._finalizeReady());
|
|
1140
1477
|
} else {
|
|
1141
1478
|
const {email} = this.webex.config;
|
|
1142
1479
|
|
|
1143
|
-
this.collectPreauthCatalog(email ? {email} : undefined)
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1480
|
+
Promise.race([this.collectPreauthCatalog(email ? {email} : undefined), timeout])
|
|
1481
|
+
.catch((error) => {
|
|
1482
|
+
this.initFailed = true;
|
|
1483
|
+
this.logger.error(
|
|
1484
|
+
`services: failed to init initial services when no credentials available, ${error?.message}`
|
|
1485
|
+
);
|
|
1486
|
+
})
|
|
1487
|
+
.finally(() => this._finalizeReady());
|
|
1488
|
+
// Listen for when credentials become available to fetch the full catalog.
|
|
1489
|
+
// This handles fresh login where 'loaded' fires before OAuth completes.
|
|
1490
|
+
this.listenToOnce(this.webex, 'change:canAuthorize', () => {
|
|
1491
|
+
if (this.webex.canAuthorize && !catalog.status.postauth.ready) {
|
|
1492
|
+
this.initServiceCatalogs()
|
|
1493
|
+
.then(() => {
|
|
1494
|
+
catalog.isReady = true;
|
|
1495
|
+
})
|
|
1496
|
+
.catch((error) => {
|
|
1497
|
+
this.logger.error(
|
|
1498
|
+
`services: failed to init service catalogs after auth, ${error?.message}`
|
|
1499
|
+
);
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1148
1502
|
});
|
|
1149
1503
|
}
|
|
1150
1504
|
});
|