@pie-players/pie-players-shared 0.3.74 → 1.0.0-beta.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.
Files changed (86) hide show
  1. package/README.md +88 -75
  2. package/dist/components/vendor/nds/README.md +4 -5
  3. package/dist/config/profile.d.ts +1 -1
  4. package/dist/config/profile.js +1 -1
  5. package/dist/i18n/index.d.ts +1 -1
  6. package/dist/i18n/index.js +1 -1
  7. package/dist/i18n/language-tags.d.ts +9 -1
  8. package/dist/i18n/language-tags.js +11 -1
  9. package/dist/i18n/messages/en-US.d.ts +33 -28
  10. package/dist/i18n/messages/en-US.js +34 -29
  11. package/dist/i18n/messages/nl-NL.d.ts +33 -28
  12. package/dist/i18n/messages/nl-NL.js +34 -29
  13. package/dist/i18n/provider.d.ts +14 -7
  14. package/dist/i18n/provider.js +26 -29
  15. package/dist/i18n/types.d.ts +4 -5
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +0 -1
  18. package/dist/instrumentation/providers/BaseInstrumentationProvider.js +0 -14
  19. package/dist/loader-config.d.ts +0 -2
  20. package/dist/loader-config.js +0 -2
  21. package/dist/loaders/element-loader.js +13 -6
  22. package/dist/loaders/esm-adapter.d.ts +11 -0
  23. package/dist/loaders/esm-adapter.js +74 -45
  24. package/dist/loaders/iife-adapter.d.ts +3 -3
  25. package/dist/loaders/iife-adapter.js +36 -35
  26. package/dist/loaders/index.d.ts +4 -2
  27. package/dist/loaders/index.js +2 -1
  28. package/dist/loaders/mathjax-events.d.ts +22 -0
  29. package/dist/loaders/mathjax-events.js +65 -0
  30. package/dist/loaders/preloaded-registration.d.ts +18 -2
  31. package/dist/loaders/preloaded-registration.js +14 -2
  32. package/dist/media/index.d.ts +4 -6
  33. package/dist/media/index.js +6 -8
  34. package/dist/object/index.d.ts +8 -3
  35. package/dist/object/index.js +8 -6
  36. package/dist/pie/asset-handler.d.ts +0 -17
  37. package/dist/pie/asset-handler.js +1 -1
  38. package/dist/pie/authoring.js +5 -7
  39. package/dist/pie/elements-rendered.d.ts +23 -0
  40. package/dist/pie/elements-rendered.js +96 -0
  41. package/dist/pie/index.d.ts +4 -6
  42. package/dist/pie/index.js +4 -7
  43. package/dist/pie/initialization.d.ts +13 -14
  44. package/dist/pie/initialization.js +61 -75
  45. package/dist/pie/instrumentation-event-bridge.js +23 -1
  46. package/dist/pie/instrumentation-event-map.js +11 -18
  47. package/dist/pie/item-controller.js +4 -0
  48. package/dist/pie/item-session-contract.js +7 -9
  49. package/dist/pie/logger.d.ts +6 -0
  50. package/dist/pie/logger.js +10 -0
  51. package/dist/pie/math-assets.d.ts +48 -0
  52. package/dist/pie/math-assets.js +81 -0
  53. package/dist/pie/resource-monitor.d.ts +34 -32
  54. package/dist/pie/resource-monitor.js +152 -231
  55. package/dist/pie/scoring.d.ts +1 -6
  56. package/dist/pie/scoring.js +3 -8
  57. package/dist/pie/session-commit.js +1 -1
  58. package/dist/pie/stages.d.ts +11 -47
  59. package/dist/pie/stages.js +11 -47
  60. package/dist/pie/updates.js +5 -4
  61. package/dist/pie/utils.d.ts +0 -2
  62. package/dist/pie/utils.js +0 -2
  63. package/dist/security/sanitize-item-markup.d.ts +8 -4
  64. package/dist/security/sanitize-item-markup.js +52 -6
  65. package/dist/security/sanitize-svg-icon.js +2 -2
  66. package/dist/security/wrap-overwide-images.d.ts +1 -1
  67. package/dist/security/wrap-overwide-images.js +20 -1
  68. package/dist/security/wrap-overwide.d.ts +8 -0
  69. package/dist/security/wrap-overwide.js +47 -3
  70. package/dist/timed-media/media-element-source.js +1 -2
  71. package/dist/types/index.d.ts +88 -153
  72. package/dist/types/index.js +2 -2
  73. package/dist/ui/attribute-coercion.d.ts +5 -0
  74. package/dist/ui/attribute-coercion.js +6 -4
  75. package/dist/ui/content-styles.d.ts +6 -10
  76. package/dist/ui/content-styles.js +8 -13
  77. package/dist/ui/first-focusable.d.ts +0 -1
  78. package/dist/ui/first-focusable.js +0 -1
  79. package/dist/ui/focus-trap.js +2 -3
  80. package/dist/ui/scope-css.d.ts +23 -26
  81. package/dist/ui/scope-css.js +23 -26
  82. package/package.json +6 -4
  83. package/dist/loaders/mathjax-version-conflict.d.ts +0 -17
  84. package/dist/loaders/mathjax-version-conflict.js +0 -51
  85. package/dist/pie/stage-tracker.d.ts +0 -50
  86. package/dist/pie/stage-tracker.js +0 -105
