@pie-players/pie-players-shared 0.3.73 → 0.3.74

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 (123) hide show
  1. package/README.md +27 -1
  2. package/dist/components/vendor/nds/README.md +7 -5
  3. package/dist/config/profile.d.ts +0 -2
  4. package/dist/config/profile.js +2 -8
  5. package/dist/i18n/index.d.ts +2 -3
  6. package/dist/i18n/index.js +2 -3
  7. package/dist/i18n/messages/en-US.d.ts +10 -0
  8. package/dist/i18n/messages/en-US.js +12 -2
  9. package/dist/i18n/messages/nl-NL.d.ts +6 -0
  10. package/dist/i18n/messages/nl-NL.js +8 -2
  11. package/dist/i18n/provider.d.ts +4 -5
  12. package/dist/i18n/provider.js +4 -5
  13. package/dist/index.d.ts +4 -2
  14. package/dist/index.js +3 -1
  15. package/dist/instrumentation/index.d.ts +5 -4
  16. package/dist/instrumentation/index.js +5 -4
  17. package/dist/instrumentation/new-relic-agent.d.ts +20 -0
  18. package/dist/instrumentation/new-relic-agent.js +24 -0
  19. package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts +5 -4
  20. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts +7 -7
  21. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js +21 -32
  22. package/dist/instrumentation/types.d.ts +7 -3
  23. package/dist/loader-config.d.ts +16 -6
  24. package/dist/loaders/controller-loading.d.ts +14 -0
  25. package/dist/loaders/controller-loading.js +12 -0
  26. package/dist/loaders/defaults.d.ts +10 -0
  27. package/dist/loaders/defaults.js +10 -0
  28. package/dist/loaders/element-loader.d.ts +20 -8
  29. package/dist/loaders/element-loader.js +102 -36
  30. package/dist/loaders/element-package-policy.d.ts +2 -0
  31. package/dist/loaders/element-package-policy.js +2 -1
  32. package/dist/loaders/esm-adapter.d.ts +18 -0
  33. package/dist/loaders/esm-adapter.js +617 -102
  34. package/dist/loaders/iife-adapter.d.ts +0 -5
  35. package/dist/loaders/iife-adapter.js +3 -9
  36. package/dist/loaders/index.d.ts +10 -3
  37. package/dist/loaders/index.js +8 -3
  38. package/dist/loaders/mathjax-version-conflict.d.ts +17 -0
  39. package/dist/loaders/mathjax-version-conflict.js +51 -0
  40. package/dist/loaders/module-shim.d.ts +12 -0
  41. package/dist/loaders/module-shim.js +12 -0
  42. package/dist/loaders/preloaded-alignment.d.ts +24 -0
  43. package/dist/loaders/preloaded-alignment.js +57 -0
  44. package/dist/loaders/preloaded-authored-tags.d.ts +26 -0
  45. package/dist/loaders/preloaded-authored-tags.js +69 -0
  46. package/dist/loaders/preloaded-registration.d.ts +56 -0
  47. package/dist/loaders/preloaded-registration.js +129 -0
  48. package/dist/media/index.d.ts +69 -0
  49. package/dist/media/index.js +194 -0
  50. package/dist/pie/authoring.d.ts +1 -1
  51. package/dist/pie/authoring.js +25 -18
  52. package/dist/pie/element-announcements.d.ts +39 -0
  53. package/dist/pie/element-announcements.js +67 -0
  54. package/dist/pie/element-observer.d.ts +3 -1
  55. package/dist/pie/element-observer.js +1 -0
  56. package/dist/pie/index.d.ts +4 -4
  57. package/dist/pie/index.js +5 -3
  58. package/dist/pie/initialization.js +13 -22
  59. package/dist/pie/initialize-element.d.ts +3 -0
  60. package/dist/pie/initialize-element.js +3 -4
  61. package/dist/pie/instrumentation-event-map.js +2 -9
  62. package/dist/pie/instrumentation-provider-resolution.d.ts +2 -1
  63. package/dist/pie/instrumentation-provider-resolution.js +0 -1
  64. package/dist/pie/item-session-contract.d.ts +8 -0
  65. package/dist/pie/item-session-contract.js +30 -9
  66. package/dist/pie/markup-math.d.ts +20 -0
  67. package/dist/pie/markup-math.js +118 -0
  68. package/dist/pie/math-rendering.d.ts +10 -21
  69. package/dist/pie/math-rendering.js +50 -35
  70. package/dist/pie/registry.d.ts +13 -1
  71. package/dist/pie/registry.js +39 -1
  72. package/dist/pie/resource-monitor.d.ts +6 -33
  73. package/dist/pie/resource-monitor.js +14 -213
  74. package/dist/pie/scoring.d.ts +11 -2
  75. package/dist/pie/scoring.js +18 -16
  76. package/dist/pie/session-commit.d.ts +16 -0
  77. package/dist/pie/session-commit.js +33 -5
  78. package/dist/pie/session-snapshot.d.ts +80 -0
  79. package/dist/pie/session-snapshot.js +166 -0
  80. package/dist/pie/tag-names.d.ts +1 -0
  81. package/dist/pie/tag-names.js +1 -0
  82. package/dist/pie/types.d.ts +4 -5
  83. package/dist/pie/types.js +1 -0
  84. package/dist/pie/updates.d.ts +2 -2
  85. package/dist/pie/updates.js +33 -15
  86. package/dist/pie/versioned-tag.d.ts +4 -0
  87. package/dist/pie/versioned-tag.js +4 -0
  88. package/dist/security/index.d.ts +2 -3
  89. package/dist/security/index.js +2 -3
  90. package/dist/security/sanitize-item-markup.d.ts +0 -2
  91. package/dist/security/sanitize-item-markup.js +0 -4
  92. package/dist/security/sanitize-svg-icon.d.ts +0 -2
  93. package/dist/security/sanitize-svg-icon.js +0 -5
  94. package/dist/security/wrap-overwide.d.ts +5 -0
  95. package/dist/security/wrap-overwide.js +16 -0
  96. package/dist/timed-media/policy.js +5 -16
  97. package/dist/types/index.d.ts +30 -311
  98. package/dist/types/index.js +0 -68
  99. package/dist/ui/attribute-coercion.d.ts +12 -0
  100. package/dist/ui/attribute-coercion.js +33 -0
  101. package/dist/ui/content-styles.d.ts +18 -16
  102. package/dist/ui/content-styles.js +67 -36
  103. package/dist/ui/overlay-containment.d.ts +31 -0
  104. package/dist/ui/overlay-containment.js +63 -0
  105. package/dist/ui/overlay-placement.d.ts +110 -0
  106. package/dist/ui/overlay-placement.js +222 -0
  107. package/dist/ui/pointer-gesture.d.ts +32 -0
  108. package/dist/ui/pointer-gesture.js +56 -0
  109. package/dist/ui/pointer-rotate.d.ts +29 -0
  110. package/dist/ui/pointer-rotate.js +39 -0
  111. package/package.json +21 -17
  112. package/dist/pie/component-context.d.ts +0 -21
  113. package/dist/pie/component-context.js +0 -29
  114. package/dist/pie/player-initializer.d.ts +0 -54
  115. package/dist/pie/player-initializer.js +0 -122
  116. package/dist/security/wrap-model-rich-content.d.ts +0 -1
  117. package/dist/security/wrap-model-rich-content.js +0 -40
  118. package/dist/server/npm-auth-env.d.ts +0 -13
  119. package/dist/server/npm-auth-env.js +0 -64
  120. package/dist/server/npm-registry.d.ts +0 -7
  121. package/dist/server/npm-registry.js +0 -59
  122. package/dist/ui/debug-panel-persistence.d.ts +0 -17
  123. package/dist/ui/debug-panel-persistence.js +0 -37
