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