@pie-players/pie-assessment-player 0.3.73 → 0.3.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,9 @@ Primary entrypoint:
8
8
 
9
9
  - `@pie-players/pie-assessment-player`
10
10
 
11
+ The entry is bundler-only: it imports the toolkit, players-shared and the
12
+ section player by bare specifier.
13
+
11
14
  ## AssessmentController
12
15
 
13
16
  `AssessmentController` is the domain authority for an assessment attempt. It
@@ -40,10 +43,10 @@ pending or failed, and after disconnect. A waiter resolves to `null` when its
40
43
  active attempt fails or is retired, or its own timeout expires. A waiter's
41
44
  timeout does not cancel a still-active initialization.
42
45
 
43
- Connect the element and assign `assessmentId`, `attemptId`, `assessment`, and
44
- `hooks` in the same turn, or assign them before connecting. These assignments
45
- are batched; changing assessment, attempt, or hooks later retires the previous
46
- controller and starts a new one. Replace object properties to update them;
46
+ Connect the element and assign `assessmentId`, `attemptId`, `assessment`,
47
+ `hooks` and `session` in the same turn, or assign them before connecting. These
48
+ assignments are batched; changing assessment, attempt, hooks or session later
49
+ retires the previous controller and starts a new one. Replace object properties to update them;
47
50
  mutating a previously assigned object in place is not an update signal. Runtime
48
51
  props, locale, and navigation visibility do not reload the assessment.
49
52
 
@@ -68,7 +71,8 @@ controller.navigateTo("section-2");
68
71
  controller.navigateNext();
69
72
  controller.navigatePrevious();
70
73
 
71
- // persist on whatever cadence the host wants; submit() always persists.
74
+ // persist on whatever cadence the host wants; saves run in call order.
75
+ // submit() saves first and rejects, unsubmitted, if that save fails.
72
76
  await controller.persist();
73
77
  await controller.submit();
74
78
  unsubscribe();
@@ -79,10 +83,20 @@ shape the persistence strategy load/save methods exchange and the same
79
83
  shape per-section bridges roll up into via `updateSectionSession(sectionId,
80
84
  snapshot)`.
81
85
 
86
+ The element's `session` property takes that shape too. The controller's first
87
+ hydrate applies an assigned session in place of the strategy's `loadSession`;
88
+ the strategy still receives every `persist()`. The value is consumed once: a
89
+ rebuild for another reason, such as an `assessment` change, hydrates from the
90
+ strategy. Assigning a session equal to the controller's, as writing
91
+ `assessment-session-changed` back into the property does, is a no-op, and so is
92
+ `null` once the controller is ready. Reading `session` returns the assigned value
93
+ until the controller is ready and a copy of the controller's session after.
94
+
82
95
  The default element captures the outgoing section before navigation replaces it,
83
- including navigation through the controller. A returning section waits for its
84
- canonical `engine-ready` stage and restores its saved section snapshot before
85
- accepting input or session updates. The section region stays `aria-busy` during
96
+ including navigation through the controller. A returning section receives its
97
+ saved section snapshot as the section player's `session` property, which its
98
+ controller applies before the canonical `engine-ready` stage; the section accepts
99
+ input and session updates from `engine-ready` on. The section region stays `aria-busy` during
86
100
  this handoff. Failed restoration leaves the saved snapshot intact, reports
87
101
  `assessment-error` and `onError` with phase `navigation`, and offers Retry.
88
102
  Navigating away or disconnecting retires that handoff. This in-memory restoration
@@ -98,9 +112,11 @@ change only — section-level events flow through the embedded
98
112
 
99
113
  - `assessment-route-changed` — section-level navigation moved (index / id,
100
114
  `previousSectionId`, `canNext` / `canPrevious`).
101
- - `assessment-session-applied` — `hydrate()` loaded a persisted session.
115
+ - `assessment-session-applied` — `hydrate()` applied a session, persisted or
116
+ assigned as `session`.
102
117
  - `assessment-session-changed` — assessment session mutated (navigation,
103
- per-section snapshot upsert).
118
+ per-section snapshot upsert). The active section's snapshot is upserted once
119
+ for each `session-changed` it emits.
104
120
  - `assessment-progress-changed` — visited-section count flipped.
105
121
  - `assessment-submission-state-changed` — `submit()` recorded the final
106
122
  state.
@@ -189,16 +205,75 @@ Toolkit tool/backend operational stream (when toolkit is mounted):
189
205
  - `pie-tool-backend-call-start|success|error`
190
206
  - `pie-tool-library-load-start|success|error`
191
207
 
