@lessonkit/core 1.3.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -2
- package/dist/chunk-KFXFQ6B2.js +813 -0
- package/dist/index.cjs +720 -130
- package/dist/index.d.cts +63 -488
- package/dist/index.d.ts +63 -488
- package/dist/index.js +509 -658
- package/dist/testing-BFr8oEfw.d.cts +608 -0
- package/dist/testing-BFr8oEfw.d.ts +608 -0
- package/dist/testing.cjs +58 -0
- package/dist/testing.d.cts +1 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +12 -0
- package/package.json +11 -6
- package/telemetry-catalog.v3.json +207 -14
package/dist/index.cjs
CHANGED
|
@@ -22,11 +22,15 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ACCORDION_FORBIDDEN_CHILD_TYPES: () => ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
24
24
|
ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES: () => ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
|
|
25
|
+
BLOCKS_14_PAGE_SLIDE: () => BLOCKS_14_PAGE_SLIDE,
|
|
26
|
+
BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES: () => BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES,
|
|
27
|
+
BRANCH_NODE_ALLOWED_CHILD_TYPES: () => BRANCH_NODE_ALLOWED_CHILD_TYPES,
|
|
25
28
|
COMPOUND_MAX_NESTING_DEPTH: () => COMPOUND_MAX_NESTING_DEPTH,
|
|
26
29
|
COMPOUND_RESUME_SCHEMA_VERSION: () => COMPOUND_RESUME_SCHEMA_VERSION,
|
|
27
30
|
ID_MAX_LENGTH: () => ID_MAX_LENGTH,
|
|
28
31
|
ID_PATTERN: () => ID_PATTERN,
|
|
29
32
|
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES: () => INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
33
|
+
INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES: () => INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
|
|
30
34
|
PAGE_ALLOWED_CHILD_TYPES: () => PAGE_ALLOWED_CHILD_TYPES,
|
|
31
35
|
SESSION_STORAGE_KEY: () => SESSION_STORAGE_KEY,
|
|
32
36
|
SLIDE_ALLOWED_CHILD_TYPES: () => SLIDE_ALLOWED_CHILD_TYPES,
|
|
@@ -34,6 +38,7 @@ __export(index_exports, {
|
|
|
34
38
|
TELEMETRY_EVENT_CATALOG: () => TELEMETRY_EVENT_CATALOG,
|
|
35
39
|
TELEMETRY_EVENT_CATALOG_V2: () => TELEMETRY_EVENT_CATALOG_V2,
|
|
36
40
|
TELEMETRY_EVENT_CATALOG_V3: () => TELEMETRY_EVENT_CATALOG_V3,
|
|
41
|
+
TIMED_CUE_ALLOWED_CHILD_TYPES: () => TIMED_CUE_ALLOWED_CHILD_TYPES,
|
|
37
42
|
assertNever: () => assertNever,
|
|
38
43
|
assertValidId: () => assertValidId,
|
|
39
44
|
buildCourseStartedTelemetryEvent: () => buildCourseStartedTelemetryEvent,
|
|
@@ -69,11 +74,14 @@ __export(index_exports, {
|
|
|
69
74
|
hasCourseStarted: () => hasCourseStarted,
|
|
70
75
|
hasCourseStartedEmittedToTracking: () => hasCourseStartedEmittedToTracking,
|
|
71
76
|
hasCourseStartedPipelineDelivered: () => hasCourseStartedPipelineDelivered,
|
|
77
|
+
hasCourseStartedXapiSent: () => hasCourseStartedXapiSent,
|
|
72
78
|
isChildTypeAllowed: () => isChildTypeAllowed,
|
|
79
|
+
isLifecycleTelemetryEvent: () => isLifecycleTelemetryEvent,
|
|
73
80
|
loadCompoundState: () => loadCompoundState,
|
|
74
81
|
markCourseStarted: () => markCourseStarted,
|
|
75
82
|
markCourseStartedEmittedToTracking: () => markCourseStartedEmittedToTracking,
|
|
76
83
|
markCourseStartedPipelineDelivered: () => markCourseStartedPipelineDelivered,
|
|
84
|
+
markCourseStartedXapiSent: () => markCourseStartedXapiSent,
|
|
77
85
|
migrateCourseStartedMark: () => migrateCourseStartedMark,
|
|
78
86
|
nowIso: () => nowIso,
|
|
79
87
|
parseBlockId: () => parseBlockId,
|
|
@@ -92,6 +100,7 @@ __export(index_exports, {
|
|
|
92
100
|
telemetryCatalogVersion: () => telemetryCatalogVersion,
|
|
93
101
|
tryBuildTelemetryEvent: () => tryBuildTelemetryEvent,
|
|
94
102
|
tryEmitCourseStarted: () => tryEmitCourseStarted,
|
|
103
|
+
validateBranchGraph: () => validateBranchGraph,
|
|
95
104
|
validateId: () => validateId
|
|
96
105
|
});
|
|
97
106
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -169,12 +178,26 @@ function uniqueFallbackId(input, usedIds) {
|
|
|
169
178
|
const hash = shortHash(input);
|
|
170
179
|
for (let n = 0; n < 100; n++) {
|
|
171
180
|
const candidate = (n === 0 ? `id-${hash}` : `id-${hash}-${n}`).slice(0, 64);
|
|
172
|
-
const
|
|
173
|
-
if (
|
|
181
|
+
const validated = validateId(candidate);
|
|
182
|
+
if (validated.ok && !usedIds.has(validated.id)) return validated.id;
|
|
183
|
+
}
|
|
184
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
185
|
+
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
186
|
+
const candidate = `id-${hash}-${randomSuffix}`.slice(0, 64);
|
|
187
|
+
const validated = validateId(candidate);
|
|
188
|
+
if (validated.ok && !usedIds.has(validated.id)) return validated.id;
|
|
174
189
|
}
|
|
175
190
|
const timed = `id-${hash}-${Date.now().toString(36)}`.slice(0, 64);
|
|
176
|
-
const
|
|
177
|
-
|
|
191
|
+
const timedValidated = validateId(timed);
|
|
192
|
+
if (timedValidated.ok && !usedIds.has(timedValidated.id)) return timedValidated.id;
|
|
193
|
+
const cryptoApi = globalThis.crypto;
|
|
194
|
+
for (let attempt = 0; attempt < 1e3; attempt++) {
|
|
195
|
+
const suffix = typeof cryptoApi?.randomUUID === "function" ? cryptoApi.randomUUID().replace(/-/g, "").slice(0, 12) : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
196
|
+
const candidate = `id-${hash}-${suffix}`.slice(0, 64);
|
|
197
|
+
const validated = validateId(candidate);
|
|
198
|
+
if (validated.ok && !usedIds.has(validated.id)) return validated.id;
|
|
199
|
+
}
|
|
200
|
+
throw new Error(`[lessonkit] unable to derive unique id for input: ${input.slice(0, 32)}`);
|
|
178
201
|
}
|
|
179
202
|
function slugifyId(input) {
|
|
180
203
|
const slug = input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-").slice(0, 64);
|
|
@@ -216,6 +239,13 @@ function buildLessonkitUrn(parts) {
|
|
|
216
239
|
}
|
|
217
240
|
urn += `:block:${blockId}`;
|
|
218
241
|
}
|
|
242
|
+
if (parts.nodeId !== void 0) {
|
|
243
|
+
const nodeId = assertValidId(parts.nodeId, "blockId");
|
|
244
|
+
if (parts.blockId === void 0) {
|
|
245
|
+
throw new Error("buildLessonkitUrn: nodeId requires blockId");
|
|
246
|
+
}
|
|
247
|
+
urn += `:node:${nodeId}`;
|
|
248
|
+
}
|
|
219
249
|
return urn;
|
|
220
250
|
}
|
|
221
251
|
|
|
@@ -260,19 +290,25 @@ function isPlainSerializableChildState(value) {
|
|
|
260
290
|
(entry) => isValidChildResumeValue(entry)
|
|
261
291
|
);
|
|
262
292
|
}
|
|
263
|
-
function parseCompoundResumeState(raw) {
|
|
293
|
+
function parseCompoundResumeState(raw, opts) {
|
|
264
294
|
if (!raw || typeof raw !== "object") return null;
|
|
265
295
|
const obj = raw;
|
|
266
296
|
if (obj.schemaVersion !== COMPOUND_RESUME_SCHEMA_VERSION) return null;
|
|
267
297
|
if (typeof obj.activePageIndex !== "number" || !Number.isFinite(obj.activePageIndex)) return null;
|
|
268
298
|
const childStates = {};
|
|
299
|
+
const droppedChildKeys = [];
|
|
269
300
|
if (obj.childStates && typeof obj.childStates === "object" && !Array.isArray(obj.childStates)) {
|
|
270
301
|
for (const [key, value] of Object.entries(obj.childStates)) {
|
|
271
302
|
if (isPlainSerializableChildState(value)) {
|
|
272
303
|
childStates[key] = value;
|
|
304
|
+
} else {
|
|
305
|
+
droppedChildKeys.push(key);
|
|
273
306
|
}
|
|
274
307
|
}
|
|
275
308
|
}
|
|
309
|
+
if (droppedChildKeys.length > 0) {
|
|
310
|
+
opts?.onDroppedChildKeys?.(droppedChildKeys);
|
|
311
|
+
}
|
|
276
312
|
const activeChapterIndex = typeof obj.activeChapterIndex === "number" && Number.isFinite(obj.activeChapterIndex) ? obj.activeChapterIndex : void 0;
|
|
277
313
|
return {
|
|
278
314
|
schemaVersion: COMPOUND_RESUME_SCHEMA_VERSION,
|
|
@@ -297,17 +333,21 @@ var COMPOUND_STATE_PREFIX = "lessonkit:compound:";
|
|
|
297
333
|
function compoundStateStorageKey(courseId, compoundId) {
|
|
298
334
|
return `${COMPOUND_STATE_PREFIX}${courseId}:${compoundId}`;
|
|
299
335
|
}
|
|
300
|
-
function loadCompoundState(storage, courseId, compoundId) {
|
|
336
|
+
function loadCompoundState(storage, courseId, compoundId, opts) {
|
|
301
337
|
const key = compoundStateStorageKey(courseId, compoundId);
|
|
302
338
|
const raw = storage.getItem(key);
|
|
303
339
|
if (!raw) return null;
|
|
304
340
|
try {
|
|
305
|
-
const parsed = parseCompoundResumeState(JSON.parse(raw));
|
|
306
|
-
if (parsed === null
|
|
307
|
-
|
|
341
|
+
const parsed = parseCompoundResumeState(JSON.parse(raw), opts);
|
|
342
|
+
if (parsed === null) {
|
|
343
|
+
opts?.onCorrupt?.();
|
|
344
|
+
if (isDevEnvironment()) {
|
|
345
|
+
console.warn(`[lessonkit] Ignoring corrupt compound resume state at ${key}`);
|
|
346
|
+
}
|
|
308
347
|
}
|
|
309
348
|
return parsed;
|
|
310
349
|
} catch {
|
|
350
|
+
opts?.onCorrupt?.();
|
|
311
351
|
if (isDevEnvironment()) {
|
|
312
352
|
console.warn(`[lessonkit] Ignoring corrupt compound resume state at ${key}`);
|
|
313
353
|
}
|
|
@@ -322,10 +362,23 @@ function clearCompoundState(storage, courseId, compoundId) {
|
|
|
322
362
|
}
|
|
323
363
|
|
|
324
364
|
// src/compoundAllowlists.ts
|
|
365
|
+
var PAGE_AND_SLIDE_14_BLOCKS = [
|
|
366
|
+
"Video",
|
|
367
|
+
"Summary",
|
|
368
|
+
"ImagePairing",
|
|
369
|
+
"ImageSequencing",
|
|
370
|
+
"MemoryGame",
|
|
371
|
+
"InformationWall",
|
|
372
|
+
"ParallaxSlideshow",
|
|
373
|
+
"Questionnaire",
|
|
374
|
+
"Essay",
|
|
375
|
+
"ArithmeticQuiz"
|
|
376
|
+
];
|
|
325
377
|
var PAGE_ALLOWED_CHILD_TYPES = [
|
|
326
378
|
"Text",
|
|
327
379
|
"Heading",
|
|
328
380
|
"Image",
|
|
381
|
+
"Video",
|
|
329
382
|
"Scenario",
|
|
330
383
|
"Reflection",
|
|
331
384
|
"Quiz",
|
|
@@ -335,6 +388,15 @@ var PAGE_ALLOWED_CHILD_TYPES = [
|
|
|
335
388
|
"DragAndDrop",
|
|
336
389
|
"DragTheWords",
|
|
337
390
|
"MarkTheWords",
|
|
391
|
+
"Summary",
|
|
392
|
+
"ImagePairing",
|
|
393
|
+
"ImageSequencing",
|
|
394
|
+
"MemoryGame",
|
|
395
|
+
"InformationWall",
|
|
396
|
+
"ParallaxSlideshow",
|
|
397
|
+
"Questionnaire",
|
|
398
|
+
"Essay",
|
|
399
|
+
"ArithmeticQuiz",
|
|
338
400
|
"Accordion",
|
|
339
401
|
"DialogCards",
|
|
340
402
|
"Flashcards",
|
|
@@ -342,13 +404,51 @@ var PAGE_ALLOWED_CHILD_TYPES = [
|
|
|
342
404
|
"FindHotspot",
|
|
343
405
|
"FindMultipleHotspots",
|
|
344
406
|
"ImageSlider",
|
|
407
|
+
"Embed",
|
|
408
|
+
"Chart",
|
|
345
409
|
"ProgressTracker"
|
|
346
410
|
];
|
|
411
|
+
var BRANCH_NODE_ALLOWED_CHILD_TYPES = [
|
|
412
|
+
"Text",
|
|
413
|
+
"Heading",
|
|
414
|
+
"Image",
|
|
415
|
+
"Video",
|
|
416
|
+
"Scenario",
|
|
417
|
+
"Reflection",
|
|
418
|
+
"Quiz",
|
|
419
|
+
"KnowledgeCheck",
|
|
420
|
+
"TrueFalse",
|
|
421
|
+
"FillInTheBlanks",
|
|
422
|
+
"DragAndDrop",
|
|
423
|
+
"DragTheWords",
|
|
424
|
+
"MarkTheWords",
|
|
425
|
+
"Summary",
|
|
426
|
+
"ImagePairing",
|
|
427
|
+
"ImageSequencing",
|
|
428
|
+
"MemoryGame",
|
|
429
|
+
"InformationWall",
|
|
430
|
+
"ParallaxSlideshow",
|
|
431
|
+
"Questionnaire",
|
|
432
|
+
"Essay",
|
|
433
|
+
"ArithmeticQuiz",
|
|
434
|
+
"Accordion",
|
|
435
|
+
"DialogCards",
|
|
436
|
+
"Flashcards",
|
|
437
|
+
"ImageHotspots",
|
|
438
|
+
"FindHotspot",
|
|
439
|
+
"FindMultipleHotspots",
|
|
440
|
+
"ImageSlider",
|
|
441
|
+
"Embed",
|
|
442
|
+
"Chart",
|
|
443
|
+
"BranchChoice"
|
|
444
|
+
];
|
|
445
|
+
var BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES = ["BranchNode"];
|
|
347
446
|
var INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES = ["Page"];
|
|
348
447
|
var SLIDE_ALLOWED_CHILD_TYPES = [
|
|
349
448
|
"Text",
|
|
350
449
|
"Heading",
|
|
351
450
|
"Image",
|
|
451
|
+
"Video",
|
|
352
452
|
"Scenario",
|
|
353
453
|
"Reflection",
|
|
354
454
|
"Quiz",
|
|
@@ -358,15 +458,42 @@ var SLIDE_ALLOWED_CHILD_TYPES = [
|
|
|
358
458
|
"DragAndDrop",
|
|
359
459
|
"DragTheWords",
|
|
360
460
|
"MarkTheWords",
|
|
461
|
+
"Summary",
|
|
462
|
+
"ImagePairing",
|
|
463
|
+
"ImageSequencing",
|
|
464
|
+
"MemoryGame",
|
|
465
|
+
"InformationWall",
|
|
466
|
+
"ParallaxSlideshow",
|
|
467
|
+
"Questionnaire",
|
|
468
|
+
"Essay",
|
|
469
|
+
"ArithmeticQuiz",
|
|
361
470
|
"Accordion",
|
|
362
471
|
"DialogCards",
|
|
363
472
|
"Flashcards",
|
|
364
473
|
"ImageHotspots",
|
|
365
474
|
"FindHotspot",
|
|
366
475
|
"FindMultipleHotspots",
|
|
367
|
-
"ImageSlider"
|
|
476
|
+
"ImageSlider",
|
|
477
|
+
"Embed",
|
|
478
|
+
"Chart"
|
|
368
479
|
];
|
|
369
480
|
var SLIDE_DECK_ALLOWED_CHILD_TYPES = ["Slide"];
|
|
481
|
+
var TIMED_CUE_ALLOWED_CHILD_TYPES = [
|
|
482
|
+
"Text",
|
|
483
|
+
"Heading",
|
|
484
|
+
"Image",
|
|
485
|
+
"Quiz",
|
|
486
|
+
"TrueFalse",
|
|
487
|
+
"FillInTheBlanks",
|
|
488
|
+
"Summary",
|
|
489
|
+
"ImagePairing",
|
|
490
|
+
"ImageSequencing",
|
|
491
|
+
"MemoryGame",
|
|
492
|
+
"Questionnaire",
|
|
493
|
+
"Essay",
|
|
494
|
+
"ArithmeticQuiz"
|
|
495
|
+
];
|
|
496
|
+
var INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES = ["TimedCue"];
|
|
370
497
|
var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
|
|
371
498
|
"TrueFalse",
|
|
372
499
|
"FillInTheBlanks",
|
|
@@ -376,21 +503,34 @@ var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
|
|
|
376
503
|
"Quiz",
|
|
377
504
|
"KnowledgeCheck",
|
|
378
505
|
"FindHotspot",
|
|
379
|
-
"FindMultipleHotspots"
|
|
506
|
+
"FindMultipleHotspots",
|
|
507
|
+
"Summary",
|
|
508
|
+
"ImagePairing",
|
|
509
|
+
"ImageSequencing",
|
|
510
|
+
"ArithmeticQuiz",
|
|
511
|
+
"Essay"
|
|
380
512
|
];
|
|
381
513
|
var ALLOWLISTS = {
|
|
382
514
|
Page: PAGE_ALLOWED_CHILD_TYPES,
|
|
383
515
|
InteractiveBook: INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
384
516
|
Slide: SLIDE_ALLOWED_CHILD_TYPES,
|
|
385
517
|
SlideDeck: SLIDE_DECK_ALLOWED_CHILD_TYPES,
|
|
386
|
-
|
|
518
|
+
TimedCue: TIMED_CUE_ALLOWED_CHILD_TYPES,
|
|
519
|
+
InteractiveVideo: INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
|
|
520
|
+
AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
|
|
521
|
+
BranchingScenario: BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES,
|
|
522
|
+
BranchNode: BRANCH_NODE_ALLOWED_CHILD_TYPES
|
|
387
523
|
};
|
|
388
524
|
var COMPOUND_MAX_NESTING_DEPTH = {
|
|
389
525
|
Page: 1,
|
|
390
526
|
InteractiveBook: 2,
|
|
391
527
|
Slide: 1,
|
|
392
528
|
SlideDeck: 2,
|
|
393
|
-
|
|
529
|
+
TimedCue: 1,
|
|
530
|
+
InteractiveVideo: 2,
|
|
531
|
+
AssessmentSequence: 1,
|
|
532
|
+
BranchingScenario: 2,
|
|
533
|
+
BranchNode: 1
|
|
394
534
|
};
|
|
395
535
|
function getAllowedChildTypes(parent) {
|
|
396
536
|
return ALLOWLISTS[parent];
|
|
@@ -399,6 +539,83 @@ function isChildTypeAllowed(parent, childType) {
|
|
|
399
539
|
return ALLOWLISTS[parent].includes(childType);
|
|
400
540
|
}
|
|
401
541
|
var ACCORDION_FORBIDDEN_CHILD_TYPES = ["Accordion"];
|
|
542
|
+
var BLOCKS_14_PAGE_SLIDE = PAGE_AND_SLIDE_14_BLOCKS;
|
|
543
|
+
|
|
544
|
+
// src/branchGraph.ts
|
|
545
|
+
function validateBranchGraph(startNodeId, nodes) {
|
|
546
|
+
const issues = [];
|
|
547
|
+
if (nodes.length === 0) {
|
|
548
|
+
issues.push({ code: "empty_graph", message: "Branch graph has no nodes" });
|
|
549
|
+
return { ok: false, issues, reachableNodeIds: [] };
|
|
550
|
+
}
|
|
551
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
552
|
+
for (const node of nodes) {
|
|
553
|
+
if (nodeIds.has(node.nodeId)) {
|
|
554
|
+
issues.push({
|
|
555
|
+
code: "duplicate_node_id",
|
|
556
|
+
message: `Duplicate nodeId "${node.nodeId}"`,
|
|
557
|
+
nodeId: node.nodeId
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
nodeIds.add(node.nodeId);
|
|
561
|
+
}
|
|
562
|
+
if (!nodeIds.has(startNodeId)) {
|
|
563
|
+
issues.push({
|
|
564
|
+
code: "start_not_found",
|
|
565
|
+
message: `startNodeId "${startNodeId}" does not match any BranchNode`,
|
|
566
|
+
nodeId: startNodeId
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (nodes.length > 1 && nodeIds.has(startNodeId)) {
|
|
570
|
+
const startNode = nodes.find((n) => n.nodeId === startNodeId);
|
|
571
|
+
if (startNode && startNode.choices.length === 0) {
|
|
572
|
+
issues.push({
|
|
573
|
+
code: "start_no_choices",
|
|
574
|
+
message: `startNodeId "${startNodeId}" has no BranchChoice children in a multi-node scenario`,
|
|
575
|
+
nodeId: startNodeId
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
for (const node of nodes) {
|
|
580
|
+
for (const choice of node.choices) {
|
|
581
|
+
if (!nodeIds.has(choice.targetNodeId)) {
|
|
582
|
+
issues.push({
|
|
583
|
+
code: "unknown_target",
|
|
584
|
+
message: `Choice from "${node.nodeId}" references unknown target "${choice.targetNodeId}"`,
|
|
585
|
+
nodeId: node.nodeId
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
591
|
+
if (nodeIds.has(startNodeId)) {
|
|
592
|
+
const queue = [startNodeId];
|
|
593
|
+
while (queue.length > 0) {
|
|
594
|
+
const current = queue.shift();
|
|
595
|
+
if (reachable.has(current)) continue;
|
|
596
|
+
reachable.add(current);
|
|
597
|
+
const node = nodes.find((n) => n.nodeId === current);
|
|
598
|
+
if (!node) continue;
|
|
599
|
+
for (const choice of node.choices) {
|
|
600
|
+
if (!reachable.has(choice.targetNodeId)) queue.push(choice.targetNodeId);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
for (const nodeId of nodeIds) {
|
|
605
|
+
if (!reachable.has(nodeId)) {
|
|
606
|
+
issues.push({
|
|
607
|
+
code: "unreachable_node",
|
|
608
|
+
message: `Node "${nodeId}" is not reachable from startNodeId "${startNodeId}"`,
|
|
609
|
+
nodeId
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
ok: issues.length === 0,
|
|
615
|
+
issues,
|
|
616
|
+
reachableNodeIds: [...reachable]
|
|
617
|
+
};
|
|
618
|
+
}
|
|
402
619
|
|
|
403
620
|
// src/telemetryCatalog.ts
|
|
404
621
|
var telemetryCatalogVersion = 1;
|
|
@@ -554,6 +771,70 @@ var TELEMETRY_EVENT_CATALOG_V3 = [
|
|
|
554
771
|
dataFields: ["blockId", "slideIndex"],
|
|
555
772
|
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
556
773
|
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
name: "video_cue_reached",
|
|
777
|
+
description: "Learner reached a timed cue in an Interactive Video",
|
|
778
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
779
|
+
dataFields: ["blockId", "cueIndex", "atSeconds", "cueLabel"],
|
|
780
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
781
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
name: "video_segment_completed",
|
|
785
|
+
description: "Learner completed a timed segment in an Interactive Video",
|
|
786
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
787
|
+
dataFields: ["blockId", "segmentIndex", "atSeconds", "segmentLabel"],
|
|
788
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
|
|
789
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
name: "memory_card_flipped",
|
|
793
|
+
description: "Learner flipped a memory game card",
|
|
794
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
795
|
+
dataFields: ["blockId", "cardIndex", "face"],
|
|
796
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
797
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
name: "information_wall_search",
|
|
801
|
+
description: "Learner searched an information wall",
|
|
802
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
803
|
+
dataFields: ["blockId", "query", "resultCount"],
|
|
804
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
805
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
name: "parallax_slide_viewed",
|
|
809
|
+
description: "Learner viewed a slide in a parallax slideshow",
|
|
810
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
811
|
+
dataFields: ["blockId", "slideIndex"],
|
|
812
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
813
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
name: "questionnaire_submitted",
|
|
817
|
+
description: "Learner submitted an unscored questionnaire",
|
|
818
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
819
|
+
dataFields: ["blockId", "fieldCount"],
|
|
820
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
|
|
821
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
name: "branch_node_viewed",
|
|
825
|
+
description: "Learner viewed a node in a BranchingScenario",
|
|
826
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
827
|
+
dataFields: ["blockId", "nodeId", "nodeIndex", "nodeTitle"],
|
|
828
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
829
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}:node:{nodeId}"
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
name: "branch_selected",
|
|
833
|
+
description: "Learner selected a branch choice in a BranchingScenario",
|
|
834
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
835
|
+
dataFields: ["blockId", "fromNodeId", "toNodeId", "label", "scoreWeight"],
|
|
836
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
|
|
837
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}:node:{toNodeId}"
|
|
557
838
|
}
|
|
558
839
|
];
|
|
559
840
|
function buildTelemetryCatalogV3() {
|
|
@@ -561,34 +842,36 @@ function buildTelemetryCatalogV3() {
|
|
|
561
842
|
}
|
|
562
843
|
|
|
563
844
|
// src/internal/sinkInvoke.ts
|
|
564
|
-
function
|
|
565
|
-
let result;
|
|
845
|
+
async function invokeTrackingSinkWithResult(sink, event) {
|
|
566
846
|
try {
|
|
567
|
-
result = sink(event);
|
|
847
|
+
const result = sink(event);
|
|
848
|
+
if (result != null && typeof result.then === "function") {
|
|
849
|
+
await result;
|
|
850
|
+
}
|
|
851
|
+
return true;
|
|
568
852
|
} catch (err) {
|
|
569
853
|
warnDev("[lessonkit] tracking sink failed:", err);
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
if (result != null && typeof result.catch === "function") {
|
|
573
|
-
void result.catch((err) => warnDev("[lessonkit] tracking sink failed:", err));
|
|
854
|
+
return false;
|
|
574
855
|
}
|
|
575
856
|
}
|
|
576
|
-
function
|
|
857
|
+
function invokeTrackingSink(sink, event) {
|
|
577
858
|
let result;
|
|
578
859
|
try {
|
|
579
|
-
result =
|
|
860
|
+
result = sink(event);
|
|
580
861
|
} catch (err) {
|
|
581
|
-
warnDev(
|
|
582
|
-
|
|
862
|
+
warnDev("[lessonkit] tracking sink failed:", err);
|
|
863
|
+
throw err;
|
|
583
864
|
}
|
|
584
865
|
if (result != null && typeof result.catch === "function") {
|
|
585
|
-
void result.catch(
|
|
586
|
-
(err) => warnDev(`[lessonkit] telemetry sink "${sinkId}" failed:`, err)
|
|
587
|
-
);
|
|
866
|
+
void result.catch((err) => warnDev("[lessonkit] tracking sink failed:", err));
|
|
588
867
|
}
|
|
589
868
|
}
|
|
590
869
|
|
|
591
870
|
// src/trackingClient.ts
|
|
871
|
+
function eventDedupKey(event) {
|
|
872
|
+
const id = event.id?.trim();
|
|
873
|
+
return id || void 0;
|
|
874
|
+
}
|
|
592
875
|
function createTrackingClient(opts) {
|
|
593
876
|
const sink = opts?.sink;
|
|
594
877
|
const batchSink = opts?.batchSink;
|
|
@@ -606,8 +889,19 @@ function createTrackingClient(opts) {
|
|
|
606
889
|
let disposed2 = false;
|
|
607
890
|
return {
|
|
608
891
|
track: (event) => {
|
|
609
|
-
if (disposed2) return;
|
|
610
|
-
if (sink)
|
|
892
|
+
if (disposed2) return false;
|
|
893
|
+
if (sink) {
|
|
894
|
+
try {
|
|
895
|
+
invokeTrackingSink(sink, event);
|
|
896
|
+
} catch {
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return true;
|
|
900
|
+
},
|
|
901
|
+
deliver: async (event) => {
|
|
902
|
+
if (disposed2) return false;
|
|
903
|
+
if (!sink) return true;
|
|
904
|
+
return invokeTrackingSinkWithResult(sink, event);
|
|
611
905
|
},
|
|
612
906
|
dispose: () => {
|
|
613
907
|
disposed2 = true;
|
|
@@ -615,19 +909,28 @@ function createTrackingClient(opts) {
|
|
|
615
909
|
};
|
|
616
910
|
}
|
|
617
911
|
if (!sink && !batchSink) {
|
|
618
|
-
return { track: () =>
|
|
619
|
-
} };
|
|
912
|
+
return { track: () => true };
|
|
620
913
|
}
|
|
621
914
|
const buffer = [];
|
|
915
|
+
const pendingDeliverIds = /* @__PURE__ */ new Set();
|
|
622
916
|
let flushInFlight = null;
|
|
623
|
-
let inflightExitBatch = null;
|
|
624
917
|
let disposed = false;
|
|
625
918
|
let disposing = false;
|
|
626
919
|
let intervalId;
|
|
920
|
+
const clearPendingDeliverIds = (events) => {
|
|
921
|
+
for (const event of events) {
|
|
922
|
+
const key = eventDedupKey(event);
|
|
923
|
+
if (key) pendingDeliverIds.delete(key);
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
const isEventBuffered = (event) => {
|
|
927
|
+
const key = eventDedupKey(event);
|
|
928
|
+
if (!key) return false;
|
|
929
|
+
return buffer.some((buffered) => eventDedupKey(buffered) === key);
|
|
930
|
+
};
|
|
627
931
|
const runFlush = () => {
|
|
628
932
|
if (!buffer.length) return Promise.resolve(true);
|
|
629
933
|
const events = buffer.splice(0, buffer.length);
|
|
630
|
-
inflightExitBatch = events;
|
|
631
934
|
let succeeded = false;
|
|
632
935
|
return Promise.resolve().then(async () => {
|
|
633
936
|
if (batchSink) {
|
|
@@ -648,12 +951,13 @@ function createTrackingClient(opts) {
|
|
|
648
951
|
buffer.unshift(...events);
|
|
649
952
|
}
|
|
650
953
|
}).then(async () => {
|
|
954
|
+
if (succeeded) {
|
|
955
|
+
clearPendingDeliverIds(events);
|
|
956
|
+
}
|
|
651
957
|
if (succeeded && buffer.length > 0 && !disposed) {
|
|
652
958
|
return runFlush();
|
|
653
959
|
}
|
|
654
960
|
return succeeded;
|
|
655
|
-
}).finally(() => {
|
|
656
|
-
inflightExitBatch = null;
|
|
657
961
|
});
|
|
658
962
|
};
|
|
659
963
|
const flush = () => {
|
|
@@ -674,44 +978,66 @@ function createTrackingClient(opts) {
|
|
|
674
978
|
if (!delivered) break;
|
|
675
979
|
}
|
|
676
980
|
if (buffer.length > 0) {
|
|
981
|
+
const droppedCount = buffer.length;
|
|
677
982
|
if (isDevEnvironment()) {
|
|
678
983
|
console.warn(
|
|
679
|
-
`[lessonkit] dropped ${
|
|
984
|
+
`[lessonkit] dropped ${droppedCount} buffered telemetry event(s) after dispose flush cap`
|
|
680
985
|
);
|
|
681
986
|
}
|
|
987
|
+
for (let i = 0; i < droppedCount; i++) {
|
|
988
|
+
opts?.onBufferDrop?.();
|
|
989
|
+
}
|
|
682
990
|
buffer.length = 0;
|
|
991
|
+
pendingDeliverIds.clear();
|
|
683
992
|
}
|
|
684
993
|
};
|
|
685
994
|
intervalId = flushIntervalMs > 0 ? globalThis.setInterval(() => void flush(), flushIntervalMs) : void 0;
|
|
686
995
|
intervalId?.unref?.();
|
|
996
|
+
const track = (event) => {
|
|
997
|
+
if (disposed || disposing) return false;
|
|
998
|
+
const key = eventDedupKey(event);
|
|
999
|
+
if (key && (pendingDeliverIds.has(key) || isEventBuffered(event))) {
|
|
1000
|
+
return true;
|
|
1001
|
+
}
|
|
1002
|
+
if (buffer.length >= maxBufferSize) {
|
|
1003
|
+
opts?.onBufferDrop?.();
|
|
1004
|
+
if (!warnedBufferCap && isDevEnvironment()) {
|
|
1005
|
+
warnedBufferCap = true;
|
|
1006
|
+
console.warn(
|
|
1007
|
+
`[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; new events are dropped until the buffer drains.`
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
buffer.push(event);
|
|
1013
|
+
if (buffer.length >= maxBatchSize) void flush();
|
|
1014
|
+
return true;
|
|
1015
|
+
};
|
|
687
1016
|
return {
|
|
688
|
-
track
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
warnedBufferCap = true;
|
|
694
|
-
console.warn(
|
|
695
|
-
`[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; new events are dropped until the buffer drains.`
|
|
696
|
-
);
|
|
697
|
-
}
|
|
698
|
-
return;
|
|
1017
|
+
track,
|
|
1018
|
+
deliver: async (event) => {
|
|
1019
|
+
const key = eventDedupKey(event);
|
|
1020
|
+
if (key && (pendingDeliverIds.has(key) || isEventBuffered(event))) {
|
|
1021
|
+
return flush();
|
|
699
1022
|
}
|
|
700
|
-
|
|
701
|
-
if (
|
|
1023
|
+
if (!track(event)) return false;
|
|
1024
|
+
if (key) pendingDeliverIds.add(key);
|
|
1025
|
+
return flush();
|
|
702
1026
|
},
|
|
703
1027
|
flush,
|
|
704
1028
|
flushOnExit: opts?.exitBatchSink ? () => {
|
|
705
|
-
const
|
|
706
|
-
const fromInflight = inflightExitBatch ? [...inflightExitBatch] : [];
|
|
707
|
-
const events = [...fromInflight, ...fromBuffer];
|
|
1029
|
+
const events = buffer.splice(0, buffer.length);
|
|
708
1030
|
if (!events.length) return;
|
|
709
1031
|
try {
|
|
710
1032
|
const result = opts.exitBatchSink(events);
|
|
711
1033
|
if (result != null && typeof result.catch === "function") {
|
|
712
|
-
void result.
|
|
1034
|
+
void result.then(() => {
|
|
1035
|
+
clearPendingDeliverIds(events);
|
|
1036
|
+
}).catch(() => {
|
|
713
1037
|
buffer.unshift(...events);
|
|
714
1038
|
});
|
|
1039
|
+
} else {
|
|
1040
|
+
clearPendingDeliverIds(events);
|
|
715
1041
|
}
|
|
716
1042
|
} catch {
|
|
717
1043
|
buffer.unshift(...events);
|
|
@@ -733,10 +1059,21 @@ function createTrackingClient(opts) {
|
|
|
733
1059
|
}
|
|
734
1060
|
|
|
735
1061
|
// src/ids.ts
|
|
1062
|
+
function randomSessionIdFallback() {
|
|
1063
|
+
const g = globalThis;
|
|
1064
|
+
if (g.crypto?.getRandomValues) {
|
|
1065
|
+
const bytes = new Uint8Array(16);
|
|
1066
|
+
g.crypto.getRandomValues(bytes);
|
|
1067
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1068
|
+
}
|
|
1069
|
+
throw new Error(
|
|
1070
|
+
"[lessonkit] createSessionId requires crypto.randomUUID or crypto.getRandomValues"
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
736
1073
|
function createSessionId() {
|
|
737
1074
|
const g = globalThis;
|
|
738
1075
|
if (g.crypto?.randomUUID) return g.crypto.randomUUID();
|
|
739
|
-
return
|
|
1076
|
+
return randomSessionIdFallback();
|
|
740
1077
|
}
|
|
741
1078
|
|
|
742
1079
|
// src/time.ts
|
|
@@ -940,6 +1277,109 @@ var TELEMETRY_EVENT_REGISTRY = {
|
|
|
940
1277
|
data: opts.data
|
|
941
1278
|
};
|
|
942
1279
|
}
|
|
1280
|
+
},
|
|
1281
|
+
video_cue_reached: {
|
|
1282
|
+
requiresLessonId: true,
|
|
1283
|
+
build: (opts, base) => {
|
|
1284
|
+
if (opts.name !== "video_cue_reached") throw new Error("unexpected event");
|
|
1285
|
+
const lessonId = opts.lessonId;
|
|
1286
|
+
if (!lessonId) throw new Error("video_cue_reached requires active lessonId");
|
|
1287
|
+
return {
|
|
1288
|
+
name: "video_cue_reached",
|
|
1289
|
+
...base,
|
|
1290
|
+
lessonId,
|
|
1291
|
+
data: opts.data
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
},
|
|
1295
|
+
video_segment_completed: {
|
|
1296
|
+
requiresLessonId: true,
|
|
1297
|
+
build: (opts, base) => {
|
|
1298
|
+
if (opts.name !== "video_segment_completed") throw new Error("unexpected event");
|
|
1299
|
+
const lessonId = opts.lessonId;
|
|
1300
|
+
if (!lessonId) throw new Error("video_segment_completed requires active lessonId");
|
|
1301
|
+
return {
|
|
1302
|
+
name: "video_segment_completed",
|
|
1303
|
+
...base,
|
|
1304
|
+
lessonId,
|
|
1305
|
+
data: opts.data
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
},
|
|
1309
|
+
memory_card_flipped: {
|
|
1310
|
+
build: (opts, base) => {
|
|
1311
|
+
if (opts.name !== "memory_card_flipped") throw new Error("unexpected event");
|
|
1312
|
+
return {
|
|
1313
|
+
name: "memory_card_flipped",
|
|
1314
|
+
...base,
|
|
1315
|
+
lessonId: opts.lessonId,
|
|
1316
|
+
data: opts.data
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
},
|
|
1320
|
+
information_wall_search: {
|
|
1321
|
+
build: (opts, base) => {
|
|
1322
|
+
if (opts.name !== "information_wall_search") throw new Error("unexpected event");
|
|
1323
|
+
return {
|
|
1324
|
+
name: "information_wall_search",
|
|
1325
|
+
...base,
|
|
1326
|
+
lessonId: opts.lessonId,
|
|
1327
|
+
data: opts.data
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
},
|
|
1331
|
+
parallax_slide_viewed: {
|
|
1332
|
+
build: (opts, base) => {
|
|
1333
|
+
if (opts.name !== "parallax_slide_viewed") throw new Error("unexpected event");
|
|
1334
|
+
return {
|
|
1335
|
+
name: "parallax_slide_viewed",
|
|
1336
|
+
...base,
|
|
1337
|
+
lessonId: opts.lessonId,
|
|
1338
|
+
data: opts.data
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
},
|
|
1342
|
+
questionnaire_submitted: {
|
|
1343
|
+
requiresLessonId: true,
|
|
1344
|
+
build: (opts, base) => {
|
|
1345
|
+
if (opts.name !== "questionnaire_submitted") throw new Error("unexpected event");
|
|
1346
|
+
const lessonId = opts.lessonId;
|
|
1347
|
+
if (!lessonId) throw new Error("questionnaire_submitted requires active lessonId");
|
|
1348
|
+
return {
|
|
1349
|
+
name: "questionnaire_submitted",
|
|
1350
|
+
...base,
|
|
1351
|
+
lessonId,
|
|
1352
|
+
data: opts.data
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
},
|
|
1356
|
+
branch_node_viewed: {
|
|
1357
|
+
requiresLessonId: true,
|
|
1358
|
+
build: (opts, base) => {
|
|
1359
|
+
if (opts.name !== "branch_node_viewed") throw new Error("unexpected event");
|
|
1360
|
+
const lessonId = opts.lessonId;
|
|
1361
|
+
if (!lessonId) throw new Error("branch_node_viewed requires active lessonId");
|
|
1362
|
+
return {
|
|
1363
|
+
name: "branch_node_viewed",
|
|
1364
|
+
...base,
|
|
1365
|
+
lessonId,
|
|
1366
|
+
data: opts.data
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1370
|
+
branch_selected: {
|
|
1371
|
+
requiresLessonId: true,
|
|
1372
|
+
build: (opts, base) => {
|
|
1373
|
+
if (opts.name !== "branch_selected") throw new Error("unexpected event");
|
|
1374
|
+
const lessonId = opts.lessonId;
|
|
1375
|
+
if (!lessonId) throw new Error("branch_selected requires active lessonId");
|
|
1376
|
+
return {
|
|
1377
|
+
name: "branch_selected",
|
|
1378
|
+
...base,
|
|
1379
|
+
lessonId,
|
|
1380
|
+
data: opts.data
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
943
1383
|
}
|
|
944
1384
|
};
|
|
945
1385
|
function buildTelemetryEventFromRegistry(opts) {
|
|
@@ -972,8 +1412,8 @@ function buildTelemetryEvent(opts) {
|
|
|
972
1412
|
}
|
|
973
1413
|
function tryBuildTelemetryEvent(opts) {
|
|
974
1414
|
const entry = getTelemetryEventRegistryEntry(opts.name);
|
|
975
|
-
if (entry.requiresLessonId && !opts.lessonId
|
|
976
|
-
if (isDevEnvironment()) {
|
|
1415
|
+
if (entry.requiresLessonId && !opts.lessonId) {
|
|
1416
|
+
if (isDevEnvironment() && entry.tryBuildMissingLessonWarning) {
|
|
977
1417
|
if (entry.tryBuildMissingLessonWarning === "quiz" && !warnedMissingQuizLesson) {
|
|
978
1418
|
warnedMissingQuizLesson = true;
|
|
979
1419
|
console.warn(
|
|
@@ -993,21 +1433,44 @@ function tryBuildTelemetryEvent(opts) {
|
|
|
993
1433
|
}
|
|
994
1434
|
|
|
995
1435
|
// src/telemetryPipeline.ts
|
|
996
|
-
|
|
997
|
-
|
|
1436
|
+
var LIFECYCLE_TELEMETRY_EVENTS = /* @__PURE__ */ new Set([
|
|
1437
|
+
"course_started",
|
|
1438
|
+
"course_completed",
|
|
1439
|
+
"lesson_started",
|
|
1440
|
+
"lesson_completed",
|
|
1441
|
+
"lesson_time_on_task"
|
|
1442
|
+
]);
|
|
1443
|
+
function isLifecycleTelemetryEvent(name) {
|
|
1444
|
+
return LIFECYCLE_TELEMETRY_EVENTS.has(name);
|
|
1445
|
+
}
|
|
1446
|
+
async function invokeSink(sink, event, emitCtx) {
|
|
1447
|
+
let result;
|
|
1448
|
+
try {
|
|
1449
|
+
result = sink.emit(event, emitCtx);
|
|
1450
|
+
} catch (err) {
|
|
1451
|
+
warnDev(`[lessonkit] telemetry sink "${sink.id}" failed:`, err);
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (result != null && typeof result.then === "function") {
|
|
1455
|
+
try {
|
|
1456
|
+
await result;
|
|
1457
|
+
} catch (err) {
|
|
1458
|
+
warnDev(`[lessonkit] telemetry sink "${sink.id}" failed:`, err);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
998
1461
|
}
|
|
999
1462
|
function createTelemetryPipeline(sinks) {
|
|
1000
1463
|
const list = [...sinks];
|
|
1001
1464
|
return {
|
|
1002
1465
|
sinks: list,
|
|
1003
|
-
emit(event, ctx) {
|
|
1466
|
+
async emit(event, ctx) {
|
|
1004
1467
|
const emitCtx = ctx ?? {
|
|
1005
1468
|
courseId: event.courseId,
|
|
1006
1469
|
sessionId: event.sessionId,
|
|
1007
1470
|
attemptId: event.attemptId
|
|
1008
1471
|
};
|
|
1009
1472
|
for (const sink of list) {
|
|
1010
|
-
invokeSink(sink, event, emitCtx);
|
|
1473
|
+
await invokeSink(sink, event, emitCtx);
|
|
1011
1474
|
}
|
|
1012
1475
|
}
|
|
1013
1476
|
};
|
|
@@ -1029,14 +1492,44 @@ function createDefaultClock() {
|
|
|
1029
1492
|
};
|
|
1030
1493
|
}
|
|
1031
1494
|
function createNoopStorage() {
|
|
1495
|
+
const memory = /* @__PURE__ */ new Map();
|
|
1032
1496
|
return {
|
|
1033
|
-
getItem: () => null,
|
|
1034
|
-
setItem: () =>
|
|
1497
|
+
getItem: (key) => memory.get(key) ?? null,
|
|
1498
|
+
setItem: (key, value) => {
|
|
1499
|
+
memory.set(key, value);
|
|
1500
|
+
return true;
|
|
1501
|
+
},
|
|
1502
|
+
removeItem: (key) => {
|
|
1503
|
+
memory.delete(key);
|
|
1504
|
+
},
|
|
1505
|
+
resetForTests: () => {
|
|
1506
|
+
memory.clear();
|
|
1507
|
+
}
|
|
1035
1508
|
};
|
|
1036
1509
|
}
|
|
1037
1510
|
function createMemoryBackedSessionStorage(session) {
|
|
1038
1511
|
const memory = /* @__PURE__ */ new Map();
|
|
1512
|
+
const tombstones = /* @__PURE__ */ new Set();
|
|
1039
1513
|
let warnedPersistFailure = false;
|
|
1514
|
+
const syncFromStorageEvent = (key, newValue) => {
|
|
1515
|
+
if (key === null) {
|
|
1516
|
+
memory.clear();
|
|
1517
|
+
tombstones.clear();
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
tombstones.delete(key);
|
|
1521
|
+
if (newValue === null) {
|
|
1522
|
+
memory.delete(key);
|
|
1523
|
+
} else {
|
|
1524
|
+
memory.set(key, newValue);
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
if (typeof window !== "undefined") {
|
|
1528
|
+
window.addEventListener("storage", (event) => {
|
|
1529
|
+
if (event.storageArea !== sessionStorage) return;
|
|
1530
|
+
syncFromStorageEvent(event.key, event.newValue);
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1040
1533
|
const warnPersistFailure = () => {
|
|
1041
1534
|
if (warnedPersistFailure) return;
|
|
1042
1535
|
warnedPersistFailure = true;
|
|
@@ -1049,6 +1542,7 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
1049
1542
|
};
|
|
1050
1543
|
return {
|
|
1051
1544
|
getItem: (key) => {
|
|
1545
|
+
if (tombstones.has(key)) return null;
|
|
1052
1546
|
if (memory.has(key)) return memory.get(key);
|
|
1053
1547
|
try {
|
|
1054
1548
|
const value = session.getItem(key);
|
|
@@ -1059,6 +1553,7 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
1059
1553
|
}
|
|
1060
1554
|
},
|
|
1061
1555
|
setItem: (key, value) => {
|
|
1556
|
+
tombstones.delete(key);
|
|
1062
1557
|
memory.set(key, value);
|
|
1063
1558
|
try {
|
|
1064
1559
|
session.setItem(key, value);
|
|
@@ -1072,12 +1567,15 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
1072
1567
|
memory.delete(key);
|
|
1073
1568
|
try {
|
|
1074
1569
|
session.removeItem(key);
|
|
1570
|
+
tombstones.delete(key);
|
|
1075
1571
|
} catch {
|
|
1076
1572
|
warnPersistFailure();
|
|
1573
|
+
tombstones.add(key);
|
|
1077
1574
|
}
|
|
1078
1575
|
},
|
|
1079
1576
|
resetForTests: () => {
|
|
1080
1577
|
memory.clear();
|
|
1578
|
+
tombstones.clear();
|
|
1081
1579
|
}
|
|
1082
1580
|
};
|
|
1083
1581
|
}
|
|
@@ -1149,6 +1647,11 @@ function createProgressController() {
|
|
|
1149
1647
|
}
|
|
1150
1648
|
return { didComplete: false };
|
|
1151
1649
|
}
|
|
1650
|
+
if (!lessonStartTimes.has(lessonId) && isDevEnvironment()) {
|
|
1651
|
+
console.warn(
|
|
1652
|
+
`[lessonkit] completeLesson("${lessonId}") called without activating the lesson first`
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1152
1655
|
completedLessonIds = new Set(completedLessonIds).add(lessonId);
|
|
1153
1656
|
if (activeLessonId === lessonId) {
|
|
1154
1657
|
activeLessonId = void 0;
|
|
@@ -1169,7 +1672,6 @@ function createProgressController() {
|
|
|
1169
1672
|
// src/session.ts
|
|
1170
1673
|
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
1171
1674
|
var volatileSessionIds = /* @__PURE__ */ new WeakMap();
|
|
1172
|
-
var sharedVolatileSessionId = null;
|
|
1173
1675
|
function isDevEnvironment2() {
|
|
1174
1676
|
const g = globalThis;
|
|
1175
1677
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
@@ -1180,10 +1682,23 @@ function getTabSessionId(storage) {
|
|
|
1180
1682
|
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
1181
1683
|
var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
|
|
1182
1684
|
var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
|
|
1685
|
+
var COURSE_STARTED_XAPI_PREFIX = "lessonkit:course_started_xapi:";
|
|
1686
|
+
function sessionKeySegment(sessionId) {
|
|
1687
|
+
const validated = validateId(sessionId);
|
|
1688
|
+
return validated.ok ? validated.id : encodeURIComponent(sessionId);
|
|
1689
|
+
}
|
|
1183
1690
|
function resolveSessionId(storage, provided) {
|
|
1184
1691
|
if (provided !== void 0) {
|
|
1185
1692
|
const trimmed = provided.trim();
|
|
1186
|
-
if (trimmed.length > 0)
|
|
1693
|
+
if (trimmed.length > 0) {
|
|
1694
|
+
const validated = validateId(trimmed);
|
|
1695
|
+
if (validated.ok) return validated.id;
|
|
1696
|
+
if (isDevEnvironment2()) {
|
|
1697
|
+
console.warn(
|
|
1698
|
+
`[lessonkit] Invalid sessionId "${trimmed}"; falling back to tab or generated id.`
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1187
1702
|
}
|
|
1188
1703
|
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
1189
1704
|
if (existing) return existing;
|
|
@@ -1192,27 +1707,27 @@ function resolveSessionId(storage, provided) {
|
|
|
1192
1707
|
const id = createSessionId();
|
|
1193
1708
|
const persisted = storage.setItem(SESSION_STORAGE_KEY, id);
|
|
1194
1709
|
if (!persisted) {
|
|
1195
|
-
|
|
1196
|
-
sharedVolatileSessionId = id;
|
|
1197
|
-
}
|
|
1198
|
-
volatileSessionIds.set(storage, sharedVolatileSessionId);
|
|
1710
|
+
volatileSessionIds.set(storage, id);
|
|
1199
1711
|
if (isDevEnvironment2()) {
|
|
1200
1712
|
console.warn(
|
|
1201
|
-
"[lessonkit] session id could not be persisted;
|
|
1713
|
+
"[lessonkit] session id could not be persisted; using in-memory id for this storage."
|
|
1202
1714
|
);
|
|
1203
1715
|
}
|
|
1204
|
-
return
|
|
1716
|
+
return id;
|
|
1205
1717
|
}
|
|
1206
1718
|
return id;
|
|
1207
1719
|
}
|
|
1208
1720
|
function courseStartedStorageKey(sessionId, courseId) {
|
|
1209
|
-
return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
1721
|
+
return `${COURSE_STARTED_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
1210
1722
|
}
|
|
1211
1723
|
function courseStartedTrackingStorageKey(sessionId, courseId) {
|
|
1212
|
-
return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
1724
|
+
return `${COURSE_STARTED_TRACKING_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
1213
1725
|
}
|
|
1214
1726
|
function courseStartedPipelineStorageKey(sessionId, courseId) {
|
|
1215
|
-
return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
1727
|
+
return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
1728
|
+
}
|
|
1729
|
+
function courseStartedXapiStorageKey(sessionId, courseId) {
|
|
1730
|
+
return `${COURSE_STARTED_XAPI_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
1216
1731
|
}
|
|
1217
1732
|
function hasCourseStarted(storage, sessionId, courseId) {
|
|
1218
1733
|
if (!courseId) return false;
|
|
@@ -1238,49 +1753,82 @@ function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
|
1238
1753
|
if (!courseId) return false;
|
|
1239
1754
|
return storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
|
|
1240
1755
|
}
|
|
1756
|
+
function hasCourseStartedXapiSent(storage, sessionId, courseId) {
|
|
1757
|
+
if (!courseId) return false;
|
|
1758
|
+
return storage.getItem(courseStartedXapiStorageKey(sessionId, courseId)) === "1";
|
|
1759
|
+
}
|
|
1760
|
+
function markCourseStartedXapiSent(storage, sessionId, courseId) {
|
|
1761
|
+
if (!courseId) return false;
|
|
1762
|
+
return storage.setItem(courseStartedXapiStorageKey(sessionId, courseId), "1");
|
|
1763
|
+
}
|
|
1241
1764
|
function resetSharedVolatileSessionIdForTests() {
|
|
1242
|
-
|
|
1765
|
+
}
|
|
1766
|
+
function migrateStorageMark(storage, fromKey, toKey, hasMark) {
|
|
1767
|
+
if (!hasMark) return;
|
|
1768
|
+
if (storage.setItem(toKey, "1")) {
|
|
1769
|
+
storage.removeItem?.(fromKey);
|
|
1770
|
+
}
|
|
1243
1771
|
}
|
|
1244
1772
|
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
1245
1773
|
if (!courseId || fromSessionId === toSessionId) return;
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
storage
|
|
1257
|
-
|
|
1774
|
+
migrateStorageMark(
|
|
1775
|
+
storage,
|
|
1776
|
+
courseStartedStorageKey(fromSessionId, courseId),
|
|
1777
|
+
courseStartedStorageKey(toSessionId, courseId),
|
|
1778
|
+
hasCourseStarted(storage, fromSessionId, courseId)
|
|
1779
|
+
);
|
|
1780
|
+
migrateStorageMark(
|
|
1781
|
+
storage,
|
|
1782
|
+
courseStartedTrackingStorageKey(fromSessionId, courseId),
|
|
1783
|
+
courseStartedTrackingStorageKey(toSessionId, courseId),
|
|
1784
|
+
hasCourseStartedEmittedToTracking(storage, fromSessionId, courseId)
|
|
1785
|
+
);
|
|
1786
|
+
migrateStorageMark(
|
|
1787
|
+
storage,
|
|
1788
|
+
courseStartedPipelineStorageKey(fromSessionId, courseId),
|
|
1789
|
+
courseStartedPipelineStorageKey(toSessionId, courseId),
|
|
1790
|
+
hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)
|
|
1791
|
+
);
|
|
1792
|
+
migrateStorageMark(
|
|
1793
|
+
storage,
|
|
1794
|
+
courseStartedXapiStorageKey(fromSessionId, courseId),
|
|
1795
|
+
courseStartedXapiStorageKey(toSessionId, courseId),
|
|
1796
|
+
hasCourseStartedXapiSent(storage, fromSessionId, courseId)
|
|
1797
|
+
);
|
|
1258
1798
|
}
|
|
1259
1799
|
|
|
1260
1800
|
// src/runtime/courseLifecycle.ts
|
|
1261
|
-
var courseStartedEmitFlights = /* @__PURE__ */ new
|
|
1801
|
+
var courseStartedEmitFlights = /* @__PURE__ */ new Map();
|
|
1262
1802
|
function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
|
|
1263
1803
|
const flightKey = `${ctx.sessionId}:${ctx.courseId}`;
|
|
1264
1804
|
const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
1265
1805
|
if (alreadyEmittedToSink) {
|
|
1266
|
-
return { emitted: true, marked };
|
|
1806
|
+
return Promise.resolve({ emitted: true, marked });
|
|
1267
1807
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1808
|
+
const existing = courseStartedEmitFlights.get(flightKey);
|
|
1809
|
+
if (existing) {
|
|
1810
|
+
return existing;
|
|
1270
1811
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1812
|
+
const flight = Promise.resolve().then(() => {
|
|
1813
|
+
try {
|
|
1814
|
+
const emitted = deps.emitCourseStartedEvent(ctx);
|
|
1815
|
+
if (emitted && !marked) {
|
|
1816
|
+
markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
1817
|
+
}
|
|
1818
|
+
return {
|
|
1819
|
+
emitted,
|
|
1820
|
+
marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
|
|
1821
|
+
};
|
|
1822
|
+
} catch {
|
|
1823
|
+
return { emitted: false, marked };
|
|
1824
|
+
} finally {
|
|
1825
|
+
if (courseStartedEmitFlights.get(flightKey) === flight) {
|
|
1826
|
+
courseStartedEmitFlights.delete(flightKey);
|
|
1827
|
+
}
|
|
1276
1828
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
};
|
|
1281
|
-
} finally {
|
|
1282
|
-
courseStartedEmitFlights.delete(flightKey);
|
|
1283
|
-
}
|
|
1829
|
+
});
|
|
1830
|
+
courseStartedEmitFlights.set(flightKey, flight);
|
|
1831
|
+
return flight;
|
|
1284
1832
|
}
|
|
1285
1833
|
function buildCourseStartedTelemetryEvent(ctx) {
|
|
1286
1834
|
return buildTelemetryEvent({
|
|
@@ -1329,6 +1877,20 @@ function warnDuplicatePlugin(id) {
|
|
|
1329
1877
|
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
|
|
1330
1878
|
console.warn(`[lessonkit] plugin id "${id}" was registered more than once; using the latest definition`);
|
|
1331
1879
|
}
|
|
1880
|
+
function stableUserHash(user) {
|
|
1881
|
+
if (!user) return "";
|
|
1882
|
+
const keys = Object.keys(user).sort();
|
|
1883
|
+
const normalized = {};
|
|
1884
|
+
for (const key of keys) {
|
|
1885
|
+
normalized[key] = user[key];
|
|
1886
|
+
}
|
|
1887
|
+
let h = 0;
|
|
1888
|
+
const serialized = JSON.stringify(normalized);
|
|
1889
|
+
for (let i = 0; i < serialized.length; i++) {
|
|
1890
|
+
h = Math.imul(31, h) + serialized.charCodeAt(i) >>> 0;
|
|
1891
|
+
}
|
|
1892
|
+
return h.toString(36);
|
|
1893
|
+
}
|
|
1332
1894
|
function createPluginRegistry(plugins = []) {
|
|
1333
1895
|
const registry = /* @__PURE__ */ new Map();
|
|
1334
1896
|
for (const plugin of plugins) {
|
|
@@ -1370,7 +1932,7 @@ function createPluginRegistry(plugins = []) {
|
|
|
1370
1932
|
const composeTrackingSink = (sink, ctxSource) => {
|
|
1371
1933
|
if (!sink) return void 0;
|
|
1372
1934
|
const resolveCtx = () => typeof ctxSource === "function" ? ctxSource() : ctxSource;
|
|
1373
|
-
const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}\0${ctx.user
|
|
1935
|
+
const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}\0${stableUserHash(ctx.user)}`;
|
|
1374
1936
|
const layers = [];
|
|
1375
1937
|
let composed = sink;
|
|
1376
1938
|
for (const plugin of list) {
|
|
@@ -1435,6 +1997,7 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1435
1997
|
let courseId = configSnapshot.courseId;
|
|
1436
1998
|
let progress = createProgressController();
|
|
1437
1999
|
let pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
2000
|
+
let disposed = false;
|
|
1438
2001
|
const getPluginCtx = () => buildPluginContext({
|
|
1439
2002
|
courseId,
|
|
1440
2003
|
sessionId,
|
|
@@ -1468,28 +2031,24 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1468
2031
|
if (!event) return null;
|
|
1469
2032
|
return applyPluginsToEvent(event);
|
|
1470
2033
|
};
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
if (event === null) return;
|
|
1475
|
-
const eventLessonId = "lessonId" in event ? event.lessonId : lessonId;
|
|
1476
|
-
const eventData = "data" in event ? event.data : data;
|
|
1477
|
-
emitFn(event.name, eventData, eventLessonId);
|
|
1478
|
-
};
|
|
2034
|
+
const emitLifecycleEvent = (emitFn, name, data, lessonId) => {
|
|
2035
|
+
const event = buildAndApply(name, data, lessonId);
|
|
2036
|
+
if (event) emitFn(event);
|
|
1479
2037
|
};
|
|
1480
2038
|
syncSessionFromConfig(configSnapshot);
|
|
1481
2039
|
const track = (name, data, emit, lessonId) => {
|
|
2040
|
+
if (disposed) return;
|
|
1482
2041
|
const event = buildAndApply(name, data, lessonId);
|
|
1483
2042
|
if (!event) return;
|
|
1484
2043
|
emit(event);
|
|
1485
2044
|
};
|
|
1486
2045
|
const emitLessonCompletedEvents = (lessonId, durationMs, emitFn) => {
|
|
1487
|
-
|
|
1488
|
-
wrapped("lesson_completed", { lessonId, durationMs }, lessonId);
|
|
2046
|
+
emitLifecycleEvent(emitFn, "lesson_completed", { lessonId, durationMs }, lessonId);
|
|
1489
2047
|
if (durationMs !== void 0) {
|
|
1490
|
-
|
|
2048
|
+
emitLifecycleEvent(emitFn, "lesson_time_on_task", { lessonId, durationMs }, lessonId);
|
|
1491
2049
|
}
|
|
1492
2050
|
};
|
|
2051
|
+
const autoCompleteOnLessonSwitch = () => configSnapshot.autoCompleteOnLessonSwitch ?? true;
|
|
1493
2052
|
return {
|
|
1494
2053
|
get config() {
|
|
1495
2054
|
return configSnapshot;
|
|
@@ -1502,7 +2061,12 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1502
2061
|
},
|
|
1503
2062
|
getProgressState: () => progress.getState(),
|
|
1504
2063
|
getSession,
|
|
2064
|
+
migrateSessionMarks(fromSessionId, toSessionId) {
|
|
2065
|
+
if (disposed) return;
|
|
2066
|
+
migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId);
|
|
2067
|
+
},
|
|
1505
2068
|
updateConfig(next) {
|
|
2069
|
+
if (disposed) return;
|
|
1506
2070
|
const previousCourseId = courseId;
|
|
1507
2071
|
const sessionKeyBefore = JSON.stringify({ sessionId, attemptId, user });
|
|
1508
2072
|
if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
|
|
@@ -1510,6 +2074,9 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1510
2074
|
if (next.runtimeVersion === "v1") warnRuntimeV1Deprecated();
|
|
1511
2075
|
configSnapshot.runtimeVersion = next.runtimeVersion;
|
|
1512
2076
|
}
|
|
2077
|
+
if (next.autoCompleteOnLessonSwitch !== void 0) {
|
|
2078
|
+
configSnapshot.autoCompleteOnLessonSwitch = next.autoCompleteOnLessonSwitch;
|
|
2079
|
+
}
|
|
1513
2080
|
if (next.session !== void 0) {
|
|
1514
2081
|
configSnapshot.session = { ...configSnapshot.session, ...next.session };
|
|
1515
2082
|
}
|
|
@@ -1518,61 +2085,75 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1518
2085
|
if (next.courseId !== void 0 && next.courseId !== previousCourseId) {
|
|
1519
2086
|
progress = createProgressController();
|
|
1520
2087
|
}
|
|
1521
|
-
if (next.plugins !== void 0 && next.plugins !==
|
|
2088
|
+
if (next.plugins !== void 0 && next.plugins !== configSnapshot.plugins) {
|
|
1522
2089
|
pluginHost?.disposeAll();
|
|
1523
2090
|
configSnapshot.plugins = next.plugins;
|
|
1524
2091
|
pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
1525
|
-
|
|
2092
|
+
if (!configSnapshot.deferPluginSetup) {
|
|
2093
|
+
pluginHost?.setupAll(getPluginCtx());
|
|
2094
|
+
}
|
|
1526
2095
|
} else if (next.session !== void 0 && sessionKeyBefore !== sessionKeyAfter && pluginHost && !configSnapshot.deferPluginSetup) {
|
|
1527
2096
|
pluginHost.disposeAll();
|
|
1528
2097
|
pluginHost.setupAll(getPluginCtx());
|
|
1529
2098
|
}
|
|
1530
2099
|
},
|
|
1531
2100
|
setActiveLesson(lessonId, emitFn) {
|
|
1532
|
-
|
|
2101
|
+
if (disposed) return;
|
|
1533
2102
|
const current = progress.getState();
|
|
1534
2103
|
if (current.activeLessonId === lessonId) return;
|
|
1535
|
-
if (current.completedLessonIds.has(lessonId)) {
|
|
1536
|
-
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
1537
|
-
return;
|
|
1538
|
-
}
|
|
1539
2104
|
const previous = current.activeLessonId;
|
|
1540
|
-
if (previous && previous !== lessonId) {
|
|
2105
|
+
if (autoCompleteOnLessonSwitch() && previous && previous !== lessonId && !current.completedLessonIds.has(previous)) {
|
|
1541
2106
|
const completed = progress.completeLesson(previous, clock.nowMs());
|
|
1542
2107
|
if (completed.didComplete) {
|
|
1543
|
-
emitLessonCompletedEvents(previous, completed.durationMs,
|
|
2108
|
+
emitLessonCompletedEvents(previous, completed.durationMs, emitFn);
|
|
1544
2109
|
}
|
|
1545
2110
|
}
|
|
2111
|
+
if (current.completedLessonIds.has(lessonId)) {
|
|
2112
|
+
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
1546
2115
|
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
1547
|
-
|
|
2116
|
+
emitLifecycleEvent(emitFn, "lesson_started", { lessonId }, lessonId);
|
|
1548
2117
|
},
|
|
1549
2118
|
completeLesson(lessonId, emitFn) {
|
|
2119
|
+
if (disposed) return;
|
|
1550
2120
|
completeLessonWithTelemetry({
|
|
1551
2121
|
progress,
|
|
1552
2122
|
lessonId,
|
|
1553
2123
|
nowMs: clock.nowMs(),
|
|
1554
|
-
emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs,
|
|
2124
|
+
emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, emitFn)
|
|
1555
2125
|
});
|
|
1556
2126
|
},
|
|
1557
2127
|
completeCourse(emitFn) {
|
|
2128
|
+
if (disposed) return;
|
|
1558
2129
|
completeCourseWithTelemetry({
|
|
1559
2130
|
progress,
|
|
1560
2131
|
nowMs: clock.nowMs(),
|
|
1561
|
-
emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs,
|
|
1562
|
-
emitCourseCompleted: () =>
|
|
2132
|
+
emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, emitFn),
|
|
2133
|
+
emitCourseCompleted: () => emitLifecycleEvent(emitFn, "course_completed")
|
|
1563
2134
|
});
|
|
1564
2135
|
},
|
|
1565
2136
|
track,
|
|
1566
|
-
scoreAssessment(input,
|
|
1567
|
-
if (!pluginHost) return null;
|
|
1568
|
-
return pluginHost.scoreAssessment(
|
|
2137
|
+
scoreAssessment(input, lessonId) {
|
|
2138
|
+
if (disposed || !pluginHost) return null;
|
|
2139
|
+
return pluginHost.scoreAssessment(
|
|
2140
|
+
{ ...input, lessonId: input.lessonId ?? lessonId },
|
|
2141
|
+
getPluginCtx()
|
|
2142
|
+
);
|
|
1569
2143
|
},
|
|
1570
2144
|
resetForCourseChange(nextCourseId) {
|
|
2145
|
+
if (disposed) return;
|
|
1571
2146
|
configSnapshot.courseId = nextCourseId;
|
|
1572
2147
|
courseId = nextCourseId;
|
|
1573
2148
|
progress = createProgressController();
|
|
2149
|
+
pluginHost?.disposeAll();
|
|
2150
|
+
if (!configSnapshot.deferPluginSetup) {
|
|
2151
|
+
pluginHost?.setupAll(getPluginCtx());
|
|
2152
|
+
}
|
|
1574
2153
|
},
|
|
1575
2154
|
dispose() {
|
|
2155
|
+
if (disposed) return;
|
|
2156
|
+
disposed = true;
|
|
1576
2157
|
pluginHost?.disposeAll();
|
|
1577
2158
|
}
|
|
1578
2159
|
};
|
|
@@ -1592,11 +2173,15 @@ function defineLifecyclePlugin(plugin) {
|
|
|
1592
2173
|
0 && (module.exports = {
|
|
1593
2174
|
ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
1594
2175
|
ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
|
|
2176
|
+
BLOCKS_14_PAGE_SLIDE,
|
|
2177
|
+
BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES,
|
|
2178
|
+
BRANCH_NODE_ALLOWED_CHILD_TYPES,
|
|
1595
2179
|
COMPOUND_MAX_NESTING_DEPTH,
|
|
1596
2180
|
COMPOUND_RESUME_SCHEMA_VERSION,
|
|
1597
2181
|
ID_MAX_LENGTH,
|
|
1598
2182
|
ID_PATTERN,
|
|
1599
2183
|
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
2184
|
+
INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
|
|
1600
2185
|
PAGE_ALLOWED_CHILD_TYPES,
|
|
1601
2186
|
SESSION_STORAGE_KEY,
|
|
1602
2187
|
SLIDE_ALLOWED_CHILD_TYPES,
|
|
@@ -1604,6 +2189,7 @@ function defineLifecyclePlugin(plugin) {
|
|
|
1604
2189
|
TELEMETRY_EVENT_CATALOG,
|
|
1605
2190
|
TELEMETRY_EVENT_CATALOG_V2,
|
|
1606
2191
|
TELEMETRY_EVENT_CATALOG_V3,
|
|
2192
|
+
TIMED_CUE_ALLOWED_CHILD_TYPES,
|
|
1607
2193
|
assertNever,
|
|
1608
2194
|
assertValidId,
|
|
1609
2195
|
buildCourseStartedTelemetryEvent,
|
|
@@ -1639,11 +2225,14 @@ function defineLifecyclePlugin(plugin) {
|
|
|
1639
2225
|
hasCourseStarted,
|
|
1640
2226
|
hasCourseStartedEmittedToTracking,
|
|
1641
2227
|
hasCourseStartedPipelineDelivered,
|
|
2228
|
+
hasCourseStartedXapiSent,
|
|
1642
2229
|
isChildTypeAllowed,
|
|
2230
|
+
isLifecycleTelemetryEvent,
|
|
1643
2231
|
loadCompoundState,
|
|
1644
2232
|
markCourseStarted,
|
|
1645
2233
|
markCourseStartedEmittedToTracking,
|
|
1646
2234
|
markCourseStartedPipelineDelivered,
|
|
2235
|
+
markCourseStartedXapiSent,
|
|
1647
2236
|
migrateCourseStartedMark,
|
|
1648
2237
|
nowIso,
|
|
1649
2238
|
parseBlockId,
|
|
@@ -1662,5 +2251,6 @@ function defineLifecyclePlugin(plugin) {
|
|
|
1662
2251
|
telemetryCatalogVersion,
|
|
1663
2252
|
tryBuildTelemetryEvent,
|
|
1664
2253
|
tryEmitCourseStarted,
|
|
2254
|
+
validateBranchGraph,
|
|
1665
2255
|
validateId
|
|
1666
2256
|
});
|