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

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 (137) hide show
  1. package/README.md +30 -2
  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 +11 -24
  8. package/dist/i18n/messages/en-US.js +13 -26
  9. package/dist/i18n/messages/nl-NL.d.ts +7 -24
  10. package/dist/i18n/messages/nl-NL.js +9 -26
  11. package/dist/i18n/provider.d.ts +4 -5
  12. package/dist/i18n/provider.js +4 -5
  13. package/dist/i18n/types.d.ts +4 -5
  14. package/dist/index.d.ts +4 -2
  15. package/dist/index.js +3 -1
  16. package/dist/instrumentation/index.d.ts +5 -4
  17. package/dist/instrumentation/index.js +5 -4
  18. package/dist/instrumentation/new-relic-agent.d.ts +20 -0
  19. package/dist/instrumentation/new-relic-agent.js +24 -0
  20. package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts +5 -4
  21. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts +7 -7
  22. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js +21 -32
  23. package/dist/instrumentation/types.d.ts +7 -3
  24. package/dist/loader-config.d.ts +16 -6
  25. package/dist/loaders/controller-loading.d.ts +14 -0
  26. package/dist/loaders/controller-loading.js +12 -0
  27. package/dist/loaders/defaults.d.ts +10 -0
  28. package/dist/loaders/defaults.js +10 -0
  29. package/dist/loaders/element-loader.d.ts +20 -8
  30. package/dist/loaders/element-loader.js +102 -36
  31. package/dist/loaders/element-package-policy.d.ts +2 -0
  32. package/dist/loaders/element-package-policy.js +2 -1
  33. package/dist/loaders/esm-adapter.d.ts +29 -0
  34. package/dist/loaders/esm-adapter.js +635 -102
  35. package/dist/loaders/iife-adapter.d.ts +0 -5
  36. package/dist/loaders/iife-adapter.js +3 -9
  37. package/dist/loaders/index.d.ts +12 -3
  38. package/dist/loaders/index.js +9 -3
  39. package/dist/loaders/mathjax-events.d.ts +22 -0
  40. package/dist/loaders/mathjax-events.js +65 -0
  41. package/dist/loaders/module-shim.d.ts +12 -0
  42. package/dist/loaders/module-shim.js +12 -0
  43. package/dist/loaders/preloaded-alignment.d.ts +24 -0
  44. package/dist/loaders/preloaded-alignment.js +57 -0
  45. package/dist/loaders/preloaded-authored-tags.d.ts +26 -0
  46. package/dist/loaders/preloaded-authored-tags.js +69 -0
  47. package/dist/loaders/preloaded-registration.d.ts +72 -0
  48. package/dist/loaders/preloaded-registration.js +141 -0
  49. package/dist/media/index.d.ts +69 -0
  50. package/dist/media/index.js +194 -0
  51. package/dist/pie/authoring.d.ts +1 -1
  52. package/dist/pie/authoring.js +25 -18
  53. package/dist/pie/element-announcements.d.ts +39 -0
  54. package/dist/pie/element-announcements.js +67 -0
  55. package/dist/pie/element-observer.d.ts +3 -1
  56. package/dist/pie/element-observer.js +1 -0
  57. package/dist/pie/elements-rendered.d.ts +25 -0
  58. package/dist/pie/elements-rendered.js +96 -0
  59. package/dist/pie/index.d.ts +8 -10
  60. package/dist/pie/index.js +9 -10
  61. package/dist/pie/initialization.js +13 -22
  62. package/dist/pie/initialize-element.d.ts +3 -0
  63. package/dist/pie/initialize-element.js +3 -4
  64. package/dist/pie/instrumentation-event-bridge.js +23 -1
  65. package/dist/pie/instrumentation-event-map.js +11 -25
  66. package/dist/pie/instrumentation-provider-resolution.d.ts +2 -1
  67. package/dist/pie/instrumentation-provider-resolution.js +0 -1
  68. package/dist/pie/item-controller.js +4 -0
  69. package/dist/pie/item-session-contract.d.ts +8 -0
  70. package/dist/pie/item-session-contract.js +30 -9
  71. package/dist/pie/logger.d.ts +6 -0
  72. package/dist/pie/logger.js +10 -0
  73. package/dist/pie/markup-math.d.ts +20 -0
  74. package/dist/pie/markup-math.js +118 -0
  75. package/dist/pie/math-assets.d.ts +48 -0
  76. package/dist/pie/math-assets.js +81 -0
  77. package/dist/pie/math-rendering.d.ts +10 -21
  78. package/dist/pie/math-rendering.js +50 -35
  79. package/dist/pie/registry.d.ts +13 -1
  80. package/dist/pie/registry.js +39 -1
  81. package/dist/pie/resource-monitor.d.ts +6 -33
  82. package/dist/pie/resource-monitor.js +14 -213
  83. package/dist/pie/scoring.d.ts +12 -8
  84. package/dist/pie/scoring.js +21 -24
  85. package/dist/pie/session-commit.d.ts +16 -0
  86. package/dist/pie/session-commit.js +34 -6
  87. package/dist/pie/session-snapshot.d.ts +80 -0
  88. package/dist/pie/session-snapshot.js +166 -0
  89. package/dist/pie/stages.d.ts +10 -47
  90. package/dist/pie/stages.js +10 -47
  91. package/dist/pie/tag-names.d.ts +1 -0
  92. package/dist/pie/tag-names.js +1 -0
  93. package/dist/pie/types.d.ts +4 -5
  94. package/dist/pie/types.js +1 -0
  95. package/dist/pie/updates.d.ts +2 -2
  96. package/dist/pie/updates.js +33 -15
  97. package/dist/pie/versioned-tag.d.ts +4 -0
  98. package/dist/pie/versioned-tag.js +4 -0
  99. package/dist/security/index.d.ts +2 -3
  100. package/dist/security/index.js +2 -3
  101. package/dist/security/sanitize-item-markup.d.ts +4 -2
  102. package/dist/security/sanitize-item-markup.js +48 -6
  103. package/dist/security/sanitize-svg-icon.d.ts +0 -2
  104. package/dist/security/sanitize-svg-icon.js +0 -5
  105. package/dist/security/wrap-overwide-images.js +19 -0
  106. package/dist/security/wrap-overwide.d.ts +13 -0
  107. package/dist/security/wrap-overwide.js +63 -3
  108. package/dist/timed-media/policy.js +5 -16
  109. package/dist/types/index.d.ts +67 -423
  110. package/dist/types/index.js +0 -68
  111. package/dist/ui/attribute-coercion.d.ts +12 -0
  112. package/dist/ui/attribute-coercion.js +33 -0
  113. package/dist/ui/content-styles.d.ts +18 -16
  114. package/dist/ui/content-styles.js +67 -36
  115. package/dist/ui/overlay-containment.d.ts +31 -0
  116. package/dist/ui/overlay-containment.js +63 -0
  117. package/dist/ui/overlay-placement.d.ts +110 -0
  118. package/dist/ui/overlay-placement.js +222 -0
  119. package/dist/ui/pointer-gesture.d.ts +32 -0
  120. package/dist/ui/pointer-gesture.js +56 -0
  121. package/dist/ui/pointer-rotate.d.ts +29 -0
  122. package/dist/ui/pointer-rotate.js +39 -0
  123. package/package.json +23 -18
  124. package/dist/pie/component-context.d.ts +0 -21
  125. package/dist/pie/component-context.js +0 -29
  126. package/dist/pie/player-initializer.d.ts +0 -54
  127. package/dist/pie/player-initializer.js +0 -122
  128. package/dist/pie/stage-tracker.d.ts +0 -50
  129. package/dist/pie/stage-tracker.js +0 -105
  130. package/dist/security/wrap-model-rich-content.d.ts +0 -1
  131. package/dist/security/wrap-model-rich-content.js +0 -40
  132. package/dist/server/npm-auth-env.d.ts +0 -13
  133. package/dist/server/npm-auth-env.js +0 -64
  134. package/dist/server/npm-registry.d.ts +0 -7
  135. package/dist/server/npm-registry.js +0 -59
  136. package/dist/ui/debug-panel-persistence.d.ts +0 -17
  137. package/dist/ui/debug-panel-persistence.js +0 -37
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Registration for the `preloaded` strategy: the host's bundler resolves the
3
+ * element packages, and the players load nothing.
4
+ *
5
+ * Each element is defined under the versioned tag `toPackageVersionedTag`
6
+ * derives from its base tag and the registered version, recorded in
7
+ * `window.PIE_REGISTRY`, and recorded by package in
8
+ * `window.PIE_PRELOADED_ELEMENTS`. The item player and the section player
9
+ * rewrite every authored spec of a registered package to that version
10
+ * (`alignPreloadedElementVersions`), derive the versioned tags from the result,
11
+ * define the ones content names under another base tag
12
+ * (`defineAuthoredPreloadedTags`) and assert them (`assertRegistered`).
13
+ *
14
+ * A hosted player takes models from the server and resolves no controller. A
15
+ * player that is not hosted runs the controller's `model()` in the browser, so
16
+ * it needs the `controller` an entry registers; the item player warns about
17
+ * each tag it renders without one.
18
+ *
19
+ * Bundled by the host, the elements' copies of the MathJax adapter find no npm
20
+ * root in their module URLs, so the registration also takes where they load
21
+ * MathJax's fonts and speech from (`options.math`): a root, or each file's URL.
22
+ */
23
+ import { defineCustomElementSafely } from "../pie/custom-element-define.js";
24
+ import { mathAssetOptionsError, setMathAssetOptions, } from "../pie/math-assets.js";
25
+ import { writeRegistryEntry } from "../pie/registry.js";
26
+ import { BundleType, isCustomElementConstructor, Status } from "../pie/types.js";
27
+ import { parsePackageName } from "../pie/utils.js";
28
+ import { toPackageVersionedTag } from "../pie/versioned-tag.js";
29
+ import { isExactSemver } from "./element-package-policy.js";
30
+ import { pickElementClass } from "./esm-adapter.js";
31
+ /**
32
+ * Register host-bundled elements for the `preloaded` strategy.
33
+ *
34
+ * Synchronous: every tag is in `customElements` when the call returns.
35
+ * Idempotent: a tag that is already defined keeps its definition and its
36
+ * registry entry, which only gains a controller it lacked. Every entry is
37
+ * validated before any is registered. A package registers at one version per
38
+ * page, because the players align every authored version of a package to the
39
+ * registered one. `options.math` replaces the page's math asset options it
40
+ * sets, and keeps the others.
41
+ */
42
+ export function registerPreloadedElements(elements, options = {}) {
43
+ if (typeof window === "undefined" || typeof customElements === "undefined") {
44
+ throw new Error("[registerPreloadedElements] requires a browser with customElements");
45
+ }
46
+ const preloaded = preloadedPackageSpecs();
47
+ const resolved = elements.map(resolveElement);
48
+ assertOneVersionPerPackage(resolved, preloaded);
49
+ const mathError = options.math === undefined ? undefined : mathAssetOptionsError(options.math);
50
+ if (mathError) {
51
+ throw new Error(`[registerPreloadedElements] options.math: ${mathError}`);
52
+ }
53
+ if (options.math)
54
+ setMathAssetOptions(options.math);
55
+ for (const { packageName, spec, tagName, elementClass, controller } of resolved) {
56
+ defineCustomElementSafely(tagName, class extends elementClass {
57
+ }, `preloaded element tag for ${packageName}`);
58
+ writeRegistryEntry({
59
+ package: spec,
60
+ status: Status.loaded,
61
+ tagName,
62
+ element: elementClass,
63
+ ...(controller ? { controller } : {}),
64
+ bundleType: controller ? BundleType.clientPlayer : BundleType.player,
65
+ });
66
+ preloaded[packageName] = spec;
67
+ }
68
+ }
69
+ function resolveElement(element, index) {
70
+ const at = `[registerPreloadedElements] entry ${index}`;
71
+ if (!element || typeof element !== "object") {
72
+ throw new Error(`${at}: expected { tag, package, version, element }`);
73
+ }
74
+ const { package: packageName, version, tag } = element;
75
+ if (!isBarePackageName(packageName)) {
76
+ throw new Error(`${at}: package must be a bare package name, got ${JSON.stringify(packageName)}`);
77
+ }
78
+ if (typeof version !== "string" || !isExactSemver(version)) {
79
+ throw new Error(`${at} (${packageName}): version must be the installed version, exact, such as 1.2.3; got ${JSON.stringify(version)}`);
80
+ }
81
+ if (typeof tag !== "string" || !tag) {
82
+ throw new Error(`${at} (${packageName}): tag must be the base tag to register, got ${JSON.stringify(tag)}`);
83
+ }
84
+ const candidate = typeof element.element === "function"
85
+ ? element.element
86
+ : pickElementClass(element.element, "delivery");
87
+ if (!isCustomElementConstructor(candidate)) {
88
+ throw new Error(`${at} (${packageName}): element must be the ./browser/delivery module or its default export`);
89
+ }
90
+ let controller;
91
+ if (element.controller !== undefined) {
92
+ controller = resolveController(element.controller);
93
+ if (!controller) {
94
+ throw new Error(`${at} (${packageName}): controller must be the ./browser/controller module or its default export`);
95
+ }
96
+ }
97
+ const spec = `${packageName}@${version}`;
98
+ return {
99
+ packageName,
100
+ spec,
101
+ tagName: toPackageVersionedTag(tag, spec),
102
+ elementClass: candidate,
103
+ controller,
104
+ };
105
+ }
106
+ function resolveController(value) {
107
+ if (!value || typeof value !== "object")
108
+ return undefined;
109
+ const module = value;
110
+ const candidate = (typeof module.model === "function" ? module : module.default);
111
+ return candidate && typeof candidate.model === "function"
112
+ ? candidate
113
+ : undefined;
114
+ }
115
+ function assertOneVersionPerPackage(resolved, preloaded) {
116
+ const specs = new Map(Object.entries(preloaded));
117
+ for (const { packageName, spec } of resolved) {
118
+ const existing = specs.get(packageName);
119
+ if (existing !== undefined && existing !== spec) {
120
+ throw new Error(`[registerPreloadedElements] ${packageName} is registered as ${existing}; ` +
121
+ `${spec} cannot also be registered, because the players align every ` +
122
+ "authored version of a package to the registered one");
123
+ }
124
+ specs.set(packageName, spec);
125
+ }
126
+ }
127
+ function isBarePackageName(value) {
128
+ if (typeof value !== "string" || !value)
129
+ return false;
130
+ try {
131
+ return parsePackageName(value).name === value;
132
+ }
133
+ catch {
134
+ return false;
135
+ }
136
+ }
137
+ function preloadedPackageSpecs() {
138
+ const host = window;
139
+ host.PIE_PRELOADED_ELEMENTS ??= {};
140
+ return host.PIE_PRELOADED_ELEMENTS;
141
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Validation primitives shared by every catalog card whose payload references
3
+ * media.
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.
11
+ *
12
+ * Lives here, behind its own subpath, so a PIE element can validate authored
13
+ * media without importing the assessment toolkit, whose root entry declares
14
+ * side effects and brings the TTS and calculator packages with it. The toolkit
15
+ * root re-exports every name. Nothing here runs at import time.
16
+ */
17
+ import type { MediaFragmentRange, MediaSource } from "../types/index.js";
18
+ /**
19
+ * The only `MediaAssetRef.version` this build renders.
20
+ *
21
+ * `media-asset-contract.md` requires unknown-version rejection for runtime
22
+ * rendering: bumping to `2` obliges every consumer to accept both for as long as
23
+ * any producer emits `1`, so a card claiming a version this build does not
24
+ * implement must not be rendered on a guess at which fields still mean what they
25
+ * did.
26
+ */
27
+ export declare const SUPPORTED_MEDIA_ASSET_VERSION = 1;
28
+ /**
29
+ * Whether a media reference claims a version this build cannot render.
30
+ *
31
+ * An absent `version` is accepted rather than rejected — the same posture the
32
+ * rest of this module takes toward absent fields, and the one `media.kind` and
33
+ * `matchesRequestedSignLanguage` already take: only a positive claim of
34
+ * something else is refused. Producers predate the field, and treating its
35
+ * absence as a rejection would drop cards that are otherwise wholly valid.
36
+ */
37
+ export declare function isUnsupportedMediaAssetVersion(version: unknown): boolean;
38
+ export declare function isSafeMediaSrc(raw: unknown): raw is string;
39
+ export declare function normalizeMediaSources(raw: unknown): MediaSource[];
40
+ export declare function normalizeMediaFragment(raw: unknown): MediaFragmentRange | undefined;
41
+ export declare function trimmedOrUndefined(value: unknown): string | undefined;
42
+ /**
43
+ * Hold a media element to one fragment's range, and return the disposer.
44
+ *
45
+ * The `#t=` URI `applyMediaFragment` writes is a hint browsers honour at neither
46
+ * bound reliably, so every consumer that means "play only this slice" enforces it
47
+ * here instead of reimplementing the pair. Both shipped consumers do —
48
+ * `SignLanguageMediaRegion` pausing at the end, `TTSService.playRecordedAudio`
49
+ * ending the clip so the chunk sequence advances — which is why the end action is
50
+ * the caller's and only the arithmetic is shared.
51
+ *
52
+ * The start seek is forward only: a browser that did honour the URI has already
53
+ * positioned past the start, and seeking back would replay audio the learner has
54
+ * heard. `onReachedEnd` may fire more than once and must tolerate it; polling
55
+ * stops while the element is paused, so an element left sitting past its end bound
56
+ * goes quiet rather than being told repeatedly.
57
+ */
58
+ export declare function enforceMediaFragment(element: HTMLMediaElement, fragment: MediaFragmentRange | undefined, onReachedEnd: () => void): () => void;
59
+ /**
60
+ * Apply a fragment range to a source URL as a Media Fragments URI, so one
61
+ * recording can serve several content nodes.
62
+ *
63
+ * The URI is a hint only: browsers honour both bounds inconsistently, so the
64
+ * caller enforces the range itself with `enforceMediaFragment` — seek forward
65
+ * to the start once metadata is available, and stop at the end.
66
+ * `SignLanguageMediaRegion` and `TTSService.playRecordedAudio` are the two
67
+ * shipped consumers that do so.
68
+ */
69
+ export declare function applyMediaFragment(src: string, fragment?: MediaFragmentRange): string;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Validation primitives shared by every catalog card whose payload references
3
+ * media.
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.
11
+ *
12
+ * Lives here, behind its own subpath, so a PIE element can validate authored
13
+ * media without importing the assessment toolkit, whose root entry declares
14
+ * side effects and brings the TTS and calculator packages with it. The toolkit
15
+ * root re-exports every name. Nothing here runs at import time.
16
+ */
17
+ /**
18
+ * Media source URLs are handed to a media element in the learner's browser. Only
19
+ * schemes such an element can actually fetch are allowed; anything else is
20
+ * dropped so an authored `javascript:` / `file:` URL cannot ride into the DOM.
21
+ * Relative and protocol-relative URLs are allowed — host content is commonly
22
+ * served from the same origin as the player.
23
+ */
24
+ const DISALLOWED_SRC_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
25
+ const ALLOWED_SRC_SCHEMES = new Set(["http:", "https:", "data:", "blob:"]);
26
+ /**
27
+ * The only `MediaAssetRef.version` this build renders.
28
+ *
29
+ * `media-asset-contract.md` requires unknown-version rejection for runtime
30
+ * rendering: bumping to `2` obliges every consumer to accept both for as long as
31
+ * any producer emits `1`, so a card claiming a version this build does not
32
+ * implement must not be rendered on a guess at which fields still mean what they
33
+ * did.
34
+ */
35
+ export const SUPPORTED_MEDIA_ASSET_VERSION = 1;
36
+ /**
37
+ * Whether a media reference claims a version this build cannot render.
38
+ *
39
+ * An absent `version` is accepted rather than rejected — the same posture the
40
+ * rest of this module takes toward absent fields, and the one `media.kind` and
41
+ * `matchesRequestedSignLanguage` already take: only a positive claim of
42
+ * something else is refused. Producers predate the field, and treating its
43
+ * absence as a rejection would drop cards that are otherwise wholly valid.
44
+ */
45
+ export function isUnsupportedMediaAssetVersion(version) {
46
+ return version !== undefined && version !== SUPPORTED_MEDIA_ASSET_VERSION;
47
+ }
48
+ export function isSafeMediaSrc(raw) {
49
+ if (typeof raw !== "string")
50
+ return false;
51
+ const src = raw.trim();
52
+ if (!src)
53
+ return false;
54
+ // Relative ("/video.mp4", "video.mp4") and protocol-relative ("//cdn/x.mp4")
55
+ // forms carry no scheme to check and inherit the document's.
56
+ if (src.startsWith("//") || !DISALLOWED_SRC_SCHEME.test(src))
57
+ return true;
58
+ const scheme = src.slice(0, src.indexOf(":") + 1).toLowerCase();
59
+ return ALLOWED_SRC_SCHEMES.has(scheme);
60
+ }
61
+ export function normalizeMediaSources(raw) {
62
+ if (!Array.isArray(raw))
63
+ return [];
64
+ const sources = [];
65
+ for (const entry of raw) {
66
+ if (!entry || typeof entry !== "object")
67
+ continue;
68
+ const candidate = entry;
69
+ if (!isSafeMediaSrc(candidate.src))
70
+ continue;
71
+ const source = { src: candidate.src.trim() };
72
+ if (typeof candidate.type === "string" && candidate.type.trim()) {
73
+ source.type = candidate.type.trim();
74
+ }
75
+ if (Number.isFinite(candidate.width))
76
+ source.width = candidate.width;
77
+ if (Number.isFinite(candidate.height))
78
+ source.height = candidate.height;
79
+ // Deduplicated by `src`, because the signing region renders `<source>`
80
+ // elements in an `{#each}` keyed on exactly that: an authored card listing
81
+ // one URL twice — the same file under two MIME types is the plausible way —
82
+ // would otherwise throw Svelte's duplicate-key error and take the whole
83
+ // region down rather than degrade. The first entry wins, so authored order
84
+ // still decides which encoding the browser is offered first.
85
+ if (sources.some((existing) => existing.src === source.src))
86
+ continue;
87
+ sources.push(source);
88
+ }
89
+ return sources;
90
+ }
91
+ export function normalizeMediaFragment(raw) {
92
+ if (!raw || typeof raw !== "object")
93
+ return undefined;
94
+ const candidate = raw;
95
+ const start = Number(candidate.startSeconds);
96
+ if (!Number.isFinite(start) || start < 0)
97
+ return undefined;
98
+ const end = Number(candidate.endSeconds);
99
+ // An end at or before the start would produce a zero/negative slice; treat
100
+ // it as "no end" rather than a range that can never play.
101
+ if (!Number.isFinite(end) || end <= start)
102
+ return { startSeconds: start };
103
+ return { startSeconds: start, endSeconds: end };
104
+ }
105
+ export function trimmedOrUndefined(value) {
106
+ if (typeof value !== "string")
107
+ return undefined;
108
+ const trimmed = value.trim();
109
+ return trimmed || undefined;
110
+ }
111
+ /**
112
+ * How often the end bound is re-checked while a slice is playing. `timeupdate`
113
+ * alone fires about four times a second, which is loose enough to leak a sliver
114
+ * of the next node's recording.
115
+ */
116
+ const END_CHECK_INTERVAL_MS = 100;
117
+ /** `HTMLMediaElement.HAVE_METADATA`, which not every DOM implementation exposes. */
118
+ const HAVE_METADATA = 1;
119
+ /**
120
+ * Hold a media element to one fragment's range, and return the disposer.
121
+ *
122
+ * The `#t=` URI `applyMediaFragment` writes is a hint browsers honour at neither
123
+ * bound reliably, so every consumer that means "play only this slice" enforces it
124
+ * here instead of reimplementing the pair. Both shipped consumers do —
125
+ * `SignLanguageMediaRegion` pausing at the end, `TTSService.playRecordedAudio`
126
+ * ending the clip so the chunk sequence advances — which is why the end action is
127
+ * the caller's and only the arithmetic is shared.
128
+ *
129
+ * The start seek is forward only: a browser that did honour the URI has already
130
+ * positioned past the start, and seeking back would replay audio the learner has
131
+ * heard. `onReachedEnd` may fire more than once and must tolerate it; polling
132
+ * stops while the element is paused, so an element left sitting past its end bound
133
+ * goes quiet rather than being told repeatedly.
134
+ */
135
+ export function enforceMediaFragment(element, fragment, onReachedEnd) {
136
+ if (!fragment)
137
+ return () => { };
138
+ const { startSeconds, endSeconds } = fragment;
139
+ let seekToStart;
140
+ if (startSeconds > 0) {
141
+ seekToStart = () => {
142
+ if (element.currentTime < startSeconds) {
143
+ element.currentTime = startSeconds;
144
+ }
145
+ };
146
+ // Seeking before metadata is available only sets the default start
147
+ // position, so wait for it unless it already landed.
148
+ if (element.readyState >= HAVE_METADATA)
149
+ seekToStart();
150
+ else
151
+ element.addEventListener("loadedmetadata", seekToStart);
152
+ }
153
+ let checkEnd;
154
+ let endPoll;
155
+ if (endSeconds !== undefined) {
156
+ checkEnd = () => {
157
+ if (element.currentTime >= endSeconds)
158
+ onReachedEnd();
159
+ };
160
+ element.addEventListener("timeupdate", checkEnd);
161
+ endPoll = setInterval(() => {
162
+ if (!element.paused)
163
+ checkEnd?.();
164
+ }, END_CHECK_INTERVAL_MS);
165
+ }
166
+ return () => {
167
+ if (seekToStart)
168
+ element.removeEventListener("loadedmetadata", seekToStart);
169
+ if (checkEnd)
170
+ element.removeEventListener("timeupdate", checkEnd);
171
+ if (endPoll !== undefined)
172
+ clearInterval(endPoll);
173
+ };
174
+ }
175
+ /**
176
+ * Apply a fragment range to a source URL as a Media Fragments URI, so one
177
+ * recording can serve several content nodes.
178
+ *
179
+ * The URI is a hint only: browsers honour both bounds inconsistently, so the
180
+ * caller enforces the range itself with `enforceMediaFragment` — seek forward
181
+ * to the start once metadata is available, and stop at the end.
182
+ * `SignLanguageMediaRegion` and `TTSService.playRecordedAudio` are the two
183
+ * shipped consumers that do so.
184
+ */
185
+ export function applyMediaFragment(src, fragment) {
186
+ if (!fragment)
187
+ return src;
188
+ // Never stack a second fragment onto a URL that already carries one — the
189
+ // authored value wins.
190
+ if (src.includes("#"))
191
+ return src;
192
+ const end = fragment.endSeconds !== undefined ? `,${fragment.endSeconds}` : "";
193
+ return `${src}#t=${fragment.startSeconds}${end}`;
194
+ }
@@ -10,7 +10,7 @@ export type InitializedConfigureModel = {
10
10
  export type AuthoringValidationResult = {
11
11
  hasErrors: boolean;
12
12
  validatedModels: Array<PieModel & {
13
- validation?: unknown;
13
+ errors?: unknown;
14
14
  }>;
15
15
  };
16
16
  export type AuthoringMediaHandlers = {
@@ -105,14 +105,16 @@ export function initializeAuthoringConfigures(config, configuration = {}, option
105
105
  function resolveValidationController(configureElement) {
106
106
  return findPieController(configureElement.localName);
107
107
  }
108
- function hasValidationErrors(validation) {
109
- if (!validation || typeof validation !== "object")
108
+ // A controller's `validate` returns a field → message map. ebsr returns one map
109
+ // per part, so an empty ebsr result is `{ partA: {}, partB: {} }`.
110
+ function hasValidationErrors(errors) {
111
+ if (!isRecord(errors))
110
112
  return false;
111
- const validationRecord = validation;
112
- if (validationRecord.hasErrors === true)
113
- return true;
114
- const errors = validationRecord.errors;
115
- return Array.isArray(errors) && errors.length > 0;
113
+ if (isRecord(errors.partA) && isRecord(errors.partB)) {
114
+ return (Object.keys(errors.partA).length > 0 ||
115
+ Object.keys(errors.partB).length > 0);
116
+ }
117
+ return Object.keys(errors).length > 0;
116
118
  }
117
119
  export async function validateAuthoringModels(config, configuration = {}, options = {}) {
118
120
  const container = options.container ?? document;
@@ -129,20 +131,25 @@ export async function validateAuthoringModels(config, configuration = {}, option
129
131
  continue;
130
132
  const controller = resolveValidationController(element);
131
133
  const elementConfiguration = resolveConfigureConfiguration(configuration, elementTag, String(packageSpec));
132
- const validation = typeof controller?.validate === "function"
133
- ? await controller.validate(model, elementConfiguration)
134
- : undefined;
135
- validatedModels.push({
136
- ...model,
137
- ...(isRecord(validation) ? validation : {}),
138
- id: model.id,
139
- element: model.element,
140
- validation,
141
- });
134
+ if (typeof controller?.validate !== "function") {
135
+ validatedModels.push(model);
136
+ continue;
137
+ }
138
+ const errors = await controller.validate(model, elementConfiguration);
139
+ const validatedModel = { ...model, errors };
140
+ validatedModels.push(validatedModel);
141
+ // Configure elements render inline messages from `model.errors`. Skip an
142
+ // unchanged set: some configure elements emit `model.updated` when their
143
+ // model is assigned, and a host validating on every update would loop.
144
+ const configureElement = element;
145
+ if (JSON.stringify(configureElement.model?.errors) !==
146
+ JSON.stringify(errors)) {
147
+ configureElement.model = validatedModel;
148
+ }
142
149
  }
143
150
  }
144
151
  return {
145
- hasErrors: validatedModels.some((model) => hasValidationErrors(model.validation)),
152
+ hasErrors: validatedModels.some((model) => hasValidationErrors(model.errors)),
146
153
  validatedModels,
147
154
  };
148
155
  }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Which delivery-element `session-changed` announcements a renderer forwards.
3
+ *
4
+ * Every update pass hands each element its session again, and an element that
5
+ * announces from its `session` setter announces on every pass, with its own
6
+ * unchanged `complete`. Forwarded, those echoes follow every learner response
7
+ * and let the last element to announce set the item's completion for a host.
8
+ */
9
+ /**
10
+ * JSON with object keys in sorted order, so a session entry rebuilt with its
11
+ * keys in another order signs the same. An object met a second time is written
12
+ * as `"[Circular]"`. `null` when the value cannot be serialized.
13
+ */
14
+ export declare function sessionSignature(value: unknown): string | null;
15
+ export interface ElementAnnouncement {
16
+ /** The element that dispatched. */
17
+ element: EventTarget | null;
18
+ /** `complete` from the event detail. */
19
+ complete: unknown;
20
+ /** The session the element holds. */
21
+ session: unknown;
22
+ /** A commit at a teardown, navigation or page-hidden seam. */
23
+ commit: boolean;
24
+ }
25
+ /**
26
+ * Admits an announcement when it is the element's first, or when its `complete`
27
+ * or the element's session differs from the last announcement admitted from
28
+ * that element. A commit is always admitted. Keyed by the element instance:
29
+ * `component` repeats across two elements of one type.
30
+ */
31
+ export declare function createElementAnnouncementFilter(): (announcement: ElementAnnouncement) => boolean;
32
+ /**
33
+ * Whether an element holds a session other than its entry of the item's
34
+ * session, by identity and by content. Before the renderer's update pass, that
35
+ * is the placeholder its first initialization pass handed it, and a `complete`
36
+ * it announces describes the placeholder. An element with no readable session
37
+ * holds none.
38
+ */
39
+ export declare function holdsPlaceholderSession(held: unknown, entry: unknown): boolean;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Which delivery-element `session-changed` announcements a renderer forwards.
3
+ *
4
+ * Every update pass hands each element its session again, and an element that
5
+ * announces from its `session` setter announces on every pass, with its own
6
+ * unchanged `complete`. Forwarded, those echoes follow every learner response
7
+ * and let the last element to announce set the item's completion for a host.
8
+ */
9
+ /**
10
+ * JSON with object keys in sorted order, so a session entry rebuilt with its
11
+ * keys in another order signs the same. An object met a second time is written
12
+ * as `"[Circular]"`. `null` when the value cannot be serialized.
13
+ */
14
+ export function sessionSignature(value) {
15
+ try {
16
+ const seen = new WeakSet();
17
+ const json = JSON.stringify(value, (_key, nested) => {
18
+ if (!nested || typeof nested !== "object" || Array.isArray(nested)) {
19
+ return nested;
20
+ }
21
+ if (seen.has(nested))
22
+ return "[Circular]";
23
+ seen.add(nested);
24
+ const record = nested;
25
+ const sorted = {};
26
+ for (const key of Object.keys(record).sort())
27
+ sorted[key] = record[key];
28
+ return sorted;
29
+ });
30
+ return json ?? "undefined";
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ /**
37
+ * Admits an announcement when it is the element's first, or when its `complete`
38
+ * or the element's session differs from the last announcement admitted from
39
+ * that element. A commit is always admitted. Keyed by the element instance:
40
+ * `component` repeats across two elements of one type.
41
+ */
42
+ export function createElementAnnouncementFilter() {
43
+ const admitted = new WeakMap();
44
+ return ({ element, complete, session, commit }) => {
45
+ if (!element || typeof element !== "object")
46
+ return true;
47
+ const signature = sessionSignature({ complete, session });
48
+ if (signature === null)
49
+ return true;
50
+ if (!commit && admitted.get(element) === signature)
51
+ return false;
52
+ admitted.set(element, signature);
53
+ return true;
54
+ };
55
+ }
56
+ /**
57
+ * Whether an element holds a session other than its entry of the item's
58
+ * session, by identity and by content. Before the renderer's update pass, that
59
+ * is the placeholder its first initialization pass handed it, and a `complete`
60
+ * it announces describes the placeholder. An element with no readable session
61
+ * holds none.
62
+ */
63
+ export function holdsPlaceholderSession(held, entry) {
64
+ if (held === undefined || held === entry)
65
+ return false;
66
+ return sessionSignature(held) !== sessionSignature(entry);
67
+ }
@@ -20,7 +20,7 @@
20
20
  * recently registered, silently never bound.
21
21
  */
22
22
  import type { ConfigEntity, Env } from "../types/index.js";
23
- import type { EventListenersMap } from "./types.js";
23
+ import type { BundleType, EventListenersMap } from "./types.js";
24
24
  /**
25
25
  * What one registration binds a PIE element with.
26
26
  */
@@ -29,6 +29,8 @@ export interface PieElementContext {
29
29
  session: any[];
30
30
  env?: Env;
31
31
  eventListeners?: EventListenersMap;
32
+ /** The delivering player's bundle type, see `findPieController`. */
33
+ bundleType?: BundleType;
32
34
  }
33
35
  /**
34
36
  * Reads the context to bind with.
@@ -46,6 +46,7 @@ const bindElement = (element, contexts) => {
46
46
  session: context.session,
47
47
  env: context.env,
48
48
  eventListeners: context.eventListeners?.[tagName],
49
+ bundleType: context.bundleType,
49
50
  });
50
51
  if (bound)
51
52
  return;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * When the PIE elements of a player have rendered.
3
+ *
4
+ * An element renders after its model and session are assigned, in a task of
5
+ * its own: legacy React elements debounce the render and commit through the
6
+ * React scheduler, so the commit lands tens of milliseconds after the
7
+ * assignment. No event marks it in both element generations — `model-set` is
8
+ * dispatched by about half of them, some before rendering — so this reads the
9
+ * DOM.
10
+ *
11
+ * An element has rendered once it holds content, and so has every custom
12
+ * element defined below it: an `ebsr` paints its parts, which then render
13
+ * themselves. Some elements render nothing, a rubric for a student among them,
14
+ * so an element still empty when the subtree has had no mutation for
15
+ * `quietMs` counts as rendered. A render slower than that window, with no
16
+ * mutation before it, is missed rather than waited for.
17
+ */
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
+ /**
22
+ * Resolves once every element of `configs` in `root` has rendered, or when
23
+ * `signal` aborts. Never rejects.
24
+ */
25
+ export declare function elementsRendered(root: Element, configs: ConfigEntity[], signal: AbortSignal, quietMs?: number): Promise<void>;