@lessonkit/core 1.4.0 → 1.6.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-PEWFPVQ6.js → chunk-NGCHHJSM.js} +357 -50
- package/dist/index.cjs +822 -131
- package/dist/index.d.cts +142 -15
- package/dist/index.d.ts +142 -15
- package/dist/index.js +534 -138
- package/dist/{testing-BhVGckZ5.d.cts → testing-CzgxF1Ru.d.cts} +193 -8
- package/dist/{testing-BhVGckZ5.d.ts → testing-CzgxF1Ru.d.ts} +193 -8
- package/dist/testing.cjs +1 -3
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/package.json +3 -3
- package/telemetry-catalog.v3.json +177 -0
package/dist/index.cjs
CHANGED
|
@@ -23,12 +23,16 @@ __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
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,
|
|
26
28
|
COMPOUND_MAX_NESTING_DEPTH: () => COMPOUND_MAX_NESTING_DEPTH,
|
|
27
29
|
COMPOUND_RESUME_SCHEMA_VERSION: () => COMPOUND_RESUME_SCHEMA_VERSION,
|
|
30
|
+
GAME_MAP_ALLOWED_CHILD_TYPES: () => GAME_MAP_ALLOWED_CHILD_TYPES,
|
|
28
31
|
ID_MAX_LENGTH: () => ID_MAX_LENGTH,
|
|
29
32
|
ID_PATTERN: () => ID_PATTERN,
|
|
30
33
|
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES: () => INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
31
34
|
INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES: () => INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
|
|
35
|
+
MAP_STAGE_ALLOWED_CHILD_TYPES: () => MAP_STAGE_ALLOWED_CHILD_TYPES,
|
|
32
36
|
PAGE_ALLOWED_CHILD_TYPES: () => PAGE_ALLOWED_CHILD_TYPES,
|
|
33
37
|
SESSION_STORAGE_KEY: () => SESSION_STORAGE_KEY,
|
|
34
38
|
SLIDE_ALLOWED_CHILD_TYPES: () => SLIDE_ALLOWED_CHILD_TYPES,
|
|
@@ -72,11 +76,14 @@ __export(index_exports, {
|
|
|
72
76
|
hasCourseStarted: () => hasCourseStarted,
|
|
73
77
|
hasCourseStartedEmittedToTracking: () => hasCourseStartedEmittedToTracking,
|
|
74
78
|
hasCourseStartedPipelineDelivered: () => hasCourseStartedPipelineDelivered,
|
|
79
|
+
hasCourseStartedXapiSent: () => hasCourseStartedXapiSent,
|
|
75
80
|
isChildTypeAllowed: () => isChildTypeAllowed,
|
|
81
|
+
isLifecycleTelemetryEvent: () => isLifecycleTelemetryEvent,
|
|
76
82
|
loadCompoundState: () => loadCompoundState,
|
|
77
83
|
markCourseStarted: () => markCourseStarted,
|
|
78
84
|
markCourseStartedEmittedToTracking: () => markCourseStartedEmittedToTracking,
|
|
79
85
|
markCourseStartedPipelineDelivered: () => markCourseStartedPipelineDelivered,
|
|
86
|
+
markCourseStartedXapiSent: () => markCourseStartedXapiSent,
|
|
80
87
|
migrateCourseStartedMark: () => migrateCourseStartedMark,
|
|
81
88
|
nowIso: () => nowIso,
|
|
82
89
|
parseBlockId: () => parseBlockId,
|
|
@@ -95,6 +102,7 @@ __export(index_exports, {
|
|
|
95
102
|
telemetryCatalogVersion: () => telemetryCatalogVersion,
|
|
96
103
|
tryBuildTelemetryEvent: () => tryBuildTelemetryEvent,
|
|
97
104
|
tryEmitCourseStarted: () => tryEmitCourseStarted,
|
|
105
|
+
validateBranchGraph: () => validateBranchGraph,
|
|
98
106
|
validateId: () => validateId
|
|
99
107
|
});
|
|
100
108
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -172,12 +180,26 @@ function uniqueFallbackId(input, usedIds) {
|
|
|
172
180
|
const hash = shortHash(input);
|
|
173
181
|
for (let n = 0; n < 100; n++) {
|
|
174
182
|
const candidate = (n === 0 ? `id-${hash}` : `id-${hash}-${n}`).slice(0, 64);
|
|
175
|
-
const
|
|
176
|
-
if (
|
|
183
|
+
const validated = validateId(candidate);
|
|
184
|
+
if (validated.ok && !usedIds.has(validated.id)) return validated.id;
|
|
185
|
+
}
|
|
186
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
187
|
+
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
188
|
+
const candidate = `id-${hash}-${randomSuffix}`.slice(0, 64);
|
|
189
|
+
const validated = validateId(candidate);
|
|
190
|
+
if (validated.ok && !usedIds.has(validated.id)) return validated.id;
|
|
177
191
|
}
|
|
178
192
|
const timed = `id-${hash}-${Date.now().toString(36)}`.slice(0, 64);
|
|
179
|
-
const
|
|
180
|
-
|
|
193
|
+
const timedValidated = validateId(timed);
|
|
194
|
+
if (timedValidated.ok && !usedIds.has(timedValidated.id)) return timedValidated.id;
|
|
195
|
+
const cryptoApi = globalThis.crypto;
|
|
196
|
+
for (let attempt = 0; attempt < 1e3; attempt++) {
|
|
197
|
+
const suffix = typeof cryptoApi?.randomUUID === "function" ? cryptoApi.randomUUID().replace(/-/g, "").slice(0, 12) : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
198
|
+
const candidate = `id-${hash}-${suffix}`.slice(0, 64);
|
|
199
|
+
const validated = validateId(candidate);
|
|
200
|
+
if (validated.ok && !usedIds.has(validated.id)) return validated.id;
|
|
201
|
+
}
|
|
202
|
+
throw new Error(`[lessonkit] unable to derive unique id for input: ${input.slice(0, 32)}`);
|
|
181
203
|
}
|
|
182
204
|
function slugifyId(input) {
|
|
183
205
|
const slug = input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-").slice(0, 64);
|
|
@@ -219,6 +241,13 @@ function buildLessonkitUrn(parts) {
|
|
|
219
241
|
}
|
|
220
242
|
urn += `:block:${blockId}`;
|
|
221
243
|
}
|
|
244
|
+
if (parts.nodeId !== void 0) {
|
|
245
|
+
const nodeId = assertValidId(parts.nodeId, "nodeId");
|
|
246
|
+
if (parts.blockId === void 0) {
|
|
247
|
+
throw new Error("buildLessonkitUrn: nodeId requires blockId");
|
|
248
|
+
}
|
|
249
|
+
urn += `:node:${nodeId}`;
|
|
250
|
+
}
|
|
222
251
|
return urn;
|
|
223
252
|
}
|
|
224
253
|
|
|
@@ -263,23 +292,31 @@ function isPlainSerializableChildState(value) {
|
|
|
263
292
|
(entry) => isValidChildResumeValue(entry)
|
|
264
293
|
);
|
|
265
294
|
}
|
|
266
|
-
function parseCompoundResumeState(raw) {
|
|
295
|
+
function parseCompoundResumeState(raw, opts) {
|
|
267
296
|
if (!raw || typeof raw !== "object") return null;
|
|
268
297
|
const obj = raw;
|
|
269
298
|
if (obj.schemaVersion !== COMPOUND_RESUME_SCHEMA_VERSION) return null;
|
|
270
299
|
if (typeof obj.activePageIndex !== "number" || !Number.isFinite(obj.activePageIndex)) return null;
|
|
271
300
|
const childStates = {};
|
|
301
|
+
const droppedChildKeys = [];
|
|
272
302
|
if (obj.childStates && typeof obj.childStates === "object" && !Array.isArray(obj.childStates)) {
|
|
273
303
|
for (const [key, value] of Object.entries(obj.childStates)) {
|
|
274
304
|
if (isPlainSerializableChildState(value)) {
|
|
275
305
|
childStates[key] = value;
|
|
306
|
+
} else {
|
|
307
|
+
droppedChildKeys.push(key);
|
|
276
308
|
}
|
|
277
309
|
}
|
|
278
310
|
}
|
|
311
|
+
if (droppedChildKeys.length > 0) {
|
|
312
|
+
opts?.onDroppedChildKeys?.(droppedChildKeys);
|
|
313
|
+
}
|
|
279
314
|
const activeChapterIndex = typeof obj.activeChapterIndex === "number" && Number.isFinite(obj.activeChapterIndex) ? obj.activeChapterIndex : void 0;
|
|
315
|
+
const rawPageIndex = Math.max(0, Math.floor(obj.activePageIndex));
|
|
316
|
+
const activePageIndex = typeof opts?.pageCount === "number" && opts.pageCount > 0 ? clampCompoundPageIndex(rawPageIndex, opts.pageCount) : rawPageIndex;
|
|
280
317
|
return {
|
|
281
318
|
schemaVersion: COMPOUND_RESUME_SCHEMA_VERSION,
|
|
282
|
-
activePageIndex
|
|
319
|
+
activePageIndex,
|
|
283
320
|
...activeChapterIndex !== void 0 ? { activeChapterIndex: Math.max(0, Math.floor(activeChapterIndex)) } : {},
|
|
284
321
|
childStates
|
|
285
322
|
};
|
|
@@ -300,17 +337,21 @@ var COMPOUND_STATE_PREFIX = "lessonkit:compound:";
|
|
|
300
337
|
function compoundStateStorageKey(courseId, compoundId) {
|
|
301
338
|
return `${COMPOUND_STATE_PREFIX}${courseId}:${compoundId}`;
|
|
302
339
|
}
|
|
303
|
-
function loadCompoundState(storage, courseId, compoundId) {
|
|
340
|
+
function loadCompoundState(storage, courseId, compoundId, opts) {
|
|
304
341
|
const key = compoundStateStorageKey(courseId, compoundId);
|
|
305
342
|
const raw = storage.getItem(key);
|
|
306
343
|
if (!raw) return null;
|
|
307
344
|
try {
|
|
308
|
-
const parsed = parseCompoundResumeState(JSON.parse(raw));
|
|
309
|
-
if (parsed === null
|
|
310
|
-
|
|
345
|
+
const parsed = parseCompoundResumeState(JSON.parse(raw), opts);
|
|
346
|
+
if (parsed === null) {
|
|
347
|
+
opts?.onCorrupt?.();
|
|
348
|
+
if (isDevEnvironment()) {
|
|
349
|
+
console.warn(`[lessonkit] Ignoring corrupt compound resume state at ${key}`);
|
|
350
|
+
}
|
|
311
351
|
}
|
|
312
352
|
return parsed;
|
|
313
353
|
} catch {
|
|
354
|
+
opts?.onCorrupt?.();
|
|
314
355
|
if (isDevEnvironment()) {
|
|
315
356
|
console.warn(`[lessonkit] Ignoring corrupt compound resume state at ${key}`);
|
|
316
357
|
}
|
|
@@ -367,8 +408,110 @@ var PAGE_ALLOWED_CHILD_TYPES = [
|
|
|
367
408
|
"FindHotspot",
|
|
368
409
|
"FindMultipleHotspots",
|
|
369
410
|
"ImageSlider",
|
|
411
|
+
"Embed",
|
|
412
|
+
"Chart",
|
|
413
|
+
"Table",
|
|
414
|
+
"ImageJuxtaposition",
|
|
415
|
+
"Timeline",
|
|
416
|
+
"ImageSequence",
|
|
417
|
+
"Collage",
|
|
418
|
+
"AudioRecorder",
|
|
419
|
+
"CombinationLock",
|
|
420
|
+
"QrContent",
|
|
421
|
+
"Crossword",
|
|
422
|
+
"AdventCalendar",
|
|
370
423
|
"ProgressTracker"
|
|
371
424
|
];
|
|
425
|
+
var BRANCH_NODE_ALLOWED_CHILD_TYPES = [
|
|
426
|
+
"Text",
|
|
427
|
+
"Heading",
|
|
428
|
+
"Image",
|
|
429
|
+
"Video",
|
|
430
|
+
"Scenario",
|
|
431
|
+
"Reflection",
|
|
432
|
+
"Quiz",
|
|
433
|
+
"KnowledgeCheck",
|
|
434
|
+
"TrueFalse",
|
|
435
|
+
"FillInTheBlanks",
|
|
436
|
+
"DragAndDrop",
|
|
437
|
+
"DragTheWords",
|
|
438
|
+
"MarkTheWords",
|
|
439
|
+
"Summary",
|
|
440
|
+
"ImagePairing",
|
|
441
|
+
"ImageSequencing",
|
|
442
|
+
"MemoryGame",
|
|
443
|
+
"InformationWall",
|
|
444
|
+
"ParallaxSlideshow",
|
|
445
|
+
"Questionnaire",
|
|
446
|
+
"Essay",
|
|
447
|
+
"ArithmeticQuiz",
|
|
448
|
+
"Accordion",
|
|
449
|
+
"DialogCards",
|
|
450
|
+
"Flashcards",
|
|
451
|
+
"ImageHotspots",
|
|
452
|
+
"FindHotspot",
|
|
453
|
+
"FindMultipleHotspots",
|
|
454
|
+
"ImageSlider",
|
|
455
|
+
"Embed",
|
|
456
|
+
"Chart",
|
|
457
|
+
"Table",
|
|
458
|
+
"ImageJuxtaposition",
|
|
459
|
+
"Timeline",
|
|
460
|
+
"ImageSequence",
|
|
461
|
+
"Collage",
|
|
462
|
+
"AudioRecorder",
|
|
463
|
+
"CombinationLock",
|
|
464
|
+
"QrContent",
|
|
465
|
+
"Crossword",
|
|
466
|
+
"AdventCalendar",
|
|
467
|
+
"BranchChoice"
|
|
468
|
+
];
|
|
469
|
+
var BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES = ["BranchNode"];
|
|
470
|
+
var GAME_MAP_ALLOWED_CHILD_TYPES = ["MapStage"];
|
|
471
|
+
var MAP_STAGE_ALLOWED_CHILD_TYPES = [
|
|
472
|
+
"Text",
|
|
473
|
+
"Heading",
|
|
474
|
+
"Image",
|
|
475
|
+
"Video",
|
|
476
|
+
"Scenario",
|
|
477
|
+
"Reflection",
|
|
478
|
+
"Quiz",
|
|
479
|
+
"KnowledgeCheck",
|
|
480
|
+
"TrueFalse",
|
|
481
|
+
"FillInTheBlanks",
|
|
482
|
+
"DragAndDrop",
|
|
483
|
+
"DragTheWords",
|
|
484
|
+
"MarkTheWords",
|
|
485
|
+
"Summary",
|
|
486
|
+
"ImagePairing",
|
|
487
|
+
"ImageSequencing",
|
|
488
|
+
"MemoryGame",
|
|
489
|
+
"InformationWall",
|
|
490
|
+
"ParallaxSlideshow",
|
|
491
|
+
"Questionnaire",
|
|
492
|
+
"Essay",
|
|
493
|
+
"ArithmeticQuiz",
|
|
494
|
+
"Accordion",
|
|
495
|
+
"DialogCards",
|
|
496
|
+
"Flashcards",
|
|
497
|
+
"ImageHotspots",
|
|
498
|
+
"FindHotspot",
|
|
499
|
+
"FindMultipleHotspots",
|
|
500
|
+
"ImageSlider",
|
|
501
|
+
"Embed",
|
|
502
|
+
"Chart",
|
|
503
|
+
"Table",
|
|
504
|
+
"ImageJuxtaposition",
|
|
505
|
+
"Timeline",
|
|
506
|
+
"ImageSequence",
|
|
507
|
+
"Collage",
|
|
508
|
+
"AudioRecorder",
|
|
509
|
+
"CombinationLock",
|
|
510
|
+
"QrContent",
|
|
511
|
+
"Crossword",
|
|
512
|
+
"AdventCalendar",
|
|
513
|
+
"MapExit"
|
|
514
|
+
];
|
|
372
515
|
var INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES = ["Page"];
|
|
373
516
|
var SLIDE_ALLOWED_CHILD_TYPES = [
|
|
374
517
|
"Text",
|
|
@@ -399,7 +542,19 @@ var SLIDE_ALLOWED_CHILD_TYPES = [
|
|
|
399
542
|
"ImageHotspots",
|
|
400
543
|
"FindHotspot",
|
|
401
544
|
"FindMultipleHotspots",
|
|
402
|
-
"ImageSlider"
|
|
545
|
+
"ImageSlider",
|
|
546
|
+
"Embed",
|
|
547
|
+
"Chart",
|
|
548
|
+
"Table",
|
|
549
|
+
"ImageJuxtaposition",
|
|
550
|
+
"Timeline",
|
|
551
|
+
"ImageSequence",
|
|
552
|
+
"Collage",
|
|
553
|
+
"AudioRecorder",
|
|
554
|
+
"CombinationLock",
|
|
555
|
+
"QrContent",
|
|
556
|
+
"Crossword",
|
|
557
|
+
"AdventCalendar"
|
|
403
558
|
];
|
|
404
559
|
var SLIDE_DECK_ALLOWED_CHILD_TYPES = ["Slide"];
|
|
405
560
|
var TIMED_CUE_ALLOWED_CHILD_TYPES = [
|
|
@@ -441,7 +596,11 @@ var ALLOWLISTS = {
|
|
|
441
596
|
SlideDeck: SLIDE_DECK_ALLOWED_CHILD_TYPES,
|
|
442
597
|
TimedCue: TIMED_CUE_ALLOWED_CHILD_TYPES,
|
|
443
598
|
InteractiveVideo: INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
|
|
444
|
-
AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES
|
|
599
|
+
AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
|
|
600
|
+
BranchingScenario: BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES,
|
|
601
|
+
BranchNode: BRANCH_NODE_ALLOWED_CHILD_TYPES,
|
|
602
|
+
GameMap: GAME_MAP_ALLOWED_CHILD_TYPES,
|
|
603
|
+
MapStage: MAP_STAGE_ALLOWED_CHILD_TYPES
|
|
445
604
|
};
|
|
446
605
|
var COMPOUND_MAX_NESTING_DEPTH = {
|
|
447
606
|
Page: 1,
|
|
@@ -450,7 +609,11 @@ var COMPOUND_MAX_NESTING_DEPTH = {
|
|
|
450
609
|
SlideDeck: 2,
|
|
451
610
|
TimedCue: 1,
|
|
452
611
|
InteractiveVideo: 2,
|
|
453
|
-
AssessmentSequence: 1
|
|
612
|
+
AssessmentSequence: 1,
|
|
613
|
+
BranchingScenario: 2,
|
|
614
|
+
BranchNode: 1,
|
|
615
|
+
GameMap: 2,
|
|
616
|
+
MapStage: 1
|
|
454
617
|
};
|
|
455
618
|
function getAllowedChildTypes(parent) {
|
|
456
619
|
return ALLOWLISTS[parent];
|
|
@@ -461,6 +624,82 @@ function isChildTypeAllowed(parent, childType) {
|
|
|
461
624
|
var ACCORDION_FORBIDDEN_CHILD_TYPES = ["Accordion"];
|
|
462
625
|
var BLOCKS_14_PAGE_SLIDE = PAGE_AND_SLIDE_14_BLOCKS;
|
|
463
626
|
|
|
627
|
+
// src/branchGraph.ts
|
|
628
|
+
function validateBranchGraph(startNodeId, nodes) {
|
|
629
|
+
const issues = [];
|
|
630
|
+
if (nodes.length === 0) {
|
|
631
|
+
issues.push({ code: "empty_graph", message: "Branch graph has no nodes" });
|
|
632
|
+
return { ok: false, issues, reachableNodeIds: [] };
|
|
633
|
+
}
|
|
634
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
635
|
+
for (const node of nodes) {
|
|
636
|
+
if (nodeIds.has(node.nodeId)) {
|
|
637
|
+
issues.push({
|
|
638
|
+
code: "duplicate_node_id",
|
|
639
|
+
message: `Duplicate nodeId "${node.nodeId}"`,
|
|
640
|
+
nodeId: node.nodeId
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
nodeIds.add(node.nodeId);
|
|
644
|
+
}
|
|
645
|
+
if (!nodeIds.has(startNodeId)) {
|
|
646
|
+
issues.push({
|
|
647
|
+
code: "start_not_found",
|
|
648
|
+
message: `startNodeId "${startNodeId}" does not match any BranchNode`,
|
|
649
|
+
nodeId: startNodeId
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
if (nodes.length > 1 && nodeIds.has(startNodeId)) {
|
|
653
|
+
const startNode = nodes.find((n) => n.nodeId === startNodeId);
|
|
654
|
+
if (startNode && startNode.choices.length === 0) {
|
|
655
|
+
issues.push({
|
|
656
|
+
code: "start_no_choices",
|
|
657
|
+
message: `startNodeId "${startNodeId}" has no BranchChoice children in a multi-node scenario`,
|
|
658
|
+
nodeId: startNodeId
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
for (const node of nodes) {
|
|
663
|
+
for (const choice of node.choices) {
|
|
664
|
+
if (!nodeIds.has(choice.targetNodeId)) {
|
|
665
|
+
issues.push({
|
|
666
|
+
code: "unknown_target",
|
|
667
|
+
message: `Choice from "${node.nodeId}" references unknown target "${choice.targetNodeId}"`,
|
|
668
|
+
nodeId: node.nodeId
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
674
|
+
if (nodeIds.has(startNodeId)) {
|
|
675
|
+
const queue = [startNodeId];
|
|
676
|
+
while (queue.length > 0) {
|
|
677
|
+
const current = queue.shift();
|
|
678
|
+
if (reachable.has(current)) continue;
|
|
679
|
+
reachable.add(current);
|
|
680
|
+
const node = nodes.find((n) => n.nodeId === current);
|
|
681
|
+
if (!node) continue;
|
|
682
|
+
for (const choice of node.choices) {
|
|
683
|
+
if (!reachable.has(choice.targetNodeId)) queue.push(choice.targetNodeId);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
for (const nodeId of nodeIds) {
|
|
688
|
+
if (!reachable.has(nodeId)) {
|
|
689
|
+
issues.push({
|
|
690
|
+
code: "unreachable_node",
|
|
691
|
+
message: `Node "${nodeId}" is not reachable from startNodeId "${startNodeId}"`,
|
|
692
|
+
nodeId
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return {
|
|
697
|
+
ok: issues.length === 0,
|
|
698
|
+
issues,
|
|
699
|
+
reachableNodeIds: [...reachable]
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
464
703
|
// src/telemetryCatalog.ts
|
|
465
704
|
var telemetryCatalogVersion = 1;
|
|
466
705
|
var TELEMETRY_EVENT_CATALOG = [
|
|
@@ -663,6 +902,94 @@ var TELEMETRY_EVENT_CATALOG_V3 = [
|
|
|
663
902
|
dataFields: ["blockId", "fieldCount"],
|
|
664
903
|
xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
|
|
665
904
|
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
name: "branch_node_viewed",
|
|
908
|
+
description: "Learner viewed a node in a BranchingScenario",
|
|
909
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
910
|
+
dataFields: ["blockId", "nodeId", "nodeIndex", "nodeTitle"],
|
|
911
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
912
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}:node:{nodeId}"
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
name: "branch_selected",
|
|
916
|
+
description: "Learner selected a branch choice in a BranchingScenario",
|
|
917
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
918
|
+
dataFields: ["blockId", "fromNodeId", "toNodeId", "label", "scoreWeight"],
|
|
919
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
|
|
920
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}:node:{toNodeId}"
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
name: "image_juxtaposition_changed",
|
|
924
|
+
description: "Learner adjusted the before/after divider",
|
|
925
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
926
|
+
dataFields: ["blockId", "position"],
|
|
927
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/interacted",
|
|
928
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
name: "timeline_event_viewed",
|
|
932
|
+
description: "Learner focused a timeline event",
|
|
933
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
934
|
+
dataFields: ["blockId", "eventId"],
|
|
935
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
936
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
name: "image_sequence_changed",
|
|
940
|
+
description: "Learner changed the image sequence frame",
|
|
941
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
942
|
+
dataFields: ["blockId", "frameIndex"],
|
|
943
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/interacted",
|
|
944
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
name: "audio_recording_started",
|
|
948
|
+
description: "Learner started an audio recording",
|
|
949
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
950
|
+
dataFields: ["blockId"],
|
|
951
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/interacted",
|
|
952
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
name: "audio_recording_completed",
|
|
956
|
+
description: "Learner completed an audio recording",
|
|
957
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
958
|
+
dataFields: ["blockId"],
|
|
959
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
|
|
960
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
name: "qr_content_revealed",
|
|
964
|
+
description: "Learner revealed QR hidden content",
|
|
965
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
966
|
+
dataFields: ["blockId"],
|
|
967
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
968
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
name: "advent_door_opened",
|
|
972
|
+
description: "Learner opened an advent calendar door",
|
|
973
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
974
|
+
dataFields: ["blockId", "doorId", "day"],
|
|
975
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/interacted",
|
|
976
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
name: "map_stage_viewed",
|
|
980
|
+
description: "Learner viewed a stage in a GameMap",
|
|
981
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
982
|
+
dataFields: ["blockId", "stageId", "stageIndex", "stageLabel"],
|
|
983
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
984
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}:stage:{stageId}"
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
name: "map_exit_selected",
|
|
988
|
+
description: "Learner selected a map exit in a GameMap",
|
|
989
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
990
|
+
dataFields: ["blockId", "fromStageId", "toStageId", "label", "scoreWeight"],
|
|
991
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
|
|
992
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}:stage:{toStageId}"
|
|
666
993
|
}
|
|
667
994
|
];
|
|
668
995
|
function buildTelemetryCatalogV3() {
|
|
@@ -694,22 +1021,12 @@ function invokeTrackingSink(sink, event) {
|
|
|
694
1021
|
void result.catch((err) => warnDev("[lessonkit] tracking sink failed:", err));
|
|
695
1022
|
}
|
|
696
1023
|
}
|
|
697
|
-
function invokePipelineSink(sinkId, emit) {
|
|
698
|
-
let result;
|
|
699
|
-
try {
|
|
700
|
-
result = emit();
|
|
701
|
-
} catch (err) {
|
|
702
|
-
warnDev(`[lessonkit] telemetry sink "${sinkId}" failed:`, err);
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
if (result != null && typeof result.catch === "function") {
|
|
706
|
-
void result.catch(
|
|
707
|
-
(err) => warnDev(`[lessonkit] telemetry sink "${sinkId}" failed:`, err)
|
|
708
|
-
);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
1024
|
|
|
712
1025
|
// src/trackingClient.ts
|
|
1026
|
+
function eventDedupKey(event) {
|
|
1027
|
+
const id = event.id?.trim();
|
|
1028
|
+
return id || void 0;
|
|
1029
|
+
}
|
|
713
1030
|
function createTrackingClient(opts) {
|
|
714
1031
|
const sink = opts?.sink;
|
|
715
1032
|
const batchSink = opts?.batchSink;
|
|
@@ -727,13 +1044,14 @@ function createTrackingClient(opts) {
|
|
|
727
1044
|
let disposed2 = false;
|
|
728
1045
|
return {
|
|
729
1046
|
track: (event) => {
|
|
730
|
-
if (disposed2) return;
|
|
1047
|
+
if (disposed2) return false;
|
|
731
1048
|
if (sink) {
|
|
732
1049
|
try {
|
|
733
1050
|
invokeTrackingSink(sink, event);
|
|
734
1051
|
} catch {
|
|
735
1052
|
}
|
|
736
1053
|
}
|
|
1054
|
+
return true;
|
|
737
1055
|
},
|
|
738
1056
|
deliver: async (event) => {
|
|
739
1057
|
if (disposed2) return false;
|
|
@@ -746,19 +1064,32 @@ function createTrackingClient(opts) {
|
|
|
746
1064
|
};
|
|
747
1065
|
}
|
|
748
1066
|
if (!sink && !batchSink) {
|
|
749
|
-
return { track: () =>
|
|
750
|
-
} };
|
|
1067
|
+
return { track: () => true };
|
|
751
1068
|
}
|
|
752
1069
|
const buffer = [];
|
|
1070
|
+
const pendingDeliverIds = /* @__PURE__ */ new Set();
|
|
753
1071
|
let flushInFlight = null;
|
|
754
|
-
let inflightExitBatch = null;
|
|
755
1072
|
let disposed = false;
|
|
756
1073
|
let disposing = false;
|
|
757
1074
|
let intervalId;
|
|
1075
|
+
const clearPendingDeliverIds = (events) => {
|
|
1076
|
+
for (const event of events) {
|
|
1077
|
+
const key = eventDedupKey(event);
|
|
1078
|
+
if (key) pendingDeliverIds.delete(key);
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
const isEventBuffered = (event) => {
|
|
1082
|
+
const key = eventDedupKey(event);
|
|
1083
|
+
if (!key) return false;
|
|
1084
|
+
return buffer.some((buffered) => eventDedupKey(buffered) === key);
|
|
1085
|
+
};
|
|
758
1086
|
const runFlush = () => {
|
|
759
1087
|
if (!buffer.length) return Promise.resolve(true);
|
|
760
1088
|
const events = buffer.splice(0, buffer.length);
|
|
761
|
-
|
|
1089
|
+
for (const event of events) {
|
|
1090
|
+
const key = eventDedupKey(event);
|
|
1091
|
+
if (key) pendingDeliverIds.add(key);
|
|
1092
|
+
}
|
|
762
1093
|
let succeeded = false;
|
|
763
1094
|
return Promise.resolve().then(async () => {
|
|
764
1095
|
if (batchSink) {
|
|
@@ -768,7 +1099,7 @@ function createTrackingClient(opts) {
|
|
|
768
1099
|
try {
|
|
769
1100
|
await sink?.(events[i]);
|
|
770
1101
|
} catch {
|
|
771
|
-
buffer.unshift(...events
|
|
1102
|
+
buffer.unshift(...events);
|
|
772
1103
|
return;
|
|
773
1104
|
}
|
|
774
1105
|
}
|
|
@@ -779,12 +1110,13 @@ function createTrackingClient(opts) {
|
|
|
779
1110
|
buffer.unshift(...events);
|
|
780
1111
|
}
|
|
781
1112
|
}).then(async () => {
|
|
1113
|
+
if (succeeded) {
|
|
1114
|
+
clearPendingDeliverIds(events);
|
|
1115
|
+
}
|
|
782
1116
|
if (succeeded && buffer.length > 0 && !disposed) {
|
|
783
1117
|
return runFlush();
|
|
784
1118
|
}
|
|
785
1119
|
return succeeded;
|
|
786
|
-
}).finally(() => {
|
|
787
|
-
inflightExitBatch = null;
|
|
788
1120
|
});
|
|
789
1121
|
};
|
|
790
1122
|
const flush = () => {
|
|
@@ -805,18 +1137,27 @@ function createTrackingClient(opts) {
|
|
|
805
1137
|
if (!delivered) break;
|
|
806
1138
|
}
|
|
807
1139
|
if (buffer.length > 0) {
|
|
1140
|
+
const droppedCount = buffer.length;
|
|
808
1141
|
if (isDevEnvironment()) {
|
|
809
1142
|
console.warn(
|
|
810
|
-
`[lessonkit] dropped ${
|
|
1143
|
+
`[lessonkit] dropped ${droppedCount} buffered telemetry event(s) after dispose flush cap`
|
|
811
1144
|
);
|
|
812
1145
|
}
|
|
1146
|
+
for (let i = 0; i < droppedCount; i++) {
|
|
1147
|
+
opts?.onBufferDrop?.();
|
|
1148
|
+
}
|
|
813
1149
|
buffer.length = 0;
|
|
1150
|
+
pendingDeliverIds.clear();
|
|
814
1151
|
}
|
|
815
1152
|
};
|
|
816
1153
|
intervalId = flushIntervalMs > 0 ? globalThis.setInterval(() => void flush(), flushIntervalMs) : void 0;
|
|
817
1154
|
intervalId?.unref?.();
|
|
818
1155
|
const track = (event) => {
|
|
819
|
-
if (disposed || disposing) return;
|
|
1156
|
+
if (disposed || disposing) return false;
|
|
1157
|
+
const key = eventDedupKey(event);
|
|
1158
|
+
if (key && (pendingDeliverIds.has(key) || isEventBuffered(event))) {
|
|
1159
|
+
return true;
|
|
1160
|
+
}
|
|
820
1161
|
if (buffer.length >= maxBufferSize) {
|
|
821
1162
|
opts?.onBufferDrop?.();
|
|
822
1163
|
if (!warnedBufferCap && isDevEnvironment()) {
|
|
@@ -825,29 +1166,37 @@ function createTrackingClient(opts) {
|
|
|
825
1166
|
`[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; new events are dropped until the buffer drains.`
|
|
826
1167
|
);
|
|
827
1168
|
}
|
|
828
|
-
return;
|
|
1169
|
+
return false;
|
|
829
1170
|
}
|
|
830
1171
|
buffer.push(event);
|
|
831
1172
|
if (buffer.length >= maxBatchSize) void flush();
|
|
1173
|
+
return true;
|
|
832
1174
|
};
|
|
833
1175
|
return {
|
|
834
1176
|
track,
|
|
835
1177
|
deliver: async (event) => {
|
|
836
|
-
|
|
1178
|
+
const key = eventDedupKey(event);
|
|
1179
|
+
if (key && (pendingDeliverIds.has(key) || isEventBuffered(event))) {
|
|
1180
|
+
return flush();
|
|
1181
|
+
}
|
|
1182
|
+
if (!track(event)) return false;
|
|
1183
|
+
if (key) pendingDeliverIds.add(key);
|
|
837
1184
|
return flush();
|
|
838
1185
|
},
|
|
839
1186
|
flush,
|
|
840
1187
|
flushOnExit: opts?.exitBatchSink ? () => {
|
|
841
|
-
const
|
|
842
|
-
const fromInflight = inflightExitBatch ? [...inflightExitBatch] : [];
|
|
843
|
-
const events = [...fromInflight, ...fromBuffer];
|
|
1188
|
+
const events = buffer.splice(0, buffer.length);
|
|
844
1189
|
if (!events.length) return;
|
|
845
1190
|
try {
|
|
846
1191
|
const result = opts.exitBatchSink(events);
|
|
847
1192
|
if (result != null && typeof result.catch === "function") {
|
|
848
|
-
void result.
|
|
1193
|
+
void result.then(() => {
|
|
1194
|
+
clearPendingDeliverIds(events);
|
|
1195
|
+
}).catch(() => {
|
|
849
1196
|
buffer.unshift(...events);
|
|
850
1197
|
});
|
|
1198
|
+
} else {
|
|
1199
|
+
clearPendingDeliverIds(events);
|
|
851
1200
|
}
|
|
852
1201
|
} catch {
|
|
853
1202
|
buffer.unshift(...events);
|
|
@@ -869,10 +1218,24 @@ function createTrackingClient(opts) {
|
|
|
869
1218
|
}
|
|
870
1219
|
|
|
871
1220
|
// src/ids.ts
|
|
1221
|
+
function randomSessionIdFallback() {
|
|
1222
|
+
const g = globalThis;
|
|
1223
|
+
if (g.crypto?.getRandomValues) {
|
|
1224
|
+
const bytes = new Uint8Array(16);
|
|
1225
|
+
g.crypto.getRandomValues(bytes);
|
|
1226
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1227
|
+
return `s-${hex}`;
|
|
1228
|
+
}
|
|
1229
|
+
throw new Error(
|
|
1230
|
+
"[lessonkit] createSessionId requires crypto.randomUUID or crypto.getRandomValues"
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
872
1233
|
function createSessionId() {
|
|
873
1234
|
const g = globalThis;
|
|
874
|
-
if (g.crypto?.randomUUID)
|
|
875
|
-
|
|
1235
|
+
if (g.crypto?.randomUUID) {
|
|
1236
|
+
return `s-${g.crypto.randomUUID().replace(/-/g, "")}`;
|
|
1237
|
+
}
|
|
1238
|
+
return randomSessionIdFallback();
|
|
876
1239
|
}
|
|
877
1240
|
|
|
878
1241
|
// src/time.ts
|
|
@@ -1151,6 +1514,118 @@ var TELEMETRY_EVENT_REGISTRY = {
|
|
|
1151
1514
|
data: opts.data
|
|
1152
1515
|
};
|
|
1153
1516
|
}
|
|
1517
|
+
},
|
|
1518
|
+
branch_node_viewed: {
|
|
1519
|
+
requiresLessonId: true,
|
|
1520
|
+
build: (opts, base) => {
|
|
1521
|
+
if (opts.name !== "branch_node_viewed") throw new Error("unexpected event");
|
|
1522
|
+
const lessonId = opts.lessonId;
|
|
1523
|
+
if (!lessonId) throw new Error("branch_node_viewed requires active lessonId");
|
|
1524
|
+
return {
|
|
1525
|
+
name: "branch_node_viewed",
|
|
1526
|
+
...base,
|
|
1527
|
+
lessonId,
|
|
1528
|
+
data: opts.data
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
},
|
|
1532
|
+
branch_selected: {
|
|
1533
|
+
requiresLessonId: true,
|
|
1534
|
+
build: (opts, base) => {
|
|
1535
|
+
if (opts.name !== "branch_selected") throw new Error("unexpected event");
|
|
1536
|
+
const lessonId = opts.lessonId;
|
|
1537
|
+
if (!lessonId) throw new Error("branch_selected requires active lessonId");
|
|
1538
|
+
return {
|
|
1539
|
+
name: "branch_selected",
|
|
1540
|
+
...base,
|
|
1541
|
+
lessonId,
|
|
1542
|
+
data: opts.data
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
},
|
|
1546
|
+
image_juxtaposition_changed: {
|
|
1547
|
+
build: (opts, base) => ({
|
|
1548
|
+
name: "image_juxtaposition_changed",
|
|
1549
|
+
...base,
|
|
1550
|
+
lessonId: opts.lessonId,
|
|
1551
|
+
data: opts.data
|
|
1552
|
+
})
|
|
1553
|
+
},
|
|
1554
|
+
timeline_event_viewed: {
|
|
1555
|
+
build: (opts, base) => ({
|
|
1556
|
+
name: "timeline_event_viewed",
|
|
1557
|
+
...base,
|
|
1558
|
+
lessonId: opts.lessonId,
|
|
1559
|
+
data: opts.data
|
|
1560
|
+
})
|
|
1561
|
+
},
|
|
1562
|
+
image_sequence_changed: {
|
|
1563
|
+
build: (opts, base) => ({
|
|
1564
|
+
name: "image_sequence_changed",
|
|
1565
|
+
...base,
|
|
1566
|
+
lessonId: opts.lessonId,
|
|
1567
|
+
data: opts.data
|
|
1568
|
+
})
|
|
1569
|
+
},
|
|
1570
|
+
audio_recording_started: {
|
|
1571
|
+
build: (opts, base) => ({
|
|
1572
|
+
name: "audio_recording_started",
|
|
1573
|
+
...base,
|
|
1574
|
+
lessonId: opts.lessonId,
|
|
1575
|
+
data: opts.data
|
|
1576
|
+
})
|
|
1577
|
+
},
|
|
1578
|
+
audio_recording_completed: {
|
|
1579
|
+
build: (opts, base) => ({
|
|
1580
|
+
name: "audio_recording_completed",
|
|
1581
|
+
...base,
|
|
1582
|
+
lessonId: opts.lessonId,
|
|
1583
|
+
data: opts.data
|
|
1584
|
+
})
|
|
1585
|
+
},
|
|
1586
|
+
qr_content_revealed: {
|
|
1587
|
+
build: (opts, base) => ({
|
|
1588
|
+
name: "qr_content_revealed",
|
|
1589
|
+
...base,
|
|
1590
|
+
lessonId: opts.lessonId,
|
|
1591
|
+
data: opts.data
|
|
1592
|
+
})
|
|
1593
|
+
},
|
|
1594
|
+
advent_door_opened: {
|
|
1595
|
+
build: (opts, base) => ({
|
|
1596
|
+
name: "advent_door_opened",
|
|
1597
|
+
...base,
|
|
1598
|
+
lessonId: opts.lessonId,
|
|
1599
|
+
data: opts.data
|
|
1600
|
+
})
|
|
1601
|
+
},
|
|
1602
|
+
map_stage_viewed: {
|
|
1603
|
+
requiresLessonId: true,
|
|
1604
|
+
build: (opts, base) => {
|
|
1605
|
+
if (opts.name !== "map_stage_viewed") throw new Error("unexpected event");
|
|
1606
|
+
const lessonId = opts.lessonId;
|
|
1607
|
+
if (!lessonId) throw new Error("map_stage_viewed requires active lessonId");
|
|
1608
|
+
return {
|
|
1609
|
+
name: "map_stage_viewed",
|
|
1610
|
+
...base,
|
|
1611
|
+
lessonId,
|
|
1612
|
+
data: opts.data
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
},
|
|
1616
|
+
map_exit_selected: {
|
|
1617
|
+
requiresLessonId: true,
|
|
1618
|
+
build: (opts, base) => {
|
|
1619
|
+
if (opts.name !== "map_exit_selected") throw new Error("unexpected event");
|
|
1620
|
+
const lessonId = opts.lessonId;
|
|
1621
|
+
if (!lessonId) throw new Error("map_exit_selected requires active lessonId");
|
|
1622
|
+
return {
|
|
1623
|
+
name: "map_exit_selected",
|
|
1624
|
+
...base,
|
|
1625
|
+
lessonId,
|
|
1626
|
+
data: opts.data
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1154
1629
|
}
|
|
1155
1630
|
};
|
|
1156
1631
|
function buildTelemetryEventFromRegistry(opts) {
|
|
@@ -1183,8 +1658,8 @@ function buildTelemetryEvent(opts) {
|
|
|
1183
1658
|
}
|
|
1184
1659
|
function tryBuildTelemetryEvent(opts) {
|
|
1185
1660
|
const entry = getTelemetryEventRegistryEntry(opts.name);
|
|
1186
|
-
if (entry.requiresLessonId && !opts.lessonId
|
|
1187
|
-
if (isDevEnvironment()) {
|
|
1661
|
+
if (entry.requiresLessonId && !opts.lessonId) {
|
|
1662
|
+
if (isDevEnvironment() && entry.tryBuildMissingLessonWarning) {
|
|
1188
1663
|
if (entry.tryBuildMissingLessonWarning === "quiz" && !warnedMissingQuizLesson) {
|
|
1189
1664
|
warnedMissingQuizLesson = true;
|
|
1190
1665
|
console.warn(
|
|
@@ -1204,21 +1679,44 @@ function tryBuildTelemetryEvent(opts) {
|
|
|
1204
1679
|
}
|
|
1205
1680
|
|
|
1206
1681
|
// src/telemetryPipeline.ts
|
|
1207
|
-
|
|
1208
|
-
|
|
1682
|
+
var LIFECYCLE_TELEMETRY_EVENTS = /* @__PURE__ */ new Set([
|
|
1683
|
+
"course_started",
|
|
1684
|
+
"course_completed",
|
|
1685
|
+
"lesson_started",
|
|
1686
|
+
"lesson_completed",
|
|
1687
|
+
"lesson_time_on_task"
|
|
1688
|
+
]);
|
|
1689
|
+
function isLifecycleTelemetryEvent(name) {
|
|
1690
|
+
return LIFECYCLE_TELEMETRY_EVENTS.has(name);
|
|
1691
|
+
}
|
|
1692
|
+
async function invokeSink(sink, event, emitCtx) {
|
|
1693
|
+
let result;
|
|
1694
|
+
try {
|
|
1695
|
+
result = sink.emit(event, emitCtx);
|
|
1696
|
+
} catch (err) {
|
|
1697
|
+
warnDev(`[lessonkit] telemetry sink "${sink.id}" failed:`, err);
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
if (result != null && typeof result.then === "function") {
|
|
1701
|
+
try {
|
|
1702
|
+
await result;
|
|
1703
|
+
} catch (err) {
|
|
1704
|
+
warnDev(`[lessonkit] telemetry sink "${sink.id}" failed:`, err);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1209
1707
|
}
|
|
1210
1708
|
function createTelemetryPipeline(sinks) {
|
|
1211
1709
|
const list = [...sinks];
|
|
1212
1710
|
return {
|
|
1213
1711
|
sinks: list,
|
|
1214
|
-
emit(event, ctx) {
|
|
1712
|
+
async emit(event, ctx) {
|
|
1215
1713
|
const emitCtx = ctx ?? {
|
|
1216
1714
|
courseId: event.courseId,
|
|
1217
1715
|
sessionId: event.sessionId,
|
|
1218
1716
|
attemptId: event.attemptId
|
|
1219
1717
|
};
|
|
1220
1718
|
for (const sink of list) {
|
|
1221
|
-
invokeSink(sink, event, emitCtx);
|
|
1719
|
+
await invokeSink(sink, event, emitCtx);
|
|
1222
1720
|
}
|
|
1223
1721
|
}
|
|
1224
1722
|
};
|
|
@@ -1240,14 +1738,44 @@ function createDefaultClock() {
|
|
|
1240
1738
|
};
|
|
1241
1739
|
}
|
|
1242
1740
|
function createNoopStorage() {
|
|
1741
|
+
const memory = /* @__PURE__ */ new Map();
|
|
1243
1742
|
return {
|
|
1244
|
-
getItem: () => null,
|
|
1245
|
-
setItem: () =>
|
|
1743
|
+
getItem: (key) => memory.get(key) ?? null,
|
|
1744
|
+
setItem: (key, value) => {
|
|
1745
|
+
memory.set(key, value);
|
|
1746
|
+
return true;
|
|
1747
|
+
},
|
|
1748
|
+
removeItem: (key) => {
|
|
1749
|
+
memory.delete(key);
|
|
1750
|
+
},
|
|
1751
|
+
resetForTests: () => {
|
|
1752
|
+
memory.clear();
|
|
1753
|
+
}
|
|
1246
1754
|
};
|
|
1247
1755
|
}
|
|
1248
1756
|
function createMemoryBackedSessionStorage(session) {
|
|
1249
1757
|
const memory = /* @__PURE__ */ new Map();
|
|
1758
|
+
const tombstones = /* @__PURE__ */ new Set();
|
|
1250
1759
|
let warnedPersistFailure = false;
|
|
1760
|
+
const syncFromStorageEvent = (key, newValue) => {
|
|
1761
|
+
if (key === null) {
|
|
1762
|
+
memory.clear();
|
|
1763
|
+
tombstones.clear();
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
tombstones.delete(key);
|
|
1767
|
+
if (newValue === null) {
|
|
1768
|
+
memory.delete(key);
|
|
1769
|
+
} else {
|
|
1770
|
+
memory.set(key, newValue);
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
if (typeof window !== "undefined") {
|
|
1774
|
+
window.addEventListener("storage", (event) => {
|
|
1775
|
+
if (event.storageArea !== sessionStorage) return;
|
|
1776
|
+
syncFromStorageEvent(event.key, event.newValue);
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1251
1779
|
const warnPersistFailure = () => {
|
|
1252
1780
|
if (warnedPersistFailure) return;
|
|
1253
1781
|
warnedPersistFailure = true;
|
|
@@ -1258,24 +1786,31 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
1258
1786
|
);
|
|
1259
1787
|
}
|
|
1260
1788
|
};
|
|
1789
|
+
const bypassCacheForKey = (key) => key === "lessonkit:sessionId" || key.startsWith("lessonkit:course_started");
|
|
1261
1790
|
return {
|
|
1262
1791
|
getItem: (key) => {
|
|
1263
|
-
if (
|
|
1792
|
+
if (tombstones.has(key)) return null;
|
|
1793
|
+
if (!bypassCacheForKey(key) && memory.has(key)) return memory.get(key);
|
|
1264
1794
|
try {
|
|
1265
1795
|
const value = session.getItem(key);
|
|
1266
1796
|
if (value !== null) memory.set(key, value);
|
|
1797
|
+
else if (bypassCacheForKey(key)) memory.delete(key);
|
|
1267
1798
|
return value;
|
|
1268
1799
|
} catch {
|
|
1269
1800
|
return memory.get(key) ?? null;
|
|
1270
1801
|
}
|
|
1271
1802
|
},
|
|
1272
1803
|
setItem: (key, value) => {
|
|
1273
|
-
|
|
1804
|
+
tombstones.delete(key);
|
|
1274
1805
|
try {
|
|
1275
1806
|
session.setItem(key, value);
|
|
1807
|
+
memory.set(key, value);
|
|
1276
1808
|
return true;
|
|
1277
1809
|
} catch {
|
|
1278
1810
|
warnPersistFailure();
|
|
1811
|
+
if (!bypassCacheForKey(key)) {
|
|
1812
|
+
memory.set(key, value);
|
|
1813
|
+
}
|
|
1279
1814
|
return false;
|
|
1280
1815
|
}
|
|
1281
1816
|
},
|
|
@@ -1283,12 +1818,15 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
1283
1818
|
memory.delete(key);
|
|
1284
1819
|
try {
|
|
1285
1820
|
session.removeItem(key);
|
|
1821
|
+
tombstones.delete(key);
|
|
1286
1822
|
} catch {
|
|
1287
1823
|
warnPersistFailure();
|
|
1824
|
+
tombstones.add(key);
|
|
1288
1825
|
}
|
|
1289
1826
|
},
|
|
1290
1827
|
resetForTests: () => {
|
|
1291
1828
|
memory.clear();
|
|
1829
|
+
tombstones.clear();
|
|
1292
1830
|
}
|
|
1293
1831
|
};
|
|
1294
1832
|
}
|
|
@@ -1360,6 +1898,11 @@ function createProgressController() {
|
|
|
1360
1898
|
}
|
|
1361
1899
|
return { didComplete: false };
|
|
1362
1900
|
}
|
|
1901
|
+
if (!lessonStartTimes.has(lessonId) && isDevEnvironment()) {
|
|
1902
|
+
console.warn(
|
|
1903
|
+
`[lessonkit] completeLesson("${lessonId}") called without activating the lesson first`
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1363
1906
|
completedLessonIds = new Set(completedLessonIds).add(lessonId);
|
|
1364
1907
|
if (activeLessonId === lessonId) {
|
|
1365
1908
|
activeLessonId = void 0;
|
|
@@ -1380,7 +1923,6 @@ function createProgressController() {
|
|
|
1380
1923
|
// src/session.ts
|
|
1381
1924
|
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
1382
1925
|
var volatileSessionIds = /* @__PURE__ */ new WeakMap();
|
|
1383
|
-
var sharedVolatileSessionId = null;
|
|
1384
1926
|
function isDevEnvironment2() {
|
|
1385
1927
|
const g = globalThis;
|
|
1386
1928
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
@@ -1391,39 +1933,62 @@ function getTabSessionId(storage) {
|
|
|
1391
1933
|
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
1392
1934
|
var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
|
|
1393
1935
|
var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
|
|
1936
|
+
var COURSE_STARTED_XAPI_PREFIX = "lessonkit:course_started_xapi:";
|
|
1937
|
+
function sessionKeySegment(sessionId) {
|
|
1938
|
+
const validated = validateId(sessionId);
|
|
1939
|
+
return validated.ok ? validated.id : encodeURIComponent(sessionId);
|
|
1940
|
+
}
|
|
1394
1941
|
function resolveSessionId(storage, provided) {
|
|
1395
1942
|
if (provided !== void 0) {
|
|
1396
1943
|
const trimmed = provided.trim();
|
|
1397
|
-
if (trimmed.length > 0)
|
|
1944
|
+
if (trimmed.length > 0) {
|
|
1945
|
+
const validated = validateId(trimmed);
|
|
1946
|
+
if (validated.ok) return validated.id;
|
|
1947
|
+
if (isDevEnvironment2()) {
|
|
1948
|
+
console.warn(
|
|
1949
|
+
`[lessonkit] Invalid sessionId "${trimmed}"; falling back to tab or generated id.`
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1398
1953
|
}
|
|
1399
1954
|
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
1400
|
-
if (existing)
|
|
1955
|
+
if (existing) {
|
|
1956
|
+
const trimmedExisting = existing.trim();
|
|
1957
|
+
const validatedExisting = validateId(trimmedExisting);
|
|
1958
|
+
if (validatedExisting.ok) return validatedExisting.id;
|
|
1959
|
+
storage.removeItem?.(SESSION_STORAGE_KEY);
|
|
1960
|
+
if (isDevEnvironment2()) {
|
|
1961
|
+
console.warn(
|
|
1962
|
+
`[lessonkit] Invalid stored sessionId "${existing}"; generating a new id.`
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1401
1966
|
const volatile = volatileSessionIds.get(storage);
|
|
1402
1967
|
if (volatile) return volatile;
|
|
1403
1968
|
const id = createSessionId();
|
|
1404
1969
|
const persisted = storage.setItem(SESSION_STORAGE_KEY, id);
|
|
1405
1970
|
if (!persisted) {
|
|
1406
|
-
|
|
1407
|
-
sharedVolatileSessionId = id;
|
|
1408
|
-
}
|
|
1409
|
-
volatileSessionIds.set(storage, sharedVolatileSessionId);
|
|
1971
|
+
volatileSessionIds.set(storage, id);
|
|
1410
1972
|
if (isDevEnvironment2()) {
|
|
1411
1973
|
console.warn(
|
|
1412
|
-
"[lessonkit] session id could not be persisted;
|
|
1974
|
+
"[lessonkit] session id could not be persisted; using in-memory id for this storage."
|
|
1413
1975
|
);
|
|
1414
1976
|
}
|
|
1415
|
-
return
|
|
1977
|
+
return id;
|
|
1416
1978
|
}
|
|
1417
1979
|
return id;
|
|
1418
1980
|
}
|
|
1419
1981
|
function courseStartedStorageKey(sessionId, courseId) {
|
|
1420
|
-
return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
1982
|
+
return `${COURSE_STARTED_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
1421
1983
|
}
|
|
1422
1984
|
function courseStartedTrackingStorageKey(sessionId, courseId) {
|
|
1423
|
-
return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
1985
|
+
return `${COURSE_STARTED_TRACKING_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
1424
1986
|
}
|
|
1425
1987
|
function courseStartedPipelineStorageKey(sessionId, courseId) {
|
|
1426
|
-
return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
1988
|
+
return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
1989
|
+
}
|
|
1990
|
+
function courseStartedXapiStorageKey(sessionId, courseId) {
|
|
1991
|
+
return `${COURSE_STARTED_XAPI_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
1427
1992
|
}
|
|
1428
1993
|
function hasCourseStarted(storage, sessionId, courseId) {
|
|
1429
1994
|
if (!courseId) return false;
|
|
@@ -1449,49 +2014,93 @@ function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
|
1449
2014
|
if (!courseId) return false;
|
|
1450
2015
|
return storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
|
|
1451
2016
|
}
|
|
2017
|
+
function hasCourseStartedXapiSent(storage, sessionId, courseId) {
|
|
2018
|
+
if (!courseId) return false;
|
|
2019
|
+
return storage.getItem(courseStartedXapiStorageKey(sessionId, courseId)) === "1";
|
|
2020
|
+
}
|
|
2021
|
+
function markCourseStartedXapiSent(storage, sessionId, courseId) {
|
|
2022
|
+
if (!courseId) return false;
|
|
2023
|
+
return storage.setItem(courseStartedXapiStorageKey(sessionId, courseId), "1");
|
|
2024
|
+
}
|
|
1452
2025
|
function resetSharedVolatileSessionIdForTests() {
|
|
1453
|
-
|
|
2026
|
+
}
|
|
2027
|
+
function migrateStorageMark(storage, fromKey, toKey, hasMark) {
|
|
2028
|
+
if (!hasMark) return;
|
|
2029
|
+
if (storage.setItem(toKey, "1")) {
|
|
2030
|
+
storage.removeItem?.(fromKey);
|
|
2031
|
+
}
|
|
1454
2032
|
}
|
|
1455
2033
|
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
1456
2034
|
if (!courseId || fromSessionId === toSessionId) return;
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
storage
|
|
1468
|
-
|
|
2035
|
+
migrateStorageMark(
|
|
2036
|
+
storage,
|
|
2037
|
+
courseStartedStorageKey(fromSessionId, courseId),
|
|
2038
|
+
courseStartedStorageKey(toSessionId, courseId),
|
|
2039
|
+
hasCourseStarted(storage, fromSessionId, courseId)
|
|
2040
|
+
);
|
|
2041
|
+
migrateStorageMark(
|
|
2042
|
+
storage,
|
|
2043
|
+
courseStartedTrackingStorageKey(fromSessionId, courseId),
|
|
2044
|
+
courseStartedTrackingStorageKey(toSessionId, courseId),
|
|
2045
|
+
hasCourseStartedEmittedToTracking(storage, fromSessionId, courseId)
|
|
2046
|
+
);
|
|
2047
|
+
migrateStorageMark(
|
|
2048
|
+
storage,
|
|
2049
|
+
courseStartedPipelineStorageKey(fromSessionId, courseId),
|
|
2050
|
+
courseStartedPipelineStorageKey(toSessionId, courseId),
|
|
2051
|
+
hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)
|
|
2052
|
+
);
|
|
2053
|
+
migrateStorageMark(
|
|
2054
|
+
storage,
|
|
2055
|
+
courseStartedXapiStorageKey(fromSessionId, courseId),
|
|
2056
|
+
courseStartedXapiStorageKey(toSessionId, courseId),
|
|
2057
|
+
hasCourseStartedXapiSent(storage, fromSessionId, courseId)
|
|
2058
|
+
);
|
|
1469
2059
|
}
|
|
1470
2060
|
|
|
1471
2061
|
// src/runtime/courseLifecycle.ts
|
|
1472
|
-
var courseStartedEmitFlights = /* @__PURE__ */ new
|
|
2062
|
+
var courseStartedEmitFlights = /* @__PURE__ */ new Map();
|
|
1473
2063
|
function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
|
|
1474
2064
|
const flightKey = `${ctx.sessionId}:${ctx.courseId}`;
|
|
1475
2065
|
const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
1476
2066
|
if (alreadyEmittedToSink) {
|
|
1477
|
-
|
|
2067
|
+
const markPersisted = marked ? true : markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
2068
|
+
return Promise.resolve({
|
|
2069
|
+
emitted: true,
|
|
2070
|
+
marked: markPersisted
|
|
2071
|
+
});
|
|
1478
2072
|
}
|
|
1479
|
-
if (
|
|
1480
|
-
return {
|
|
2073
|
+
if (marked && hasCourseStartedEmittedToTracking(ctx.storage, ctx.sessionId, ctx.courseId)) {
|
|
2074
|
+
return Promise.resolve({
|
|
2075
|
+
emitted: true,
|
|
2076
|
+
marked: true
|
|
2077
|
+
});
|
|
1481
2078
|
}
|
|
1482
|
-
courseStartedEmitFlights.
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
if (emitted && !marked) {
|
|
1486
|
-
markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
1487
|
-
}
|
|
1488
|
-
return {
|
|
1489
|
-
emitted,
|
|
1490
|
-
marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
|
|
1491
|
-
};
|
|
1492
|
-
} finally {
|
|
1493
|
-
courseStartedEmitFlights.delete(flightKey);
|
|
2079
|
+
const existing = courseStartedEmitFlights.get(flightKey);
|
|
2080
|
+
if (existing) {
|
|
2081
|
+
return existing;
|
|
1494
2082
|
}
|
|
2083
|
+
const flight = Promise.resolve().then(() => {
|
|
2084
|
+
try {
|
|
2085
|
+
const emitted = deps.emitCourseStartedEvent(ctx);
|
|
2086
|
+
const markPersisted = emitted && !marked ? markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId) : marked;
|
|
2087
|
+
return {
|
|
2088
|
+
emitted,
|
|
2089
|
+
marked: markPersisted
|
|
2090
|
+
};
|
|
2091
|
+
} catch {
|
|
2092
|
+
return {
|
|
2093
|
+
emitted: false,
|
|
2094
|
+
marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
|
|
2095
|
+
};
|
|
2096
|
+
} finally {
|
|
2097
|
+
if (courseStartedEmitFlights.get(flightKey) === flight) {
|
|
2098
|
+
courseStartedEmitFlights.delete(flightKey);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
});
|
|
2102
|
+
courseStartedEmitFlights.set(flightKey, flight);
|
|
2103
|
+
return flight;
|
|
1495
2104
|
}
|
|
1496
2105
|
function buildCourseStartedTelemetryEvent(ctx) {
|
|
1497
2106
|
return buildTelemetryEvent({
|
|
@@ -1519,7 +2128,16 @@ function completeCourseWithTelemetry(opts) {
|
|
|
1519
2128
|
});
|
|
1520
2129
|
}
|
|
1521
2130
|
const result = opts.progress.completeCourse();
|
|
1522
|
-
if (!result.didComplete)
|
|
2131
|
+
if (!result.didComplete) {
|
|
2132
|
+
const after = opts.progress.getState();
|
|
2133
|
+
if (after.activeLessonId) {
|
|
2134
|
+
const lessonResult = opts.progress.completeLesson(after.activeLessonId, opts.nowMs);
|
|
2135
|
+
if (lessonResult.didComplete) {
|
|
2136
|
+
opts.emitLessonCompleted(after.activeLessonId, lessonResult.durationMs);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
return false;
|
|
2140
|
+
}
|
|
1523
2141
|
opts.emitCourseCompleted();
|
|
1524
2142
|
return true;
|
|
1525
2143
|
}
|
|
@@ -1540,6 +2158,20 @@ function warnDuplicatePlugin(id) {
|
|
|
1540
2158
|
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
|
|
1541
2159
|
console.warn(`[lessonkit] plugin id "${id}" was registered more than once; using the latest definition`);
|
|
1542
2160
|
}
|
|
2161
|
+
function stableUserHash(user) {
|
|
2162
|
+
if (!user) return "";
|
|
2163
|
+
const keys = Object.keys(user).sort();
|
|
2164
|
+
const normalized = {};
|
|
2165
|
+
for (const key of keys) {
|
|
2166
|
+
normalized[key] = user[key];
|
|
2167
|
+
}
|
|
2168
|
+
let h = 0;
|
|
2169
|
+
const serialized = JSON.stringify(normalized);
|
|
2170
|
+
for (let i = 0; i < serialized.length; i++) {
|
|
2171
|
+
h = Math.imul(31, h) + serialized.charCodeAt(i) >>> 0;
|
|
2172
|
+
}
|
|
2173
|
+
return h.toString(36);
|
|
2174
|
+
}
|
|
1543
2175
|
function createPluginRegistry(plugins = []) {
|
|
1544
2176
|
const registry = /* @__PURE__ */ new Map();
|
|
1545
2177
|
for (const plugin of plugins) {
|
|
@@ -1581,7 +2213,7 @@ function createPluginRegistry(plugins = []) {
|
|
|
1581
2213
|
const composeTrackingSink = (sink, ctxSource) => {
|
|
1582
2214
|
if (!sink) return void 0;
|
|
1583
2215
|
const resolveCtx = () => typeof ctxSource === "function" ? ctxSource() : ctxSource;
|
|
1584
|
-
const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}\0${ctx.user
|
|
2216
|
+
const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}\0${stableUserHash(ctx.user)}`;
|
|
1585
2217
|
const layers = [];
|
|
1586
2218
|
let composed = sink;
|
|
1587
2219
|
for (const plugin of list) {
|
|
@@ -1628,6 +2260,10 @@ function resolvePluginHost(plugins) {
|
|
|
1628
2260
|
if (Array.isArray(plugins) && plugins.length > 0) return createPluginRegistry(plugins);
|
|
1629
2261
|
return null;
|
|
1630
2262
|
}
|
|
2263
|
+
function pluginListFingerprint(plugins) {
|
|
2264
|
+
if (!plugins || !Array.isArray(plugins)) return null;
|
|
2265
|
+
return plugins.map((p) => `${p.id}\0${p.version ?? ""}`).join("\n");
|
|
2266
|
+
}
|
|
1631
2267
|
function warnRuntimeV1Deprecated() {
|
|
1632
2268
|
const g = globalThis;
|
|
1633
2269
|
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
|
|
@@ -1640,12 +2276,20 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1640
2276
|
const storage = ports.storage ?? createSessionStoragePort();
|
|
1641
2277
|
const clock = ports.clock ?? createDefaultClock();
|
|
1642
2278
|
const configSnapshot = { ...config };
|
|
2279
|
+
const hasExplicitSessionId = Boolean(configSnapshot.session?.sessionId?.trim());
|
|
2280
|
+
let autoSessionId;
|
|
1643
2281
|
let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
|
|
2282
|
+
if (!hasExplicitSessionId) {
|
|
2283
|
+
autoSessionId = sessionId;
|
|
2284
|
+
}
|
|
1644
2285
|
let attemptId = configSnapshot.session?.attemptId;
|
|
1645
2286
|
let user = configSnapshot.session?.user;
|
|
1646
2287
|
let courseId = configSnapshot.courseId;
|
|
2288
|
+
let configuredSessionId = configSnapshot.session?.sessionId;
|
|
1647
2289
|
let progress = createProgressController();
|
|
1648
2290
|
let pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
2291
|
+
let pluginFingerprint = pluginListFingerprint(configSnapshot.plugins);
|
|
2292
|
+
let disposed = false;
|
|
1649
2293
|
const getPluginCtx = () => buildPluginContext({
|
|
1650
2294
|
courseId,
|
|
1651
2295
|
sessionId,
|
|
@@ -1656,12 +2300,6 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1656
2300
|
pluginHost?.setupAll(getPluginCtx());
|
|
1657
2301
|
}
|
|
1658
2302
|
const getSession = () => ({ sessionId, attemptId, user });
|
|
1659
|
-
const syncSessionFromConfig = (next) => {
|
|
1660
|
-
sessionId = resolveSessionId(storage, next.session?.sessionId);
|
|
1661
|
-
attemptId = next.session?.attemptId;
|
|
1662
|
-
user = next.session?.user;
|
|
1663
|
-
courseId = next.courseId;
|
|
1664
|
-
};
|
|
1665
2303
|
const applyPluginsToEvent = (event) => {
|
|
1666
2304
|
if (!pluginHost) return event;
|
|
1667
2305
|
return pluginHost.runTelemetry(event, getPluginCtx());
|
|
@@ -1679,28 +2317,23 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1679
2317
|
if (!event) return null;
|
|
1680
2318
|
return applyPluginsToEvent(event);
|
|
1681
2319
|
};
|
|
1682
|
-
const
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
if (event === null) return;
|
|
1686
|
-
const eventLessonId = "lessonId" in event ? event.lessonId : lessonId;
|
|
1687
|
-
const eventData = "data" in event ? event.data : data;
|
|
1688
|
-
emitFn(event.name, eventData, eventLessonId);
|
|
1689
|
-
};
|
|
2320
|
+
const emitLifecycleEvent = (emitFn, name, data, lessonId) => {
|
|
2321
|
+
const event = buildAndApply(name, data, lessonId);
|
|
2322
|
+
if (event) emitFn(event);
|
|
1690
2323
|
};
|
|
1691
|
-
syncSessionFromConfig(configSnapshot);
|
|
1692
2324
|
const track = (name, data, emit, lessonId) => {
|
|
2325
|
+
if (disposed) return;
|
|
1693
2326
|
const event = buildAndApply(name, data, lessonId);
|
|
1694
2327
|
if (!event) return;
|
|
1695
2328
|
emit(event);
|
|
1696
2329
|
};
|
|
1697
2330
|
const emitLessonCompletedEvents = (lessonId, durationMs, emitFn) => {
|
|
1698
|
-
|
|
1699
|
-
wrapped("lesson_completed", { lessonId, durationMs }, lessonId);
|
|
2331
|
+
emitLifecycleEvent(emitFn, "lesson_completed", { lessonId, durationMs }, lessonId);
|
|
1700
2332
|
if (durationMs !== void 0) {
|
|
1701
|
-
|
|
2333
|
+
emitLifecycleEvent(emitFn, "lesson_time_on_task", { lessonId, durationMs }, lessonId);
|
|
1702
2334
|
}
|
|
1703
2335
|
};
|
|
2336
|
+
const autoCompleteOnLessonSwitch = () => configSnapshot.autoCompleteOnLessonSwitch ?? true;
|
|
1704
2337
|
return {
|
|
1705
2338
|
get config() {
|
|
1706
2339
|
return configSnapshot;
|
|
@@ -1713,7 +2346,12 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1713
2346
|
},
|
|
1714
2347
|
getProgressState: () => progress.getState(),
|
|
1715
2348
|
getSession,
|
|
2349
|
+
migrateSessionMarks(fromSessionId, toSessionId) {
|
|
2350
|
+
if (disposed) return;
|
|
2351
|
+
migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId);
|
|
2352
|
+
},
|
|
1716
2353
|
updateConfig(next) {
|
|
2354
|
+
if (disposed) return;
|
|
1717
2355
|
const previousCourseId = courseId;
|
|
1718
2356
|
const sessionKeyBefore = JSON.stringify({ sessionId, attemptId, user });
|
|
1719
2357
|
if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
|
|
@@ -1721,35 +2359,69 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1721
2359
|
if (next.runtimeVersion === "v1") warnRuntimeV1Deprecated();
|
|
1722
2360
|
configSnapshot.runtimeVersion = next.runtimeVersion;
|
|
1723
2361
|
}
|
|
2362
|
+
if (next.autoCompleteOnLessonSwitch !== void 0) {
|
|
2363
|
+
configSnapshot.autoCompleteOnLessonSwitch = next.autoCompleteOnLessonSwitch;
|
|
2364
|
+
}
|
|
2365
|
+
if (next.courseId !== void 0) {
|
|
2366
|
+
courseId = next.courseId;
|
|
2367
|
+
}
|
|
1724
2368
|
if (next.session !== void 0) {
|
|
2369
|
+
const previousSessionId = sessionId;
|
|
1725
2370
|
configSnapshot.session = { ...configSnapshot.session, ...next.session };
|
|
2371
|
+
const explicitSessionId = configSnapshot.session?.sessionId?.trim();
|
|
2372
|
+
if (explicitSessionId) {
|
|
2373
|
+
sessionId = resolveSessionId(storage, explicitSessionId);
|
|
2374
|
+
autoSessionId = void 0;
|
|
2375
|
+
} else {
|
|
2376
|
+
sessionId = autoSessionId ?? resolveSessionId(storage, void 0);
|
|
2377
|
+
if (!autoSessionId) autoSessionId = sessionId;
|
|
2378
|
+
}
|
|
2379
|
+
attemptId = configSnapshot.session?.attemptId;
|
|
2380
|
+
user = configSnapshot.session?.user;
|
|
2381
|
+
if (previousSessionId !== sessionId) {
|
|
2382
|
+
const prevExplicit = configuredSessionId?.trim();
|
|
2383
|
+
const nextExplicit = configSnapshot.session?.sessionId?.trim();
|
|
2384
|
+
const isExplicitLearnerSwap = Boolean(prevExplicit) && Boolean(nextExplicit) && prevExplicit !== nextExplicit;
|
|
2385
|
+
if (!isExplicitLearnerSwap) {
|
|
2386
|
+
migrateCourseStartedMark(storage, previousSessionId, sessionId, courseId);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
configuredSessionId = configSnapshot.session?.sessionId;
|
|
1726
2390
|
}
|
|
1727
|
-
syncSessionFromConfig(configSnapshot);
|
|
1728
2391
|
const sessionKeyAfter = JSON.stringify({ sessionId, attemptId, user });
|
|
1729
2392
|
if (next.courseId !== void 0 && next.courseId !== previousCourseId) {
|
|
1730
2393
|
progress = createProgressController();
|
|
1731
|
-
}
|
|
1732
|
-
if (next.plugins !== void 0 && next.plugins !== configSnapshot.plugins) {
|
|
1733
|
-
pluginHost?.disposeAll();
|
|
1734
|
-
configSnapshot.plugins = next.plugins;
|
|
1735
|
-
pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
1736
2394
|
if (!configSnapshot.deferPluginSetup) {
|
|
2395
|
+
pluginHost?.disposeAll();
|
|
1737
2396
|
pluginHost?.setupAll(getPluginCtx());
|
|
1738
2397
|
}
|
|
2398
|
+
}
|
|
2399
|
+
if (next.plugins !== void 0) {
|
|
2400
|
+
const nextFingerprint = pluginListFingerprint(next.plugins);
|
|
2401
|
+
const pluginsChanged = next.plugins !== configSnapshot.plugins || nextFingerprint !== null && nextFingerprint !== pluginFingerprint;
|
|
2402
|
+
if (pluginsChanged) {
|
|
2403
|
+
pluginHost?.disposeAll();
|
|
2404
|
+
configSnapshot.plugins = next.plugins;
|
|
2405
|
+
pluginFingerprint = nextFingerprint;
|
|
2406
|
+
pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
2407
|
+
if (!configSnapshot.deferPluginSetup) {
|
|
2408
|
+
pluginHost?.setupAll(getPluginCtx());
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
1739
2411
|
} else if (next.session !== void 0 && sessionKeyBefore !== sessionKeyAfter && pluginHost && !configSnapshot.deferPluginSetup) {
|
|
1740
2412
|
pluginHost.disposeAll();
|
|
1741
2413
|
pluginHost.setupAll(getPluginCtx());
|
|
1742
2414
|
}
|
|
1743
2415
|
},
|
|
1744
2416
|
setActiveLesson(lessonId, emitFn) {
|
|
1745
|
-
|
|
2417
|
+
if (disposed) return;
|
|
1746
2418
|
const current = progress.getState();
|
|
1747
2419
|
if (current.activeLessonId === lessonId) return;
|
|
1748
2420
|
const previous = current.activeLessonId;
|
|
1749
|
-
if (previous && previous !== lessonId && !current.completedLessonIds.has(previous)) {
|
|
2421
|
+
if (autoCompleteOnLessonSwitch() && previous && previous !== lessonId && !current.completedLessonIds.has(previous)) {
|
|
1750
2422
|
const completed = progress.completeLesson(previous, clock.nowMs());
|
|
1751
2423
|
if (completed.didComplete) {
|
|
1752
|
-
emitLessonCompletedEvents(previous, completed.durationMs,
|
|
2424
|
+
emitLessonCompletedEvents(previous, completed.durationMs, emitFn);
|
|
1753
2425
|
}
|
|
1754
2426
|
}
|
|
1755
2427
|
if (current.completedLessonIds.has(lessonId)) {
|
|
@@ -1757,39 +2429,50 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1757
2429
|
return;
|
|
1758
2430
|
}
|
|
1759
2431
|
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
1760
|
-
|
|
2432
|
+
emitLifecycleEvent(emitFn, "lesson_started", { lessonId }, lessonId);
|
|
1761
2433
|
},
|
|
1762
2434
|
completeLesson(lessonId, emitFn) {
|
|
2435
|
+
if (disposed) return;
|
|
1763
2436
|
completeLessonWithTelemetry({
|
|
1764
2437
|
progress,
|
|
1765
2438
|
lessonId,
|
|
1766
2439
|
nowMs: clock.nowMs(),
|
|
1767
|
-
emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs,
|
|
2440
|
+
emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, emitFn)
|
|
1768
2441
|
});
|
|
1769
2442
|
},
|
|
1770
2443
|
completeCourse(emitFn) {
|
|
2444
|
+
if (disposed) return;
|
|
1771
2445
|
completeCourseWithTelemetry({
|
|
1772
2446
|
progress,
|
|
1773
2447
|
nowMs: clock.nowMs(),
|
|
1774
|
-
emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs,
|
|
1775
|
-
emitCourseCompleted: () =>
|
|
2448
|
+
emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, emitFn),
|
|
2449
|
+
emitCourseCompleted: () => emitLifecycleEvent(emitFn, "course_completed")
|
|
1776
2450
|
});
|
|
1777
2451
|
},
|
|
1778
2452
|
track,
|
|
1779
2453
|
scoreAssessment(input, lessonId) {
|
|
1780
|
-
if (!pluginHost) return null;
|
|
2454
|
+
if (disposed || !pluginHost) return null;
|
|
1781
2455
|
return pluginHost.scoreAssessment(
|
|
1782
2456
|
{ ...input, lessonId: input.lessonId ?? lessonId },
|
|
1783
2457
|
getPluginCtx()
|
|
1784
2458
|
);
|
|
1785
2459
|
},
|
|
1786
2460
|
resetForCourseChange(nextCourseId) {
|
|
2461
|
+
if (disposed) return;
|
|
1787
2462
|
configSnapshot.courseId = nextCourseId;
|
|
1788
2463
|
courseId = nextCourseId;
|
|
1789
2464
|
progress = createProgressController();
|
|
2465
|
+
if (!configSnapshot.deferPluginSetup) {
|
|
2466
|
+
pluginHost?.disposeAll();
|
|
2467
|
+
pluginHost?.setupAll(getPluginCtx());
|
|
2468
|
+
}
|
|
1790
2469
|
},
|
|
1791
2470
|
dispose() {
|
|
1792
|
-
|
|
2471
|
+
if (disposed) return;
|
|
2472
|
+
disposed = true;
|
|
2473
|
+
if (!configSnapshot.deferPluginSetup) {
|
|
2474
|
+
pluginHost?.disposeAll();
|
|
2475
|
+
}
|
|
1793
2476
|
}
|
|
1794
2477
|
};
|
|
1795
2478
|
}
|
|
@@ -1809,12 +2492,16 @@ function defineLifecyclePlugin(plugin) {
|
|
|
1809
2492
|
ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
1810
2493
|
ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
|
|
1811
2494
|
BLOCKS_14_PAGE_SLIDE,
|
|
2495
|
+
BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES,
|
|
2496
|
+
BRANCH_NODE_ALLOWED_CHILD_TYPES,
|
|
1812
2497
|
COMPOUND_MAX_NESTING_DEPTH,
|
|
1813
2498
|
COMPOUND_RESUME_SCHEMA_VERSION,
|
|
2499
|
+
GAME_MAP_ALLOWED_CHILD_TYPES,
|
|
1814
2500
|
ID_MAX_LENGTH,
|
|
1815
2501
|
ID_PATTERN,
|
|
1816
2502
|
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
1817
2503
|
INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
|
|
2504
|
+
MAP_STAGE_ALLOWED_CHILD_TYPES,
|
|
1818
2505
|
PAGE_ALLOWED_CHILD_TYPES,
|
|
1819
2506
|
SESSION_STORAGE_KEY,
|
|
1820
2507
|
SLIDE_ALLOWED_CHILD_TYPES,
|
|
@@ -1858,11 +2545,14 @@ function defineLifecyclePlugin(plugin) {
|
|
|
1858
2545
|
hasCourseStarted,
|
|
1859
2546
|
hasCourseStartedEmittedToTracking,
|
|
1860
2547
|
hasCourseStartedPipelineDelivered,
|
|
2548
|
+
hasCourseStartedXapiSent,
|
|
1861
2549
|
isChildTypeAllowed,
|
|
2550
|
+
isLifecycleTelemetryEvent,
|
|
1862
2551
|
loadCompoundState,
|
|
1863
2552
|
markCourseStarted,
|
|
1864
2553
|
markCourseStartedEmittedToTracking,
|
|
1865
2554
|
markCourseStartedPipelineDelivered,
|
|
2555
|
+
markCourseStartedXapiSent,
|
|
1866
2556
|
migrateCourseStartedMark,
|
|
1867
2557
|
nowIso,
|
|
1868
2558
|
parseBlockId,
|
|
@@ -1881,5 +2571,6 @@ function defineLifecyclePlugin(plugin) {
|
|
|
1881
2571
|
telemetryCatalogVersion,
|
|
1882
2572
|
tryBuildTelemetryEvent,
|
|
1883
2573
|
tryEmitCourseStarted,
|
|
2574
|
+
validateBranchGraph,
|
|
1884
2575
|
validateId
|
|
1885
2576
|
});
|