@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.
Files changed (59) hide show
  1. package/README.md +88 -77
  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 -5
  10. package/dist/i18n/messages/en-US.js +33 -5
  11. package/dist/i18n/messages/nl-NL.d.ts +33 -5
  12. package/dist/i18n/messages/nl-NL.js +33 -5
  13. package/dist/i18n/provider.d.ts +14 -7
  14. package/dist/i18n/provider.js +26 -29
  15. package/dist/index.js +0 -1
  16. package/dist/instrumentation/providers/BaseInstrumentationProvider.js +0 -14
  17. package/dist/loader-config.d.ts +0 -2
  18. package/dist/loader-config.js +0 -2
  19. package/dist/loaders/element-loader.js +13 -6
  20. package/dist/loaders/esm-adapter.js +48 -37
  21. package/dist/loaders/iife-adapter.d.ts +3 -3
  22. package/dist/loaders/iife-adapter.js +36 -35
  23. package/dist/media/index.d.ts +4 -6
  24. package/dist/media/index.js +6 -8
  25. package/dist/object/index.d.ts +8 -3
  26. package/dist/object/index.js +8 -6
  27. package/dist/pie/asset-handler.d.ts +0 -17
  28. package/dist/pie/asset-handler.js +1 -1
  29. package/dist/pie/authoring.js +5 -7
  30. package/dist/pie/elements-rendered.d.ts +0 -2
  31. package/dist/pie/elements-rendered.js +1 -1
  32. package/dist/pie/initialization.d.ts +13 -14
  33. package/dist/pie/initialization.js +61 -75
  34. package/dist/pie/item-session-contract.js +7 -9
  35. package/dist/pie/resource-monitor.d.ts +34 -32
  36. package/dist/pie/resource-monitor.js +152 -231
  37. package/dist/pie/stages.d.ts +4 -3
  38. package/dist/pie/stages.js +4 -3
  39. package/dist/pie/updates.js +5 -4
  40. package/dist/pie/utils.d.ts +0 -2
  41. package/dist/pie/utils.js +0 -2
  42. package/dist/security/sanitize-item-markup.d.ts +4 -4
  43. package/dist/security/sanitize-item-markup.js +4 -4
  44. package/dist/security/sanitize-svg-icon.js +2 -2
  45. package/dist/security/wrap-overwide-images.d.ts +1 -1
  46. package/dist/security/wrap-overwide-images.js +1 -1
  47. package/dist/timed-media/media-element-source.js +1 -2
  48. package/dist/types/index.d.ts +62 -52
  49. package/dist/types/index.js +2 -2
  50. package/dist/ui/attribute-coercion.d.ts +5 -0
  51. package/dist/ui/attribute-coercion.js +6 -4
  52. package/dist/ui/content-styles.d.ts +6 -10
  53. package/dist/ui/content-styles.js +8 -13
  54. package/dist/ui/first-focusable.d.ts +0 -1
  55. package/dist/ui/first-focusable.js +0 -1
  56. package/dist/ui/focus-trap.js +2 -3
  57. package/dist/ui/scope-css.d.ts +23 -26
  58. package/dist/ui/scope-css.js +23 -26
  59. package/package.json +3 -2
@@ -2,12 +2,10 @@
2
2
  * Validation primitives shared by every catalog card whose payload references
3
3
  * media.
4
4
  *
5
- * Extracted from `sign-language-cards.ts` when `spoken` cards gained a recorded
6
- * audio form: both card types take authored, wire-facing URLs and hand them to a
7
- * media element in the learner's browser, so both need the same scheme
8
- * allow-list and the same "treat as absent, never as partially valid" posture.
9
- * Two copies of a URL allow-list is one copy that gets a fix and one that does
10
- * not.
5
+ * `sign-language` and recorded `spoken` cards both hand authored, wire-facing
6
+ * URLs to a media element in the learner's browser, so both take the same
7
+ * scheme allow-list and the same "treat as absent, never as partially valid"
8
+ * posture from here.
11
9
  *