@@ -21,13 +21,30 @@ const DEFAULT_CONFIG = {
21
21
  maxRetryDelay: 5000,
22
22
  debug: false,
23
23
  };
24
- // Constants
25
24
  const MAX_URL_LENGTH = 80;
26
- const URL_TRUNCATE_LENGTH = 77;
25
+ /** Leaves room for the "..." prefix within MAX_URL_LENGTH. */
26
+ const URL_TRUNCATE_LENGTH = MAX_URL_LENGTH - "...".length;
27
27
  const MEDIA_RETRY_RECONCILIATION_TIMEOUT_MS = 1200;
28
28
  const MEDIA_RETRY_RECONCILIATION_POLL_MS = 75;
29
+ /** The elements of each initiator type that carry a resource URL. */
30
+ const RESOURCE_SELECTOR_BY_INITIATOR = new Map([
31
+ ["img", "img[src]"],
32
+ ["audio", "audio[src]"],
33
+ ["video", "video[src]"],
34
+ ["link", "link[href]"],
35
+ ["source", "source[src]"],
36
+ ]);
37
+ /** How a retry reloads each element type, as named in the retry log. */
38
+ const RETRY_STRATEGY_BY_TAG = new Map([
39
+ ["img", "Cache-busting URL"],
40
+ ["link", "Cache-busting URL"],
41
+ ["audio", "element.load()"],
42
+ ["video", "element.load()"],
43
+ ]);
29
44
  /**
30
- * Tracks resource loads and provides retry capability
45
+ * Tracks the resource loads of one container and retries failed ones with
46
+ * exponential backoff and cache-busting URLs. The events it dispatches are
47
+ * listed in the event contract above.
31
48
  */
