@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.
- package/README.md +658 -714
- package/custom-elements.json +1315 -0
- package/dist/attempt/TestSession.d.ts +1 -21
- package/dist/attempt/TestSession.js +16 -102
- package/dist/components/ItemScope.custom-element.d.ts +1 -0
- package/dist/components/ItemScope.custom-element.js +1 -0
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-3zrc4p3d.js +4 -0
- package/dist/components/chunks/ItemToolBar-66hcwmfw.js +2 -0
- package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
- package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
- package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-kkj69ptr.js} +1 -1
- package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
- package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
- package/dist/components/item-scope-element.d.ts +2 -0
- package/dist/components/item-scope-element.js +2 -0
- package/dist/context/assessment-toolkit-context.d.ts +33 -6
- package/dist/index.d.ts +25 -50
- package/dist/index.js +15 -45
- package/dist/policy/core/PolicySource.d.ts +2 -2
- package/dist/policy/core/ToolPolicyEngine.d.ts +118 -34
- package/dist/policy/core/ToolPolicyEngine.js +203 -56
- package/dist/policy/core/compose-decision.d.ts +28 -7
- package/dist/policy/core/compose-decision.js +95 -74
- package/dist/policy/core/decision-types.d.ts +98 -55
- package/dist/policy/core/feature-decision.d.ts +28 -19
- package/dist/policy/core/feature-decision.js +15 -11
- package/dist/policy/core/pnp-policy-inputs.d.ts +29 -35
- package/dist/policy/core/pnp-policy-inputs.js +40 -88
- package/dist/policy/core/provenance.d.ts +10 -5
- package/dist/policy/core/provenance.js +1 -1
- package/dist/policy/core/tool-parameters.d.ts +11 -0
- package/dist/policy/core/tool-parameters.js +15 -0
- package/dist/policy/engine.d.ts +7 -23
- package/dist/policy/engine.js +5 -20
- package/dist/policy/sources/PnpPolicySource.d.ts +43 -68
- package/dist/policy/sources/PnpPolicySource.js +133 -132
- package/dist/runtime/SectionControllerBinding.d.ts +283 -0
- package/dist/runtime/SectionControllerBinding.js +456 -0
- package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
- package/dist/runtime/SectionRuntimeEngine.js +30 -372
- package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
- package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
- package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
- package/dist/runtime/adapter/dom-event-bridge.js +17 -44
- package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -3
- package/dist/runtime/adapter/subscriber-fanout.js +2 -6
- package/dist/runtime/catalog-context.d.ts +19 -0
- package/dist/runtime/catalog-context.js +28 -0
- package/dist/runtime/composition-emit-scheduler.d.ts +12 -18
- package/dist/runtime/composition-emit-scheduler.js +10 -16
- package/dist/runtime/content-language.d.ts +32 -0
- package/dist/runtime/content-language.js +46 -0
- package/dist/runtime/content-region.d.ts +13 -0
- package/dist/runtime/content-region.js +29 -0
- package/dist/runtime/content-registration.d.ts +13 -0
- package/dist/runtime/content-registration.js +25 -0
- package/dist/runtime/core/SectionEngineCore.d.ts +2 -4
- package/dist/runtime/core/SectionEngineCore.js +2 -6
- package/dist/runtime/core/cohort.d.ts +1 -6
- package/dist/runtime/core/cohort.js +1 -10
- package/dist/runtime/core/engine-input.d.ts +18 -30
- package/dist/runtime/core/engine-input.js +11 -12
- package/dist/runtime/core/engine-output.d.ts +17 -40
- package/dist/runtime/core/engine-output.js +9 -21
- package/dist/runtime/core/engine-readiness.d.ts +5 -37
- package/dist/runtime/core/engine-readiness.js +8 -41
- package/dist/runtime/core/engine-resolver.d.ts +46 -74
- package/dist/runtime/core/engine-resolver.js +18 -43
- package/dist/runtime/core/engine-state.d.ts +28 -36
- package/dist/runtime/core/engine-state.js +18 -21
- package/dist/runtime/core/engine-transition.d.ts +14 -19
- package/dist/runtime/core/engine-transition.js +74 -53
- package/dist/runtime/engine.d.ts +16 -19
- package/dist/runtime/engine.js +12 -19
- package/dist/runtime/page-warnings.d.ts +13 -0
- package/dist/runtime/page-warnings.js +22 -0
- package/dist/runtime/policy-input-forwarding.d.ts +25 -0
- package/dist/runtime/policy-input-forwarding.js +25 -0
- package/dist/runtime/registration-events.d.ts +22 -14
- package/dist/runtime/runtime-event-claim.d.ts +8 -0
- package/dist/runtime/runtime-event-claim.js +15 -0
- package/dist/runtime/runtime-id.d.ts +1 -1
- package/dist/runtime/runtime-id.js +1 -1
- package/dist/runtime/shell-event-bridge.d.ts +45 -0
- package/dist/runtime/shell-event-bridge.js +127 -0
- package/dist/runtime/shell-registration.d.ts +73 -0
- package/dist/runtime/shell-registration.js +95 -0
- package/dist/runtime/shell-scope.d.ts +68 -0
- package/dist/runtime/shell-scope.js +194 -0
- package/dist/runtime/tool-host-contract.d.ts +8 -14
- package/dist/runtime/tool-host-contract.js +12 -12
- package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
- package/dist/runtime/unclaimed-registration-watch.js +23 -0
- package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
- package/dist/services/AccessibilityCatalogResolver.js +29 -42
- package/dist/services/ElementToolStateStore.d.ts +42 -67
- package/dist/services/ElementToolStateStore.js +65 -71
- package/dist/services/HighlightCoordinator.d.ts +28 -40
- package/dist/services/HighlightCoordinator.js +459 -381
- package/dist/services/RangeSerializer.d.ts +11 -33
- package/dist/services/RangeSerializer.js +88 -59
- package/dist/services/SSMLExtractor.d.ts +8 -31
- package/dist/services/SSMLExtractor.js +18 -40
- package/dist/services/TTSService.d.ts +159 -119
- package/dist/services/TTSService.js +937 -822
- package/dist/services/ToolCoordinator.d.ts +42 -102
- package/dist/services/ToolCoordinator.js +178 -256
- package/dist/services/ToolRegistry.d.ts +152 -133
- package/dist/services/ToolRegistry.js +192 -178
- package/dist/services/ToolkitCoordinator.d.ts +233 -237
- package/dist/services/ToolkitCoordinator.js +615 -557
- package/dist/services/audio-handoff.d.ts +6 -7
- package/dist/services/audio-handoff.js +20 -14
- package/dist/services/catalog-owner.d.ts +0 -1
- package/dist/services/framework-error-bus.d.ts +3 -6
- package/dist/services/framework-error-bus.js +3 -6
- package/dist/services/framework-error.d.ts +51 -13
- package/dist/services/framework-error.js +19 -20
- package/dist/services/interfaces.d.ts +189 -184
- package/dist/services/section-controller-types.d.ts +7 -7
- package/dist/services/section-session-assignment.d.ts +21 -0
- package/dist/services/section-session-assignment.js +80 -0
- package/dist/services/spoken-audio-cards.d.ts +2 -6
- package/dist/services/spoken-audio-cards.js +2 -6
- package/dist/services/tool-config-defaults.d.ts +3 -3
- package/dist/services/tool-config-defaults.js +3 -3
- package/dist/services/tool-config-validation.d.ts +9 -12
- package/dist/services/tool-config-validation.js +47 -87
- package/dist/services/tool-context.d.ts +7 -31
- package/dist/services/tool-context.js +99 -65
- package/dist/services/tool-failure.d.ts +30 -0
- package/dist/services/tool-failure.js +11 -0
- package/dist/services/tool-icons.d.ts +5 -5
- package/dist/services/tool-icons.js +6 -5
- package/dist/services/tool-instance-id.d.ts +8 -7
- package/dist/services/tool-instance-id.js +12 -24
- package/dist/services/tool-providers/TTSToolProvider.d.ts +20 -58
- package/dist/services/tool-providers/TTSToolProvider.js +32 -85
- package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -11
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +29 -60
- package/dist/services/tool-providers/ToolProviderRegistry.js +128 -144
- package/dist/services/tool-request.d.ts +31 -12
- package/dist/services/tool-request.js +31 -16
- package/dist/services/toolbar-decided-tools.d.ts +16 -0
- package/dist/services/toolbar-decided-tools.js +29 -0
- package/dist/services/toolbar-items.js +2 -5
- package/dist/services/tools-config-normalizer.d.ts +33 -29
- package/dist/services/tools-config-normalizer.js +26 -28
- package/dist/services/tts/browser-provider.d.ts +39 -6
- package/dist/services/tts/browser-provider.js +280 -160
- package/dist/services/tts/catalog-span-alignment.js +14 -7
- package/dist/services/tts/flat-tree.d.ts +64 -0
- package/dist/services/tts/flat-tree.js +269 -0
- package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
- package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
- package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
- package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
- package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
- package/dist/services/tts/generated-speech/index.d.ts +1 -1
- package/dist/services/tts/generated-speech/index.js +1 -1
- package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
- package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
- package/dist/services/tts/generated-speech/types.d.ts +1 -1
- package/dist/services/tts/generated-speech/types.js +1 -1
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +22 -16
- package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
- package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
- package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
- package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
- package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
- package/dist/services/tts/math-aware-text-processing.js +42 -47
- package/dist/services/tts/math-control-names.d.ts +21 -0
- package/dist/services/tts/math-control-names.js +142 -0
- package/dist/services/tts/math-speech.d.ts +12 -1
- package/dist/services/tts/math-speech.js +54 -18
- package/dist/services/tts/mathml-sanitization.d.ts +7 -0
- package/dist/services/tts/mathml-sanitization.js +9 -1
- package/dist/services/tts/provider-options.d.ts +52 -0
- package/dist/services/tts/provider-options.js +1 -0
- package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
- package/dist/services/tts/ssml/spoken-text.js +29 -0
- package/dist/services/tts/start-failure.d.ts +4 -0
- package/dist/services/tts/start-failure.js +19 -0
- package/dist/services/tts/text-processing.d.ts +9 -0
- package/dist/services/tts/text-processing.js +46 -33
- package/dist/services/tts/text-segmentation.d.ts +12 -0
- package/dist/services/tts/text-segmentation.js +60 -10
- package/dist/services/tts-runtime-config.d.ts +54 -26
- package/dist/services/tts-runtime-config.js +27 -112
- package/dist/tools/content-capability-resolution.d.ts +9 -11
- package/dist/tools/content-capability-resolution.js +8 -45
- package/dist/tools/registration.d.ts +59 -0
- package/dist/tools/registration.js +55 -0
- package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
- package/dist/tools/registrations/toolbar-registration-helpers.js +8 -2
- package/dist/tools/tool-surface-host.d.ts +6 -10
- package/dist/tools/tool-surface-host.js +45 -50
- package/dist/tools/tool-tag-map.d.ts +5 -15
- package/dist/tools/tool-tag-map.js +6 -20
- package/dist/utils/structural-equality.d.ts +11 -0
- package/dist/utils/structural-equality.js +47 -0
- package/package.json +15 -29
- package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
- package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
- package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
- package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
- package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
- package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
- package/dist/context/runtime-context-consumer.d.ts +0 -13
- package/dist/context/runtime-context-consumer.js +0 -18
- package/dist/item-loader.d.ts +0 -44
- package/dist/item-loader.js +0 -61
- package/dist/policy/core/engine-context.d.ts +0 -29
- package/dist/policy/core/engine-context.js +0 -27
- package/dist/policy/core/policy-source-tag.d.ts +0 -31
- package/dist/policy/core/policy-source-tag.js +0 -28
- package/dist/policy/internal.d.ts +0 -25
- package/dist/policy/internal.js +0 -25
- package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
- package/dist/runtime/adapter/coordinator-bridge.js +0 -126
- package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
- package/dist/runtime/adapter/framework-error-bridge.js +0 -46
- package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
- package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
- package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
- package/dist/runtime/core/engine-stage-derivation.js +0 -29
- package/dist/runtime/engine-context.d.ts +0 -39
- package/dist/runtime/engine-context.js +0 -31
- package/dist/runtime/internal.d.ts +0 -43
- package/dist/runtime/internal.js +0 -45
- package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
- package/dist/runtime/section-runtime-engine-host-context.js +0 -57
- package/dist/runtime/stage-emit-gate.d.ts +0 -92
- package/dist/runtime/stage-emit-gate.js +0 -82
- package/dist/services/ContextVariableStore.d.ts +0 -113
- package/dist/services/ContextVariableStore.js +0 -206
- package/dist/services/I18nService.d.ts +0 -47
- package/dist/services/I18nService.js +0 -84
- package/dist/services/ThemeProvider.d.ts +0 -95
- package/dist/services/ThemeProvider.js +0 -334
- package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
- package/dist/services/defaultPersonalNeedsProfile.js +0 -26
- package/dist/services/pnp-standard-features.d.ts +0 -238
- package/dist/services/pnp-standard-features.js +0 -338
- package/dist/services/tts-control-events.d.ts +0 -13
- package/dist/services/tts-control-events.js +0 -7
- package/dist/tools/client.d.ts +0 -10
- package/dist/tools/client.js +0 -10
- package/dist/tools/internal.d.ts +0 -44
- package/dist/tools/internal.js +0 -39
- package/dist/utils/logger.d.ts +0 -6
- 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
|
+
}
|