@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,23 @@
1
+ import { warnOncePerDocument } from "./page-warnings.js";
2
+ import { PIE_REGISTER_EVENT, } from "./registration-events.js";
3
+ /**
4
+ * A toolkit stops every registration it claims, so one that reaches the
5
+ * document went unclaimed: its item's catalogs are not filed and it does not
6
+ * count toward its section's loading. Installed from a toolkit's host at
7
+ * connect, once per document across every copy of the toolkit.
8
+ */
9
+ export function watchForUnclaimedRegistrations(doc) {
10
+ if (!doc)
11
+ return;
12
+ const slot = Symbol.for("pie.assessmentToolkit.unclaimedRegistrationWatch");
13
+ const slots = doc;
14
+ if (slots[slot])
15
+ return;
16
+ slots[slot] = true;
17
+ doc.addEventListener(PIE_REGISTER_EVENT, (event) => {
18
+ const detail = event
19
+ .detail;
20
+ const itemId = typeof detail?.itemId === "string" ? detail.itemId : "";
21
+ warnOncePerDocument(doc, "unclaimedRegistration", `[pie-assessment-toolkit] Item "${itemId}" registered, but no toolkit claimed the registration, so its accessibility catalogs are not filed and it does not count toward its section's loading. Render its shell or scope inside the <pie-assessment-toolkit> that answered it. Reported once per page.`);
22
+ });
23
+ }
@@ -55,25 +55,17 @@ export type CatalogType = "spoken" | "sign-language" | "transcript" | "braille"
55
55
  /**
56
56
  * The catalog types PIE names, plus the rule for the ones it does not.
57
57
  *
58
- * The type above stays open on purpose: QTI treats the support vocabulary as
59
- * extensible, and closing it here would reject content PIE has no reason to
60
- * reject and could not usefully validate anyway, since catalogs arrive as
61
- * authored JSON rather than through this type. Keeping it open cost something
62
- * though — the named literals were documentation only, so a card written
63
- * `"spokn"` was a perfectly valid `CatalogType` that no reader would ever ask
64
- * for, and it failed by being invisible rather than by failing. That is what
65
- * `isKnownCatalogType` and the warnings below are for: the openness stays, the
66
- * silence does not.
58
+ * `CatalogType` stays open because QTI treats the support vocabulary as
59
+ * extensible, and catalogs arrive as authored JSON this type never validates.
60
+ * The trade: a card written `"spokn"` is a valid `CatalogType` no reader asks
61
+ * for, so `isKnownCatalogType` and the warnings below report it.
67
62
  */
68
63
  export declare const KNOWN_CATALOG_TYPES: ReadonlySet<string>;
69
64
  export declare function isKnownCatalogType(type: string): boolean;
70
65
  /**
71
- * Which of a card's two content slots it fills.
72
- *
73
- * Not a new field on the card and not a second discriminant: the card already
74
- * says which form it is by carrying `content` or `payload`, and the
75
- * exactly-one-of invariant is what makes that unambiguous. This names the
76
- * distinction so a lookup can ask for one.
66
+ * Which of a card's two content slots it fills. The card says so by carrying
67
+ * `content` or `payload`, and the exactly-one-of invariant makes that
68
+ * unambiguous; this type names the distinction so a lookup can ask for one.
77
69
  */
78
70
  export type CatalogCardForm = "content" | "payload";
79
71
  export declare const catalogCardForm: (card: CatalogCard) => CatalogCardForm;
