@pie-players/pie-players-shared 0.3.75 → 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.
- package/README.md +88 -77
- package/dist/components/vendor/nds/README.md +4 -5
- package/dist/config/profile.d.ts +1 -1
- package/dist/config/profile.js +1 -1
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/language-tags.d.ts +9 -1
- package/dist/i18n/language-tags.js +11 -1
- package/dist/i18n/messages/en-US.d.ts +33 -5
- package/dist/i18n/messages/en-US.js +33 -5
- package/dist/i18n/messages/nl-NL.d.ts +33 -5
- package/dist/i18n/messages/nl-NL.js +33 -5
- package/dist/i18n/provider.d.ts +14 -7
- package/dist/i18n/provider.js +26 -29
- package/dist/index.js +0 -1
- package/dist/instrumentation/providers/BaseInstrumentationProvider.js +0 -14
- package/dist/loader-config.d.ts +0 -2
- package/dist/loader-config.js +0 -2
- package/dist/loaders/element-loader.js +13 -6
- package/dist/loaders/esm-adapter.js +48 -37
- package/dist/loaders/iife-adapter.d.ts +3 -3
- package/dist/loaders/iife-adapter.js +36 -35
- package/dist/media/index.d.ts +4 -6
- package/dist/media/index.js +6 -8
- package/dist/object/index.d.ts +8 -3
- package/dist/object/index.js +8 -6
- package/dist/pie/asset-handler.d.ts +0 -17
- package/dist/pie/asset-handler.js +1 -1
- package/dist/pie/authoring.js +5 -7
- package/dist/pie/elements-rendered.d.ts +0 -2
- package/dist/pie/elements-rendered.js +1 -1
- package/dist/pie/initialization.d.ts +13 -14
- package/dist/pie/initialization.js +61 -75
- package/dist/pie/item-session-contract.js +7 -9
- package/dist/pie/resource-monitor.d.ts +34 -32
- package/dist/pie/resource-monitor.js +152 -231
- package/dist/pie/stages.d.ts +4 -3
- package/dist/pie/stages.js +4 -3
- package/dist/pie/updates.js +5 -4
- package/dist/pie/utils.d.ts +0 -2
- package/dist/pie/utils.js +0 -2
- package/dist/security/sanitize-item-markup.d.ts +4 -4
- package/dist/security/sanitize-item-markup.js +4 -4
- package/dist/security/sanitize-svg-icon.js +2 -2
- package/dist/security/wrap-overwide-images.d.ts +1 -1
- package/dist/security/wrap-overwide-images.js +1 -1
- package/dist/timed-media/media-element-source.js +1 -2
- package/dist/types/index.d.ts +62 -52
- package/dist/types/index.js +2 -2
- package/dist/ui/attribute-coercion.d.ts +5 -0
- package/dist/ui/attribute-coercion.js +6 -4
- package/dist/ui/content-styles.d.ts +6 -10
- package/dist/ui/content-styles.js +8 -13
- package/dist/ui/first-focusable.d.ts +0 -1
- package/dist/ui/first-focusable.js +0 -1
- package/dist/ui/focus-trap.js +2 -3
- package/dist/ui/scope-css.d.ts +23 -26
- package/dist/ui/scope-css.js +23 -26
- package/package.json +3 -2
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
269
|
-
*
|
|
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
|
-
//
|
|
383
|
-
//
|
|
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,
|
|
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
|
-
|
|
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
|
|
412
|
-
|
|
413
|
-
//
|
|
414
|
-
//
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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.
|
|
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.
|
|
660
|
+
if (isStale() || !this.isMediaRetryRecovered(url))
|
|
665
661
|
return;
|
|
666
|
-
|
|
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
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
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
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
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
|
-
|
|
823
|
-
if (
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
|
|
900
|
-
|
|
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
|
-
|
|
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 =
|
|
995
|
-
const strategy = elementTag
|
|
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
|
-
*
|
|
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 =
|
|
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,
|
|
1056
|
+
composed: true,
|
|
1136
1057
|
});
|
|
1137
1058
|
this.container.dispatchEvent(event);
|
|
1138
1059
|
}
|