@memberjunction/react-runtime 5.1.0 → 5.3.0

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.
@@ -103,8 +103,8 @@ export class LibraryLoader {
103
103
  static async loadLibrariesFromConfig(options?: ConfigLoadOptions, debug?: boolean): Promise<LibraryLoadResult> {
104
104
  // Always load core runtime libraries first
105
105
  const coreLibraries = getCoreRuntimeLibraries(debug);
106
- const corePromises = coreLibraries.map(lib =>
107
- this.loadScript(lib.cdnUrl, lib.globalVariable, debug)
106
+ const corePromises = coreLibraries.map(lib =>
107
+ this.loadScript(lib.cdnUrl, lib.globalVariable, debug, lib.fallbackCdnUrls)
108
108
  );
109
109
 
110
110
  const coreResults = await Promise.all(corePromises);
@@ -157,8 +157,8 @@ export class LibraryLoader {
157
157
  });
158
158
 
159
159
  // Load plugin libraries
160
- const pluginPromises = pluginLibraries.map(lib =>
161
- this.loadScript(lib.cdnUrl, lib.globalVariable, debug)
160
+ const pluginPromises = pluginLibraries.map(lib =>
161
+ this.loadScript(lib.cdnUrl, lib.globalVariable, debug, lib.fallbackCdnUrls)
162
162
  );
163
163
 
164
164
  const pluginResults = await Promise.all(pluginPromises);
@@ -219,10 +219,25 @@ export class LibraryLoader {
219
219
  }
220
220
 
221
221
  /**
222
- * Load a script from URL
222
+ * Load a script from URL, with optional fallback CDN URLs.
223
+ * Tries the primary URL first, then each fallback in order until one succeeds.
223
224
  */
224
- private static async loadScript(url: string, globalName: string, debug: boolean = false): Promise<any> {
225
- // Check if already loaded
225
+ private static async loadScript(
226
+ url: string,
227
+ globalName: string,
228
+ debug: boolean = false,
229
+ fallbackUrls?: string[]
230
+ ): Promise<any> {
231
+ // Check if the global is already available (loaded by any URL)
232
+ const existingGlobal = typeof window !== 'undefined' ? (window as any)[globalName] : undefined;
233
+ if (existingGlobal) {
234
+ if (debug) {
235
+ console.log(`✅ Library '${globalName}' already available globally`);
236
+ }
237
+ return existingGlobal;
238
+ }
239
+
240
+ // Check if already loaded from this URL
226
241
  const existing = this.loadedResources.get(url);
227
242
  if (existing) {
228
243
  if (debug) {
@@ -231,13 +246,71 @@ export class LibraryLoader {
231
246
  return existing.promise;
232
247
  }
233
248
 
249
+ // Try the primary URL first
250
+ try {
251
+ return await this.loadScriptFromUrl(url, globalName, debug);
252
+ } catch (primaryError) {
253
+ // If no fallbacks, rethrow immediately
254
+ if (!fallbackUrls || fallbackUrls.length === 0) {
255
+ throw primaryError;
256
+ }
257
+
258
+ // Clean up the failed primary entry so fallbacks can try fresh
259
+ this.cleanupFailedScript(url);
260
+
261
+ if (debug) {
262
+ console.warn(`⚠️ Primary CDN failed for '${globalName}', trying fallback CDNs...`);
263
+ }
264
+
265
+ // Try each fallback URL in order
266
+ for (let i = 0; i < fallbackUrls.length; i++) {
267
+ const fallbackUrl = fallbackUrls[i];
268
+ try {
269
+ const result = await this.loadScriptFromUrl(fallbackUrl, globalName, debug);
270
+ console.log(`✅ Library '${globalName}' loaded from fallback CDN: ${fallbackUrl}`);
271
+ return result;
272
+ } catch (fallbackError) {
273
+ this.cleanupFailedScript(fallbackUrl);
274
+ if (debug) {
275
+ console.warn(`⚠️ Fallback CDN ${i + 1}/${fallbackUrls.length} failed for '${globalName}'`);
276
+ }
277
+ }
278
+ }
279
+
280
+ // All URLs failed
281
+ const allUrls = [url, ...fallbackUrls];
282
+ throw new Error(
283
+ `Failed to load '${globalName}' from all CDN sources: ${allUrls.join(', ')}`
284
+ );
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Remove a failed script from the cache and DOM so a fallback URL can be tried.
290
+ */
291
+ private static cleanupFailedScript(url: string): void {
292
+ const failed = this.loadedResources.get(url);
293
+ if (failed?.element?.parentNode) {
294
+ failed.element.parentNode.removeChild(failed.element);
295
+ }
296
+ this.loadedResources.delete(url);
297
+ }
298
+
299
+ /**
300
+ * Load a single script from a specific URL. This is the low-level loader
301
+ * that handles DOM script element creation and global variable detection.
302
+ */
303
+ private static loadScriptFromUrl(url: string, globalName: string, debug: boolean = false): Promise<any> {
304
+ // Check if already cached for this URL
305
+ const existing = this.loadedResources.get(url);
306
+ if (existing) {
307
+ return existing.promise;
308
+ }
309
+
234
310
  const promise = new Promise((resolve, reject) => {
235
311
  // Check if global already exists
236
312
  const existingGlobal = (window as any)[globalName];
237
313
  if (existingGlobal) {
238
- if (debug) {
239
- console.log(`✅ Library '${globalName}' already available globally`);
240
- }
241
314
  resolve(existingGlobal);
242
315
  return;
243
316
  }
@@ -249,11 +322,12 @@ export class LibraryLoader {
249
322
  return;
250
323
  }
251
324
 
252
- // Create new script
325
+ // Create new script — no crossOrigin attribute so the browser uses standard
326
+ // "no-cors" mode, which avoids CORS failures when CDN headers are missing
327
+ // or browser tracking prevention interferes with the response.
253
328
  const script = document.createElement('script');
254
329
  script.src = url;
255
330
  script.async = true;
256
- script.crossOrigin = 'anonymous';
257
331
 
258
332
  const cleanup = () => {
259
333
  script.removeEventListener('load', onLoad);
@@ -262,7 +336,7 @@ export class LibraryLoader {
262
336
 
263
337
  const onLoad = async () => {
264
338
  cleanup();
265
-
339
+
266
340
  // Use progressive delay if enabled, otherwise use original behavior
267
341
  if (LibraryLoader.enableProgressiveDelay) {
268
342
  try {
@@ -281,7 +355,7 @@ export class LibraryLoader {
281
355
  resolve(global);
282
356
  } else {
283
357
  // Some libraries may take a moment to initialize
284
- const timeoutId = resourceManager.setTimeout(
358
+ resourceManager.setTimeout(
285
359
  LIBRARY_LOADER_COMPONENT_ID,
286
360
  () => {
287
361
  const delayedGlobal = (window as any)[globalName];
@@ -317,14 +391,14 @@ export class LibraryLoader {
317
391
  // These are harmless and expected when loading minified libraries from CDNs
318
392
  // that reference source maps which aren't available. This doesn't affect functionality.
319
393
  document.head.appendChild(script);
320
-
394
+
321
395
  // Register the script element for cleanup
322
396
  resourceManager.registerDOMElement(LIBRARY_LOADER_COMPONENT_ID, script);
323
397
  });
324
398
 
325
- this.loadedResources.set(url, {
326
- element: document.querySelector(`script[src="${url}"]`)!,
327
- promise
399
+ this.loadedResources.set(url, {
400
+ element: document.querySelector(`script[src="${url}"]`)!,
401
+ promise
328
402
  });
329
403
 
330
404
  return promise;