@@ -3,8 +3,20 @@
3
3
  *
4
4
  * Manages the global PIE registry that tracks loaded elements.
5
5
  */
6
- import type { PieRegistry } from "./types.js";
6
+ import type { Entry, PieRegistry } from "./types.js";
7
7
  /**
8
8
  * Get or create the global PIE registry
9
9
  */
10
10
  export declare const pieRegistry: () => PieRegistry;
11
+ /**
12
+ * Record a tag in `window.PIE_REGISTRY`. Every registration path writes through
13
+ * here: the IIFE and ESM loaders, bundle initialization, and
14
+ * `registerPreloadedElements`.
15
+ *
16
+ * The first loaded entry for a tag `customElements` holds describes that
17
+ * definition, which no later load replaces, so it keeps its package and
18
+ * element. A later write fills only what it left empty: a controller, with the
19
+ * bundle type that supplied it, and a config. Any other entry is replaced.
20
+ * Returns the entry the registry holds afterwards.
21
+ */
22
+ export declare function writeRegistryEntry(entry: Entry): Entry;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Manages the global PIE registry that tracks loaded elements.
5
5
  */
6
- import { isPieRegistryAvailable } from "./types.js";
6
+ import { isPieRegistryAvailable, Status } from "./types.js";
7
7
  /**
8
8
  * Get or create the global PIE registry
9
9
  */
@@ -18,3 +18,41 @@ export const pieRegistry = () => {
18
18
  }
19
19
  return registry;
20
20
  };
