@pie-players/pie-assessment-toolkit 0.3.74 → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/README.md +658 -714
  2. package/custom-elements.json +1315 -0
  3. package/dist/attempt/TestSession.d.ts +1 -21
  4. package/dist/attempt/TestSession.js +16 -102
  5. package/dist/components/ItemScope.custom-element.d.ts +1 -0
  6. package/dist/components/ItemScope.custom-element.js +1 -0
  7. package/dist/components/ItemToolBar.custom-element.js +1 -1
  8. package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
  9. package/dist/components/SectionToolBar.custom-element.js +1 -1
  10. package/dist/components/chunks/ItemToolBar-3zrc4p3d.js +4 -0
  11. package/dist/components/chunks/ItemToolBar-66hcwmfw.js +2 -0
  12. package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
  13. package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
  14. package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-kkj69ptr.js} +1 -1
  15. package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
  16. package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
  17. package/dist/components/item-scope-element.d.ts +2 -0
  18. package/dist/components/item-scope-element.js +2 -0
  19. package/dist/context/assessment-toolkit-context.d.ts +33 -6
  20. package/dist/index.d.ts +25 -50
  21. package/dist/index.js +15 -45
  22. package/dist/policy/core/PolicySource.d.ts +2 -2
  23. package/dist/policy/core/ToolPolicyEngine.d.ts +118 -34
  24. package/dist/policy/core/ToolPolicyEngine.js +203 -56
  25. package/dist/policy/core/compose-decision.d.ts +28 -7
  26. package/dist/policy/core/compose-decision.js +95 -74
  27. package/dist/policy/core/decision-types.d.ts +98 -55
  28. package/dist/policy/core/feature-decision.d.ts +28 -19
  29. package/dist/policy/core/feature-decision.js +15 -11
  30. package/dist/policy/core/pnp-policy-inputs.d.ts +29 -35
  31. package/dist/policy/core/pnp-policy-inputs.js +40 -88
  32. package/dist/policy/core/provenance.d.ts +10 -5
  33. package/dist/policy/core/provenance.js +1 -1
  34. package/dist/policy/core/tool-parameters.d.ts +11 -0
  35. package/dist/policy/core/tool-parameters.js +15 -0
  36. package/dist/policy/engine.d.ts +7 -23
  37. package/dist/policy/engine.js +5 -20
  38. package/dist/policy/sources/PnpPolicySource.d.ts +43 -68
  39. package/dist/policy/sources/PnpPolicySource.js +133 -132
  40. package/dist/runtime/SectionControllerBinding.d.ts +283 -0
  41. package/dist/runtime/SectionControllerBinding.js +456 -0
  42. package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
  43. package/dist/runtime/SectionRuntimeEngine.js +30 -372
  44. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
  45. package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
  46. package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
  47. package/dist/runtime/adapter/dom-event-bridge.js +17 -44
  48. package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -3
  49. package/dist/runtime/adapter/subscriber-fanout.js +2 -6
  50. package/dist/runtime/catalog-context.d.ts +19 -0
  51. package/dist/runtime/catalog-context.js +28 -0
  52. package/dist/runtime/composition-emit-scheduler.d.ts +12 -18
  53. package/dist/runtime/composition-emit-scheduler.js +10 -16
  54. package/dist/runtime/content-language.d.ts +32 -0
  55. package/dist/runtime/content-language.js +46 -0
  56. package/dist/runtime/content-region.d.ts +13 -0
  57. package/dist/runtime/content-region.js +29 -0
  58. package/dist/runtime/content-registration.d.ts +13 -0
  59. package/dist/runtime/content-registration.js +25 -0
  60. package/dist/runtime/core/SectionEngineCore.d.ts +2 -4
  61. package/dist/runtime/core/SectionEngineCore.js +2 -6
  62. package/dist/runtime/core/cohort.d.ts +1 -6
  63. package/dist/runtime/core/cohort.js +1 -10
  64. package/dist/runtime/core/engine-input.d.ts +18 -30
  65. package/dist/runtime/core/engine-input.js +11 -12
  66. package/dist/runtime/core/engine-output.d.ts +17 -40
  67. package/dist/runtime/core/engine-output.js +9 -21
  68. package/dist/runtime/core/engine-readiness.d.ts +5 -37
  69. package/dist/runtime/core/engine-readiness.js +8 -41
  70. package/dist/runtime/core/engine-resolver.d.ts +46 -74
  71. package/dist/runtime/core/engine-resolver.js +18 -43
  72. package/dist/runtime/core/engine-state.d.ts +28 -36
  73. package/dist/runtime/core/engine-state.js +18 -21
  74. package/dist/runtime/core/engine-transition.d.ts +14 -19
  75. package/dist/runtime/core/engine-transition.js +74 -53
  76. package/dist/runtime/engine.d.ts +16 -19
  77. package/dist/runtime/engine.js +12 -19
  78. package/dist/runtime/page-warnings.d.ts +13 -0
  79. package/dist/runtime/page-warnings.js +22 -0
  80. package/dist/runtime/policy-input-forwarding.d.ts +25 -0
  81. package/dist/runtime/policy-input-forwarding.js +25 -0
  82. package/dist/runtime/registration-events.d.ts +22 -14
  83. package/dist/runtime/runtime-event-claim.d.ts +8 -0
  84. package/dist/runtime/runtime-event-claim.js +15 -0
  85. package/dist/runtime/runtime-id.d.ts +1 -1
  86. package/dist/runtime/runtime-id.js +1 -1
  87. package/dist/runtime/shell-event-bridge.d.ts +45 -0
  88. package/dist/runtime/shell-event-bridge.js +127 -0
  89. package/dist/runtime/shell-registration.d.ts +73 -0
  90. package/dist/runtime/shell-registration.js +95 -0
  91. package/dist/runtime/shell-scope.d.ts +68 -0
  92. package/dist/runtime/shell-scope.js +194 -0
  93. package/dist/runtime/tool-host-contract.d.ts +8 -14
  94. package/dist/runtime/tool-host-contract.js +12 -12
  95. package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
  96. package/dist/runtime/unclaimed-registration-watch.js +23 -0
  97. package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
  98. package/dist/services/AccessibilityCatalogResolver.js +29 -42
  99. package/dist/services/ElementToolStateStore.d.ts +42 -67
  100. package/dist/services/ElementToolStateStore.js +65 -71
  101. package/dist/services/HighlightCoordinator.d.ts +28 -40
  102. package/dist/services/HighlightCoordinator.js +459 -381
  103. package/dist/services/RangeSerializer.d.ts +11 -33
  104. package/dist/services/RangeSerializer.js +88 -59
  105. package/dist/services/SSMLExtractor.d.ts +8 -31
  106. package/dist/services/SSMLExtractor.js +18 -40
  107. package/dist/services/TTSService.d.ts +159 -119
  108. package/dist/services/TTSService.js +937 -822
  109. package/dist/services/ToolCoordinator.d.ts +42 -102
  110. package/dist/services/ToolCoordinator.js +178 -256
  111. package/dist/services/ToolRegistry.d.ts +152 -133
  112. package/dist/services/ToolRegistry.js +192 -178
  113. package/dist/services/ToolkitCoordinator.d.ts +233 -237
  114. package/dist/services/ToolkitCoordinator.js +615 -557
  115. package/dist/services/audio-handoff.d.ts +6 -7
  116. package/dist/services/audio-handoff.js +20 -14
  117. package/dist/services/catalog-owner.d.ts +0 -1
  118. package/dist/services/framework-error-bus.d.ts +3 -6
  119. package/dist/services/framework-error-bus.js +3 -6
  120. package/dist/services/framework-error.d.ts +51 -13
  121. package/dist/services/framework-error.js +19 -20
  122. package/dist/services/interfaces.d.ts +189 -184
  123. package/dist/services/section-controller-types.d.ts +7 -7
  124. package/dist/services/section-session-assignment.d.ts +21 -0
  125. package/dist/services/section-session-assignment.js +80 -0
  126. package/dist/services/spoken-audio-cards.d.ts +2 -6
  127. package/dist/services/spoken-audio-cards.js +2 -6
  128. package/dist/services/tool-config-defaults.d.ts +3 -3
  129. package/dist/services/tool-config-defaults.js +3 -3
  130. package/dist/services/tool-config-validation.d.ts +9 -12
  131. package/dist/services/tool-config-validation.js +47 -87
  132. package/dist/services/tool-context.d.ts +7 -31
  133. package/dist/services/tool-context.js +99 -65
  134. package/dist/services/tool-failure.d.ts +30 -0
  135. package/dist/services/tool-failure.js +11 -0
  136. package/dist/services/tool-icons.d.ts +5 -5
  137. package/dist/services/tool-icons.js +6 -5
  138. package/dist/services/tool-instance-id.d.ts +8 -7
  139. package/dist/services/tool-instance-id.js +12 -24
  140. package/dist/services/tool-providers/TTSToolProvider.d.ts +20 -58
  141. package/dist/services/tool-providers/TTSToolProvider.js +32 -85
  142. package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -11
  143. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +29 -60
  144. package/dist/services/tool-providers/ToolProviderRegistry.js +128 -144
  145. package/dist/services/tool-request.d.ts +31 -12
  146. package/dist/services/tool-request.js +31 -16
  147. package/dist/services/toolbar-decided-tools.d.ts +16 -0
  148. package/dist/services/toolbar-decided-tools.js +29 -0
  149. package/dist/services/toolbar-items.js +2 -5
  150. package/dist/services/tools-config-normalizer.d.ts +33 -29
  151. package/dist/services/tools-config-normalizer.js +26 -28
  152. package/dist/services/tts/browser-provider.d.ts +39 -6
  153. package/dist/services/tts/browser-provider.js +280 -160
  154. package/dist/services/tts/catalog-span-alignment.js +14 -7
  155. package/dist/services/tts/flat-tree.d.ts +64 -0
  156. package/dist/services/tts/flat-tree.js +269 -0
  157. package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
  158. package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
  159. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
  160. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
  161. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
  162. package/dist/services/tts/generated-speech/index.d.ts +1 -1
  163. package/dist/services/tts/generated-speech/index.js +1 -1
  164. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
  165. package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
  166. package/dist/services/tts/generated-speech/types.d.ts +1 -1
  167. package/dist/services/tts/generated-speech/types.js +1 -1
  168. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +22 -16
  169. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
  170. package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
  171. package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
  172. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
  173. package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
  174. package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
  175. package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
  176. package/dist/services/tts/math-aware-text-processing.js +42 -47
  177. package/dist/services/tts/math-control-names.d.ts +21 -0
  178. package/dist/services/tts/math-control-names.js +142 -0
  179. package/dist/services/tts/math-speech.d.ts +12 -1
  180. package/dist/services/tts/math-speech.js +54 -18
  181. package/dist/services/tts/mathml-sanitization.d.ts +7 -0
  182. package/dist/services/tts/mathml-sanitization.js +9 -1
  183. package/dist/services/tts/provider-options.d.ts +52 -0
  184. package/dist/services/tts/provider-options.js +1 -0
  185. package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
  186. package/dist/services/tts/ssml/spoken-text.js +29 -0
  187. package/dist/services/tts/start-failure.d.ts +4 -0
  188. package/dist/services/tts/start-failure.js +19 -0
  189. package/dist/services/tts/text-processing.d.ts +9 -0
  190. package/dist/services/tts/text-processing.js +46 -33
  191. package/dist/services/tts/text-segmentation.d.ts +12 -0
  192. package/dist/services/tts/text-segmentation.js +60 -10
  193. package/dist/services/tts-runtime-config.d.ts +54 -26
  194. package/dist/services/tts-runtime-config.js +27 -112
  195. package/dist/tools/content-capability-resolution.d.ts +9 -11
  196. package/dist/tools/content-capability-resolution.js +8 -45
  197. package/dist/tools/registration.d.ts +59 -0
  198. package/dist/tools/registration.js +55 -0
  199. package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
  200. package/dist/tools/registrations/toolbar-registration-helpers.js +8 -2
  201. package/dist/tools/tool-surface-host.d.ts +6 -10
  202. package/dist/tools/tool-surface-host.js +45 -50
  203. package/dist/tools/tool-tag-map.d.ts +5 -15
  204. package/dist/tools/tool-tag-map.js +6 -20
  205. package/dist/utils/structural-equality.d.ts +11 -0
  206. package/dist/utils/structural-equality.js +47 -0
  207. package/package.json +15 -29
  208. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
  209. package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
  210. package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
  211. package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
  212. package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
  213. package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
  214. package/dist/context/runtime-context-consumer.d.ts +0 -13
  215. package/dist/context/runtime-context-consumer.js +0 -18
  216. package/dist/item-loader.d.ts +0 -44
  217. package/dist/item-loader.js +0 -61
  218. package/dist/policy/core/engine-context.d.ts +0 -29
  219. package/dist/policy/core/engine-context.js +0 -27
  220. package/dist/policy/core/policy-source-tag.d.ts +0 -31
  221. package/dist/policy/core/policy-source-tag.js +0 -28
  222. package/dist/policy/internal.d.ts +0 -25
  223. package/dist/policy/internal.js +0 -25
  224. package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
  225. package/dist/runtime/adapter/coordinator-bridge.js +0 -126
  226. package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
  227. package/dist/runtime/adapter/framework-error-bridge.js +0 -46
  228. package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
  229. package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
  230. package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
  231. package/dist/runtime/core/engine-stage-derivation.js +0 -29
  232. package/dist/runtime/engine-context.d.ts +0 -39
  233. package/dist/runtime/engine-context.js +0 -31
  234. package/dist/runtime/internal.d.ts +0 -43
  235. package/dist/runtime/internal.js +0 -45
  236. package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
  237. package/dist/runtime/section-runtime-engine-host-context.js +0 -57
  238. package/dist/runtime/stage-emit-gate.d.ts +0 -92
  239. package/dist/runtime/stage-emit-gate.js +0 -82
  240. package/dist/services/ContextVariableStore.d.ts +0 -113
  241. package/dist/services/ContextVariableStore.js +0 -206
  242. package/dist/services/I18nService.d.ts +0 -47
  243. package/dist/services/I18nService.js +0 -84
  244. package/dist/services/ThemeProvider.d.ts +0 -95
  245. package/dist/services/ThemeProvider.js +0 -334
  246. package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
  247. package/dist/services/defaultPersonalNeedsProfile.js +0 -26
  248. package/dist/services/pnp-standard-features.d.ts +0 -238
  249. package/dist/services/pnp-standard-features.js +0 -338
  250. package/dist/services/tts-control-events.d.ts +0 -13
  251. package/dist/services/tts-control-events.js +0 -7
  252. package/dist/tools/client.d.ts +0 -10
  253. package/dist/tools/client.js +0 -10
  254. package/dist/tools/internal.d.ts +0 -44
  255. package/dist/tools/internal.js +0 -39
  256. package/dist/utils/logger.d.ts +0 -6
  257. package/dist/utils/logger.js +0 -10