208
+ ## Preloaded elements
209
+
210
+ With `player-type="preloaded"` (or `playerType = "preloaded"`) the section
211
+ players load no element code, and their pre-warm asserts that every tag the
212
+ content names is registered. The host registers the elements with
213
+ `registerPreloadedElements` from `@pie-players/pie-item-player/preloaded`
214
+ before the assessment player mounts:
215
+
216
+ ```ts
217
+ import { registerPreloadedElements } from "@pie-players/pie-item-player/preloaded";
218
+ import * as delivery from "@pie-element/multiple-choice/browser/delivery";
219
+ import * as controller from "@pie-element/multiple-choice/browser/controller";
220
+ import manifest from "../package.json"; // pins "@pie-element/multiple-choice" exactly
221
+
222
+ registerPreloadedElements(
223
+ [
224
+ {
225
+ tag: "pie-element-multiple-choice",
226
+ package: "@pie-element/multiple-choice",
227
+ version: manifest.dependencies["@pie-element/multiple-choice"],
228
+ element: delivery,
229
+ controller,
230
+ },
231
+ ],
232
+ { math: { assetRoot: "https://assets.example.com/npm" } },
233
+ );
234
+ ```
235
+
236
+ - A tag missing at pre-warm leaves the section's items unmounted and raises a
237
+ non-recoverable `element-preload` framework error.
238
+ - Install element packages with `npm install --save-exact`. npm otherwise saves
239
+ a caret range, which registration rejects as a `version`, and which a fresh
240
+ install can resolve to another release line: `^13.4.0-next.15` resolves to
241
+ the legacy `13.4.4`, which has no `./browser/*` modules.
242
+ - Install every pie-elements-ng package from one release, in one install from
243
+ the same dist-tag, and upgrade them together. Elements whose `./browser/*`
244
+ builds typeset on `window.MathJax` share the MathJax the first of them loads,
245
+ in the build and configuration of that element's release, so in a mixed set
246
+ an element can typeset with a MathJax it was not built for. Elements that
247
+ bundle their own MathJax share none
248
+ ([One MathJax version per page](../../docs/item-player/loading-strategies.md#one-mathjax-version-per-page)).
249
+ - Pass `math.assetRoot`, an npm root serving the fonts and speech the elements'
250
+ bundled MathJax loads, or `math.assetUrls`, each file's URL; without either,
251
+ elements on adapter 0.1.3 or later render without web fonts and speech
252
+ ([MathJax assets](../../docs/item-player/loading-strategies.md#mathjax-assets)).
253
+ - Register one version per package; registering a second version throws.
254
+ - Register each package's `controller` unless the item players are hosted
255
+ (`sectionPlayerRuntime.player.hosted`, or an enabled
256
+ `sectionPlayerRuntime.player.backend.delivery`). A player that is not hosted
257
+ runs `model()` in the browser and warns for each tag registered without one.
258
+ - Only `@pie-element/*` builds from pie-elements-ng publish
259
+ `./browser/delivery` and `./browser/controller`.
260
+ - Under TypeScript, the `package.json` import needs `resolveJsonModule`, and a
261
+ package version that ships no declarations for `./browser/*` needs a
262
+ `declare module` shim for those subpaths.
263
+
264
+ See [`strategy="preloaded"`](../../docs/item-player/loading-strategies.md#strategypreloaded)
265
+ for the registration contract.
266
+
192
267
  ## Content trust boundary
193
268
 
194
269
  Assessment markup reaches the DOM via the underlying
195
270
  `<pie-item-player>` element, which now sanitizes item / passage markup by
196
- default through DOMPurify. See
197
- [pie-item-player README](./README.md#content-trust-boundary)
271
+ default through DOMPurify. See the
272
+ [pie-item-player README](../item-player/README.md#content-trust-boundary)
198
273
  for the allow-list, opt-out mechanics (`trust-markup`), and the
199
274
  `sanitizeMarkup` property override. Assessment-player hosts forward these
200
- settings by setting `runtime.player.trustMarkup` /
201
- `runtime.player.sanitizeMarkup` on the shared runtime they pass into the
202
- section-player instances; the section-player runtime flattens `runtime.player.*`
275
+ settings by setting `sectionPlayerRuntime.player.trustMarkup` /
276
+ `sectionPlayerRuntime.player.sanitizeMarkup` on the assessment player; it passes
277
+ that runtime to each section player, whose runtime flattens `runtime.player.*`
203
278
  fields onto the embedded `<pie-item-player>` (see the section-player README
204
279
  for the exact forwarding shape).
@@ -1,7 +1,7 @@
1
1
  import { Env } from '@pie-players/pie-players-shared/types';
2
2
  import { AssessmentControllerHandle } from '../controller/AssessmentController.js';
3
3
  import { AssessmentPlayerProgressSnapshot, AssessmentPlayerRuntimeHostContract, AssessmentPlayerSnapshot } from '../contracts/runtime-host-contract.js';
4
- import { AssessmentDefinition, AssessmentPlayerHooks, AssessmentPlayerRuntimeConfig } from '../types.js';
4
+ import { AssessmentDefinition, AssessmentPlayerHooks, AssessmentPlayerRuntimeConfig, AssessmentSession } from '../types.js';
5
5
  export declare class AssessmentPlayerDefaultElement extends HTMLElement implements AssessmentPlayerRuntimeHostContract {
6
6
  static get observedAttributes(): string[];
7
7
  private _assessmentId;
@@ -16,6 +16,13 @@ export declare class AssessmentPlayerDefaultElement extends HTMLElement implemen
16
16
  private _env;
17
17
  private _coordinator;
18
18
  private _hooks;
19
+ /**
20
+ * The assigned session until a controller initializes from it. A later
21
+ * rebuild, such as one an `assessment` change causes, hydrates from the
22
+ * persistence strategy rather than reapplying a value the learner has since
23
+ * moved past.
24
+ */
25
+ private pendingSession;
19
26
  private _showNavigation;
20
27
  private _sectionPlayerLayout;
21
28
  private _playerType;
@@ -63,6 +70,12 @@ export declare class AssessmentPlayerDefaultElement extends HTMLElement implemen
63
70
  set assessment(value: AssessmentDefinition | null);
64
71
  get hooks(): AssessmentPlayerHooks | null;
65
72
  set hooks(value: AssessmentPlayerHooks | null);
73
+ /**
74
+ * Before a controller is ready, the assigned session; after, a copy of the
75
+ * controller's.
76
+ */
77
+ get session(): AssessmentSession | null;
78
+ set session(value: AssessmentSession | null);
66
79
  get locale(): string;
67
80
  set locale(value: string);
68
81
  get env(): Env | null;
@@ -133,6 +146,10 @@ export declare class AssessmentPlayerDefaultElement extends HTMLElement implemen
133
146
  private updateCurrentSectionRuntime;
134
147
  private cleanupTtsForSectionNavigation;
135
148
  private syncCurrentSectionSessionIntoAssessment;
149
+ /**
150
+ * `restoring` is whether the section was handed a saved session, which it
151
+ * applies while it creates its controller, before `engine-ready`.
152
+ */
136
153
  private attachSectionControllerReadyListener;
137
154
  private render;
138
155
  /** Locale and chrome changes must not remount a section or reload its attempt. */
@@ -3,6 +3,7 @@ import { AssessmentPlayerRuntimeConfig } from '../types.js';
3
3
  export type ResolveAssessmentSectionPlayerRuntimeArgs = {
4
4
  sectionPlayerRuntime?: AssessmentPlayerRuntimeConfig["sectionPlayerRuntime"];
5
5
  playerType: "iife" | "esm" | "preloaded";
6
+ assessmentId?: string;
6
7
  attemptId?: string;
7
8
  env?: Record<string, unknown> | null;
8
9
  coordinator?: unknown;
@@ -1,7 +1,6 @@
1
1
  export declare const ASSESSMENT_PLAYER_PUBLIC_EVENTS: {
2
2
  readonly controllerReady: "assessment-controller-ready";
3
3
  readonly navigationRequested: "assessment-navigation-requested";
4
- readonly submitRequested: "assessment-submit-requested";
5
4
  readonly routeChanged: "assessment-route-changed";
6
5
  readonly sessionApplied: "assessment-session-applied";
7
6
  readonly sessionChanged: "assessment-session-changed";
@@ -83,8 +83,8 @@ export interface AssessmentControllerHandle {
83
83
  * persistence strategy.
84
84
  *
85
85
  * Hosts call this on whatever cadence they choose (debounced on
86
- * change, on visibility change, on submit). `submit()` always
87
- * `persist()`s as its final step.
86
+ * change, on visibility change, on submit). Saves run in call order.
87
+ * A failed save goes to `onError` and the returned promise resolves.
88
88
  */
89
89
  persist(): Promise<void>;
90
90
  /**
@@ -123,9 +123,9 @@ export interface AssessmentControllerHandle {
123
123
  */
124
124
  navigatePrevious(): boolean;
125
125
  /**
126
- * Mark the assessment as submitted, emit
127
- * `assessment-submission-state-changed`, and `persist()` the final
128
- * snapshot.
126
+ * Save the final snapshot, then mark the assessment as submitted and
127
+ * emit `assessment-submission-state-changed`. A failed save goes to
128
+ * `onError` and rejects, and the assessment stays unsubmitted.
129
129
  */
130
130
  submit(): Promise<void>;
131
131
  /**
@@ -174,16 +174,25 @@ export declare class AssessmentController implements AssessmentControllerHandle
174
174
  private session;
175
175
  private readiness;
176
176
  private submitted;
177
+ private saveQueue;
177
178
  private persistenceStrategy?;
178
179
  private initializationPromise?;
179
180
  private disposalPromise?;
180
181
  private disposed;
181
182
  private readonly storageContext;
183
+ /** Taken by the first hydrate; a later `hydrate()` loads from the strategy. */
184
+ private initialSession;
182
185
  constructor(args: {
183
186
  assessmentId: string;
184
187
  attemptId?: string;
185
188
  assessment: AssessmentDefinition | null;
186
189
  hooks?: AssessmentPlayerHooks;
190
+ /**
191
+ * A host-supplied session, applied by the first hydrate in place of the
192
+ * persistence strategy's `loadSession` result. The controller keeps a
193
+ * copy, so later updates never write into the host's object.
194
+ */
195
+ initialSession?: AssessmentSession | null;
187
196
  });
188
197
  private emit;
189
198
  private assertActive;
@@ -196,6 +205,11 @@ export declare class AssessmentController implements AssessmentControllerHandle
196
205
  hydrate(): Promise<void>;
197
206
  dispose(): Promise<void>;
198
207
  persist(): Promise<void>;
208
+ /**
209
+ * Saves run one at a time, in call order, so a slow older write can never
210
+ * land after a newer one. A failed save rejects only its own caller.
211
+ */
212
+ private save;
199
213
  getSession(): AssessmentSession | null;
200
214
  getRuntimeState(): AssessmentControllerRuntimeState;
201
215
  private buildNavigationSnapshot;
@@ -43,6 +43,7 @@ var e = {
43
43
  items: "Vragen",
44
44
  section: "Onderdeel",
45
45
  loadingSection: "Onderdeel wordt geladen…",
46
+ sectionLoadError: "Het onderdeel kon niet worden geladen.",
46
47
  loadingPassage: "Tekst wordt geladen…",
47
48
  configurationError: "Configuratiefout",
48
49
  playerError: "Fout in de speler",
@@ -108,6 +109,11 @@ var e = {
108
109
  centerA11y: "Venster centreren",
109
110
  closeA11y: "Hulpmiddel sluiten"
110
111
  },
112
+ placement: {
113
+ controlsA11y: "Verplaatsen en draaien",
114
+ rotateClockwiseA11y: "{degrees}° rechtsom draaien",
115
+ rotateCounterclockwiseA11y: "{degrees}° linksom draaien"
116
+ },
111
117
  announce: {
112
118
  movedUp: "Omhoog verplaatst naar {position}",
113
119
  movedDown: "Omlaag verplaatst naar {position}",
@@ -135,8 +141,8 @@ var e = {
135
141
  },
136
142
  graph: {
137
143
  name: "Grafiek",
138
- description: "Grafische rekenmachine en assenstelsel",
139
- buttonA11y: "Grafiek, grafische rekenmachine",
144
+ description: "Assenstelsel",
145
+ buttonA11y: "Grafiek, assenstelsel",
140
146
  tooltip: "Grafiek",
141
147
  toolA11y: "Grafiekhulpmiddel — punten en lijnen tekenen in een assenstelsel",
142
148
  canvasA11y: "Tekenvlak — gebruik de hulpmiddelen om punten toe te voegen en lijnen te tekenen",
@@ -300,30 +306,6 @@ var e = {
300
306
  textToSpeech: {
301
307
  name: "Voorlezen",
302
308
  description: "Inhoud laten voorlezen",
303
- toolA11y: "Voorleeshulp",
304
- title: "Voorlezen",
305
- initializing: "Wordt gestart…",
306
- initFailed: "Voorlezen kon niet worden gestart.",
307
- selectText: "Selecteer tekst op de pagina om die te laten voorlezen.",
308
- speed: "Snelheid:",
309
- play: "Voorlezen",
310
- pause: "Pauzeren",
311
- resume: "Hervatten",
312
- stop: "Stoppen",
313
- speaking: "Aan het voorlezen…",
314
- paused: "Gepauzeerd",
315
- charactersSelected: {
316
- one: "{count} teken geselecteerd",
317
- other: "{count} tekens geselecteerd"
318
- },
319
- rate: {
320
- slow: "Langzaam",
321
- slower: "Langzamer",
322
- normal: "Normaal",
323
- faster: "Sneller",
324
- fast: "Snel",
325
- veryFast: "Zeer snel"
326
- },
327
309
  inline: {
328
310
  controlsA11y: "Voorleesknoppen",
329
311
  playbackSpeedA11y: "Voorleessnelheid",
@@ -429,6 +411,7 @@ var e = {
429
411
  resolvedTools: "Toegekende hulpmiddelen (toolkit)",
430
412
  provenanceSummary: "Overzicht van herkomst van het hulpmiddelenbeleid",
431
413
  perToolDecisions: "Beslissingen per hulpmiddel",
414
+ policyDiagnostics: "Diagnose van het hulpmiddelenbeleid",
432
415
  profileReadOnly: "PNP-profiel (alleen lezen)"
433
416
  },
434
417
  tts: {