21
+ /**
22
+ * Record a tag in `window.PIE_REGISTRY`. Every registration path writes through
23
+ * here: the IIFE and ESM loaders, bundle initialization, and
24
+ * `registerPreloadedElements`.
25
+ *
26
+ * The first loaded entry for a tag `customElements` holds describes that
27
+ * definition, which no later load replaces, so it keeps its package and
28
+ * element. A later write fills only what it left empty: a controller, with the
29
+ * bundle type that supplied it, and a config. Any other entry is replaced.
30
+ * Returns the entry the registry holds afterwards.
31
+ */
32
+ export function writeRegistryEntry(entry) {
33
+ const registry = pieRegistry();
34
+ const existing = registry[entry.tagName];
35
+ if (!existing ||
36
+ existing.status !== Status.loaded ||
37
+ !isDefinedTag(entry.tagName)) {
38
+ registry[entry.tagName] = entry;
39
+ return entry;
40
+ }
41
+ const fillsController = !existing.controller && !!entry.controller;
42
+ const fillsConfig = existing.config == null && entry.config != null;
43
+ if (!fillsController && !fillsConfig)
44
+ return existing;
45
+ const filled = { ...existing };
46
+ if (fillsController) {
47
+ filled.controller = entry.controller;
48
+ filled.bundleType = entry.bundleType;
49
+ }
50
+ if (fillsConfig)
51
+ filled.config = entry.config;
52
+ registry[entry.tagName] = filled;
53
+ return filled;
54
+ }
55
+ function isDefinedTag(tagName) {
56
+ return (typeof customElements !== "undefined" &&
57
+ customElements.get(tagName) !== undefined);
58
+ }
@@ -7,11 +7,11 @@
7
7
  * Features:
8
8
  * - Tracks resource load timing with PerformanceObserver
9
9
  * - Detects and retries failed resource loads
10
- * - Sends instrumentation to New Relic
10
+ * - With `trackPageActions`, reports loads, retries and failures to the
11
+ * instrumentation provider it is given
11
12
  * - Works with all resource types (audio, video, img, link)
12
13
  */
13
14
  import type { InstrumentationProvider } from "../instrumentation/types.js";