12
10
  * Lives here, behind its own subpath, so a PIE element can validate authored
13
11
  * media without importing the assessment toolkit, whose root entry declares
@@ -2,12 +2,10 @@
2
2
  * Validation primitives shared by every catalog card whose payload references
3
3
  * media.
4
4
  *
5
- * Extracted from `sign-language-cards.ts` when `spoken` cards gained a recorded
6
- * audio form: both card types take authored, wire-facing URLs and hand them to a
7
- * media element in the learner's browser, so both need the same scheme
8
- * allow-list and the same "treat as absent, never as partially valid" posture.
9
- * Two copies of a URL allow-list is one copy that gets a fix and one that does
10
- * not.
5
+ * `sign-language` and recorded `spoken` cards both hand authored, wire-facing
6
+ * URLs to a media element in the learner's browser, so both take the same
7
+ * scheme allow-list and the same "treat as absent, never as partially valid"
8
+ * posture from here.
11
9
  *
12
10
  * Lives here, behind its own subpath, so a PIE element can validate authored
13
11
  * media without importing the assessment toolkit, whose root entry declares
@@ -21,7 +19,7 @@
21
19
  * Relative and protocol-relative URLs are allowed — host content is commonly
22
20
  * served from the same origin as the player.
23
21
  */
24
- const DISALLOWED_SRC_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
22
+ const URL_SCHEME_PREFIX = /^[a-z][a-z0-9+.-]*:/i;
25
23
  const ALLOWED_SRC_SCHEMES = new Set(["http:", "https:", "data:", "blob:"]);
26
24
  /**
27
25
  * The only `MediaAssetRef.version` this build renders.
@@ -53,7 +51,7 @@ export function isSafeMediaSrc(raw) {
53
51
  return false;
54
52
  // Relative ("/video.mp4", "video.mp4") and protocol-relative ("//cdn/x.mp4")
55
53
  // forms carry no scheme to check and inherit the document's.
56
- if (src.startsWith("//") || !DISALLOWED_SRC_SCHEME.test(src))
54
+ if (src.startsWith("//") || !URL_SCHEME_PREFIX.test(src))
57
55
  return true;
58
56
  const scheme = src.slice(0, src.indexOf(":") + 1).toLowerCase();
59
57
  return ALLOWED_SRC_SCHEMES.has(scheme);
@@ -1,7 +1,7 @@
1
1
  /**
2
- * A problem using the spread operator merging objects is that undefined properties may override
3
- * defined properties, which is often NOT what you want (e.g. when providing default values). This
4
- * function merges objects, but ignores properties that are null or undefined.
2
+ * Merges objects left to right, skipping properties whose value is `null` or
3
+ * `undefined`, so a later object's unset field never overrides an earlier
4
+ * default the way object spread does.
5
5
  * @param objects The objects to merge.
6
6
  */
7
7
  export declare const mergeObjectsIgnoringNullUndefined: <T extends object>(...objects: T[]) => T;