@@ -0,0 +1,456 @@
1
+ /**
2
+ * Section controller binding — the toolkit's side of a section runtime.
3
+ *
4
+ * `PieAssessmentToolkit` holds one per element. It resolves the section's
5
+ * controller from the coordinator (`initialize`), keeps the registered item and
6
+ * passage shells (`RuntimeRegistry`) and which of them have loaded, replays
7
+ * both into each controller it resolves, and forwards shell events, session
8
+ * updates and host commands to that controller.
9
+ *
10
+ * The registry outlives a section switch: the shells of the section being left
11
+ * stay registered until they unmount. One membership rule routes every shell
12
+ * event, in replay or live: an event goes to the controller of the section
13
+ * whose composition renders the shell, which is the current controller or a
14
+ * cached controller of a section this binding bound earlier, and is dropped
15
+ * with a warning when no bound section renders it; see `controllerFor`. A
16
+ * cached controller re-bound on a revisit first forgets the loads of shells
17
+ * that are no longer registered, so its `section-loading-complete` waits for
18
+ * the new shells.
19
+ *
20
+ * The section's stage chain is a separate object: the section player's layout
21
+ * kernel drives a `SectionRuntimeEngine` from the readiness signals it derives.
22
+ * The two share no state.
23
+ */
24
+ import { createPieLogger, isGlobalDebugEnabled, } from "@pie-players/pie-players-shared";
25
+ import { RuntimeRegistry } from "./RuntimeRegistry.js";
26
+ const logger = createPieLogger("section-controller-binding", () => isGlobalDebugEnabled());
27
+ /**
28
+ * The `entity.id` of each of a composition model's `renderables`: the ids its
29
+ * item and passage shells register with. `null` when the model has no
30
+ * `renderables` array.
31
+ */
32
+ function readRenderableIds(model) {
33
+ const renderables = model
34
+ ?.renderables;
35
+ if (!Array.isArray(renderables))
36
+ return null;
37
+ const ids = new Set();
38
+ for (const renderable of renderables) {
39
+ const id = renderable?.entity
40
+ ?.id;
41
+ if (typeof id === "string" && id)
42
+ ids.add(id);
43
+ }
44
+ return ids;
45
+ }
46
+ /** Whether `ids` holds the shell's runtime or canonical id. */
47
+ function rendersShell(ids, shell) {
48
+ return (ids.has(shell.itemId) ||
49
+ (!!shell.canonicalItemId && ids.has(shell.canonicalItemId)));
50
+ }
51
+ export class SectionControllerBinding {
52
+ registry = new RuntimeRegistry();
53
+ controller = null;
54
+ coordinator = null;
55
+ sectionId = "";
56
+ attemptId;
57
+ unsubscribeController = null;
58
+ activeInitToken = 0;
59
+ /**
60
+ * Ids of the renderables in the current controller's composition model, read
61
+ * when `initialize` resolves it. `null` while no controller is resolved, and
62
+ * for a controller that publishes no `renderables`: its section cannot be
63
+ * told apart, so every shell reaches it.
64
+ */
65
+ sectionRenderableIds = null;
66
+ /**
67
+ * The renderable ids of every section this binding has bound, keyed by
68
+ * section and attempt, least recently bound first. `controllerFor` reads it
69
+ * to find the section that owns a shell outside the current one.
70
+ */
71
+ boundSections = new Map();
72
+ /**
73
+ * Tracks which `(canonicalItemId, contentKind)` pairs have already
74
+ * fired `handleContentLoaded` on this binding, so a section handoff can
75
+ * replay the persistent shells' loaded state into the new controller; see
76
+ * `replayRegisteredShellsIntoController`.
77
+ *
78
+ * Records loads of every registered shell, in or out of the current
79
+ * section, so a shell registered ahead of its section's controller is
80
+ * replayed as loaded. A key is dropped when its last shell unregisters,
81
+ * and on `dispose`.
82
+ */
83
+ loadedRenderableKeys = new Set();
84
+ async initialize(args) {
85
+ this.activeInitToken += 1;
86
+ const token = this.activeInitToken;
87
+ this.coordinator = args.coordinator;
88
+ this.sectionId = args.sectionId;
89
+ this.attemptId = args.attemptId;
90
+ // Before the coordinator re-activates a cached controller and replays its
91
+ // loaded state to subscribers.
92
+ this.forgetUnregisteredLoads(args.coordinator.getSectionController({
93
+ sectionId: args.sectionId,
94
+ attemptId: args.attemptId,
95
+ }));
96
+ let resolved;
97
+ try {
98
+ resolved = (await args.coordinator.getOrCreateSectionController({
99
+ sectionId: args.sectionId,
100
+ attemptId: args.attemptId,
101
+ input: {
102
+ section: args.section,
103
+ sectionId: args.sectionId,
104
+ assessmentId: args.assessmentId,
105
+ view: args.view,
106
+ },
107
+ updateExisting: true,
108
+ initialSession: args.initialSession,
109
+ createDefaultController: args.createDefaultController,
110
+ }));
111
+ }
112
+ catch (error) {
113
+ // The previous section's controller must not keep serving the
114
+ // section that failed to start.
115
+ if (token === this.activeInitToken) {
116
+ this.unsubscribeController?.();
117
+ this.unsubscribeController = null;
118
+ this.controller = null;
119
+ }
120
+ throw error;
121
+ }
122
+ if (token !== this.activeInitToken)
123
+ return;
124
+ this.unsubscribeController?.();
125
+ this.controller = resolved;
126
+ this.sectionRenderableIds = readRenderableIds(resolved.getCompositionModel?.());
127
+ const sectionKey = this.getSectionKey(args.sectionId, args.attemptId);
128
+ this.boundSections.delete(sectionKey);
129
+ this.boundSections.set(sectionKey, {
130
+ sectionId: args.sectionId,
131
+ attemptId: args.attemptId,
132
+ renderableIds: this.sectionRenderableIds,
133
+ });
134
+ // Every initialize re-feeds the registered shells of the resolved
135
+ // controller's section, a same-section `updateInput` included: the
136
+ // controller's register and load are idempotent, so a re-feed of shells
137
+ // it already tracks changes nothing.
138
+ this.replayRegisteredShellsIntoController(resolved);
139
+ args.onCompositionChanged?.(resolved.getCompositionModel?.());
140
+ this.unsubscribeController =
141
+ resolved.subscribe?.((event) => {
142
+ // Isolated deliberately: the controller's emit loop catches per
143
+ // listener, so a diagnostic handler that throws would take the
144
+ // composition republish down with it and every cue and Try would stop
145
+ // reaching the cards — a failure with no symptom except a warning.
146
+ try {
147
+ args.onControllerEvent?.(event);
148
+ }
149
+ catch (error) {
150
+ logger.warn("onControllerEvent handler threw", error);
151
+ }
152
+ args.onCompositionChanged?.(resolved.getCompositionModel?.());
153
+ }) || null;
154
+ }
155
+ /**
156
+ * Re-feed the binding's `RuntimeRegistry` and loaded-set into the
157
+ * resolved controller, limited to the shells of its section (see
158
+ * `isSectionRenderable`). Runs on every `initialize`.
159
+ *
160
+ * Two-pass order — register every shell in document order, then
161
+ * issue `handleContentLoaded` for each shell whose load already
162
+ * fired on this binding. The two-pass shape prevents
163
+ * `evaluateSectionLoadingState` from flapping
164
+ * `section-loading-complete` `false→true→false→…` while replay is
165
+ * mid-walk: any subscriber wired before the binding's own
166
+ * `controller.subscribe` (no such caller today, but cheap defense
167
+ * for future wiring) sees one clean `false→true` transition.
168
+ *
169
+ * Idempotent on the controller side: `handleContentRegistered` and
170
+ * `handleContentLoaded` return early on duplicates, so re-feeding shells
171
+ * the controller already tracks emits nothing.
172
+ */
173
+ replayRegisteredShellsIntoController(controller) {
174
+ if (!controller.handleContentRegistered)
175
+ return;
176
+ const shells = this.registry
177
+ .getOrderedShells()
178
+ .filter((shell) => this.isSectionRenderable(shell));
179
+ if (shells.length === 0)
180
+ return;
181
+ for (const shell of shells) {
182
+ const canonicalItemId = shell.canonicalItemId || shell.itemId;
183
+ controller.handleContentRegistered({
184
+ itemId: shell.itemId,
185
+ canonicalItemId,
186
+ contentKind: shell.contentKind || shell.kind,
187
+ });
188
+ }
189
+ const now = Date.now();
190
+ for (const shell of shells) {
191
+ const canonicalItemId = shell.canonicalItemId || shell.itemId;
192
+ const key = this.getLoadedKey(canonicalItemId, shell.contentKind);
193
+ if (this.loadedRenderableKeys.has(key)) {
194
+ controller.handleContentLoaded?.({
195
+ itemId: shell.itemId,
196
+ canonicalItemId,
197
+ contentKind: shell.contentKind || shell.kind,
198
+ timestamp: now,
199
+ });
200
+ }
201
+ }
202
+ }
203
+ /**
204
+ * Stable key for the binding's `loadedRenderableKeys` set. Mirrors
205
+ * the `(canonicalItemId, contentKind)` shape `SectionController`
206
+ * uses internally (see `getRenderableKey`); we only need
207
+ * consistent add/check semantics on this side, not byte-identical
208
+ * normalization with the controller.
209
+ */
210
+ getLoadedKey(canonicalItemId, contentKind) {
211
+ return `${contentKind ?? ""}:${canonicalItemId}`;
212
+ }
213
+ /**
214
+ * Whether a shell renders one of the current section's renderables: its
215
+ * runtime or canonical id is among the composition model's renderable ids.
216
+ * An item in two sections belongs to both, so its shell carries over a
217
+ * switch the way a persistent passage shell does.
218
+ */
219
+ isSectionRenderable(args) {
220
+ const ids = this.sectionRenderableIds;
221
+ return !ids || rendersShell(ids, args);
222
+ }
223
+ getSectionKey(sectionId, attemptId) {
224
+ return `${sectionId}\u0000${attemptId ?? ""}`;
225
+ }
226
+ /**
227
+ * The controller a shell event belongs to: the current controller when the
228
+ * shell renders one of its section's renderables, otherwise the cached
229
+ * controller of the most recently bound section that renders it — the
230
+ * section being left, for an outgoing shell's teardown. `null` when no bound
231
+ * section renders the shell or its controller is gone.
232
+ */
233
+ controllerFor(args) {
234
+ if (this.isSectionRenderable(args))
235
+ return this.controller;
236
+ const current = this.getSectionKey(this.sectionId, this.attemptId);
237
+ const sections = [...this.boundSections.entries()].reverse();
238
+ for (const [key, section] of sections) {
239
+ if (key === current || !section.renderableIds)
240
+ continue;
241
+ if (!rendersShell(section.renderableIds, args))
242
+ continue;
243
+ const owner = this.coordinator?.getSectionController({
244
+ sectionId: section.sectionId,
245
+ attemptId: section.attemptId,
246
+ });
247
+ if (owner)
248
+ return owner;
249
+ }
250
+ return null;
251
+ }
252
+ /** `controllerFor`, warning when the event has nowhere to go. */
253
+ requireControllerFor(args, what) {
254
+ const controller = this.controllerFor(args);
255
+ if (!controller) {
256
+ logger.warn(`Dropped ${what} for "${args.itemId}": no section this toolkit bound renders it.`);
257
+ }
258
+ return controller;
259
+ }
260
+ /**
261
+ * Forget, on a cached controller about to be re-bound, every load whose
262
+ * shell is no longer registered. A shell that unmounted while its section
263
+ * was not current told that section's controller through `controllerFor`;
264
+ * this covers one whose unregister never arrived.
265
+ */
266
+ forgetUnregisteredLoads(cached) {
267
+ if (!cached || cached === this.controller)
268
+ return;
269
+ const loaded = cached.getRuntimeState?.()?.loadedRenderables ?? [];
270
+ if (loaded.length === 0)
271
+ return;
272
+ const live = new Set();
273
+ for (const shell of this.registry.getOrderedShells()) {
274
+ live.add(shell.itemId);
275
+ if (shell.canonicalItemId)
276
+ live.add(shell.canonicalItemId);
277
+ }
278
+ for (const renderable of loaded) {
279
+ if (live.has(renderable.canonicalItemId) || live.has(renderable.itemId)) {
280
+ continue;
281
+ }
282
+ cached.handleContentUnregistered?.({
283
+ itemId: renderable.itemId,
284
+ canonicalItemId: renderable.canonicalItemId,
285
+ contentKind: renderable.contentKind,
286
+ });
287
+ }
288
+ }
289
+ register(detail) {
290
+ return this.registry.register(detail);
291
+ }
292
+ unregister(element) {
293
+ return this.registry.unregister(element);
294
+ }
295
+ getCompositionModel() {
296
+ return this.controller?.getCompositionModel?.() ?? null;
297
+ }
298
+ getCanonicalItemId(itemId) {
299
+ const map = this.registry.getCanonicalIdMap();
300
+ return (map[itemId] || this.controller?.getCanonicalItemId?.(itemId) || itemId);
301
+ }
302
+ handleContentRegistered(detail) {
303
+ if (!this.isSectionRenderable(detail))
304
+ return;
305
+ this.controller?.handleContentRegistered?.({
306
+ itemId: detail.itemId,
307
+ canonicalItemId: detail.canonicalItemId || detail.itemId,
308
+ contentKind: detail.contentKind || detail.kind,
309
+ });
310
+ }
311
+ /**
312
+ * Called after `unregister` removed the shell from the registry. While
313
+ * another registered shell renders the same renderable — the next
314
+ * section's shell for an item both sections hold — the renderable stays
315
+ * registered and loaded.
316
+ */
317
+ handleContentUnregistered(detail) {
318
+ const canonicalItemId = detail.canonicalItemId || detail.itemId;
319
+ // Key the load-set delete with the same `contentKind`-only
320
+ // shape that `handleContentLoaded` and
321
+ // `replayRegisteredShellsIntoController` use, so add/check/
322
+ // delete keys round-trip identically. The `|| detail.kind`
323
+ // fallback is preserved for the controller-forwarded payload
324
+ // because the controller normalizes via `toSectionContentKind`
325
+ // and the `kind` field remains meaningful there.
326
+ const key = this.getLoadedKey(canonicalItemId, detail.contentKind);
327
+ const stillRendered = this.registry
328
+ .getOrderedShells()
329
+ .some((shell) => this.getLoadedKey(shell.canonicalItemId || shell.itemId, shell.contentKind) === key);
330
+ if (stillRendered)
331
+ return;
332
+ this.loadedRenderableKeys.delete(key);
333
+ this.controllerFor(detail)?.handleContentUnregistered?.({
334
+ itemId: detail.itemId,
335
+ canonicalItemId,
336
+ contentKind: detail.contentKind || detail.kind,
337
+ });
338
+ }
339
+ handleContentLoaded(args) {
340
+ const canonicalItemId = args.canonicalItemId || args.itemId;
341
+ this.loadedRenderableKeys.add(this.getLoadedKey(canonicalItemId, args.contentKind));
342
+ if (!this.isSectionRenderable(args))
343
+ return;
344
+ this.controller?.handleContentLoaded?.(args);
345
+ }
346
+ handleItemPlayerError(args) {
347
+ this.requireControllerFor(args, "an item player error")?.handleItemPlayerError?.(args);
348
+ }
349
+ reportSectionError(args) {
350
+ const controller = args.itemId
351
+ ? this.requireControllerFor({ itemId: args.itemId, canonicalItemId: args.canonicalItemId }, "a section error")
352
+ : this.controller;
353
+ controller?.reportSectionError?.(args);
354
+ }
355
+ updateItemSession(itemId, session) {
356
+ const canonicalId = this.getCanonicalItemId(itemId);
357
+ const controller = this.requireControllerFor({ itemId, canonicalItemId: canonicalId }, "a session update");
358
+ return controller?.updateItemSession?.(canonicalId, session) ?? null;
359
+ }
360
+ navigateToItem(index) {
361
+ return this.controller?.navigateToItem?.(index) ?? null;
362
+ }
363
+ /**
364
+ * Route a learner's formative action to the controller.
365
+ *
366
+ * Canonicalized here for the same reason `updateItemSession` is: the runtime
367
+ * id a card dispatches with is not necessarily the identifier the controller
368
+ * keys state by. Both ids are kept, since the composition's renderables are
369
+ * keyed by the runtime one.
370
+ */
371
+ handleFormativeAction(action) {
372
+ if (!action?.itemId)
373
+ return;
374
+ const canonicalId = action.canonicalItemId || this.getCanonicalItemId(action.itemId);
375
+ const controller = this.requireControllerFor({ itemId: action.itemId, canonicalItemId: canonicalId }, "a formative action");
376
+ if (action.action === "retry") {
377
+ controller?.retryFormativeItem?.({ itemId: canonicalId });
378
+ return;
379
+ }
380
+ controller?.recordFormativeTry?.({
381
+ itemId: canonicalId,
382
+ outcomes: action.outcomes,
383
+ });
384
+ }
385
+ /**
386
+ * Bind or release the section's Media Time Source.
387
+ *
388
+ * A pass-through, like `handleFormativeAction`: the binding routes, the
389
+ * controller decides. `renderableId` travels with the attach because whether the
390
+ * registering renderable is the one `stimulusRef` names is the controller's call
391
+ * — it validated `stimulusRef` in the first place.
392
+ */
393
+ handleMediaTimeSource(action) {
394
+ if (!action?.renderableId)
395
+ return;
396
+ const controller = this.requireControllerFor({
397
+ itemId: action.renderableId,
398
+ canonicalItemId: this.getCanonicalItemId(action.renderableId),
399
+ }, "a media time source");
400
+ if (action.action === "detach") {
401
+ controller?.detachMediaTimeSource?.({
402
+ origin: action.origin ?? "host",
403
+ });
404
+ return;
405
+ }
406
+ if (!action.source)
407
+ return;
408
+ controller?.attachMediaTimeSource?.(action.source, {
409
+ origin: action.origin ?? "host",
410
+ renderableId: action.renderableId,
411
+ });
412
+ }
413
+ /**
414
+ * Silence media audio because something else is about to speak.
415
+ *
416
+ * A pass-through like `handleMediaTimeSource`, and for the same reason: which
417
+ * source is authoritative and whether it reports `canPause` are the controller's
418
+ * to know. Returns whether media audio is now silent, or `true` where there is
419
+ * no timed-media controller to ask — nothing is playing.
420
+ */
421
+ requestMediaPauseForCompetingAudio() {
422
+ return this.controller?.pauseMediaForCompetingAudio?.() ?? true;
423
+ }
424
+ async persist() {
425
+ await this.controller?.persist?.();
426
+ }
427
+ async hydrate() {
428
+ await this.controller?.hydrate?.();
429
+ }
430
+ /**
431
+ * Releases the binding and disposes the controller of every section it
432
+ * bound, so a coordinator the host passes or an outer toolkit lends keeps
433
+ * none of this toolkit's sections, and a later mount of one restores it from
434
+ * its session. The coordinator itself stays with its owner.
435
+ */
436
+ async dispose() {
437
+ this.activeInitToken += 1;
438
+ this.unsubscribeController?.();
439
+ this.unsubscribeController = null;
440
+ const coordinator = this.coordinator;
441
+ if (coordinator) {
442
+ for (const section of this.boundSections.values()) {
443
+ await coordinator.disposeSectionController({
444
+ sectionId: section.sectionId,
445
+ attemptId: section.attemptId,
446
+ });
447
+ }
448
+ }
449
+ this.registry.clear();
450
+ this.loadedRenderableKeys.clear();
451
+ this.boundSections.clear();
452
+ this.controller = null;
453
+ this.sectionRenderableIds = null;
454
+ this.coordinator = null;
455
+ }
456
+ }