32
49
  export class ResourceMonitor {
33
50
  config;
@@ -35,13 +52,20 @@ export class ResourceMonitor {
35
52
  observer = null;
36
53
  mutationObserver = null;
37
54
  errorHandler = null;
55
+ /** Retries scheduled so far, keyed by the URL without retry parameters. */
38
56
  retryAttempts = new Map();
57
+ /** Elements that failed to load each URL; a retry reloads the first one. */
39
58
  retryTargets = new Map();
40
59
  container = null;
41
- isBrowser;
42
- containerResources = new Set(); // Track resources within our container
60
+ isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
61
+ /** URLs referenced inside the container; timing entries for others are ignored. */
62
+ containerResources = new Set();
43
63
  provider;
44
64
  started = false;
65
+ /**
66
+ * Bumped on every start and stop, so timers and listeners captured in an
67
+ * earlier lifecycle see themselves as stale.
68
+ */
45
69
  lifecycleVersion = 0;
46
70
  pendingRetryTimers = new Map();
47
71
  retryInFlight = new Set();
@@ -76,8 +100,6 @@ export class ResourceMonitor {
76
100
  else if (this.provider && this.isDebugEnabled()) {
77
101
  this.logger.debug("Skipping provider lifecycle management for injected provider");
78
102
  }
79
- this.isBrowser =
80
- typeof window !== "undefined" && typeof document !== "undefined";
81
103
  }
82
104
  /**
83
105
  * Check if debug logging is enabled (dynamically checks window.PIE_DEBUG)
@@ -219,9 +241,6 @@ export class ResourceMonitor {
219
241
  this.started = true;
220
242
  this.logger.info("✅ Resource monitoring started");
221
243
  }
222
- /**
223
- * Stop monitoring and clean up
224
- */
225
244
  stop() {
226
245
  if (!this.started)
227
246
  return;
@@ -265,8 +284,8 @@ export class ResourceMonitor {
265
284
  this.logger.info("Resource monitoring stopped");
266
285
  }
267
286
  /**
268
- * Set up MutationObserver to track DOM changes within container
269
- * This allows us to know which resources belong to our container
287
+ * Tracks resources added to the container, or retargeted inside it, so
288
+ * timing entries can be attributed to this container.
270
289
  */
271
290
  setupMutationObserver() {
272
291
  if (!this.isBrowser ||
@@ -278,13 +297,11 @@ export class ResourceMonitor {
278
297
  try {
279
298
  this.mutationObserver = new MutationObserver((mutations) => {
280
299
  for (const mutation of mutations) {
281
- // Check added nodes
282
300
  mutation.addedNodes.forEach((node) => {
283
301
  if (node instanceof HTMLElement) {
284
302
  this.scanElementForResources(node);
285
303
  }
286
304
  });
287
- // Check attribute changes (src, href changes)
288
305
  if (mutation.type === "attributes" &&
289
306
  mutation.target instanceof HTMLElement) {
290
307
  const target = mutation.target;
@@ -300,7 +317,6 @@ export class ResourceMonitor {
300
317
  }
301
318
  }
302
319
  });
303
- // Observe the container for changes
304
320
  this.mutationObserver.observe(this.container, {
305
321
  childList: true,
306
322
  subtree: true,
@@ -317,7 +333,6 @@ export class ResourceMonitor {
317
333
  * Scan an element and its descendants for resources
318
334
  */
319
335
  scanElementForResources(element) {
320
- // Check the element itself
321
336
  if (this.isResourceElement(element)) {
322
337
  const src = this.getResourceSrc(element);
323
338
  if (src) {
@@ -327,7 +342,6 @@ export class ResourceMonitor {
327
342
  }
328
343
  }
329
344
  }
330
- // Check descendants
331
345
  const resourceSelectors = [
332
346
  "img",
333
347
  "audio",
@@ -379,11 +393,11 @@ export class ResourceMonitor {
379
393
  }
380
394
  }
381
395
  });
382
- // Use 'type' (singular) instead of 'entryTypes' (plural) to support buffered option
383
- // The buffered flag only works with the newer 'type' parameter
396
+ // `buffered` replays resources that loaded before the observer
397
+ // started, and is honored only with the single `type` form.
384
398
  this.observer.observe({
385
399
  type: "resource",
386
- buffered: true, // Capture resources loaded before observer started
400
+ buffered: true,
387
401
  });
388
402
  this.logger.debug("PerformanceObserver set up successfully");
389
403
  }
@@ -391,13 +405,8 @@ export class ResourceMonitor {
391
405
  this.logger.warn("Failed to set up PerformanceObserver:", error);
392
406
  }
393
407
  }
394
- /**
395
- * Handle resource timing entry
396
- */
397
408
  handleResourceTiming(entry) {
398
- // Only track media and image resources that are relevant to PIE content
399
- const isRelevant = this.isRelevantResource(entry);
400
- if (!isRelevant) {
409
+ if (!this.isRelevantResource(entry)) {
401
410
  if (this.isDebugEnabled() &&
402
411
  (entry.initiatorType === "img" ||
403
412
  entry.initiatorType === "audio" ||
@@ -408,69 +417,17 @@ export class ResourceMonitor {
408
417
  }
409
418
  const duration = entry.duration;
410
419
  const size = entry.transferSize;
411
- const url = entry.name;
412
- const originalUrl = this.getOriginalUrl(url);
413
- // Detect actual failures: responseEnd === 0 means the request didn't complete
414
- // Note: transferSize === 0 is NOT a failure indicator (can be cache, CORS, or small resources)
415
- // We primarily rely on the error event handler for detecting failures
420
+ const originalUrl = this.getOriginalUrl(entry.name);
421
+ // A zero responseEnd means the request never completed. A zero
422
+ // transferSize does not: cached, cross-origin and tiny responses all
423
+ // report it. The error listener remains the main failure signal.
416
424
  const failed = entry.responseEnd === 0 && entry.duration > 0;
417
- // Check if this was a retry that succeeded
418
425
  const wasRetried = this.retryAttempts.has(originalUrl);
419
426
  const retryCount = this.retryAttempts.get(originalUrl) || 0;
420
- // Enhanced debug logging with detailed timing breakdown
421
- if (this.isDebugEnabled()) {
422
- const shortUrl = this.truncateUrl(url);
423
- const sizeKB = (size / 1024).toFixed(2);
424
- const status = failed ? "❌ FAILED" : "✅ SUCCESS";
425
- // Detailed timing breakdown
426
- const timingDetails = {
427
- total: `${duration.toFixed(2)}ms`,
428
- dns: entry.domainLookupEnd > 0
429
- ? `${(entry.domainLookupEnd - entry.domainLookupStart).toFixed(2)}ms`
430
- : "n/a",
431
- tcp: entry.connectEnd > 0
432
- ? `${(entry.connectEnd - entry.connectStart).toFixed(2)}ms`
433
- : "n/a",
434
- request: entry.responseStart > 0
435
- ? `${(entry.responseStart - entry.requestStart).toFixed(2)}ms`
436
- : "n/a",
437
- response: entry.responseEnd > 0
438
- ? `${(entry.responseEnd - entry.responseStart).toFixed(2)}ms`
439
- : "n/a",
440
- size: size > 0 ? `${sizeKB} KB` : "0 KB",
441
- type: entry.initiatorType,
442
- protocol: entry.nextHopProtocol || "unknown",
443
- };
444
- // Add retry context if this was retried
445
- const retryContext = wasRetried && !failed
446
- ? `\n 🔄 Retry Success: Succeeded after ${retryCount} ${retryCount === 1 ? "retry" : "retries"}`
447
- : "";
448
- this.logger.info(`📊 PIE Resource Load ${status}\n` +
449
- ` URL: ${shortUrl}\n` +
450
- ` Type: ${timingDetails.type} | Protocol: ${timingDetails.protocol}\n` +
451
- ` ⏱️ Total Time: ${timingDetails.total}\n` +
452
- ` └─ DNS Lookup: ${timingDetails.dns}\n` +
453
- ` └─ TCP Connect: ${timingDetails.tcp}\n` +
454
- ` └─ Request Time: ${timingDetails.request}\n` +
455
- ` └─ Response Time: ${timingDetails.response}\n` +
456
- ` 📦 Transfer Size: ${timingDetails.size}${retryContext}`);
457
- }
458
- else {
459
- // Simple logging when debug is off but tracking is on
460
- this.logger.debug(`Resource loaded: ${entry.name}`, {
461
- duration: `${duration.toFixed(2)}ms`,
462
- size: `${size} bytes`,
463
- type: entry.initiatorType,
464
- failed,
465
- wasRetried,
466
- retryCount,
467
- });
468
- }
469
- // Handle successful loads
427
+ this.logResourceTiming(entry, failed, wasRetried, retryCount);
470
428
  if (!failed) {
471
429
  this.handleSuccessfulLoad(originalUrl, entry, duration, size, retryCount, wasRetried);
472
430
  }
473
- // Track with instrumentation provider
474
431
  this.trackInstrumentationEvent("pie-resource-load", {
475
432
  url: entry.name,
476
433
  duration: Math.round(duration),
@@ -480,11 +437,58 @@ export class ResourceMonitor {
480
437
  wasRetried,
481
438
  retryCount,
482
439
  });
483
- // Track failed loads
484
440
  if (failed) {
485
441
  this.handleFailedLoad(originalUrl, entry, duration, retryCount, wasRetried);
486
442
  }
487
443
  }
444
+ logResourceTiming(entry, failed, wasRetried, retryCount) {
445
+ const duration = entry.duration;
446
+ const size = entry.transferSize;
447
+ if (!this.isDebugEnabled()) {
448
+ this.logger.debug(`Resource loaded: ${entry.name}`, {
449
+ duration: `${duration.toFixed(2)}ms`,
450
+ size: `${size} bytes`,
451
+ type: entry.initiatorType,
452
+ failed,
453
+ wasRetried,
454
+ retryCount,
455
+ });
456
+ return;
457
+ }
458
+ const shortUrl = this.truncateUrl(entry.name);
459
+ const sizeKB = (size / 1024).toFixed(2);
460
+ const status = failed ? "❌ FAILED" : "✅ SUCCESS";
461
+ const timingDetails = {
462
+ total: `${duration.toFixed(2)}ms`,
463
+ dns: entry.domainLookupEnd > 0
464
+ ? `${(entry.domainLookupEnd - entry.domainLookupStart).toFixed(2)}ms`
465
+ : "n/a",
466
+ tcp: entry.connectEnd > 0
467
+ ? `${(entry.connectEnd - entry.connectStart).toFixed(2)}ms`
468
+ : "n/a",
469
+ request: entry.responseStart > 0
470
+ ? `${(entry.responseStart - entry.requestStart).toFixed(2)}ms`
471
+ : "n/a",
472
+ response: entry.responseEnd > 0
473
+ ? `${(entry.responseEnd - entry.responseStart).toFixed(2)}ms`
474
+ : "n/a",
475
+ size: size > 0 ? `${sizeKB} KB` : "0 KB",
476
+ type: entry.initiatorType,
477
+ protocol: entry.nextHopProtocol || "unknown",
478
+ };
479
+ const retryContext = wasRetried && !failed
480
+ ? `\n 🔄 Retry Success: Succeeded after ${retryCount} ${retryCount === 1 ? "retry" : "retries"}`
481
+ : "";
482
+ this.logger.info(`📊 PIE Resource Load ${status}\n` +
483
+ ` URL: ${shortUrl}\n` +
484
+ ` Type: ${timingDetails.type} | Protocol: ${timingDetails.protocol}\n` +
485
+ ` ⏱️ Total Time: ${timingDetails.total}\n` +
486
+ ` └─ DNS Lookup: ${timingDetails.dns}\n` +
487
+ ` └─ TCP Connect: ${timingDetails.tcp}\n` +
488
+ ` └─ Request Time: ${timingDetails.request}\n` +
489
+ ` └─ Response Time: ${timingDetails.response}\n` +
490
+ ` 📦 Transfer Size: ${timingDetails.size}${retryContext}`);
491
+ }
488
492
  isLifecycleActive(version) {
489
493
  return this.started && this.lifecycleVersion === version;
490
494
  }
@@ -517,9 +521,6 @@ export class ResourceMonitor {
517
521
  }
518
522
  return null;
519
523
  }
520
- /**
521
- * Handle successful resource load
522
- */
523
524
  handleSuccessfulLoad(url, entry, duration, size, retryCount, wasRetried) {
524
525
  const detail = {
525
526
  url,
@@ -620,55 +621,45 @@ export class ResourceMonitor {
620
621
  this.clearPendingMediaCheck(url);
621
622
  this.clearRetryTracking(url);
622
623
  }
624
+ /**
625
+ * A retried media fetch that succeeds does not mean the element recovered:
626
+ * it can still hold an error or no source. Success waits until a media
627
+ * target reports metadata, checked on `loadedmetadata`/`canplay` and by
628
+ * polling, and the reconciliation timeout bounds the wait.
629
+ */
623
630
  reconcileMediaRetrySuccess(url, detail) {
624
631
  this.clearPendingMediaCheck(url);
625
632
  if (this.isMediaRetryRecovered(url)) {
626
- this.dispatchEvent("pie-resource-load-success", detail);
627
- this.finalizeRetrySuccess(url, detail);
633
+ this.completeMediaRetry(url, detail);
628
634
  return;
629
635
  }
630
636
  if (this.isDebugEnabled()) {
631
637
  this.logger.debug(`⏳ Waiting for media readiness before retry success: ${this.truncateUrl(url)}`);
632
638
  }
633
639
  const version = this.lifecycleVersion;
640
+ const isStale = () => !this.isLifecycleActive(version) || !this.retryAttempts.has(url);
634
641
  const mediaTargets = this.resolveMediaTargetsForUrl(url);
635
642
  const eventHandlers = [];
636
643
  const pollTimer = setInterval(() => {
637
- if (!this.isLifecycleActive(version)) {
638
- this.clearPendingMediaCheck(url);
639
- return;
640
- }
641
- if (!this.retryAttempts.has(url)) {
644
+ if (isStale()) {
642
645
  this.clearPendingMediaCheck(url);
643
646
  return;
644
647
  }
645
648
  if (this.isMediaRetryRecovered(url)) {
646
- this.dispatchEvent("pie-resource-load-success", detail);
647
- this.finalizeRetrySuccess(url, detail);
649
+ this.completeMediaRetry(url, detail);
648
650
  }
649
651
  }, MEDIA_RETRY_RECONCILIATION_POLL_MS);
650
652
  const timeoutTimer = setTimeout(() => {
651
- if (!this.isLifecycleActive(version)) {
652
- this.clearPendingMediaCheck(url);
653
- return;
654
- }
655
- if (!this.retryAttempts.has(url)) {
656
- this.clearPendingMediaCheck(url);
657
- return;
658
- }
659
653
  this.clearPendingMediaCheck(url);
654
+ if (isStale())
655
+ return;
660
656
  this.handleMediaRetryReconciliationTimeout(url, detail);
661
657
  }, MEDIA_RETRY_RECONCILIATION_TIMEOUT_MS);
662
658
  for (const { mediaEl } of mediaTargets) {
663
659
  const handler = () => {
664
- if (!this.isLifecycleActive(version))
660
+ if (isStale() || !this.isMediaRetryRecovered(url))
665
661
  return;
666
- if (!this.retryAttempts.has(url))
667
- return;
668
- if (!this.isMediaRetryRecovered(url))
669
- return;
670
- this.dispatchEvent("pie-resource-load-success", detail);
671
- this.finalizeRetrySuccess(url, detail);
662
+ this.completeMediaRetry(url, detail);
672
663
  };
673
664
  mediaEl.addEventListener("loadedmetadata", handler);
674
665
  mediaEl.addEventListener("canplay", handler);
@@ -685,6 +676,10 @@ export class ResourceMonitor {
685
676
  },
686
677
  });
687
678
  }
679
+ completeMediaRetry(url, detail) {
680
+ this.dispatchEvent("pie-resource-load-success", detail);
681
+ this.finalizeRetrySuccess(url, detail);
682
+ }
688
683
  handleMediaRetryReconciliationTimeout(url, detail) {
689
684
  const shortUrl = this.truncateUrl(url);
690
685
  this.logger.warn(`⚠️ PIE Resource Retry Ready Timeout\n` +
@@ -724,19 +719,14 @@ export class ResourceMonitor {
724
719
  mediaEl.dispatchEvent(event);
725
720
  }
726
721
  }
727
- /**
728
- * Handle failed resource load
729
- */
730
722
  handleFailedLoad(url, entry, duration, retryCount, wasRetried) {
731
723
  const shortUrl = this.truncateUrl(url);
732
724
  if (wasRetried) {
733
- // This is a retry that also failed - use warn since we'll retry again
734
725
  this.logger.warn(`⚠️ PIE Resource Retry Failed\n` +
735
726
  ` URL: ${shortUrl}\n` +
736
727
  ` Retry Attempt: ${retryCount}\n` +
737
728
  ` Remaining Attempts: ${this.config.maxRetries - retryCount}\n` +
738
729
  ` Status: Will ${retryCount >= this.config.maxRetries ? "give up" : "retry again"}`);
739
- // Dispatch retry failure event
740
730
  this.dispatchEvent("pie-resource-retry-failed", {
741
731
  url,
742
732
  resourceType: entry.initiatorType,
@@ -747,11 +737,9 @@ export class ResourceMonitor {
747
737
  });
748
738
  }
749
739
  else {
750
- // Initial failure - use warn since we'll retry
751
740
  this.logger.warn(`⚠️ PIE Resource Initial Load Failed\n` +
752
741
  ` URL: ${shortUrl}\n` +
753
742
  ` Status: Will attempt ${this.config.maxRetries} ${this.config.maxRetries === 1 ? "retry" : "retries"}`);
754
- // Dispatch initial failure event
755
743
  this.dispatchEvent("pie-resource-load-failed", {
756
744
  url,
757
745
  resourceType: entry.initiatorType,
@@ -761,7 +749,6 @@ export class ResourceMonitor {
761
749
  error: "Initial resource load failed",
762
750
  });
763
751
  }
764
- // Track error with instrumentation provider
765
752
  this.trackInstrumentationError(new Error(`Resource load failed: ${entry.name}`), {
766
753
  resourceUrl: entry.name,
767
754
  resourceType: entry.initiatorType,
@@ -771,109 +758,58 @@ export class ResourceMonitor {
771
758
  });
772
759
  }
773
760
  /**
774
- * Check if resource is relevant to our container
775
- * Uses container-scoped tracking via MutationObserver
776
- * Also retroactively checks if resource belongs to container if not yet tracked
761
+ * Whether a timing entry belongs to this container. Timing entries carry
762
+ * absolute URLs, so a suffix match against a tracked URL also counts. The
763
+ * buffered observer also reports resources that loaded before the scan
764
+ * tracked them; those are looked up in the container's DOM.
777
765
  */
778
766
  isRelevantResource(entry) {
779
767
  const url = entry.name;
780
- // Only track resources that we know are in our container
781
- let isInContainer = this.containerResources.has(url);
782
- if (!isInContainer) {
783
- // Also check if it's a relative URL that might match
784
- // (PerformanceResourceTiming gives absolute URLs)
785
- for (const containerUrl of this.containerResources) {
786
- if (url.endsWith(containerUrl) || containerUrl.endsWith(url)) {
787
- return true;
788
- }
789
- }
790
- // Retroactively check if this resource belongs to our container
791
- // This handles the case where resources load before MutationObserver scans
792
- // PerformanceObserver can capture resources that loaded before it started (buffered: true)
793
- // but if they loaded before MutationObserver scanned, they won't be in containerResources
794
- if (this.container) {
795
- // Simple check: if resource is a media/image type and we have a container,
796
- // check if any element in container has this src/href
797
- // This is a fallback for resources that loaded very quickly before scan completed
798
- const isInContainer = this.isResourceInContainer(url, entry.initiatorType);
799
- if (isInContainer) {
800
- this.containerResources.add(url);
801
- if (this.isDebugEnabled()) {
802
- this.logger.debug(`📌 Retroactively tracked resource: ${this.truncateUrl(url)}`);
803
- }
804
- return true;
805
- }
768
+ if (this.containerResources.has(url))
769
+ return true;
770
+ for (const containerUrl of this.containerResources) {
771
+ if (url.endsWith(containerUrl) || containerUrl.endsWith(url)) {
772
+ return true;
806
773
  }
807
774
  }
808
- return isInContainer;
775
+ if (!this.isResourceInContainer(url, entry.initiatorType))
776
+ return false;
777
+ this.containerResources.add(url);
778
+ if (this.isDebugEnabled()) {
779
+ this.logger.debug(`📌 Retroactively tracked resource: ${this.truncateUrl(url)}`);
780
+ }
781
+ return true;
809
782
  }
810
- /**
811
- * Check if a resource URL actually belongs to our container by checking DOM elements
812
- * This is a fallback for resources that loaded before MutationObserver scanned
813
- */
814
783
  isResourceInContainer(url, initiatorType) {
815
784
  if (!this.container) {
816
785
  return false;
817
786
  }
818
787
  try {
819
- // Check if any element in the container has this URL as src/href
820
788
  const urlObj = new URL(url);
821
789
  const urlPath = urlObj.pathname + urlObj.search;
822
- // For images, audio, video - check src attributes
823
- if (initiatorType === "img" ||
824
- initiatorType === "audio" ||
825
- initiatorType === "video") {
826
- const elements = this.container.querySelectorAll(`${initiatorType}[src]`);
827
- for (const el of elements) {
828
- const resourceEl = el;
829
- if (resourceEl.src &&
830
- (resourceEl.src === url || resourceEl.src.endsWith(urlPath))) {
831
- return true;
832
- }
833
- }
834
- }
835
- // For link elements (stylesheets) - check href
836
- if (initiatorType === "link") {
837
- const links = this.container.querySelectorAll("link[href]");
838
- for (const link of links) {
839
- const linkEl = link;
840
- if (linkEl.href &&
841
- (linkEl.href === url || linkEl.href.endsWith(urlPath))) {
842
- return true;
843
- }
844
- }
845
- }
846
- // For source elements (inside audio/video) - check src
847
- if (initiatorType === "source") {
848
- const sources = this.container.querySelectorAll("source[src]");
849
- for (const source of sources) {
850
- const sourceEl = source;
851
- if (sourceEl.src &&
852
- (sourceEl.src === url || sourceEl.src.endsWith(urlPath))) {
853
- return true;
854
- }
790
+ const selector = RESOURCE_SELECTOR_BY_INITIATOR.get(initiatorType);
791
+ if (!selector)
792
+ return false;
793
+ for (const el of this.container.querySelectorAll(selector)) {
794
+ const src = this.getResourceSrc(el);
795
+ if (src && (src === url || src.endsWith(urlPath))) {
796
+ return true;
855
797
  }
856
798
  }
857
799
  }
858
800
  catch (error) {
859
- // If URL parsing fails or querySelector fails, fall back to false
860
801
  if (this.isDebugEnabled()) {
861
802
  this.logger.debug(`Error checking if resource is in container: ${error}`);
862
803
  }
863
804
  }
864
805
  return false;
865
806
  }
866
- /**
867
- * Set up error event handler for resource loading failures
868
- */
869
807
  setupErrorHandler() {
870
808
  if (!this.container) {
871
809
  return;
872
810
  }
873
- // Use capturing phase to catch errors before they bubble
874
811
  this.errorHandler = (event) => {
875
812
  const target = event.target;
876
- // Only handle resource elements
877
813
  if (!this.isResourceElement(target)) {
878
814
  return;
879
815
  }
@@ -882,9 +818,7 @@ export class ResourceMonitor {
882
818
  if (!src) {
883
819
  return;
884
820
  }
885
- // Get the original URL without retry parameters
886
821
  const originalSrc = this.getOriginalUrl(src);
887
- // Check if we have retries remaining
888
822
  const currentRetries = this.retryAttempts.get(originalSrc) || 0;
889
823
  const remainingRetries = this.config.maxRetries - currentRetries;
890
824
  const willRetry = remainingRetries > 0;
@@ -896,14 +830,12 @@ export class ResourceMonitor {
896
830
  remainingRetries,
897
831
  willRetry,
898
832
  });
899
- // Enhanced debug logging for errors
900
- // Use warn if we'll retry, error if we've exhausted retries
833
+ const logMethod = willRetry
834
+ ? this.logger.warn.bind(this.logger)
835
+ : this.logger.error.bind(this.logger);
836
+ const icon = willRetry ? "⚠️" : "❌";
901
837
  if (this.isDebugEnabled()) {
902
838
  const shortUrl = this.truncateUrl(src);
903
- const logMethod = willRetry
904
- ? this.logger.warn.bind(this.logger)
905
- : this.logger.error.bind(this.logger);
906
- const icon = willRetry ? "⚠️" : "❌";
907
839
  logMethod(`${icon} PIE Resource Load Error\n` +
908
840
  ` Element: <${tagName}>\n` +
909
841
  ` URL: ${shortUrl}\n` +
@@ -912,21 +844,17 @@ export class ResourceMonitor {
912
844
  ` Action: ${willRetry ? "Will retry with exponential backoff" : "Max retries reached, giving up"}`, diagnostics);
913
845
  }
914
846
  else {
915
- const logMethod = willRetry
916
- ? this.logger.warn.bind(this.logger)
917
- : this.logger.error.bind(this.logger);
918
- const icon = willRetry ? "⚠️" : "❌";
919
847
  logMethod(`${icon} Resource error: ${tagName} failed to load ${src}`);
920
848
  }
921
849
  this.trackInstrumentationEvent("pie-resource-load-error", diagnostics);
922
- // Track error with instrumentation provider
923
850
  this.trackInstrumentationError(new Error(`Resource load error: ${originalSrc}`), {
924
851
  resourceType: tagName,
925
852
  ...diagnostics,
926
853
  });
927
- // Attempt retry with original URL
928
854
  this.retryResourceLoad(target, originalSrc);
929
855
  };
856
+ // Resource `error` events do not bubble, so only a capturing listener on
857
+ // the container sees them.
930
858
  this.container.addEventListener("error", this.errorHandler, true);
931
859
  this.logger.debug("Error handler attached to container");
932
860
  }
@@ -970,7 +898,6 @@ export class ResourceMonitor {
970
898
  else {
971
899
  this.logger.error(`❌ Failed to load resource after ${this.config.maxRetries} retries: ${url}`);
972
900
  }
973
- // Dispatch permanent failure event
974
901
  this.dispatchEvent("pie-resource-load-error", {
975
902
  url,
976
903
  resourceType,
@@ -978,27 +905,21 @@ export class ResourceMonitor {
978
905
  maxRetries: this.config.maxRetries,
979
906
  error: `Resource permanently failed after ${this.config.maxRetries} retries`,
980
907
  });
981
- // Track final failure with instrumentation provider
982
908
  this.trackInstrumentationError(new Error(`Resource permanently failed after ${this.config.maxRetries} retries: ${url}`), {
983
909
  resourceUrl: url,
984
910
  retryCount,
985
911
  resourceType,
986
912
  });
987
913
  }
988
- /**
989
- * Log retry schedule information
990
- */
914
+ /** Exponential backoff, capped at `maxRetryDelay`. */
915
+ retryDelay(retryCount) {
916
+ return Math.min(this.config.initialRetryDelay * 2 ** retryCount, this.config.maxRetryDelay);
917
+ }
991
918
  logRetrySchedule(url, retryCount, delay, elementTag) {
992
919
  if (this.isDebugEnabled()) {
993
920
  const shortUrl = this.truncateUrl(url);
994
- const nextDelay = Math.min(this.config.initialRetryDelay * Math.pow(2, retryCount + 1), this.config.maxRetryDelay);
995
- const strategy = elementTag === "img"
996
- ? "Cache-busting URL"
997
- : elementTag === "audio" || elementTag === "video"
998
- ? "element.load()"
999
- : elementTag === "link"
1000
- ? "Cache-busting URL"
1001
- : "URL update";
921
+ const nextDelay = this.retryDelay(retryCount + 1);
922
+ const strategy = RETRY_STRATEGY_BY_TAG.get(elementTag) ?? "URL update";
1002
923
  this.logger.info(`🔄 PIE Resource Retry Scheduled\n` +
1003
924
  ` URL: ${shortUrl}\n` +
1004
925
  ` Attempt: ${retryCount + 1}/${this.config.maxRetries}\n` +
@@ -1011,7 +932,8 @@ export class ResourceMonitor {
1011
932
  }
1012
933
  }
1013
934
  /**
1014
- * Retry loading a failed resource with exponential backoff
935
+ * Reloads one element under a cache-busting URL. Media elements also need
936
+ * `load()`, since changing a `<source>` alone does not restart the fetch.
1015
937
  */
1016
938
  performRetryLoadAttempt(element, originalSrc, attemptNumber) {
1017
939
  try {
@@ -1073,11 +995,15 @@ export class ResourceMonitor {
1073
995
  else {
1074
996
  this.logger.error(`Error during retry for ${originalSrc}:`, error);
1075
997
  }
1076
- this.trackInstrumentationError(new Error(`Retry attempt error for resource: ${originalSrc}`), {
1077
- ...diagnostics,
1078
- });
998
+ this.trackInstrumentationError(new Error(`Retry attempt error for resource: ${originalSrc}`), diagnostics);
1079
999
  }
1080
1000
  }
1001
+ /**
1002
+ * Schedules the next retry of a failed resource with exponential backoff,
1003
+ * or fails it permanently once `maxRetries` is spent. Every failing element
1004
+ * joins the URL's retry targets; concurrent errors for one URL share the
1005
+ * retry already in flight.
1006
+ */
1081
1007
  retryResourceLoad(element, originalSrc) {
1082
1008
  this.getRetryTargets(originalSrc).add(element);
1083
1009
  if (!this.started)
@@ -1093,7 +1019,7 @@ export class ResourceMonitor {
1093
1019
  this.handlePermanentFailure(originalSrc, element.tagName.toLowerCase(), currentRetries);
1094
1020
  return;
1095
1021
  }
1096
- const delay = Math.min(this.config.initialRetryDelay * Math.pow(2, currentRetries), this.config.maxRetryDelay);
1022
+ const delay = this.retryDelay(currentRetries);
1097
1023
  const attemptNumber = currentRetries + 1;
1098
1024
  this.retryInFlight.add(originalSrc);
1099
1025
  this.retryAttempts.set(originalSrc, attemptNumber);
@@ -1120,19 +1046,14 @@ export class ResourceMonitor {
1120
1046
  }, delay);
1121
1047
  this.pendingRetryTimers.set(originalSrc, timer);
1122
1048
  }
1123
- /**
1124
- * Dispatch a custom event from the container
1125
- */
1126
1049
  dispatchEvent(eventName, detail) {
1127
1050
  if (!this.container) {
1128
- // Silent return - if container is null, events can't be dispatched
1129
- // This should only happen if ResourceMonitor wasn't properly initialized
1130
1051
  return;
1131
1052
  }
1132
1053
  const event = new CustomEvent(eventName, {
1133
1054
  detail,
1134
1055
  bubbles: true,
1135
- composed: true, // Allow crossing shadow DOM boundaries
1056
+ composed: true,
1136
1057
  });
1137
1058
  this.container.dispatchEvent(event);
1138
1059
  }