@@ -9,3 +9,8 @@ export declare const mergeObjectsIgnoringNullUndefined: <T extends object>(...ob
9
9
  * Deep clone an object.
10
10
  */
11
11
  export declare const cloneDeep: <T>(value: T) => T;
12
+ /**
13
+ * A non-null object that is not an array. The prototype is not checked, so
14
+ * class instances pass.
15
+ */
16
+ export declare const isPlainRecord: (value: unknown) => value is Record<string, unknown>;
@@ -1,7 +1,7 @@
1
1
  /**
2
- * A problem using the spread operator merging objects is that undefined properties may override
3
- * defined properties, which is often NOT what you want (e.g. when providing default values). This
4
- * function merges objects, but ignores properties that are null or undefined.
2
+ * Merges objects left to right, skipping properties whose value is `null` or
3
+ * `undefined`, so a later object's unset field never overrides an earlier
4
+ * default the way object spread does.
5
5
  * @param objects The objects to merge.
6
6
  */
7
7
  export const mergeObjectsIgnoringNullUndefined = (...objects) => {
@@ -21,14 +21,11 @@ export const mergeObjectsIgnoringNullUndefined = (...objects) => {
21
21
  */
22
22
  export const cloneDeep = (value) => {
23
23
  if (value === null || typeof value !== "object") {
24
- // Primitive value (or null), just return it
25
24
  return value;
26
25
  }
27
26
  if (Array.isArray(value)) {
28
- // Array: clone each element recursively
29
27
  return value.map(cloneDeep);
30
28
  }
31
- // Object: create a new object and clone its properties
32
29
  const copy = {};
33
30
  for (const key in value) {
34
31
  if (Object.hasOwn(value, key)) {
@@ -37,3 +34,8 @@ export const cloneDeep = (value) => {
37
34
  }
38
35
  return copy;
39
36
  };
37
+ /**
38
+ * A non-null object that is not an array. The prototype is not checked, so
39
+ * class instances pass.
40
+ */
41
+ export const isPlainRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -5,23 +5,6 @@
5
5
  * Provides default DataURL-based implementation and event management.
6
6
  */
7
7
  import { type ImageHandler, type SoundHandler } from "../types/index.js";
8
- import type { AssetHandler } from "./types.js";
9
- /**
10
- * DataURL-based asset handler (default implementation)
11
- *
12
- * Converts uploaded files to base64 data URLs.
13
- * Suitable for demos and local development.
14
- */
15
- export declare class DataURLAssetHandler implements AssetHandler {
16
- private onComplete;
17
- private cancelled;
18
- private fileReader;
19
- constructor(onComplete: (src: string) => void);
20
- cancel(): void;
21
- done(err?: Error, src?: string): void;
22
- fileChosen(file: File): void;
23
- progress(percent: number, bytes: number, total: number): void;
24
- }
25
8
  /**
26
9
  * Asset Event Manager
27
10
  *
@@ -13,7 +13,7 @@ const logger = createPieLogger("asset-handler", () => isGlobalDebugEnabled());
13
13
  * Converts uploaded files to base64 data URLs.
14
14
  * Suitable for demos and local development.
15
15
  */
16
- export class DataURLAssetHandler {
16
+ class DataURLAssetHandler {
17
17
  onComplete;
18
18
  cancelled = false;
19
19
  fileReader = null;
@@ -2,10 +2,8 @@ import { AssetEventManager } from "./asset-handler.js";
2
2
  import { createPieLogger, isGlobalDebugEnabled } from "./logger.js";
3
3
  import { findPieController } from "./scoring.js";
4
4
  import { parsePackageName } from "./utils.js";
5
+ import { isPlainRecord } from "../object/index.js";
5
6
  const logger = createPieLogger("authoring", () => isGlobalDebugEnabled());
6
- function isRecord(value) {
7
- return !!value && typeof value === "object" && !Array.isArray(value);
8
- }
9
7
  function packageNameWithoutVersion(packageSpec) {
10
8
  try {
11
9
  return parsePackageName(packageSpec).name;
@@ -19,7 +17,7 @@ function packageBaseName(packageName) {
19
17
  return lastSegment || packageName;
20
18
  }
21
19
  function lookupOwn(source, keys) {
22
- if (!isRecord(source))
20
+ if (!isPlainRecord(source))
23
21
  return undefined;
24
22
  for (const key of keys) {
25
23
  if (Object.hasOwn(source, key)) {
@@ -32,7 +30,7 @@ function mergeConfiguration(deliveryConfig, authoringConfig) {
32
30
  if (authoringConfig === undefined) {
33
31
  return deliveryConfig ?? {};
34
32
  }
35
- if (isRecord(deliveryConfig) && isRecord(authoringConfig)) {
33
+ if (isPlainRecord(deliveryConfig) && isPlainRecord(authoringConfig)) {
36
34
  return {
37
35
  ...deliveryConfig,
38
36
  ...authoringConfig,
@@ -108,9 +106,9 @@ function resolveValidationController(configureElement) {
108
106
  // A controller's `validate` returns a field → message map. ebsr returns one map
109
107
  // per part, so an empty ebsr result is `{ partA: {}, partB: {} }`.
110
108
  function hasValidationErrors(errors) {
111
- if (!isRecord(errors))
109
+ if (!isPlainRecord(errors))
112
110
  return false;
113
- if (isRecord(errors.partA) && isRecord(errors.partB)) {
111
+ if (isPlainRecord(errors.partA) && isPlainRecord(errors.partB)) {
114
112
  return (Object.keys(errors.partA).length > 0 ||
115
113
  Object.keys(errors.partB).length > 0);
116
114
  }
@@ -16,8 +16,6 @@
16
16
  * mutation before it, is missed rather than waited for.
17
17
  */
18
18
  import type { ConfigEntity } from "../types/index.js";
19
- /** Longer than the 50ms render debounce legacy elements use, plus a scheduler task. */
20
- export declare const ELEMENT_RENDER_QUIET_MS = 200;
21
19
  /**
22
20
  * Resolves once every element of `configs` in `root` has rendered, or when
23
21
  * `signal` aborts. Never rejects.
@@ -16,7 +16,7 @@
16
16
  * mutation before it, is missed rather than waited for.
17
17
  */
18
18
  /** Longer than the 50ms render debounce legacy elements use, plus a scheduler task. */
19
- export const ELEMENT_RENDER_QUIET_MS = 200;
19
+ const ELEMENT_RENDER_QUIET_MS = 200;
20
20
  function hasContent(element) {
21
21
  if (element.firstElementChild || element.shadowRoot?.firstChild)
22
22
  return true;
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * PIE Initialization Module
3
3
  *
4
- * Bundle loading and element initialization logic.
5
- * This is the core of the PIE player system.
4
+ * Loads IIFE bundles into `window.pie`, defines their elements and records
5
+ * them in the PIE registry, and binds config and session to the elements in
6
+ * the container.
6
7
  */
7
8
  import type { ConfigEntity } from "../types/index.js";
8
9
  import type { LoadPieElementsOptions } from "./types.js";
@@ -26,13 +27,11 @@ import type { LoadPieElementsOptions } from "./types.js";
26
27
  * the existing `MutationObserver` / `updatePieElements` flow binds
27
28
  * them on the next reactive pass.
28
29
  *
29
- * The original blanket `window.pie not found; was the bundle inlined
30
- * correctly?` error has been removed: in the section-player + item-player
31
- * composition it produced a confusing red stack trace for a routine
32
- * timing condition that the host already handles. Genuine failures
33
- * (bundle not loaded by *anyone*) still surface — every unregistered tag
34
- * gets its own warning, and `updatePieElements` later reports any tag
35
- * that never resolves.
30
+ * A missing `window.pie` raises no error: in the section-player and
31
+ * item-player composition it is a routine timing condition the host already
32
+ * handles. A bundle nobody loaded still surfaces, as a warning per
33
+ * unregistered tag and as an `updatePieElements` report for any tag that
34
+ * never resolves.
36
35
  *
37
36
  * Binds the elements present in `opts.container` now. Elements that arrive
38
37
  * later are the container owner's concern: a player with a lifecycle calls
@@ -53,13 +52,13 @@ export declare const loadPieModule: (config: ConfigEntity, session: any[], opts?
53
52
  session: any[];
54
53
  }>;
55
54
  /**
56
- * Load a PIE bundle from a JavaScript string into window.pie (IIFE bundles only)
57
- * This only registers elements and controllers - does NOT initialize them.
58
- * For initialization, use initializePiesFromLoadedBundle after loading.
55
+ * Executes an IIFE bundle from a JavaScript string, which populates
56
+ * `window.pie`. Defines no elements: `initializePiesFromLoadedBundle`
57
+ * registers and binds them afterwards.
59
58
  */
60
59
  export declare const loadBundleFromString: (bundleJs: string) => Promise<void>;
61
60
  /**
62
- * Load a PIE bundle from a JavaScript string and initialize elements
63
- * Convenience wrapper around loadBundleFromString + loadPieModule
61
+ * Loads a PIE bundle from a JavaScript string and initializes its elements:
62
+ * `loadPieModule` over a blob URL of the source.
64
63
  */
65
64
  export declare const loadPieModuleFromString: (bundleJs: string, config: ConfigEntity, session: any[], opts?: LoadPieElementsOptions) => Promise<void>;
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * PIE Initialization Module
3
3
  *
4
- * Bundle loading and element initialization logic.
5
- * This is the core of the PIE player system.
4
+ * Loads IIFE bundles into `window.pie`, defines their elements and records
5
+ * them in the PIE registry, and binds config and session to the elements in
6
+ * the container.
6
7
  */
7
8
  import { BUILDER_BUNDLE_URL } from "../config/profile.js";
8
9
  import { DEFAULT_IIFE_BUNDLE_RETRY_CONFIG } from "../loader-config.js";
@@ -17,7 +18,6 @@ import { validateCustomElementTag } from "./tag-names.js";
17
18
  import { BundleType, isCustomElementConstructor, isPieAvailable, Status, } from "./types.js";
18
19
  import { updatePieElement } from "./updates.js";
19
20
  import { getPackageWithoutVersion, getPieElementBundlesUrl, } from "./utils.js";
20
- // Create module-level logger (respects global debug flag - pass function for dynamic checking)
21
21
  const logger = createPieLogger("pie-initialization", () => isGlobalDebugEnabled());
22
22
  /**
23
23
  * Deadline for `loadPieModule`'s bundle `<script>` load when the caller
@@ -26,7 +26,6 @@ const logger = createPieLogger("pie-initialization", () => isGlobalDebugEnabled(
26
26
  * path a host loads it through.
27
27
  */
28
28
  const DEFAULT_LOAD_TIMEOUT_MS = DEFAULT_IIFE_BUNDLE_RETRY_CONFIG.timeoutMs;
29
- // Default options for loading PIE elements
30
29
  const defaultOptions = {
31
30
  buildServiceBase: BUILDER_BUNDLE_URL,
32
31
  bundleType: BundleType.player, // Default to player.js (no controllers, server-processed models)
@@ -46,12 +45,10 @@ const updateRegisteredElement = (elementTagName, config, session, options, omitE
46
45
  });
47
46
  };
48
47
  /**
49
- * Shared element registration logic
50
- * Extracted from initializePiesFromLoadedBundle and loadPieModule to eliminate ~200 lines of duplication
51
- *
52
- * Binds the elements already present in `options.container`. Elements that
53
- * arrive later are bound by the container owner's observer — see
54
- * `element-observer.ts`.
48
+ * Defines the bundle's elements for `config.elements`, records them in the
49
+ * registry, and binds the elements already present in `options.container`.
50
+ * Elements that arrive later are bound by the container owner's observer —
51
+ * see `element-observer.ts`. Returns one promise per defined tag.
55
52
  *
56
53
  * `elementModule` may be `null`. In that case we cannot register *new*
57
54
  * tags (no element constructor source), but we can still update tags
@@ -120,59 +117,55 @@ const registerPieElementsFromBundle = (elementModule, config, session, registry,
120
117
  : [],
121
118
  bundleType: options.bundleType,
122
119
  });
123
- {
124
- // Register the element in our registry. A package without a
125
- // controller, such as a legacy `@pie-element/protractor`, renders
126
- // the model it is given under client-player.js too.
127
- logger.debug(`[registerPieElementsFromBundle] Registering ${elName} in registry${elementData.controller
128
- ? " with controller"
129
- : " (no controller; its model is used as given)"}`);
130
- writeRegistryEntry({
131
- package: pkg,
132
- status: Status.loading,
133
- tagName: elementTagName,
134
- controller: elementData.controller || null,
135
- config: elementData.config,
120
+ // A package without a controller, such as a legacy
121
+ // `@pie-element/protractor`, renders the model it is given under
122
+ // client-player.js too.
123
+ logger.debug(`[registerPieElementsFromBundle] Registering ${elName} in registry${elementData.controller
124
+ ? " with controller"
125
+ : " (no controller; its model is used as given)"}`);
126
+ writeRegistryEntry({
127
+ package: pkg,
128
+ status: Status.loading,
129
+ tagName: elementTagName,
130
+ controller: elementData.controller || null,
131
+ config: elementData.config,
132
+ bundleType: options.bundleType,
133
+ });
134
+ if (!isCustomElementConstructor(elementData.Element)) {
135
+ logger.error(`[registerPieElementsFromBundle] pie.Element for ${pkgStripped} is not a valid custom element constructor.`);
136
+ return;
137
+ }
138
+ defineCustomElementSafely(elementTagName, elementData.Element, `element tag in config.elements for ${String(pkg)}`);
139
+ // Initialize existing elements
140
+ const searchRoot = options.container || document;
141
+ const elements = searchRoot.querySelectorAll(elementTagName);
142
+ logger.debug(`[registerPieElementsFromBundle] Found ${elements.length} elements for tag '${elementTagName}'`);
143
+ elements.forEach((el) => {
144
+ initializePieElement(el, {
145
+ config,
146
+ session,
147
+ env: options.env,
148
+ eventListeners: options.eventListeners?.[elementTagName],
136
149
  bundleType: options.bundleType,
137
150
  });
138
- if (isCustomElementConstructor(elementData.Element)) {
139
- defineCustomElementSafely(elementTagName, elementData.Element, `element tag in config.elements for ${String(pkg)}`);
140
- // Initialize existing elements
141
- const searchRoot = options.container || document;
142
- const elements = searchRoot.querySelectorAll(elementTagName);
143
- logger.debug(`[registerPieElementsFromBundle] Found ${elements.length} elements for tag '${elementTagName}'`);
144
- elements.forEach((el) => {
145
- initializePieElement(el, {
146
- config,
147
- session,
148
- env: options.env,
149
- eventListeners: options.eventListeners?.[elementTagName],
150
- bundleType: options.bundleType,
151
- });
152
- });
153
- writeRegistryEntry({
154
- ...registry[elementTagName],
155
- status: Status.loaded,
156
- });
157
- promises.push(customElements.whenDefined(elementTagName).then(() => {
158
- logger.debug("[registerPieElementsFromBundle] defined custom PIE element: %s", elementTagName);
151
+ });
152
+ writeRegistryEntry({
153
+ ...registry[elementTagName],
154
+ status: Status.loaded,
155
+ });
156
+ promises.push(customElements.whenDefined(elementTagName).then(() => {
157
+ logger.debug("[registerPieElementsFromBundle] defined custom PIE element: %s", elementTagName);
158
+ }));
159
+ if (options.bundleType === BundleType.editor) {
160
+ if (isCustomElementConstructor(elementData.Configure)) {
161
+ const editorElName = getEditorElementTagName(elementTagName, String(pkg));
162
+ defineCustomElementSafely(editorElName, elementData.Configure, `editor element tag for ${String(pkg)}`);
163
+ promises.push(customElements.whenDefined(editorElName).then(() => {
164
+ logger.debug(`[registerPieElementsFromBundle] defined custom PIE editor element: ${editorElName}`);
159
165
  }));
160
- // Handle editor elements if needed
161
- if (options.bundleType === BundleType.editor) {
162
- if (isCustomElementConstructor(elementData.Configure)) {
163
- const editorElName = getEditorElementTagName(elementTagName, String(pkg));
164
- defineCustomElementSafely(editorElName, elementData.Configure, `editor element tag for ${String(pkg)}`);
165
- promises.push(customElements.whenDefined(editorElName).then(() => {
166
- logger.debug(`[registerPieElementsFromBundle] defined custom PIE editor element: ${editorElName}`);
167
- }));
168
- }
169
- else {
170
- logger.error(`[registerPieElementsFromBundle] pie.Configure for ${pkgStripped} is not a valid custom element constructor.`, elementData.configure);
171
- }
172
- }
173
166
  }
174
167
  else {
175
- logger.error(`[registerPieElementsFromBundle] pie.Element for ${pkgStripped} is not a valid custom element constructor.`);
168
+ logger.error(`[registerPieElementsFromBundle] pie.Configure for ${pkgStripped} is not a valid custom element constructor.`, elementData.configure);
176
169
  }
177
170
  }
178
171
  });
@@ -198,13 +191,11 @@ const registerPieElementsFromBundle = (elementModule, config, session, registry,
198
191
  * the existing `MutationObserver` / `updatePieElements` flow binds
199
192
  * them on the next reactive pass.
200
193
  *
201
- * The original blanket `window.pie not found; was the bundle inlined
202
- * correctly?` error has been removed: in the section-player + item-player
203
- * composition it produced a confusing red stack trace for a routine
204
- * timing condition that the host already handles. Genuine failures
205
- * (bundle not loaded by *anyone*) still surface — every unregistered tag
206
- * gets its own warning, and `updatePieElements` later reports any tag
207
- * that never resolves.
194
+ * A missing `window.pie` raises no error: in the section-player and
195
+ * item-player composition it is a routine timing condition the host already
196
+ * handles. A bundle nobody loaded still surfaces, as a warning per
197
+ * unregistered tag and as an `updatePieElements` report for any tag that
198
+ * never resolves.
208
199
  *
209
200
  * Binds the elements present in `opts.container` now. Elements that arrive
210
201
  * later are the container owner's concern: a player with a lifecycle calls
@@ -282,9 +273,7 @@ export const loadPieModule = async (config, session, opts = {}) => {
282
273
  logger.debug("[loadPieModule] window.pie available");
283
274
  const elementModule = window.pie.default;
284
275
  try {
285
- // Use shared registration logic (returns array of promises)
286
276
  const registrationPromises = registerPieElementsFromBundle(elementModule, config, session, registry, options);
287
- // Wait for all element definitions to complete
288
277
  Promise.all(registrationPromises).then(succeed, fail);
289
278
  }
290
279
  catch (error) {
@@ -321,18 +310,16 @@ export const loadPieModule = async (config, session, opts = {}) => {
321
310
  return { session };
322
311
  };
323
312
  /**
324
- * Load a PIE bundle from a JavaScript string into window.pie (IIFE bundles only)
325
- * This only registers elements and controllers - does NOT initialize them.
326
- * For initialization, use initializePiesFromLoadedBundle after loading.
313
+ * Executes an IIFE bundle from a JavaScript string, which populates
314
+ * `window.pie`. Defines no elements: `initializePiesFromLoadedBundle`
315
+ * registers and binds them afterwards.
327
316
  */
328
317
  export const loadBundleFromString = async (bundleJs) => {
329
318
  await initializeMathRendering();
330
319
  await withBlobBundleUrl(bundleJs, { stripSourceMapComment: true }, async (bundleUrl) => {
331
- // Create a script tag to execute the bundle
332
320
  const script = document.createElement("script");
333
321
  script.src = bundleUrl;
334
322
  script.type = "text/javascript"; // IIFE bundles are standard JS
335
- // Wait for script to load
336
323
  await new Promise((resolve, reject) => {
337
324
  script.onload = () => resolve();
338
325
  script.onerror = () => reject(new Error("Failed to load bundle"));
@@ -342,12 +329,11 @@ export const loadBundleFromString = async (bundleJs) => {
342
329
  });
343
330
  };
344
331
  /**
345
- * Load a PIE bundle from a JavaScript string and initialize elements
346
- * Convenience wrapper around loadBundleFromString + loadPieModule
332
+ * Loads a PIE bundle from a JavaScript string and initializes its elements:
333
+ * `loadPieModule` over a blob URL of the source.
347
334
  */
348
335
  export const loadPieModuleFromString = async (bundleJs, config, session, opts = {}) => {
349
336
  await withBlobBundleUrl(bundleJs, {}, async (bundleUrl) => {
350
- // Use existing loadPieModule with the blob URL
351
337
  await loadPieModule(config, session, { ...opts, bundleUrl });
352
338
  });
353
339
  };
@@ -1,3 +1,4 @@
1
+ import { isPlainRecord } from "../object/index.js";
1
2
  const DEFAULT_SESSION_ID = "";
2
3
  const METADATA_ONLY_SESSION_KEYS = new Set([
3
4
  "complete",
@@ -146,9 +147,6 @@ export function hasResponseField(value) {
146
147
  }
147
148
  return false;
148
149
  }
149
- function isPlainObject(value) {
150
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
151
- }
152
150
  function getMetadataComponent(sessionDetail, candidate) {
153
151
  if (typeof sessionDetail.component === "string")
154
152
  return sessionDetail.component;
@@ -194,7 +192,7 @@ function mergeSessionEntry(existing, incoming) {
194
192
  for (const [key, value] of Object.entries(incoming)) {
195
193
  const previous = merged[key];
196
194
  merged[key] =
197
- isPlainObject(previous) && isPlainObject(value)
195
+ isPlainRecord(previous) && isPlainRecord(value)
198
196
  ? mergeSessionEntry(previous, value)
199
197
  : value;
200
198
  }
@@ -334,7 +332,7 @@ function classifyItemSessionChange(args) {
334
332
  };
335
333
  }
336
334
  function hostContainerEntries(hostContainer) {
337
- if (!isPlainObject(hostContainer)) {
335
+ if (!isPlainRecord(hostContainer)) {
338
336
  return null;
339
337
  }
340
338
  // A frozen container is not a live view of anything, and a shared immutable
@@ -355,7 +353,7 @@ function hostContainerEntries(hostContainer) {
355
353
  return null;
356
354
  }
357
355
  function sessionEntryId(value) {
358
- if (!isPlainObject(value)) {
356
+ if (!isPlainRecord(value)) {
359
357
  return null;
360
358
  }
361
359
  const id = value.id;
@@ -405,7 +403,7 @@ export function ensureHostSessionEntries(hostContainer, entryIds) {
405
403
  */
406
404
  export function projectSessionIntoHostContainer(hostContainer, liveSession) {
407
405
  const entries = hostContainerEntries(hostContainer);
408
- if (!entries || !isPlainObject(hostContainer)) {
406
+ if (!entries || !isPlainRecord(hostContainer)) {
409
407
  return false;
410
408
  }
411
409
  const live = normalizeItemSessionContainer(liveSession);
@@ -417,11 +415,11 @@ export function projectSessionIntoHostContainer(hostContainer, liveSession) {
417
415
  }
418
416
  for (const nextEntry of live.data) {
419
417
  const id = sessionEntryId(nextEntry);
420
- if (!id || !isPlainObject(nextEntry)) {
418
+ if (!id || !isPlainRecord(nextEntry)) {
421
419
  continue;
422
420
  }
423
421
  const target = entries.find((entry) => sessionEntryId(entry) === id);
424
- if (!isPlainObject(target)) {
422
+ if (!isPlainRecord(target)) {
425
423
  entries.push({ ...nextEntry });
426
424
  wrote = true;
427
425
  continue;