@@ -93,17 +85,15 @@ export interface CatalogLookupOptions {
93
85
  * Preferred content form, when one catalog type legitimately has both on the
94
86
  * same node.
95
87
  *
96
- * The case this exists for is a `spoken` node carrying both a reading script
97
- * and a recording of it — which is APIP's authoring pattern and what QTI 3's
98
- * migration guidance tells you to keep, the script doubling as the source the
99
- * audio was generated from and as the fallback when it cannot play. Before
100
- * this, both resolution rungs took the first card matching type and language,
101
- * so whichever of the two was written second in the array was unreachable and
102
- * nothing said so.
88
+ * The case is a `spoken` node carrying both a reading script and a recording
89
+ * of it, which is APIP's authoring pattern and what QTI 3's migration
90
+ * guidance keeps: the script is the source the audio was generated from and
91
+ * the fallback when it cannot play. Without a form, a lookup returns the
92
+ * first card matching type and language.
103
93
  *
104
94
  * A preference, not a filter: if the requested form is not present, the other
105
- * one is still returned. Callers that cannot use a form must check what they
106
- * got, exactly as they already must for a card of a type they did not expect.
95
+ * one is still returned. Callers that cannot use a form check what they got,
96
+ * as they do for a card of a type they did not expect.
107
97
  */
108
98
  form?: CatalogCardForm;
109
99
  }