14
- import type { ComponentContext } from "./component-context.js";
15
15
  export type ResourceMonitorConfig = {
16
16
  /**
17
17
  * Enable tracking page actions/events
@@ -22,29 +22,15 @@ export type ResourceMonitorConfig = {
22
22
  /**
23
23
  * Instrumentation provider for tracking events and errors
24
24
  *
25
- * Optional. If not provided, defaults to NewRelicInstrumentationProvider.
26
- * The provider handles instrumentation gracefully - if New Relic is not available,
27
- * it will simply not track events (no errors thrown).
28
- *
29
- * To use a different provider, pass it here:
30
- *
31
- * @example
32
- * ```typescript
33
- * const provider = new ConsoleInstrumentationProvider();
34
- * await provider.initialize({ debug: true });
35
- *
36
- * const monitor = new ResourceMonitor({
37
- * trackPageActions: true,
38
- * instrumentationProvider: provider
39
- * });
40
- * ```
25
+ * The monitor constructs no provider of its own: without one it only
26
+ * retries. Players pass what `resolveInstrumentationProvider` resolves from
27
+ * their `loaderConfig`.
41
28
  */
42
29
  instrumentationProvider?: InstrumentationProvider;
43
30
  /**
44
31
  * Whether ResourceMonitor should initialize/destroy the instrumentation provider.
45
32
  *
46
- * Defaults to `true` when ResourceMonitor creates the provider internally, and
47
- * `false` when a provider is injected by the host.
33
+ * Defaults to `false`: the provider belongs to whoever passed it in.
48
34
  */
49
35
  manageProviderLifecycle?: boolean;
50
36
  /**
@@ -97,8 +83,6 @@ export declare class ResourceMonitor {
97
83
  private isBrowser;
98
84
  private containerResources;
99
85
  private provider;
100
- private readonly ownsProvider;
101
- private readonly manageProviderLifecycle;
102
86
  private started;
103
87
  private lifecycleVersion;
104
88
  private pendingRetryTimers;
@@ -229,14 +213,3 @@ export declare class ResourceMonitor {
229
213
  }>;
230
214
  };
231
215
  }
232
- /**
233
- * Create and start a resource monitor for a container
234
- */
235
- export declare function createResourceMonitor(container: HTMLElement, config?: ResourceMonitorConfig): ResourceMonitor;
236
- /**
237
- * Initialize global resource request tracking (404s for fonts/images/etc.)
238
- * Call this once when the app loads (client-side only).
239
- */
240
- export declare function initializeResourceRequestTracking(): void;
241
- export declare function getTrackedResourceRequests(): Map<string, ComponentContext[]>;
242
- export declare function clearTrackedResourceRequests(): void;
@@ -7,17 +7,15 @@
7
7
  * Features:
8
8
  * - Tracks resource load timing with PerformanceObserver
9
9
  * - Detects and retries failed resource loads
10
- * - Sends instrumentation to New Relic
10
+ * - With `trackPageActions`, reports loads, retries and failures to the
11
+ * instrumentation provider it is given
11
12
  * - Works with all resource types (audio, video, img, link)
12
13
  */
13
- import { NewRelicInstrumentationProvider } from "../instrumentation/providers/NewRelicInstrumentationProvider.js";
14
14
  import { isInstrumentationProvider } from "../instrumentation/provider-guards.js";
15
- import { getCurrentComponentContext } from "./component-context.js";
16
15
  import { createPieLogger, isGlobalDebugEnabled } from "./logger.js";
17
16
  const DEFAULT_CONFIG = {
18
17
  trackPageActions: false,
19
- instrumentationProvider: undefined,
20
- manageProviderLifecycle: undefined,
18
+ manageProviderLifecycle: false,
21
19
  maxRetries: 3,
22
20
  initialRetryDelay: 500,
23
21
  maxRetryDelay: 5000,
@@ -43,49 +41,39 @@ export class ResourceMonitor {
43
41
  isBrowser;
44
42
  containerResources = new Set(); // Track resources within our container
45
43
  provider;
46
- ownsProvider;
47
- manageProviderLifecycle;
48
44
  started = false;
49
45
  lifecycleVersion = 0;
50
46
  pendingRetryTimers = new Map();
51
47
  retryInFlight = new Set();
52
48
  pendingMediaChecks = new Map();
53
49
  constructor(config = {}) {
54
- const validInjectedProvider = isInstrumentationProvider(config.instrumentationProvider)
55
- ? config.instrumentationProvider
56
- : undefined;
57
- const hasInjectedProvider = !!validInjectedProvider;
58
- const manageProviderLifecycle = config.manageProviderLifecycle ?? !hasInjectedProvider;
59
50
  this.config = {
60
51
  trackPageActions: config.trackPageActions ?? DEFAULT_CONFIG.trackPageActions,
61
- instrumentationProvider: validInjectedProvider,
62
- manageProviderLifecycle,
52
+ manageProviderLifecycle: config.manageProviderLifecycle ??
53
+ DEFAULT_CONFIG.manageProviderLifecycle,
63
54
  maxRetries: config.maxRetries ?? DEFAULT_CONFIG.maxRetries,
64
55
  initialRetryDelay: config.initialRetryDelay ?? DEFAULT_CONFIG.initialRetryDelay,
65
56
  maxRetryDelay: config.maxRetryDelay ?? DEFAULT_CONFIG.maxRetryDelay,
66
57
  debug: config.debug ?? DEFAULT_CONFIG.debug,
67
58
  };
68
59
  this.logger = createPieLogger("resource-monitor", () => this.isDebugEnabled());
69
- if (config.instrumentationProvider && !validInjectedProvider) {
60
+ this.provider = isInstrumentationProvider(config.instrumentationProvider)
61
+ ? config.instrumentationProvider
62
+ : undefined;
63
+ if (config.instrumentationProvider && !this.provider) {
70
64
  if (this.isDebugEnabled()) {
71
65
  this.logger.warn("Ignoring invalid instrumentation provider; expected InstrumentationProvider shape");
72
66
  }
73
67
  }
74
- // Always use a provider - default to NewRelic if not specified
75
- this.ownsProvider = !this.config.instrumentationProvider;
76
- this.provider =
77
- this.config.instrumentationProvider ??
78
- new NewRelicInstrumentationProvider();
79
- this.manageProviderLifecycle = this.config.manageProviderLifecycle;
80
68
  // Initialize the provider (async, but don't block constructor)
81
- if (this.manageProviderLifecycle) {
69
+ if (this.provider && this.config.manageProviderLifecycle) {
82
70
  this.provider.initialize().catch((err) => {
83
71
  if (this.isDebugEnabled()) {
84
72
  this.logger.warn("Failed to initialize instrumentation provider:", err);
85
73
  }
86
74
  });
87
75
  }
88
- else if (this.isDebugEnabled()) {
76
+ else if (this.provider && this.isDebugEnabled()) {
89
77
  this.logger.debug("Skipping provider lifecycle management for injected provider");
90
78
  }
91
79
  this.isBrowser =
@@ -186,7 +174,7 @@ export class ResourceMonitor {
186
174
  * Track event with instrumentation provider if enabled
187
175
  */
188
176
  trackInstrumentationEvent(eventName, attributes) {
189
- if (!this.config.trackPageActions || !this.provider.isReady()) {
177
+ if (!this.config.trackPageActions || !this.provider?.isReady()) {
190
178
  return;
191
179
  }
192
180
  this.provider.trackEvent(eventName, {
@@ -199,7 +187,7 @@ export class ResourceMonitor {
199
187
  * Track error with instrumentation provider if enabled
200
188
  */
201
189
  trackInstrumentationError(error, attributes) {
202
- if (!this.config.trackPageActions || !this.provider.isReady()) {
190
+ if (!this.config.trackPageActions || !this.provider?.isReady()) {
203
191
  return;
204
192
  }
205
193
  this.provider.trackError(error, {
@@ -264,7 +252,7 @@ export class ResourceMonitor {
264
252
  this.containerResources.clear();
265
253
  this.container = null;
266
254
  this.started = false;
267
- if (this.manageProviderLifecycle) {
255
+ if (this.provider && this.config.manageProviderLifecycle) {
268
256
  try {
269
257
  this.provider.destroy();
270
258
  }
@@ -1162,190 +1150,3 @@ export class ResourceMonitor {
1162
1150
  };
1163
1151
  }
1164
1152
  }
1165
- /**
1166
- * Create and start a resource monitor for a container
1167
- */
1168
- export function createResourceMonitor(container, config = {}) {
1169
- const monitor = new ResourceMonitor(config);
1170
- monitor.start(container);
1171
- return monitor;
1172
- }
1173
- // =============================================================================
1174
- // Global resource request tracking (consolidated from the old font-request-tracker)
1175
- // =============================================================================
1176
- // Track failed resource requests (by URL)
1177
- const failedRequests = new Map();
1178
- function isResourceFile(url) {
1179
- const resourceExtensions = [
1180
- // Fonts
1181
- ".woff",
1182
- ".woff2",
1183
- ".ttf",
1184
- ".otf",
1185
- ".eot",
1186
- // Images
1187
- ".gif",
1188
- ".jpg",
1189
- ".jpeg",
1190
- ".png",
1191
- ".svg",
1192
- ".webp",
1193
- ".ico",
1194
- // Audio/Video
1195
- ".mp3",
1196
- ".mp4",
1197
- ".wav",
1198
- ".ogg",
1199
- ".webm",
1200
- // Other assets
1201
- ".pdf",
1202
- ".css",
1203
- ".js",
1204
- ];
1205
- return resourceExtensions.some((ext) => url.toLowerCase().includes(ext));
1206
- }
1207
- function getResourceType(url) {
1208
- if (url.match(/\.(woff|woff2|ttf|otf|eot)/i))
1209
- return "Font";
1210
- if (url.match(/\.(gif|jpg|jpeg|png|svg|webp|ico)/i))
1211
- return "Image";
1212
- if (url.match(/\.(mp3|mp4|wav|ogg|webm)/i))
1213
- return "Media";
1214
- if (url.match(/\.(css)/i))
1215
- return "Stylesheet";
1216
- if (url.match(/\.(js)/i))
1217
- return "Script";
1218
- return "Resource";
1219
- }
1220
- function logFailedRequest(url, status, context) {
1221
- // Only log 404s and other failures (status 0 for network errors)
1222
- if (status !== 404 && status !== 0) {
1223
- return;
1224
- }
1225
- const key = url;
1226
- if (!failedRequests.has(key)) {
1227
- failedRequests.set(key, []);
1228
- }
1229
- const contexts = failedRequests.get(key);
1230
- contexts.push({
1231
- ...(context || { componentName: "Unknown", timestamp: Date.now() }),
1232
- timestamp: Date.now(),
1233
- });
1234
- const resourceType = getResourceType(url);
1235
- const filename = url.split("/").pop() || url;
1236
- const itemId = context?.itemId || "Unknown";
1237
- const componentName = context?.componentName || "Unknown component";
1238
- const elementType = context?.elementType || "";
1239
- console.warn(`Failed ${resourceType} request`, `\n Resource: ${filename}`, `\n URL: ${url}`, `\n Item ID: ${itemId}`, `\n Mini-Player: ${componentName}`, elementType ? `\n Element Type: ${elementType}` : "", `\n Status: ${status}`, context
1240
- ? ""
1241
- : "\n Note: Component not tracked - may be from item markup or PIE element");
1242
- }
1243
- /**
1244
- * Initialize global resource request tracking (404s for fonts/images/etc.)
1245
- * Call this once when the app loads (client-side only).
1246
- */
1247
- export function initializeResourceRequestTracking() {
1248
- if (typeof window === "undefined")
1249
- return;
1250
- // Track fetch requests
1251
- const originalFetch = window.fetch;
1252
- window.fetch = async function (...args) {
1253
- // Extract URL from first argument (can be string, URL, or Request)
1254
- let url = "";
1255
- if (typeof args[0] === "string") {
1256
- url = args[0];
1257
- }
1258
- else if (args[0] instanceof URL) {
1259
- url = args[0].toString();
1260
- }
1261
- else if (args[0] instanceof Request) {
1262
- url = args[0].url;
1263
- }
1264
- if (isResourceFile(url) || url.startsWith("/")) {
1265
- const context = getCurrentComponentContext();
1266
- try {
1267
- const response = await originalFetch(...args);
1268
- if (response.status === 404) {
1269
- logFailedRequest(url, response.status, context);
1270
- }
1271
- return response;
1272
- }
1273
- catch (error) {
1274
- logFailedRequest(url, 0, context);
1275
- throw error;
1276
- }
1277
- }
1278
- return originalFetch(...args);
1279
- };
1280
- // Track XMLHttpRequest
1281
- const originalXHROpen = XMLHttpRequest.prototype.open;
1282
- XMLHttpRequest.prototype.open = function (method, url, async, username, password) {
1283
- const urlString = typeof url === "string" ? url : url.toString();
1284
- if (isResourceFile(urlString) || urlString.startsWith("/")) {
1285
- const context = getCurrentComponentContext();
1286
- this.addEventListener("load", function () {
1287
- if (this.status === 404) {
1288
- logFailedRequest(urlString, this.status, context);
1289
- }
1290
- });
1291
- this.addEventListener("error", function () {
1292
- logFailedRequest(urlString, 0, context);
1293
- });
1294
- }
1295
- return originalXHROpen.call(this, method, url, async ?? true, username ?? null, password ?? null);
1296
- };
1297
- // Intercept img tag error events (most reliable way to catch failed image loads)
1298
- const errorHandler = (event) => {
1299
- const target = event.target;
1300
- if (target instanceof HTMLImageElement && target.src) {
1301
- const url = target.src;
1302
- if (isResourceFile(url) ||
1303
- url.startsWith("/") ||
1304
- url.startsWith(window.location.origin + "/")) {
1305
- const context = getCurrentComponentContext();
1306
- const relativeUrl = url.startsWith(window.location.origin)
1307
- ? url.replace(window.location.origin, "")
1308
- : url;
1309
- logFailedRequest(relativeUrl, 404, context);
1310
- }
1311
- }
1312
- };
1313
- document.addEventListener("error", errorHandler, true);
1314
- // Track resource requests via PerformanceObserver (fonts, CSS resources, etc.)
1315
- const observer = new PerformanceObserver((list) => {
1316
- for (const entry of list.getEntries()) {
1317
- if (entry.entryType !== "resource")
1318
- continue;
1319
- const resourceEntry = entry;
1320
- const url = resourceEntry.name;
1321
- if (isResourceFile(url) ||
1322
- url.startsWith("/") ||
1323
- url.startsWith(window.location.origin + "/")) {
1324
- const perf = resourceEntry;
1325
- const failed = perf.responseEnd === 0 && perf.duration > 0;
1326
- const mightBe404 = perf.transferSize === 0 &&
1327
- perf.responseEnd > 0 &&
1328
- perf.responseStart === 0;
1329
- if (failed || mightBe404) {
1330
- const context = getCurrentComponentContext();
1331
- const relativeUrl = url.startsWith(window.location.origin)
1332
- ? url.replace(window.location.origin, "")
1333
- : url;
1334
- logFailedRequest(relativeUrl, 404, context);
1335
- }
1336
- }
1337
- }
1338
- });
1339
- try {
1340
- observer.observe({ type: "resource", buffered: true });
1341
- }
1342
- catch (e) {
1343
- console.warn("Failed to set up PerformanceObserver for resource tracking:", e);
1344
- }
1345
- }
1346
- export function getTrackedResourceRequests() {
1347
- return new Map(failedRequests);
1348
- }
1349
- export function clearTrackedResourceRequests() {
1350
- failedRequests.clear();
1351
- }
@@ -4,10 +4,17 @@
4
4
  * Scoring and outcome calculation for PIE elements.
5
5
  */
6
6
  import type { ConfigEntity, Env, OutcomeResponse, PieController, PieModel } from "../types/index.js";
7
+ import { BundleType } from "./types.js";
7
8
  /**
8
- * Find the controller for a PIE element
9
+ * Find the controller for a PIE element.
10
+ *
11
+ * `deliveryBundleType` is the bundle type of the player asking. A `player.js`
12
+ * delivery, which is what a hosted player is, resolves no controller: its
13
+ * models and scores come from the server, and the registry is shared with
14
+ * every other loader on the page, so a registered controller is no evidence
15
+ * that this player may run one.
9
16
  */
10
- export declare const findPieController: (elementName: string) => PieController | undefined;
17
+ export declare const findPieController: (elementName: string, deliveryBundleType?: BundleType) => PieController | undefined;
11
18
  export type ScorePieItemOptions = {
12
19
  /** Scope element lookup to a specific player/root. Defaults to document for existing callers. */
13
20
  container?: Pick<ParentNode, "querySelector">;
@@ -23,6 +30,8 @@ export type ScorePieItemOptions = {
23
30
  * Existing scorePieItem callers keep the filtered result shape by default.
24
31
  */
25
32
  includeMissingResults?: boolean;
33
+ /** The scoring player's bundle type; `player.js` scores nothing locally. */
34
+ bundleType?: BundleType;
26
35
  };
27
36
  export declare function scorePieItem(config: ConfigEntity, sessionData: any[], options: ScorePieItemOptions & {
28
37
  includeMissingResults: true;
@@ -10,9 +10,19 @@ import { findOrAddSession } from "./utils.js";
10
10
  // Create module-level logger (respects global debug flag - pass function for dynamic checking)
11
11
  const logger = createPieLogger("pie-scoring", () => isGlobalDebugEnabled());
12
12
  /**
13
- * Find the controller for a PIE element
13
+ * Find the controller for a PIE element.
14
+ *
15
+ * `deliveryBundleType` is the bundle type of the player asking. A `player.js`
16
+ * delivery, which is what a hosted player is, resolves no controller: its
17
+ * models and scores come from the server, and the registry is shared with
18
+ * every other loader on the page, so a registered controller is no evidence
19
+ * that this player may run one.
14
20
  */
15
- export const findPieController = (elementName) => {
21
+ export const findPieController = (elementName, deliveryBundleType) => {
22
+ if (deliveryBundleType === BundleType.player) {
23
+ logger.debug(`[findPieController] ℹ️ ${elementName} delivered as player.js; using server-processed models`);
24
+ return undefined;
25
+ }
16
26
  const registry = pieRegistry();
17
27
  logger.debug(`[findPieController] Looking for controller for: ${elementName}`);
18
28
  logger.debug(`[findPieController] Registry has entries for:`, Object.keys(registry));
@@ -30,20 +40,12 @@ export const findPieController = (elementName) => {
30
40
  controllerKeys: entry.controller ? Object.keys(entry.controller) : [],
31
41
  bundleType: entry.bundleType,
32
42
  });
33
- const controller = entry.controller;
43
+ // Registration writes `null` for a package without a controller.
44
+ const controller = entry.controller ?? undefined;
34
45
  if (!controller) {
35
- // Check if missing controller is expected based on bundle type
36
- if (entry.bundleType === BundleType.clientPlayer) {
37
- // client-player.js MUST have controllers
38
- logger.error(`[findPieController] ❌ CRITICAL: Registry entry exists for ${elementName} but controller is missing!`);
39
- logger.error(`[findPieController] Bundle type: ${entry.bundleType} (controllers required)`);
40
- logger.error(`[findPieController] Entry:`, entry);
41
- throw new Error(`No controller found for ${elementName}. client-player.js bundles MUST include controllers. Check bundle loading and registration.`);
42
- }
43
- else {
44
- // player.js doesn't have controllers - this is expected
45
- logger.debug(`[findPieController] ℹ️ No controller for ${elementName} - using server-processed models (player.js bundle)`);
46
- }
46
+ // A package without one, such as a legacy `@pie-element/protractor`,
47
+ // renders the model it is given, as `<pie-player>` passed it through.
48
+ logger.debug(`[findPieController] ℹ️ No controller for ${elementName}; its model is used as given`);
47
49
  }
48
50
  else {
49
51
  logger.debug(`[findPieController] ✅ Controller found for ${elementName} with functions:`, Object.keys(controller));
@@ -85,7 +87,7 @@ export async function scorePieItem(config, sessionData, options = {}) {
85
87
  logger.debug("found pieEl %O for model id %s", pieEl, model.id);
86
88
  const session = findOrAddSession(sessionData, model.id, model.element);
87
89
  if (pieEl) {
88
- const controller = findPieController(pieEl.localName);
90
+ const controller = findPieController(pieEl.localName, options.bundleType);
89
91
  if (controller?.outcome) {
90
92
  return {
91
93
  ...session,
@@ -17,6 +17,9 @@
17
17
  * element, and it does not reach `document`. An SPA host that unmounts the
18
18
  * player is served by `commitPendingSessions()` on the player element, called
19
19
  * before the unmount.
20
+ * - Ahead of all three, `flushPendingSessionNotifications` runs when focus
21
+ * leaves a player, so the event lands before the host acts on the click or
22
+ * key that moved it, and carries no reason.
20
23
  *
21
24
  * Two paths, per element:
22
25
  *
@@ -72,6 +75,19 @@ export declare function noteSessionObserved(target: EventTarget | null | undefin
72
75
  */
73
76
  export declare function noteSessionBaseline(root: ParentNode | null | undefined): void;
74
77
  export declare function commitPendingSessions(root: ParentNode | null | undefined, options?: CommitPendingSessionsOptions): CommitPendingSessionsResult;
78
+ /**
79
+ * Deliver every pending `session-changed` now, as an ordinary announcement.
80
+ *
81
+ * The flush a player runs when focus leaves it, ahead of whatever the learner
82
+ * is moving to, so the host hears the response while the item it belongs to is
83
+ * still current. Same two paths and the same discriminant as the commit, with
84
+ * no `sessionCommitReason`: an adopted element's event is exactly what its
85
+ * debounce would have delivered, only earlier, and the next seam finds nothing
86
+ * pending.
87
+ */
88
+ export declare function flushPendingSessionNotifications(root: ParentNode | null | undefined, options?: {
89
+ logger?: PieLogger;
90
+ }): CommitPendingSessionsResult;
75
91
  export interface BindPageLifecycleCommitOptions {
76
92
  /**
77
93
  * The subtree to commit, re-evaluated on every transition: a player's
@@ -17,6 +17,9 @@
17
17
  * element, and it does not reach `document`. An SPA host that unmounts the
18
18
  * player is served by `commitPendingSessions()` on the player element, called
19
19
  * before the unmount.
20
+ * - Ahead of all three, `flushPendingSessionNotifications` runs when focus
21
+ * leaves a player, so the event lands before the host acts on the click or
22
+ * key that moved it, and carries no reason.
20
23
  *
21
24
  * Two paths, per element:
22
25
  *
@@ -254,9 +257,14 @@ export function commitPendingSessions(root, options = {}) {
254
257
  };
255
258
  if (!root)
256
259
  return result;
257
- return markCommitsDuring(root, options.reason ?? "teardown", () => sweepPendingSessions(root, options, result));
260
+ const reason = options.reason ?? "teardown";
261
+ return markCommitsDuring(root, reason, () => sweepPendingSessions(root, reason, options.logger, result));
258
262
  }
259
- function sweepPendingSessions(root, options, result) {
263
+ /**
264
+ * `reason` is null for a flush, whose events are ordinary announcements.
265
+ */
266
+ function sweepPendingSessions(root, reason, logger, result) {
267
+ const caller = reason ? "commitPendingSessions" : "flushPendingSessionNotifications";
260
268
  for (const element of candidatesIn(root)) {
261
269
  const session = readSession(element);
262
270
  if (!isRecord(session)) {
@@ -307,7 +315,7 @@ function sweepPendingSessions(root, options, result) {
307
315
  }
308
316
  catch (error) {
309
317
  failed = true;
310
- options.logger?.warn(`[commitPendingSessions] ${element.tagName.toLowerCase()} failed to commit its pending session`, error);
318
+ logger?.warn(`[${caller}] ${element.tagName.toLowerCase()} failed to commit its pending session`, error);
311
319
  }
312
320
  finally {
313
321
  element.removeEventListener("session-changed", witness, true);
@@ -331,19 +339,39 @@ function sweepPendingSessions(root, options, result) {
331
339
  detail: {
332
340
  component: element.tagName.toLowerCase(),
333
341
  session: JSON.parse(signature),
334
- sessionCommitReason: options.reason ?? "teardown",
342
+ ...(reason ? { sessionCommitReason: reason } : {}),
335
343
  },
336
344
  }));
337
345
  writeObservedSignature(element, signature);
338
346
  result.synthesized += 1;
339
347
  }
340
348
  catch (error) {
341
- options.logger?.warn(`[commitPendingSessions] ${element.tagName.toLowerCase()} session commit dispatch failed`, error);
349
+ logger?.warn(`[${caller}] ${element.tagName.toLowerCase()} session commit dispatch failed`, error);
342
350
  result.skipped += 1;
343
351
  }
344
352
  }
345
353
  return result;
346
354
  }
355
+ /**
356
+ * Deliver every pending `session-changed` now, as an ordinary announcement.
357
+ *
358
+ * The flush a player runs when focus leaves it, ahead of whatever the learner
359
+ * is moving to, so the host hears the response while the item it belongs to is
360
+ * still current. Same two paths and the same discriminant as the commit, with
361
+ * no `sessionCommitReason`: an adopted element's event is exactly what its
362
+ * debounce would have delivered, only earlier, and the next seam finds nothing
363
+ * pending.
364
+ */
365
+ export function flushPendingSessionNotifications(root, options = {}) {
366
+ const result = {
367
+ committed: 0,
368
+ synthesized: 0,
369
+ skipped: 0,
370
+ };
371
+ if (!root)
372
+ return result;
373
+ return sweepPendingSessions(root, null, options.logger, result);
374
+ }
347
375
  /**
348
376
  * Commit on the page going away rather than on the player being removed from it.
349
377
  *