@pie-players/pie-players-shared 0.3.67 → 0.3.68
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 +59 -0
- package/dist/formative/index.d.ts +16 -0
- package/dist/formative/index.js +5 -0
- package/dist/formative/mastery.d.ts +16 -0
- package/dist/formative/mastery.js +43 -0
- package/dist/formative/outcome.d.ts +19 -0
- package/dist/formative/outcome.js +90 -0
- package/dist/formative/policy.d.ts +37 -0
- package/dist/formative/policy.js +86 -0
- package/dist/formative/session.d.ts +19 -0
- package/dist/formative/session.js +113 -0
- package/dist/formative/state.d.ts +60 -0
- package/dist/formative/state.js +140 -0
- package/dist/formative/types.d.ts +164 -0
- package/dist/formative/types.js +13 -0
- package/dist/i18n/catalogs.d.ts +38 -0
- package/dist/i18n/catalogs.js +47 -0
- package/dist/i18n/index.d.ts +28 -4
- package/dist/i18n/index.js +34 -3
- package/dist/i18n/language-tags.d.ts +51 -0
- package/dist/i18n/language-tags.js +84 -0
- package/dist/i18n/messages/en-US.d.ts +601 -0
- package/dist/i18n/messages/en-US.js +617 -0
- package/dist/i18n/messages/nl-NL.d.ts +516 -0
- package/dist/i18n/messages/nl-NL.js +516 -0
- package/dist/i18n/provider.d.ts +168 -0
- package/dist/i18n/provider.js +472 -0
- package/dist/i18n/types.d.ts +177 -28
- package/dist/i18n/types.js +6 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/pie/custom-element-define.d.ts +24 -0
- package/dist/pie/custom-element-define.js +36 -6
- package/dist/pie/index.d.ts +1 -1
- package/dist/pie/index.js +1 -1
- package/dist/pie/versioned-tag.js +3 -2
- package/dist/security/sanitize-forbidden-lists.d.ts +8 -0
- package/dist/security/sanitize-forbidden-lists.js +41 -0
- package/dist/security/sanitize-item-markup.js +3 -36
- package/dist/security/sanitize-svg-icon.js +3 -25
- package/dist/security/wrap-overwide-images.d.ts +5 -10
- package/dist/security/wrap-overwide-images.js +13 -62
- package/dist/security/wrap-overwide-tables.d.ts +5 -10
- package/dist/security/wrap-overwide-tables.js +13 -62
- package/dist/security/wrap-overwide.d.ts +51 -0
- package/dist/security/wrap-overwide.js +91 -0
- package/dist/server/npm-auth-env.d.ts +13 -0
- package/dist/server/npm-auth-env.js +64 -0
- package/dist/timed-media/index.d.ts +17 -0
- package/dist/timed-media/index.js +16 -0
- package/dist/timed-media/media-element-source.d.ts +36 -0
- package/dist/timed-media/media-element-source.js +121 -0
- package/dist/timed-media/policy.d.ts +36 -0
- package/dist/timed-media/policy.js +297 -0
- package/dist/timed-media/session.d.ts +21 -0
- package/dist/timed-media/session.js +85 -0
- package/dist/timed-media/state.d.ts +66 -0
- package/dist/timed-media/state.js +357 -0
- package/dist/timed-media/types.d.ts +343 -0
- package/dist/timed-media/types.js +12 -0
- package/dist/tools/term-lookup.d.ts +189 -0
- package/dist/tools/term-lookup.js +284 -0
- package/dist/types/index.d.ts +105 -1
- package/dist/ui/attribute-coercion.d.ts +1 -0
- package/dist/ui/attribute-coercion.js +19 -0
- package/dist/ui/first-focusable.d.ts +22 -0
- package/dist/ui/first-focusable.js +47 -0
- package/dist/ui/focus-trap.js +7 -2
- package/dist/ui/pointer-drag.d.ts +30 -0
- package/dist/ui/pointer-drag.js +34 -0
- package/package.json +35 -3
- package/dist/i18n/loader.d.ts +0 -35
- package/dist/i18n/loader.js +0 -132
- package/dist/i18n/simple-i18n.d.ts +0 -68
- package/dist/i18n/simple-i18n.js +0 -198
- package/dist/i18n/translations/ar/common.json +0 -36
- package/dist/i18n/translations/ar/toolkit.json +0 -48
- package/dist/i18n/translations/ar/tools.json +0 -103
- package/dist/i18n/translations/en/common.json +0 -36
- package/dist/i18n/translations/en/toolkit.json +0 -48
- package/dist/i18n/translations/en/tools.json +0 -103
- package/dist/i18n/translations/es/common.json +0 -36
- package/dist/i18n/translations/es/toolkit.json +0 -48
- package/dist/i18n/translations/es/tools.json +0 -103
- package/dist/i18n/translations/zh/common.json +0 -36
- package/dist/i18n/translations/zh/toolkit.json +0 -48
- package/dist/i18n/translations/zh/tools.json +0 -103
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation for authored timed-media section data.
|
|
3
|
+
*
|
|
4
|
+
* Authored, wire-facing and untrusted, so every field is checked here and the
|
|
5
|
+
* caller never hand-parses. Errors are returned rather than thrown: the section
|
|
6
|
+
* reports them as a framework error and delivers as an ordinary section, which is
|
|
7
|
+
* loud without costing the learner the content.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Applied when `playbackPolicy` is absent entirely. The restrictive values are
|
|
11
|
+
* the defaults because a timed-media section is authored to sequence: a section
|
|
12
|
+
* that forgot the policy block reads as one that wanted sequencing, and the
|
|
13
|
+
* permissive reading would silently deliver an unsequenced video.
|
|
14
|
+
*/
|
|
15
|
+
export const TIMED_MEDIA_PLAYBACK_POLICY_DEFAULTS = {
|
|
16
|
+
allowSeekAhead: false,
|
|
17
|
+
pauseOnRequiredCue: true,
|
|
18
|
+
requireMediaCompletion: false,
|
|
19
|
+
};
|
|
20
|
+
const ACTIVATIONS = [
|
|
21
|
+
"reveal",
|
|
22
|
+
"gate",
|
|
23
|
+
"metadata",
|
|
24
|
+
];
|
|
25
|
+
const GATE_CONDITIONS = [
|
|
26
|
+
"responded",
|
|
27
|
+
"correct",
|
|
28
|
+
"partial-or-better",
|
|
29
|
+
];
|
|
30
|
+
const UNKNOWN_CORRECTNESS = [
|
|
31
|
+
"release",
|
|
32
|
+
"hold",
|
|
33
|
+
];
|
|
34
|
+
const SCORING_STRATEGIES = [
|
|
35
|
+
"sum-child-outcomes",
|
|
36
|
+
"average-child-outcomes",
|
|
37
|
+
"host-defined",
|
|
38
|
+
];
|
|
39
|
+
/** A gate on correctness has to say what an unscorable item does. */
|
|
40
|
+
function isCorrectnessCondition(condition) {
|
|
41
|
+
return condition === "correct" || condition === "partial-or-better";
|
|
42
|
+
}
|
|
43
|
+
function trimmed(value) {
|
|
44
|
+
return typeof value === "string" ? value.trim() : "";
|
|
45
|
+
}
|
|
46
|
+
function normalizeRange(value) {
|
|
47
|
+
if (!value || typeof value !== "object")
|
|
48
|
+
return null;
|
|
49
|
+
const raw = value;
|
|
50
|
+
const start = Number(raw.startSeconds);
|
|
51
|
+
if (!Number.isFinite(start) || start < 0)
|
|
52
|
+
return null;
|
|
53
|
+
const end = Number(raw.endSeconds);
|
|
54
|
+
// An end at or before the start is a window that can never contain a
|
|
55
|
+
// position, so it is treated as no end — the same rule
|
|
56
|
+
// `normalizeMediaFragment` applies in the toolkit, kept identical on purpose.
|
|
57
|
+
if (!Number.isFinite(end) || end <= start)
|
|
58
|
+
return { startSeconds: start };
|
|
59
|
+
return { startSeconds: start, endSeconds: end };
|
|
60
|
+
}
|
|
61
|
+
function normalizePlaybackPolicy(value) {
|
|
62
|
+
if (value === undefined || value === null) {
|
|
63
|
+
return { policy: { ...TIMED_MEDIA_PLAYBACK_POLICY_DEFAULTS }, error: null };
|
|
64
|
+
}
|
|
65
|
+
if (typeof value !== "object") {
|
|
66
|
+
return {
|
|
67
|
+
policy: { ...TIMED_MEDIA_PLAYBACK_POLICY_DEFAULTS },
|
|
68
|
+
error: {
|
|
69
|
+
code: "invalid-playback-policy",
|
|
70
|
+
message: "timedMedia.playbackPolicy must be an object.",
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const raw = value;
|
|
75
|
+
return {
|
|
76
|
+
policy: {
|
|
77
|
+
allowSeekAhead: typeof raw.allowSeekAhead === "boolean"
|
|
78
|
+
? raw.allowSeekAhead
|
|
79
|
+
: TIMED_MEDIA_PLAYBACK_POLICY_DEFAULTS.allowSeekAhead,
|
|
80
|
+
pauseOnRequiredCue: typeof raw.pauseOnRequiredCue === "boolean"
|
|
81
|
+
? raw.pauseOnRequiredCue
|
|
82
|
+
: TIMED_MEDIA_PLAYBACK_POLICY_DEFAULTS.pauseOnRequiredCue,
|
|
83
|
+
requireMediaCompletion: typeof raw.requireMediaCompletion === "boolean"
|
|
84
|
+
? raw.requireMediaCompletion
|
|
85
|
+
: TIMED_MEDIA_PLAYBACK_POLICY_DEFAULTS.requireMediaCompletion,
|
|
86
|
+
},
|
|
87
|
+
error: null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function normalizeScoringPolicy(value) {
|
|
91
|
+
if (value === undefined || value === null) {
|
|
92
|
+
return { policy: undefined, error: null };
|
|
93
|
+
}
|
|
94
|
+
const strategy = value && typeof value === "object"
|
|
95
|
+
? value.strategy
|
|
96
|
+
: undefined;
|
|
97
|
+
const known = SCORING_STRATEGIES.find((candidate) => candidate === strategy);
|
|
98
|
+
if (!known) {
|
|
99
|
+
return {
|
|
100
|
+
policy: undefined,
|
|
101
|
+
error: {
|
|
102
|
+
code: "invalid-scoring-policy",
|
|
103
|
+
message: `timedMedia.scoringPolicy.strategy must be one of ${SCORING_STRATEGIES.join(", ")}.`,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return { policy: { strategy: known }, error: null };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Validate authored `timedMedia` against the section it belongs to.
|
|
111
|
+
*
|
|
112
|
+
* `resolveStimulusRenderableId` is supplied by the caller because the mapping
|
|
113
|
+
* from an authored `stimulusRef` to a rendered renderable id belongs to the
|
|
114
|
+
* content service that assigned that id — deriving it twice is two
|
|
115
|
+
* implementations of one mapping.
|
|
116
|
+
*/
|
|
117
|
+
export function normalizeTimedMediaSectionData(args) {
|
|
118
|
+
const errors = [];
|
|
119
|
+
if (!args.timedMedia || typeof args.timedMedia !== "object") {
|
|
120
|
+
return {
|
|
121
|
+
data: null,
|
|
122
|
+
errors: [
|
|
123
|
+
{
|
|
124
|
+
code: "missing-stimulus-ref",
|
|
125
|
+
message: "timedMedia must be an object carrying a stimulusRef.",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const raw = args.timedMedia;
|
|
131
|
+
const stimulusRef = trimmed(raw.stimulusRef);
|
|
132
|
+
if (!stimulusRef) {
|
|
133
|
+
errors.push({
|
|
134
|
+
code: "missing-stimulus-ref",
|
|
135
|
+
message: "timedMedia.stimulusRef is required.",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const stimulusRenderableId = stimulusRef
|
|
139
|
+
? args.resolveStimulusRenderableId(stimulusRef)
|
|
140
|
+
: undefined;
|
|
141
|
+
if (stimulusRef && !stimulusRenderableId) {
|
|
142
|
+
errors.push({
|
|
143
|
+
code: "unresolved-stimulus-ref",
|
|
144
|
+
message: `timedMedia.stimulusRef "${stimulusRef}" does not name a renderable in this section.`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const { policy: playbackPolicy, error: playbackError } = normalizePlaybackPolicy(raw.playbackPolicy);
|
|
148
|
+
if (playbackError)
|
|
149
|
+
errors.push(playbackError);
|
|
150
|
+
const { policy: scoringPolicy, error: scoringError } = normalizeScoringPolicy(raw.scoringPolicy);
|
|
151
|
+
if (scoringError)
|
|
152
|
+
errors.push(scoringError);
|
|
153
|
+
const allowedItemIds = new Set(args.itemIdentifiers);
|
|
154
|
+
const rawCues = Array.isArray(raw.cues) ? raw.cues : [];
|
|
155
|
+
if (rawCues.length === 0) {
|
|
156
|
+
errors.push({
|
|
157
|
+
code: "no-cues",
|
|
158
|
+
message: "timedMedia.cues must hold at least one cue.",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const seenIdentifiers = new Set();
|
|
162
|
+
const cues = [];
|
|
163
|
+
for (const [index, entry] of rawCues.entries()) {
|
|
164
|
+
const cue = (entry ?? {});
|
|
165
|
+
const identifier = trimmed(cue.identifier);
|
|
166
|
+
if (!identifier) {
|
|
167
|
+
errors.push({
|
|
168
|
+
code: "invalid-cue-identifier",
|
|
169
|
+
message: `timedMedia.cues[${index}] has no identifier.`,
|
|
170
|
+
});
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (seenIdentifiers.has(identifier)) {
|
|
174
|
+
errors.push({
|
|
175
|
+
code: "duplicate-cue-identifier",
|
|
176
|
+
message: `Cue identifier "${identifier}" appears more than once.`,
|
|
177
|
+
cueIdentifier: identifier,
|
|
178
|
+
});
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
seenIdentifiers.add(identifier);
|
|
182
|
+
const range = normalizeRange(cue.range);
|
|
183
|
+
if (!range) {
|
|
184
|
+
errors.push({
|
|
185
|
+
code: "invalid-cue-range",
|
|
186
|
+
message: `Cue "${identifier}" needs a range with a finite, non-negative startSeconds.`,
|
|
187
|
+
cueIdentifier: identifier,
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const cuePolicy = (cue.policy ?? {});
|
|
192
|
+
const activation = ACTIVATIONS.find((candidate) => candidate === cuePolicy.activation);
|
|
193
|
+
if (!activation) {
|
|
194
|
+
errors.push({
|
|
195
|
+
code: "invalid-cue-activation",
|
|
196
|
+
message: `Cue "${identifier}" needs policy.activation of ${ACTIVATIONS.join(", ")}.`,
|
|
197
|
+
cueIdentifier: identifier,
|
|
198
|
+
});
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const rawItemRefs = Array.isArray(cue.itemRefs) ? cue.itemRefs : [];
|
|
202
|
+
const itemRefs = [];
|
|
203
|
+
for (const rawItemRef of rawItemRefs) {
|
|
204
|
+
const itemRef = trimmed(rawItemRef);
|
|
205
|
+
if (!itemRef)
|
|
206
|
+
continue;
|
|
207
|
+
if (!allowedItemIds.has(itemRef)) {
|
|
208
|
+
// Reported rather than dropped quietly: a cue naming an item the
|
|
209
|
+
// section does not hold is the exact authoring slip that produces a
|
|
210
|
+
// checkpoint the learner never sees.
|
|
211
|
+
errors.push({
|
|
212
|
+
code: "unknown-item-ref",
|
|
213
|
+
message: `Cue "${identifier}" names item ref "${itemRef}", which is not in this section.`,
|
|
214
|
+
cueIdentifier: identifier,
|
|
215
|
+
});
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!itemRefs.includes(itemRef))
|
|
219
|
+
itemRefs.push(itemRef);
|
|
220
|
+
}
|
|
221
|
+
if (itemRefs.length === 0 && activation !== "metadata") {
|
|
222
|
+
errors.push({
|
|
223
|
+
code: "missing-item-refs",
|
|
224
|
+
message: `Cue "${identifier}" activates no item; only a metadata cue may name none.`,
|
|
225
|
+
cueIdentifier: identifier,
|
|
226
|
+
});
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
let releaseOn;
|
|
230
|
+
let onUnknownCorrectness;
|
|
231
|
+
if (activation === "gate") {
|
|
232
|
+
releaseOn = GATE_CONDITIONS.find((candidate) => candidate === cuePolicy.releaseOn);
|
|
233
|
+
if (!releaseOn) {
|
|
234
|
+
errors.push({
|
|
235
|
+
code: "missing-release-condition",
|
|
236
|
+
message: `Gate cue "${identifier}" needs policy.releaseOn of ${GATE_CONDITIONS.join(", ")}.`,
|
|
237
|
+
cueIdentifier: identifier,
|
|
238
|
+
});
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (isCorrectnessCondition(releaseOn)) {
|
|
242
|
+
onUnknownCorrectness = UNKNOWN_CORRECTNESS.find((candidate) => candidate === cuePolicy.onUnknownCorrectness);
|
|
243
|
+
if (!onUnknownCorrectness) {
|
|
244
|
+
errors.push({
|
|
245
|
+
code: "missing-unknown-correctness",
|
|
246
|
+
message: `Gate cue "${identifier}" releases on correctness, so policy.onUnknownCorrectness must state "release" or "hold" for an item no controller can score.`,
|
|
247
|
+
cueIdentifier: identifier,
|
|
248
|
+
});
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
// A correctness gate is only passable while the learner still has a Try
|
|
252
|
+
// to spend. Over an item with a finite budget the gate becomes
|
|
253
|
+
// unpassable the moment the budget runs out, and over an item that does
|
|
254
|
+
// not deliver formatively there is never a Try at all — in both cases
|
|
255
|
+
// nothing the learner can do releases playback, and no host action
|
|
256
|
+
// releases it either, because a forced reveal is not a correct answer.
|
|
257
|
+
// Refused here rather than released at runtime: releasing would deliver
|
|
258
|
+
// the opposite of what the author wrote, which is the silent degradation
|
|
259
|
+
// this contract exists to avoid.
|
|
260
|
+
const unpassable = itemRefs.filter((itemRef) => args.resolveItemTryBudget(itemRef) !== "unlimited");
|
|
261
|
+
if (unpassable.length > 0) {
|
|
262
|
+
errors.push({
|
|
263
|
+
code: "gate-requires-unlimited-tries",
|
|
264
|
+
message: `Gate cue "${identifier}" releases on correctness, so every item it names needs formative delivery with maxTries: "unlimited" — otherwise a learner who runs out of Tries can never release playback. Offending item refs: ${unpassable.join(", ")}.`,
|
|
265
|
+
cueIdentifier: identifier,
|
|
266
|
+
});
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
cues.push({
|
|
272
|
+
identifier,
|
|
273
|
+
range,
|
|
274
|
+
itemRefs,
|
|
275
|
+
activation,
|
|
276
|
+
releaseOn,
|
|
277
|
+
onUnknownCorrectness,
|
|
278
|
+
// A policy that switched pausing off wholesale turns every gate into a
|
|
279
|
+
// reveal, which is the author's own choice and needs no warning.
|
|
280
|
+
holdsPlayback: activation === "gate" && playbackPolicy.pauseOnRequiredCue,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (errors.length > 0)
|
|
284
|
+
return { data: null, errors };
|
|
285
|
+
return {
|
|
286
|
+
data: {
|
|
287
|
+
stimulusRef,
|
|
288
|
+
stimulusRenderableId: stimulusRenderableId ?? "",
|
|
289
|
+
// Cue order is authored order, not time order: two cues may share a start
|
|
290
|
+
// and the author's sequence decides which gate holds first.
|
|
291
|
+
cues,
|
|
292
|
+
playbackPolicy,
|
|
293
|
+
scoringPolicy,
|
|
294
|
+
},
|
|
295
|
+
errors: [],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for the timed-media slice.
|
|
3
|
+
*
|
|
4
|
+
* Same posture as the formative slice: an unreadable slice is rejected whole and
|
|
5
|
+
* delivery starts from clean cue state, while item sessions in the same snapshot
|
|
6
|
+
* are normalized separately and survive. A version bump here must never cost a
|
|
7
|
+
* learner their responses.
|
|
8
|
+
*/
|
|
9
|
+
import type { ResolvedTimedMediaSectionData, TimedMediaSectionSessionSlice } from "./types.js";
|
|
10
|
+
export declare function toTimedMediaSectionSlice(state: TimedMediaSectionSessionSlice): TimedMediaSectionSessionSlice;
|
|
11
|
+
/**
|
|
12
|
+
* Validate a persisted slice against the section it is restored into.
|
|
13
|
+
*
|
|
14
|
+
* Returns `null` for an absent slice or one this build cannot read — the caller
|
|
15
|
+
* treats both as "start clean", and the two are indistinguishable, which is what
|
|
16
|
+
* keeps pre-timed-media snapshots valid.
|
|
17
|
+
*/
|
|
18
|
+
export declare function normalizeTimedMediaSectionSlice(args: {
|
|
19
|
+
slice: unknown;
|
|
20
|
+
data: ResolvedTimedMediaSectionData | null;
|
|
21
|
+
}): TimedMediaSectionSessionSlice | null;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for the timed-media slice.
|
|
3
|
+
*
|
|
4
|
+
* Same posture as the formative slice: an unreadable slice is rejected whole and
|
|
5
|
+
* delivery starts from clean cue state, while item sessions in the same snapshot
|
|
6
|
+
* are normalized separately and survive. A version bump here must never cost a
|
|
7
|
+
* learner their responses.
|
|
8
|
+
*/
|
|
9
|
+
import { TIMED_MEDIA_SLICE_VERSION, createTimedMediaState } from "./state.js";
|
|
10
|
+
function readNonNegativeNumber(value) {
|
|
11
|
+
const numeric = typeof value === "number" ? value : Number.NaN;
|
|
12
|
+
if (!Number.isFinite(numeric) || numeric < 0)
|
|
13
|
+
return undefined;
|
|
14
|
+
return numeric;
|
|
15
|
+
}
|
|
16
|
+
function readIdentifierList(value, allowed) {
|
|
17
|
+
if (!Array.isArray(value))
|
|
18
|
+
return [];
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const entry of value) {
|
|
21
|
+
if (typeof entry !== "string")
|
|
22
|
+
continue;
|
|
23
|
+
const identifier = entry.trim();
|
|
24
|
+
// Filtered against the cues this section actually holds, matching how
|
|
25
|
+
// `normalizeApplySession` filters item sessions: a snapshot taken against
|
|
26
|
+
// an earlier revision of the section must not resurrect a cue that is gone.
|
|
27
|
+
if (!identifier || !allowed.has(identifier))
|
|
28
|
+
continue;
|
|
29
|
+
if (out.includes(identifier))
|
|
30
|
+
continue;
|
|
31
|
+
out.push(identifier);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
export function toTimedMediaSectionSlice(state) {
|
|
36
|
+
return {
|
|
37
|
+
...state,
|
|
38
|
+
version: TIMED_MEDIA_SLICE_VERSION,
|
|
39
|
+
visitedCueIdentifiers: [...state.visitedCueIdentifiers],
|
|
40
|
+
completedCueIdentifiers: [...state.completedCueIdentifiers],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validate a persisted slice against the section it is restored into.
|
|
45
|
+
*
|
|
46
|
+
* Returns `null` for an absent slice or one this build cannot read — the caller
|
|
47
|
+
* treats both as "start clean", and the two are indistinguishable, which is what
|
|
48
|
+
* keeps pre-timed-media snapshots valid.
|
|
49
|
+
*/
|
|
50
|
+
export function normalizeTimedMediaSectionSlice(args) {
|
|
51
|
+
const { slice, data } = args;
|
|
52
|
+
if (!data)
|
|
53
|
+
return null;
|
|
54
|
+
if (!slice || typeof slice !== "object")
|
|
55
|
+
return null;
|
|
56
|
+
const raw = slice;
|
|
57
|
+
if (raw.version !== TIMED_MEDIA_SLICE_VERSION)
|
|
58
|
+
return null;
|
|
59
|
+
const allowedCueIds = new Set(data.cues.map((cue) => cue.identifier));
|
|
60
|
+
const base = createTimedMediaState();
|
|
61
|
+
const mediaCurrentTime = readNonNegativeNumber(raw.mediaCurrentTime) ?? base.mediaCurrentTime;
|
|
62
|
+
const visitedCueIdentifiers = readIdentifierList(raw.visitedCueIdentifiers, allowedCueIds);
|
|
63
|
+
const completedCueIdentifiers = readIdentifierList(raw.completedCueIdentifiers, allowedCueIds);
|
|
64
|
+
const activeCueIdentifier = typeof raw.activeCueIdentifier === "string" &&
|
|
65
|
+
allowedCueIds.has(raw.activeCueIdentifier)
|
|
66
|
+
? raw.activeCueIdentifier
|
|
67
|
+
: undefined;
|
|
68
|
+
return {
|
|
69
|
+
version: TIMED_MEDIA_SLICE_VERSION,
|
|
70
|
+
mediaCurrentTime,
|
|
71
|
+
// Never below the restored position: a snapshot written before
|
|
72
|
+
// `maxPositionSeconds` existed, or one carrying a lower value than the
|
|
73
|
+
// position it also records, would hand back timeline the learner already
|
|
74
|
+
// spent under `allowSeekAhead: false`.
|
|
75
|
+
maxPositionSeconds: Math.max(readNonNegativeNumber(raw.maxPositionSeconds) ?? 0, mediaCurrentTime),
|
|
76
|
+
mediaCompleted: raw.mediaCompleted === true,
|
|
77
|
+
visitedCueIdentifiers,
|
|
78
|
+
// A cue cannot be complete without having been visited; a snapshot that says
|
|
79
|
+
// otherwise is inconsistent, and the visited set is the one that decides what
|
|
80
|
+
// is on screen.
|
|
81
|
+
completedCueIdentifiers: completedCueIdentifiers.filter((identifier) => visitedCueIdentifiers.includes(identifier)),
|
|
82
|
+
activeCueIdentifier,
|
|
83
|
+
aggregateComplete: raw.aggregateComplete === true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cue reduction: media position plus delivery state in, cue state and the
|
|
3
|
+
* effects the port must be driven with out.
|
|
4
|
+
*
|
|
5
|
+
* Pure and Node-safe, which is what makes cue policy testable without a browser
|
|
6
|
+
* and what keeps it out of every layout that hosts timed media. The caller owns
|
|
7
|
+
* the port and applies the effects; nothing here touches media.
|
|
8
|
+
*/
|
|
9
|
+
import type { MediaTimeSourceCapabilities, ResolvedTimedMediaSectionData, TimedMediaDegradation, TimedMediaDeliveryState, TimedMediaInput, TimedMediaReduction, TimedMediaSectionProjection, TimedMediaSectionSessionSlice } from "./types.js";
|
|
10
|
+
export declare const TIMED_MEDIA_SLICE_VERSION: 1;
|
|
11
|
+
export declare function createTimedMediaState(): TimedMediaSectionSessionSlice;
|
|
12
|
+
/**
|
|
13
|
+
* Advance cue state.
|
|
14
|
+
*
|
|
15
|
+
* Two things are monotonic by design. A cue stays visited and its items stay
|
|
16
|
+
* revealed once reached, because un-revealing an item a learner has already
|
|
17
|
+
* answered would take a response off the screen. And a released gate stays
|
|
18
|
+
* complete, so a retry that lowers correctness cannot re-trap a learner
|
|
19
|
+
* mid-playback — the same reasoning that keeps `firstCorrectTry` from being
|
|
20
|
+
* overwritten by a later incorrect Try.
|
|
21
|
+
*/
|
|
22
|
+
export declare function reduceTimedMediaState(args: {
|
|
23
|
+
state: TimedMediaSectionSessionSlice;
|
|
24
|
+
data: ResolvedTimedMediaSectionData;
|
|
25
|
+
delivery: TimedMediaDeliveryState;
|
|
26
|
+
input: TimedMediaInput;
|
|
27
|
+
}): TimedMediaReduction;
|
|
28
|
+
/**
|
|
29
|
+
* Which policies the attached port can actually carry out.
|
|
30
|
+
*
|
|
31
|
+
* Absent a port there is nothing to degrade *from* yet, so an unattached section
|
|
32
|
+
* reports its authored intent rather than a capability gap it has not measured.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveTimedMediaEnforcement(args: {
|
|
35
|
+
playbackPolicy: ResolvedTimedMediaSectionData["playbackPolicy"];
|
|
36
|
+
capabilities: MediaTimeSourceCapabilities | null;
|
|
37
|
+
hasGate: boolean;
|
|
38
|
+
}): {
|
|
39
|
+
pause: "enforced" | "advisory";
|
|
40
|
+
seek: "enforced" | "advisory";
|
|
41
|
+
degradations: TimedMediaDegradation[];
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Derive the view layouts read. Recomputed from state on every republish rather
|
|
45
|
+
* than stored, so there is one source of truth and no mirror to go stale.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveTimedMediaProjection(args: {
|
|
48
|
+
data: ResolvedTimedMediaSectionData;
|
|
49
|
+
state: TimedMediaSectionSessionSlice;
|
|
50
|
+
delivery: TimedMediaDeliveryState;
|
|
51
|
+
capabilities: MediaTimeSourceCapabilities | null;
|
|
52
|
+
}): TimedMediaSectionProjection;
|
|
53
|
+
/**
|
|
54
|
+
* The projection facts that change what a layout renders, as one string.
|
|
55
|
+
*
|
|
56
|
+
* One implementation for the two callers that have to agree: `SectionController`
|
|
57
|
+
* decides whether to emit on it, and the toolkit folds it into the composition
|
|
58
|
+
* revision key. Two encodings drift silently in either direction — the controller
|
|
59
|
+
* emits a change the toolkit coalesces away, or the toolkit republishes for a
|
|
60
|
+
* change the controller never announced.
|
|
61
|
+
*
|
|
62
|
+
* `mediaCurrentTime` is absent because it moves about four times a second and
|
|
63
|
+
* nothing renders it. `maxPositionSeconds` enters only as a coarse bucket; see
|
|
64
|
+
* `POSITION_PERSIST_BUCKET_SECONDS`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function timedMediaProjectionSignature(projection: TimedMediaSectionProjection | null | undefined): string;
|