@@ -241,9 +231,11 @@ export declare class AccessibilityCatalogResolver {
241
231
  */
242
232
  hasCatalog(catalogId: string): boolean;
243
233
  /**
244
- * Get alternative content for a catalog identifier
234
+ * Get alternative content for a catalog identifier.
245
235
  *
246
- * Priority: Item-level catalogs take precedence over assessment-level
236
+ * With a `context`, the catalog registered for exactly that owner comes
237
+ * first, then the one compatible scoped catalog (several are ambiguous and
238
+ * resolve to null). Item-level catalogs come next, then assessment-level.
247
239
  */
248
240
  getAlternative(catalogId: string, options: CatalogLookupOptions): ResolvedCatalog | null;
249
241
  private resolveCard;
@@ -264,11 +256,9 @@ export declare class AccessibilityCatalogResolver {
264
256
  * Get all available alternatives for a catalog identifier
265
257
  *
266
258
  * Every card goes through `resolveCard`, the same projection `getAlternative`
267
- * uses, so enumeration cannot describe a card differently from the resolution
268
- * that renders it. It was hand-rolled here once and drifted immediately: the
269
- * `signLanguage` alias was folded in on the resolution path only, so a card
270
- * that arrived under the alias rendered correctly and was still reported as
271
- * carrying no payload by anything asking what alternates exist.
259
+ * uses, so enumeration describes a card exactly as the resolution that
260
+ * renders it. A hand-rolled copy here drifted: the `signLanguage` alias was
261
+ * folded in on the resolution path only.
272
262
  */
273
263
  getAllAlternatives(catalogId: string): ResolvedCatalog[];
274
264
  /**
@@ -4,15 +4,10 @@ import { sanitizeSsmlString } from "./SSMLExtractor.js";
4
4
  /**
5
5
  * The catalog types PIE names, plus the rule for the ones it does not.
6
6
  *
7
- * The type above stays open on purpose: QTI treats the support vocabulary as
8
- * extensible, and closing it here would reject content PIE has no reason to
9
- * reject and could not usefully validate anyway, since catalogs arrive as
10
- * authored JSON rather than through this type. Keeping it open cost something
11
- * though — the named literals were documentation only, so a card written
12
- * `"spokn"` was a perfectly valid `CatalogType` that no reader would ever ask
13
- * for, and it failed by being invisible rather than by failing. That is what
14
- * `isKnownCatalogType` and the warnings below are for: the openness stays, the
15
- * silence does not.
7
+ * `CatalogType` stays open because QTI treats the support vocabulary as
8
+ * extensible, and catalogs arrive as authored JSON this type never validates.
9
+ * The trade: a card written `"spokn"` is a valid `CatalogType` no reader asks
10
+ * for, so `isKnownCatalogType` and the warnings below report it.
16
11
  */
17
12
  export const KNOWN_CATALOG_TYPES = new Set([
18
13
  "spoken",
@@ -336,9 +331,11 @@ export class AccessibilityCatalogResolver {
336
331
  Array.from(this.scopedCatalogs.values()).some((owner) => owner.catalogs.has(catalogId)));
337
332
  }
338
333
  /**
339
- * Get alternative content for a catalog identifier
334
+ * Get alternative content for a catalog identifier.
340
335
  *
341
- * Priority: Item-level catalogs take precedence over assessment-level
336
+ * With a `context`, the catalog registered for exactly that owner comes
337
+ * first, then the one compatible scoped catalog (several are ambiguous and
338
+ * resolve to null). Item-level catalogs come next, then assessment-level.
342
339
  */
343
340
  getAlternative(catalogId, options) {
344
341
  // A typo on this side is as silent as one on a card: the lookup simply finds
@@ -513,10 +510,10 @@ export class AccessibilityCatalogResolver {
513
510
  }
514
511
  return false;
515
512
  }
516
- // The single funnel every registration path runs through — the constructor and
517
- // `addItemCatalogs` by way of `indexCatalogs`, and `registerCatalogs`
518
- // directly — which is why the unknown-type report lives here rather than at
519
- // each entry point.
513
+ // The single funnel every registration path runs through (the constructor and
514
+ // `addItemCatalogs` by way of `indexCatalogs`, `registerCatalogs` and
515
+ // `registerOwner` by way of `insertScopedCatalogs`), so the unknown-type
516
+ // report lives here.
520
517
  sanitizeCatalogs(catalogs) {
521
518
  return catalogs.map((catalog) => ({
522
519
  ...catalog,
@@ -541,11 +538,10 @@ export class AccessibilityCatalogResolver {
541
538
  // Language rungs, most specific first: the requested language, then the
542
539
  // default language, then any.
543
540
  //
544
- // Each requested tag expands into its RFC 4647 lookup sequence, so `es-MX`
545
- // tries `es-mx` and then `es` before falling through to the default. Matching
546
- // was `===`, which made a POSIX `es_ES` card — what the Learnosity transform
547
- // emits — unreachable for an `es-ES` request except through the final
548
- // no-constraint rung, i.e. by accident.
541
+ // Each requested tag expands into its RFC 4647 lookup sequence over
542
+ // normalized tags, so `es-MX` tries `es-mx` and then `es` before falling
543
+ // through to the default, and a POSIX `es_ES` card (what the Learnosity
544
+ // transform emits) matches an `es-ES` request on its own rung.
549
545
  const languageRungs = [];
550
546
  const pushLookupRungs = (tag) => {
551
547
  for (const step of languageTagLookupSequence(tag)) {
@@ -563,18 +559,16 @@ export class AccessibilityCatalogResolver {
563
559
  const candidates = catalog.cards.filter((card) => card.catalog === type && matchesLanguage(card));
564
560
  if (candidates.length === 0)
565
561
  continue;
566
- // Form is preferred inside a language rung and never across them: a
567
- // recording in the requested language beats a script in that language,
568
- // but a script in the requested language beats a recording in another
569
- // one. Getting this backwards would answer a Spanish lookup with English
570
- // audio, which is worse than answering it with Spanish text.
562
+ // Language outranks form: form is preferred inside a language rung and
563
+ // never across them, so a Spanish lookup preferring a recording gets a
564
+ // Spanish recording first, then Spanish text, and never English audio
565
+ // ahead of either.
571
566
  if (form) {
572
567
  const preferred = candidates.find((card) => catalogCardForm(card) === form);
573
568
  if (preferred)
574
569
  return preferred;
575
570
  }
576
- // No preference expressed, or the preferred form is absent: first match,
577
- // which is what every caller got before form preference existed.
571
+ // No preference, or the preferred form is absent: the first match.
578
572
  return candidates[0];
579
573
  }
580
574
  return null;
@@ -583,25 +577,20 @@ export class AccessibilityCatalogResolver {
583
577
  * Get all available alternatives for a catalog identifier
584
578
  *
585
579
  * Every card goes through `resolveCard`, the same projection `getAlternative`
586
- * uses, so enumeration cannot describe a card differently from the resolution
587
- * that renders it. It was hand-rolled here once and drifted immediately: the
588
- * `signLanguage` alias was folded in on the resolution path only, so a card
589
- * that arrived under the alias rendered correctly and was still reported as
590
- * carrying no payload by anything asking what alternates exist.
580
+ * uses, so enumeration describes a card exactly as the resolution that
581
+ * renders it. A hand-rolled copy here drifted: the `signLanguage` alias was
582
+ * folded in on the resolution path only.
591
583
  */
592
584
  getAllAlternatives(catalogId) {
593
585
  const results = [];
594
- // Type, language *and* form: one catalog identifier legitimately carries
595
- // several cards of the same type in different languages, and also a script
596
- // and a recording of the same type in the *same* language. Keying on type
597
- // and language alone dropped the second of those on the floor, so anything
598
- // asking what alternates exist under-reported them.
586
+ // Keyed on type, language and form: one catalog identifier can carry cards
587
+ // of one type in several languages, and a script and a recording of one
588
+ // type in the same language.
599
589
  const claimed = new Set();
600
590
  const add = (card, source) => {
601
591
  // The language part is normalized, so a POSIX `es_ES` card and a BCP-47
602
- // `es-ES` card collapse to one entry here exactly as they now collapse on
603
- // the resolution path. Keying on the raw string would report two
604
- // alternates where resolution can only ever return one.
592
+ // `es-ES` card collapse to one entry, as on the resolution path, which
593
+ // only ever returns one of them.
605
594
  const key = `${card.catalog}|${normalizeLanguageTag(card.language)}|${catalogCardForm(card)}`;
606
595
  if (claimed.has(key))
607
596
  return;
@@ -646,7 +635,6 @@ export class AccessibilityCatalogResolver {
646
635
  getStatistics() {
647
636
  const allTypes = new Set();
648
637
  const allLanguages = new Set();
649
- // Collect from assessment catalogs
650
638
  for (const catalog of this.assessmentCatalogs.values()) {
651
639
  for (const card of catalog.cards) {
652
640
  allTypes.add(card.catalog);
@@ -654,7 +642,6 @@ export class AccessibilityCatalogResolver {
654
642
  allLanguages.add(card.language);
655
643
  }
656
644
  }
657
- // Collect from item catalogs
658
645
  for (const catalog of this.itemCatalogs.values()) {
659
646
  for (const card of catalog.cards) {
660
647
  allTypes.add(card.catalog);
@@ -1,21 +1,22 @@
1
1
  /**
2
- * ElementToolStateStore - Manages element-level ephemeral tool state
2
+ * ElementToolStateStore: ephemeral UI tool state (answer eliminations, flags and
3
+ * the like), kept out of the session data sent to the server for scoring.
3
4
  *
4
- * Provides centralized storage for UI tool state (answer eliminations, flags, etc.)
5
- * that should NOT be sent to the server for scoring. Each element is uniquely identified
6
- * across the entire assessment using composite keys.
7
- *
8
- * Key features:
9
- * - Element-level granularity (not item-level)
10
- * - Composite keys for global uniqueness: `assessmentId:sectionId:itemId:elementId`
11
- * - Cross-section state persistence
12
- * - Reactive subscriptions
13
- * - Optional persistence callback integration
14
- * - Generic solution for any tool
5
+ * State is keyed by a composite global element id,
6
+ * `assessmentId:sectionId:attemptId:itemId:elementId`, unique across the
7
+ * assessment and per attempt: two attempts at one section never share a key.
8
+ * Subscribers see every change, and an optional persistence callback
9
+ * (`setOnStateChange`) receives the serialized state.
10
+ */
11
+ /**
12
+ * The parts of a global element id. `attemptId` is `""` when the host names no
13
+ * attempt. A tool whose state belongs to an item or a section rather than to
14
+ * one element leaves `elementId`, and for a section `itemId`, as `""`.
15
15
  */
16
16
  export interface ElementIdComponents {
17
17
  assessmentId: string;
18
18
  sectionId: string;
19
+ attemptId: string;
19
20
  itemId: string;
20
21
  elementId: string;
21
22
  }
@@ -24,96 +25,70 @@ export declare class ElementToolStateStore {
24
25
  private listeners;
25
26
  private onStateChange;
26
27
  /**
27
- * Generates a globally unique element ID by combining assessment, section, item, and element IDs.
28
- * Format: `${assessmentId}:${sectionId}:${itemId}:${elementId}`
28
+ * Generates a globally unique element ID from its parts.
29
+ * Format: `${assessmentId}:${sectionId}:${attemptId}:${itemId}:${elementId}`,
30
+ * each part with `%` written as `%25` and `:` as `%3A`, so an id containing
31
+ * `:` round-trips and cannot collide with another split of the same characters.
29
32
  *
30
33
  * @example
31
- * getGlobalElementId('demo-assessment', 'section-1', 'q1', 'mc1')
32
- * // Returns: "demo-assessment:section-1:q1:mc1"
34
+ * getGlobalElementId({ assessmentId: 'demo-assessment', sectionId: 'section-1',
35
+ * attemptId: 'attempt-1', itemId: 'q1', elementId: 'mc1' })
36
+ * // Returns: "demo-assessment:section-1:attempt-1:q1:mc1"
33
37
  */
34
- getGlobalElementId(assessmentId: string, sectionId: string, itemId: string, elementId: string): string;
38
+ getGlobalElementId(parts: ElementIdComponents): string;
35
39
  /**
36
40
  * Parses a global element ID into its component parts.
37
41
  *
38
42
  * @example
39
- * parseGlobalElementId('demo-assessment:section-1:q1:mc1')
40
- * // Returns: { assessmentId: 'demo-assessment', sectionId: 'section-1', itemId: 'q1', elementId: 'mc1' }
43
+ * parseGlobalElementId('demo-assessment:section-1:attempt-1:q1:mc1')
44
+ * // Returns: { assessmentId: 'demo-assessment', sectionId: 'section-1',
45
+ * // attemptId: 'attempt-1', itemId: 'q1', elementId: 'mc1' }
41
46
  */
42
47
  parseGlobalElementId(globalElementId: string): ElementIdComponents | null;
43
48
  /**
44
49
  * Sets tool state for a specific element.
45
50
  * Notifies all subscribers and triggers persistence callback.
46
- *
47
- * @param globalElementId Composite key identifying the element
48
- * @param toolId Tool identifier (e.g., 'answerEliminator', 'flagging')
49
- * @param state Tool-specific state object
50
- */
51
- setState(globalElementId: string, toolId: string, state: any): void;
52
- /**
53
- * Gets tool state for a specific element.
54
- *
55
- * @param globalElementId Composite key identifying the element
56
- * @param toolId Tool identifier
57
- * @returns Tool state or undefined if not found
58
51
  */
59
- getState(globalElementId: string, toolId: string): any | undefined;
52
+ setState(globalElementId: string, toolId: string, state: unknown): void;
53
+ /** One tool's state for an element, or `undefined` when none is set. */
54
+ getState(globalElementId: string, toolId: string): unknown;
55
+ /** Every tool's state for an element, keyed by toolId. */
56
+ getElementState(globalElementId: string): Record<string, unknown>;
60
57
  /**
61
- * Gets all tool states for a specific element.
62
- *
63
- * @param globalElementId Composite key identifying the element
64
- * @returns Object with tool states keyed by toolId
58
+ * All state as `{ globalElementId: { toolId: state } }`, for persistence and
59
+ * debugging.
65
60
  */
66
- getElementState(globalElementId: string): Record<string, any>;
67
- /**
68
- * Gets all tool states for all elements.
69
- * Used for persistence and debugging.
70
- *
71
- * @returns Nested object: { globalElementId: { toolId: state } }
72
- */
73
- getAllState(): Record<string, Record<string, any>>;
61
+ getAllState(): Record<string, Record<string, unknown>>;
74
62
  /**
75
63
  * Subscribes to state changes.
76
- * Callback is invoked whenever any element's tool state changes.
77
- *
78
- * @param callback Function to call on state changes
79
- * @returns Unsubscribe function
64
+ * Callback is invoked whenever any element's tool state changes, including
65
+ * a `loadState` restore, with a copy of the state that it may keep. Returns
66
+ * the unsubscribe function.
80
67
  */
81
- subscribe(callback: (state: Map<string, Map<string, any>>) => void): () => void;
68
+ subscribe(callback: (state: Map<string, Map<string, unknown>>) => void): () => void;
82
69
  private _notifyListeners;
83
70
  /**
84
71
  * Sets a callback to be invoked on state changes for persistence integration.
85
72
  * Used by demo/app layer to persist tool state to localStorage or server.
86
- *
87
- * @param callback Function to call with serialized state on changes
88
73
  */
89
- setOnStateChange(callback: (state: Record<string, Record<string, any>>) => void): void;
74
+ setOnStateChange(callback: (state: Record<string, Record<string, unknown>>) => void): void;
90
75
  private _notifyStateChange;
91
76
  /**
92
77
  * Loads tool state from serialized format.
93
- * Used to restore state from localStorage or server.
94
- *
95
- * @param state Serialized state object
96
- */
97
- loadState(state: Record<string, Record<string, any>>): void;
98
- /**
99
- * Clears all tool state for a specific element.
100
- *
101
- * @param globalElementId Composite key identifying the element
78
+ * Used to restore state from localStorage or server. Subscribers are
79
+ * notified; the persistence callback is not, so a restore never writes back.
102
80
  */
81
+ loadState(state: Record<string, Record<string, unknown>>): void;
82
+ /** Clears all tool state for a specific element. */
103
83
  clearElement(globalElementId: string): void;
104
84
  /**
105
85
  * Clears state for a specific tool across all elements.
106
86
  * Useful when disabling a tool globally.
107
- *
108
- * @param toolId Tool identifier
109
87
  */
110
88
  clearTool(toolId: string): void;
111
89
  /**
112
- * Clears all tool state for a specific section.
90
+ * Clears all tool state for a specific section, across its attempts.
113
91
  * Useful when unmounting/disposing a section.
114
- *
115
- * @param assessmentId Assessment identifier
116
- * @param sectionId Section identifier
117
92
  */
118
93
  clearSection(assessmentId: string, sectionId: string): void;
119
94
  /**
@@ -1,58 +1,62 @@
1
1
  /**
2
- * ElementToolStateStore - Manages element-level ephemeral tool state
2
+ * ElementToolStateStore: ephemeral UI tool state (answer eliminations, flags and
3
+ * the like), kept out of the session data sent to the server for scoring.
3
4
  *
4
- * Provides centralized storage for UI tool state (answer eliminations, flags, etc.)
5
- * that should NOT be sent to the server for scoring. Each element is uniquely identified
6
- * across the entire assessment using composite keys.
7
- *
8
- * Key features:
9
- * - Element-level granularity (not item-level)
10
- * - Composite keys for global uniqueness: `assessmentId:sectionId:itemId:elementId`
11
- * - Cross-section state persistence
12
- * - Reactive subscriptions
13
- * - Optional persistence callback integration
14
- * - Generic solution for any tool
5
+ * State is keyed by a composite global element id,
6
+ * `assessmentId:sectionId:attemptId:itemId:elementId`, unique across the
7
+ * assessment and per attempt: two attempts at one section never share a key.
8
+ * Subscribers see every change, and an optional persistence callback
9
+ * (`setOnStateChange`) receives the serialized state.
15
10
  */
11
+ const ID_PARTS = [
12
+ "assessmentId",
13
+ "sectionId",
14
+ "attemptId",
15
+ "itemId",
16
+ "elementId",
17
+ ];
18
+ function escapeIdPart(part) {
19
+ return part.replace(/%/g, "%25").replace(/:/g, "%3A");
20
+ }
21
+ function unescapeIdPart(part) {
22
+ return part.replace(/%(25|3A)/g, (_, code) => (code === "25" ? "%" : ":"));
23
+ }
16
24
  export class ElementToolStateStore {
17
25
  elementStates = new Map();
18
26
  listeners = new Set();
19
27
  onStateChange = null;
20
28
  /**
21
- * Generates a globally unique element ID by combining assessment, section, item, and element IDs.
22
- * Format: `${assessmentId}:${sectionId}:${itemId}:${elementId}`
29
+ * Generates a globally unique element ID from its parts.
30
+ * Format: `${assessmentId}:${sectionId}:${attemptId}:${itemId}:${elementId}`,
31
+ * each part with `%` written as `%25` and `:` as `%3A`, so an id containing
32
+ * `:` round-trips and cannot collide with another split of the same characters.
23
33
  *
24
34
  * @example
25
- * getGlobalElementId('demo-assessment', 'section-1', 'q1', 'mc1')
26
- * // Returns: "demo-assessment:section-1:q1:mc1"
35
+ * getGlobalElementId({ assessmentId: 'demo-assessment', sectionId: 'section-1',
36
+ * attemptId: 'attempt-1', itemId: 'q1', elementId: 'mc1' })
37
+ * // Returns: "demo-assessment:section-1:attempt-1:q1:mc1"
27
38
  */
28
- getGlobalElementId(assessmentId, sectionId, itemId, elementId) {
29
- return `${assessmentId}:${sectionId}:${itemId}:${elementId}`;
39
+ getGlobalElementId(parts) {
40
+ return ID_PARTS.map((name) => escapeIdPart(parts[name])).join(":");
30
41
  }
31
42
  /**
32
43
  * Parses a global element ID into its component parts.
33
44
  *
34
45
  * @example
35
- * parseGlobalElementId('demo-assessment:section-1:q1:mc1')
36
- * // Returns: { assessmentId: 'demo-assessment', sectionId: 'section-1', itemId: 'q1', elementId: 'mc1' }
46
+ * parseGlobalElementId('demo-assessment:section-1:attempt-1:q1:mc1')
47
+ * // Returns: { assessmentId: 'demo-assessment', sectionId: 'section-1',
48
+ * // attemptId: 'attempt-1', itemId: 'q1', elementId: 'mc1' }
37
49
  */
38
50
  parseGlobalElementId(globalElementId) {
39
51
  const parts = globalElementId.split(":");
40
- if (parts.length !== 4)
52
+ if (parts.length !== ID_PARTS.length)
41
53
  return null;
42
- return {
43
- assessmentId: parts[0],
44
- sectionId: parts[1],
45
- itemId: parts[2],
46
- elementId: parts[3],
47
- };
54
+ const [assessmentId, sectionId, attemptId, itemId, elementId] = parts.map(unescapeIdPart);
55
+ return { assessmentId, sectionId, attemptId, itemId, elementId };
48
56
  }
49
57
  /**
50
58
  * Sets tool state for a specific element.
51
59
  * Notifies all subscribers and triggers persistence callback.
52
- *
53
- * @param globalElementId Composite key identifying the element
54
- * @param toolId Tool identifier (e.g., 'answerEliminator', 'flagging')
55
- * @param state Tool-specific state object
56
60
  */
57
61
  setState(globalElementId, toolId, state) {
58
62
  let elementState = this.elementStates.get(globalElementId);
@@ -64,22 +68,11 @@ export class ElementToolStateStore {
64
68
  this._notifyListeners();
65
69
  this._notifyStateChange();
66
70
  }
67
- /**
68
- * Gets tool state for a specific element.
69
- *
70
- * @param globalElementId Composite key identifying the element
71
- * @param toolId Tool identifier
72
- * @returns Tool state or undefined if not found
73
- */
71
+ /** One tool's state for an element, or `undefined` when none is set. */
74
72
  getState(globalElementId, toolId) {
75
73
  return this.elementStates.get(globalElementId)?.get(toolId);
76
74
  }
77
- /**
78
- * Gets all tool states for a specific element.
79
- *
80
- * @param globalElementId Composite key identifying the element
81
- * @returns Object with tool states keyed by toolId
82
- */
75
+ /** Every tool's state for an element, keyed by toolId. */
83
76
  getElementState(globalElementId) {
84
77
  const elementState = this.elementStates.get(globalElementId);
85
78
  if (!elementState)
@@ -87,10 +80,8 @@ export class ElementToolStateStore {
87
80
  return Object.fromEntries(elementState.entries());
88
81
  }
89
82
  /**
90
- * Gets all tool states for all elements.
91
- * Used for persistence and debugging.
92
- *
93
- * @returns Nested object: { globalElementId: { toolId: state } }
83
+ * All state as `{ globalElementId: { toolId: state } }`, for persistence and
84
+ * debugging.
94
85
  */
95
86
  getAllState() {
96
87
  const result = {};
@@ -101,39 +92,51 @@ export class ElementToolStateStore {
101
92
  }
102
93
  /**
103
94
  * Subscribes to state changes.
104
- * Callback is invoked whenever any element's tool state changes.
105
- *
106
- * @param callback Function to call on state changes
107
- * @returns Unsubscribe function
95
+ * Callback is invoked whenever any element's tool state changes, including
96
+ * a `loadState` restore, with a copy of the state that it may keep. Returns
97
+ * the unsubscribe function.
108
98
  */
109
99
  subscribe(callback) {
110
100
  this.listeners.add(callback);
111
101
  return () => this.listeners.delete(callback);
112
102
  }
113
103
  _notifyListeners() {
104
+ if (this.listeners.size === 0)
105
+ return;
106
+ const snapshot = new Map([...this.elementStates].map(([key, toolStates]) => [
107
+ key,
108
+ new Map(toolStates),
109
+ ]));
114
110
  for (const listener of this.listeners) {
115
- listener(this.elementStates);
111
+ try {
112
+ listener(snapshot);
113
+ }
114
+ catch (error) {
115
+ console.warn("[ElementToolStateStore] listener failed:", error);
116
+ }
116
117
  }
117
118
  }
118
119
  /**
119
120
  * Sets a callback to be invoked on state changes for persistence integration.
120
121
  * Used by demo/app layer to persist tool state to localStorage or server.
121
- *
122
- * @param callback Function to call with serialized state on changes
123
122
  */
124
123
  setOnStateChange(callback) {
125
124
  this.onStateChange = callback;
126
125
  }
127
126
  _notifyStateChange() {
128
- if (this.onStateChange) {
127
+ if (!this.onStateChange)
128
+ return;
129
+ try {
129
130
  this.onStateChange(this.getAllState());
130
131
  }
132
+ catch (error) {
133
+ console.warn("[ElementToolStateStore] state-change callback failed:", error);
134
+ }
131
135
  }
132
136
  /**
133
137
  * Loads tool state from serialized format.
134
- * Used to restore state from localStorage or server.
135
- *
136
- * @param state Serialized state object
138
+ * Used to restore state from localStorage or server. Subscribers are
139
+ * notified; the persistence callback is not, so a restore never writes back.
137
140
  */
138
141
  loadState(state) {
139
142
  this.elementStates.clear();
@@ -143,11 +146,7 @@ export class ElementToolStateStore {
143
146
  }
144
147
  this._notifyListeners();
145
148
  }
146
- /**
147
- * Clears all tool state for a specific element.
148
- *
149
- * @param globalElementId Composite key identifying the element
150
- */
149
+ /** Clears all tool state for a specific element. */
151
150
  clearElement(globalElementId) {
152
151
  this.elementStates.delete(globalElementId);
153
152
  this._notifyListeners();
@@ -156,8 +155,6 @@ export class ElementToolStateStore {
156
155
  /**
157
156
  * Clears state for a specific tool across all elements.
158
157
  * Useful when disabling a tool globally.
159
- *
160
- * @param toolId Tool identifier
161
158
  */
162
159
  clearTool(toolId) {
163
160
  for (const elementState of this.elementStates.values()) {
@@ -167,14 +164,11 @@ export class ElementToolStateStore {
167
164
  this._notifyStateChange();
168
165
  }
169
166
  /**
170
- * Clears all tool state for a specific section.
167
+ * Clears all tool state for a specific section, across its attempts.
171
168
  * Useful when unmounting/disposing a section.
172
- *
173
- * @param assessmentId Assessment identifier
174
- * @param sectionId Section identifier
175
169
  */
176
170
  clearSection(assessmentId, sectionId) {
177
- const prefix = `${assessmentId}:${sectionId}:`;
171
+ const prefix = `${escapeIdPart(assessmentId)}:${escapeIdPart(sectionId)}:`;
178
172
  const keysToDelete = [];
179
173
  for (const key of this.elementStates.keys()) {
180
174
  if (key.startsWith(prefix)) {