@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/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import {
2
+ ID_MAX_LENGTH,
3
+ ID_PATTERN,
2
4
  SESSION_STORAGE_KEY,
5
+ assertValidId,
3
6
  buildCourseStartedTelemetryEvent,
4
7
  buildTelemetryEvent,
5
8
  completeCourseWithTelemetry,
@@ -13,82 +16,33 @@ import {
13
16
  hasCourseStarted,
14
17
  hasCourseStartedEmittedToTracking,
15
18
  hasCourseStartedPipelineDelivered,
19
+ hasCourseStartedXapiSent,
16
20
  isDevEnvironment,
17
21
  markCourseStarted,
18
22
  markCourseStartedEmittedToTracking,
19
23
  markCourseStartedPipelineDelivered,
24
+ markCourseStartedXapiSent,
20
25
  migrateCourseStartedMark,
21
26
  nowIso,
27
+ parseBlockId,
28
+ parseCheckId,
29
+ parseCourseId,
30
+ parseLessonId,
22
31
  resetSharedVolatileSessionIdForTests,
23
32
  resetStoragePortForTests,
24
33
  resetTelemetryBuilderWarningsForTests,
25
34
  resolveSessionId,
26
35
  tryBuildTelemetryEvent,
27
36
  tryEmitCourseStarted,
37
+ validateId,
28
38
  warnDev
29
- } from "./chunk-PEWFPVQ6.js";
30
-
31
- // src/identityTypes.ts
32
- var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
33
- var ID_MAX_LENGTH = 64;
39
+ } from "./chunk-NGCHHJSM.js";
34
40
 
35
41
  // src/assertNever.ts
36
42
  function assertNever(value, message = "Unexpected value") {
37
43
  throw new Error(`${message}: ${String(value)}`);
38
44
  }
39
45
 
40
- // src/validateId.ts
41
- function validateId(input, path = "id") {
42
- if (typeof input !== "string") {
43
- return { ok: false, issues: [{ path, message: "id must be a string" }] };
44
- }
45
- const id = input.trim();
46
- if (!id.length) {
47
- return { ok: false, issues: [{ path, message: "id must not be empty" }] };
48
- }
49
- if (id.length > ID_MAX_LENGTH) {
50
- return {
51
- ok: false,
52
- issues: [{ path, message: `id must be at most ${ID_MAX_LENGTH} characters` }]
53
- };
54
- }
55
- if (!ID_PATTERN.test(id)) {
56
- return {
57
- ok: false,
58
- issues: [
59
- {
60
- path,
61
- message: "id must start with a letter and contain only letters, digits, underscores, and hyphens"
62
- }
63
- ]
64
- };
65
- }
66
- return { ok: true, id };
67
- }
68
- function parseCourseId(input) {
69
- const result = validateId(input, "courseId");
70
- return result.ok ? result.id : null;
71
- }
72
- function parseLessonId(input) {
73
- const result = validateId(input, "lessonId");
74
- return result.ok ? result.id : null;
75
- }
76
- function parseCheckId(input) {
77
- const result = validateId(input, "checkId");
78
- return result.ok ? result.id : null;
79
- }
80
- function parseBlockId(input) {
81
- const result = validateId(input, "blockId");
82
- return result.ok ? result.id : null;
83
- }
84
- function assertValidId(input, path = "id") {
85
- const result = validateId(input, path);
86
- if (!result.ok) {
87
- throw new Error(result.issues.map((i) => `${i.path}: ${i.message}`).join("; "));
88
- }
89
- return result.id;
90
- }
91
-
92
46
  // src/slugify.ts
93
47
  function shortHash(input) {
94
48
  let h = 0;
@@ -101,12 +55,26 @@ function uniqueFallbackId(input, usedIds) {
101
55
  const hash = shortHash(input);
102
56
  for (let n = 0; n < 100; n++) {
103
57
  const candidate = (n === 0 ? `id-${hash}` : `id-${hash}-${n}`).slice(0, 64);
104
- const validated2 = validateId(candidate);
105
- if (validated2.ok && !usedIds.has(validated2.id)) return validated2.id;
58
+ const validated = validateId(candidate);
59
+ if (validated.ok && !usedIds.has(validated.id)) return validated.id;
60
+ }
61
+ for (let attempt = 0; attempt < 100; attempt++) {
62
+ const randomSuffix = Math.random().toString(36).slice(2, 8);
63
+ const candidate = `id-${hash}-${randomSuffix}`.slice(0, 64);
64
+ const validated = validateId(candidate);
65
+ if (validated.ok && !usedIds.has(validated.id)) return validated.id;
106
66
  }
107
67
  const timed = `id-${hash}-${Date.now().toString(36)}`.slice(0, 64);
108
- const validated = validateId(timed);
109
- return validated.ok ? validated.id : `id-${hash}`;
68
+ const timedValidated = validateId(timed);
69
+ if (timedValidated.ok && !usedIds.has(timedValidated.id)) return timedValidated.id;
70
+ const cryptoApi = globalThis.crypto;
71
+ for (let attempt = 0; attempt < 1e3; attempt++) {
72
+ const suffix = typeof cryptoApi?.randomUUID === "function" ? cryptoApi.randomUUID().replace(/-/g, "").slice(0, 12) : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
73
+ const candidate = `id-${hash}-${suffix}`.slice(0, 64);
74
+ const validated = validateId(candidate);
75
+ if (validated.ok && !usedIds.has(validated.id)) return validated.id;
76
+ }
77
+ throw new Error(`[lessonkit] unable to derive unique id for input: ${input.slice(0, 32)}`);
110
78
  }
111
79
  function slugifyId(input) {
112
80
  const slug = input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-").slice(0, 64);
@@ -148,6 +116,13 @@ function buildLessonkitUrn(parts) {
148
116
  }
149
117
  urn += `:block:${blockId}`;
150
118
  }
119
+ if (parts.nodeId !== void 0) {
120
+ const nodeId = assertValidId(parts.nodeId, "nodeId");
121
+ if (parts.blockId === void 0) {
122
+ throw new Error("buildLessonkitUrn: nodeId requires blockId");
123
+ }
124
+ urn += `:node:${nodeId}`;
125
+ }
151
126
  return urn;
152
127
  }
153
128
 
@@ -192,23 +167,31 @@ function isPlainSerializableChildState(value) {
192
167
  (entry) => isValidChildResumeValue(entry)
193
168
  );
194
169
  }
195
- function parseCompoundResumeState(raw) {
170
+ function parseCompoundResumeState(raw, opts) {
196
171
  if (!raw || typeof raw !== "object") return null;
197
172
  const obj = raw;
198
173
  if (obj.schemaVersion !== COMPOUND_RESUME_SCHEMA_VERSION) return null;
199
174
  if (typeof obj.activePageIndex !== "number" || !Number.isFinite(obj.activePageIndex)) return null;
200
175
  const childStates = {};
176
+ const droppedChildKeys = [];
201
177
  if (obj.childStates && typeof obj.childStates === "object" && !Array.isArray(obj.childStates)) {
202
178
  for (const [key, value] of Object.entries(obj.childStates)) {
203
179
  if (isPlainSerializableChildState(value)) {
204
180
  childStates[key] = value;
181
+ } else {
182
+ droppedChildKeys.push(key);
205
183
  }
206
184
  }
207
185
  }
186
+ if (droppedChildKeys.length > 0) {
187
+ opts?.onDroppedChildKeys?.(droppedChildKeys);
188
+ }
208
189
  const activeChapterIndex = typeof obj.activeChapterIndex === "number" && Number.isFinite(obj.activeChapterIndex) ? obj.activeChapterIndex : void 0;
190
+ const rawPageIndex = Math.max(0, Math.floor(obj.activePageIndex));
191
+ const activePageIndex = typeof opts?.pageCount === "number" && opts.pageCount > 0 ? clampCompoundPageIndex(rawPageIndex, opts.pageCount) : rawPageIndex;
209
192
  return {
210
193
  schemaVersion: COMPOUND_RESUME_SCHEMA_VERSION,
211
- activePageIndex: Math.max(0, Math.floor(obj.activePageIndex)),
194
+ activePageIndex,
212
195
  ...activeChapterIndex !== void 0 ? { activeChapterIndex: Math.max(0, Math.floor(activeChapterIndex)) } : {},
213
196
  childStates
214
197
  };
@@ -219,17 +202,21 @@ var COMPOUND_STATE_PREFIX = "lessonkit:compound:";
219
202
  function compoundStateStorageKey(courseId, compoundId) {
220
203
  return `${COMPOUND_STATE_PREFIX}${courseId}:${compoundId}`;
221
204
  }
222
- function loadCompoundState(storage, courseId, compoundId) {
205
+ function loadCompoundState(storage, courseId, compoundId, opts) {
223
206
  const key = compoundStateStorageKey(courseId, compoundId);
224
207
  const raw = storage.getItem(key);
225
208
  if (!raw) return null;
226
209
  try {
227
- const parsed = parseCompoundResumeState(JSON.parse(raw));
228
- if (parsed === null && isDevEnvironment()) {
229
- console.warn(`[lessonkit] Ignoring corrupt compound resume state at ${key}`);
210
+ const parsed = parseCompoundResumeState(JSON.parse(raw), opts);
211
+ if (parsed === null) {
212
+ opts?.onCorrupt?.();
213
+ if (isDevEnvironment()) {
214
+ console.warn(`[lessonkit] Ignoring corrupt compound resume state at ${key}`);
215
+ }
230
216
  }
231
217
  return parsed;
232
218
  } catch {
219
+ opts?.onCorrupt?.();
233
220
  if (isDevEnvironment()) {
234
221
  console.warn(`[lessonkit] Ignoring corrupt compound resume state at ${key}`);
235
222
  }
@@ -286,8 +273,110 @@ var PAGE_ALLOWED_CHILD_TYPES = [
286
273
  "FindHotspot",
287
274
  "FindMultipleHotspots",
288
275
  "ImageSlider",
276
+ "Embed",
277
+ "Chart",
278
+ "Table",
279
+ "ImageJuxtaposition",
280
+ "Timeline",
281
+ "ImageSequence",
282
+ "Collage",
283
+ "AudioRecorder",
284
+ "CombinationLock",
285
+ "QrContent",
286
+ "Crossword",
287
+ "AdventCalendar",
289
288
  "ProgressTracker"
290
289
  ];
290
+ var BRANCH_NODE_ALLOWED_CHILD_TYPES = [
291
+ "Text",
292
+ "Heading",
293
+ "Image",
294
+ "Video",
295
+ "Scenario",
296
+ "Reflection",
297
+ "Quiz",
298
+ "KnowledgeCheck",
299
+ "TrueFalse",
300
+ "FillInTheBlanks",
301
+ "DragAndDrop",
302
+ "DragTheWords",
303
+ "MarkTheWords",
304
+ "Summary",
305
+ "ImagePairing",
306
+ "ImageSequencing",
307
+ "MemoryGame",
308
+ "InformationWall",
309
+ "ParallaxSlideshow",
310
+ "Questionnaire",
311
+ "Essay",
312
+ "ArithmeticQuiz",
313
+ "Accordion",
314
+ "DialogCards",
315
+ "Flashcards",
316
+ "ImageHotspots",
317
+ "FindHotspot",
318
+ "FindMultipleHotspots",
319
+ "ImageSlider",
320
+ "Embed",
321
+ "Chart",
322
+ "Table",
323
+ "ImageJuxtaposition",
324
+ "Timeline",
325
+ "ImageSequence",
326
+ "Collage",
327
+ "AudioRecorder",
328
+ "CombinationLock",
329
+ "QrContent",
330
+ "Crossword",
331
+ "AdventCalendar",
332
+ "BranchChoice"
333
+ ];
334
+ var BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES = ["BranchNode"];
335
+ var GAME_MAP_ALLOWED_CHILD_TYPES = ["MapStage"];
336
+ var MAP_STAGE_ALLOWED_CHILD_TYPES = [
337
+ "Text",
338
+ "Heading",
339
+ "Image",
340
+ "Video",
341
+ "Scenario",
342
+ "Reflection",
343
+ "Quiz",
344
+ "KnowledgeCheck",
345
+ "TrueFalse",
346
+ "FillInTheBlanks",
347
+ "DragAndDrop",
348
+ "DragTheWords",
349
+ "MarkTheWords",
350
+ "Summary",
351
+ "ImagePairing",
352
+ "ImageSequencing",
353
+ "MemoryGame",
354
+ "InformationWall",
355
+ "ParallaxSlideshow",
356
+ "Questionnaire",
357
+ "Essay",
358
+ "ArithmeticQuiz",
359
+ "Accordion",
360
+ "DialogCards",
361
+ "Flashcards",
362
+ "ImageHotspots",
363
+ "FindHotspot",
364
+ "FindMultipleHotspots",
365
+ "ImageSlider",
366
+ "Embed",
367
+ "Chart",
368
+ "Table",
369
+ "ImageJuxtaposition",
370
+ "Timeline",
371
+ "ImageSequence",
372
+ "Collage",
373
+ "AudioRecorder",
374
+ "CombinationLock",
375
+ "QrContent",
376
+ "Crossword",
377
+ "AdventCalendar",
378
+ "MapExit"
379
+ ];
291
380
  var INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES = ["Page"];
292
381
  var SLIDE_ALLOWED_CHILD_TYPES = [
293
382
  "Text",
@@ -318,7 +407,19 @@ var SLIDE_ALLOWED_CHILD_TYPES = [
318
407
  "ImageHotspots",
319
408
  "FindHotspot",
320
409
  "FindMultipleHotspots",
321
- "ImageSlider"
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"
322
423
  ];
323
424
  var SLIDE_DECK_ALLOWED_CHILD_TYPES = ["Slide"];
324
425
  var TIMED_CUE_ALLOWED_CHILD_TYPES = [
@@ -360,7 +461,11 @@ var ALLOWLISTS = {
360
461
  SlideDeck: SLIDE_DECK_ALLOWED_CHILD_TYPES,
361
462
  TimedCue: TIMED_CUE_ALLOWED_CHILD_TYPES,
362
463
  InteractiveVideo: INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
363
- AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES
464
+ AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
465
+ BranchingScenario: BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES,
466
+ BranchNode: BRANCH_NODE_ALLOWED_CHILD_TYPES,
467
+ GameMap: GAME_MAP_ALLOWED_CHILD_TYPES,
468
+ MapStage: MAP_STAGE_ALLOWED_CHILD_TYPES
364
469
  };
365
470
  var COMPOUND_MAX_NESTING_DEPTH = {
366
471
  Page: 1,
@@ -369,7 +474,11 @@ var COMPOUND_MAX_NESTING_DEPTH = {
369
474
  SlideDeck: 2,
370
475
  TimedCue: 1,
371
476
  InteractiveVideo: 2,
372
- AssessmentSequence: 1
477
+ AssessmentSequence: 1,
478
+ BranchingScenario: 2,
479
+ BranchNode: 1,
480
+ GameMap: 2,
481
+ MapStage: 1
373
482
  };
374
483
  function getAllowedChildTypes(parent) {
375
484
  return ALLOWLISTS[parent];
@@ -380,6 +489,82 @@ function isChildTypeAllowed(parent, childType) {
380
489
  var ACCORDION_FORBIDDEN_CHILD_TYPES = ["Accordion"];
381
490
  var BLOCKS_14_PAGE_SLIDE = PAGE_AND_SLIDE_14_BLOCKS;
382
491
 
492
+ // src/branchGraph.ts
493
+ function validateBranchGraph(startNodeId, nodes) {
494
+ const issues = [];
495
+ if (nodes.length === 0) {
496
+ issues.push({ code: "empty_graph", message: "Branch graph has no nodes" });
497
+ return { ok: false, issues, reachableNodeIds: [] };
498
+ }
499
+ const nodeIds = /* @__PURE__ */ new Set();
500
+ for (const node of nodes) {
501
+ if (nodeIds.has(node.nodeId)) {
502
+ issues.push({
503
+ code: "duplicate_node_id",
504
+ message: `Duplicate nodeId "${node.nodeId}"`,
505
+ nodeId: node.nodeId
506
+ });
507
+ }
508
+ nodeIds.add(node.nodeId);
509
+ }
510
+ if (!nodeIds.has(startNodeId)) {
511
+ issues.push({
512
+ code: "start_not_found",
513
+ message: `startNodeId "${startNodeId}" does not match any BranchNode`,
514
+ nodeId: startNodeId
515
+ });
516
+ }
517
+ if (nodes.length > 1 && nodeIds.has(startNodeId)) {
518
+ const startNode = nodes.find((n) => n.nodeId === startNodeId);
519
+ if (startNode && startNode.choices.length === 0) {
520
+ issues.push({
521
+ code: "start_no_choices",
522
+ message: `startNodeId "${startNodeId}" has no BranchChoice children in a multi-node scenario`,
523
+ nodeId: startNodeId
524
+ });
525
+ }
526
+ }
527
+ for (const node of nodes) {
528
+ for (const choice of node.choices) {
529
+ if (!nodeIds.has(choice.targetNodeId)) {
530
+ issues.push({
531
+ code: "unknown_target",
532
+ message: `Choice from "${node.nodeId}" references unknown target "${choice.targetNodeId}"`,
533
+ nodeId: node.nodeId
534
+ });
535
+ }
536
+ }
537
+ }
538
+ const reachable = /* @__PURE__ */ new Set();
539
+ if (nodeIds.has(startNodeId)) {
540
+ const queue = [startNodeId];
541
+ while (queue.length > 0) {
542
+ const current = queue.shift();
543
+ if (reachable.has(current)) continue;
544
+ reachable.add(current);
545
+ const node = nodes.find((n) => n.nodeId === current);
546
+ if (!node) continue;
547
+ for (const choice of node.choices) {
548
+ if (!reachable.has(choice.targetNodeId)) queue.push(choice.targetNodeId);
549
+ }
550
+ }
551
+ }
552
+ for (const nodeId of nodeIds) {
553
+ if (!reachable.has(nodeId)) {
554
+ issues.push({
555
+ code: "unreachable_node",
556
+ message: `Node "${nodeId}" is not reachable from startNodeId "${startNodeId}"`,
557
+ nodeId
558
+ });
559
+ }
560
+ }
561
+ return {
562
+ ok: issues.length === 0,
563
+ issues,
564
+ reachableNodeIds: [...reachable]
565
+ };
566
+ }
567
+
383
568
  // src/telemetryCatalog.ts
384
569
  var telemetryCatalogVersion = 1;
385
570
  var TELEMETRY_EVENT_CATALOG = [
@@ -582,6 +767,94 @@ var TELEMETRY_EVENT_CATALOG_V3 = [
582
767
  dataFields: ["blockId", "fieldCount"],
583
768
  xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
584
769
  urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
770
+ },
771
+ {
772
+ name: "branch_node_viewed",
773
+ description: "Learner viewed a node in a BranchingScenario",
774
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
775
+ dataFields: ["blockId", "nodeId", "nodeIndex", "nodeTitle"],
776
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
777
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}:node:{nodeId}"
778
+ },
779
+ {
780
+ name: "branch_selected",
781
+ description: "Learner selected a branch choice in a BranchingScenario",
782
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
783
+ dataFields: ["blockId", "fromNodeId", "toNodeId", "label", "scoreWeight"],
784
+ xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
785
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}:node:{toNodeId}"
786
+ },
787
+ {
788
+ name: "image_juxtaposition_changed",
789
+ description: "Learner adjusted the before/after divider",
790
+ requiredFields: ["courseId", "sessionId", "timestamp"],
791
+ dataFields: ["blockId", "position"],
792
+ xapiVerb: "http://adlnet.gov/expapi/verbs/interacted",
793
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
794
+ },
795
+ {
796
+ name: "timeline_event_viewed",
797
+ description: "Learner focused a timeline event",
798
+ requiredFields: ["courseId", "sessionId", "timestamp"],
799
+ dataFields: ["blockId", "eventId"],
800
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
801
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
802
+ },
803
+ {
804
+ name: "image_sequence_changed",
805
+ description: "Learner changed the image sequence frame",
806
+ requiredFields: ["courseId", "sessionId", "timestamp"],
807
+ dataFields: ["blockId", "frameIndex"],
808
+ xapiVerb: "http://adlnet.gov/expapi/verbs/interacted",
809
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
810
+ },
811
+ {
812
+ name: "audio_recording_started",
813
+ description: "Learner started an audio recording",
814
+ requiredFields: ["courseId", "sessionId", "timestamp"],
815
+ dataFields: ["blockId"],
816
+ xapiVerb: "http://adlnet.gov/expapi/verbs/interacted",
817
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
818
+ },
819
+ {
820
+ name: "audio_recording_completed",
821
+ description: "Learner completed an audio recording",
822
+ requiredFields: ["courseId", "sessionId", "timestamp"],
823
+ dataFields: ["blockId"],
824
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
825
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
826
+ },
827
+ {
828
+ name: "qr_content_revealed",
829
+ description: "Learner revealed QR hidden content",
830
+ requiredFields: ["courseId", "sessionId", "timestamp"],
831
+ dataFields: ["blockId"],
832
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
833
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
834
+ },
835
+ {
836
+ name: "advent_door_opened",
837
+ description: "Learner opened an advent calendar door",
838
+ requiredFields: ["courseId", "sessionId", "timestamp"],
839
+ dataFields: ["blockId", "doorId", "day"],
840
+ xapiVerb: "http://adlnet.gov/expapi/verbs/interacted",
841
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
842
+ },
843
+ {
844
+ name: "map_stage_viewed",
845
+ description: "Learner viewed a stage in a GameMap",
846
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
847
+ dataFields: ["blockId", "stageId", "stageIndex", "stageLabel"],
848
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
849
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}:stage:{stageId}"
850
+ },
851
+ {
852
+ name: "map_exit_selected",
853
+ description: "Learner selected a map exit in a GameMap",
854
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
855
+ dataFields: ["blockId", "fromStageId", "toStageId", "label", "scoreWeight"],
856
+ xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
857
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}:stage:{toStageId}"
585
858
  }
586
859
  ];
587
860
  function buildTelemetryCatalogV3() {
@@ -613,22 +886,12 @@ function invokeTrackingSink(sink, event) {
613
886
  void result.catch((err) => warnDev("[lessonkit] tracking sink failed:", err));
614
887
  }
615
888
  }
616
- function invokePipelineSink(sinkId, emit) {
617
- let result;
618
- try {
619
- result = emit();
620
- } catch (err) {
621
- warnDev(`[lessonkit] telemetry sink "${sinkId}" failed:`, err);
622
- return;
623
- }
624
- if (result != null && typeof result.catch === "function") {
625
- void result.catch(
626
- (err) => warnDev(`[lessonkit] telemetry sink "${sinkId}" failed:`, err)
627
- );
628
- }
629
- }
630
889
 
631
890
  // src/trackingClient.ts
891
+ function eventDedupKey(event) {
892
+ const id = event.id?.trim();
893
+ return id || void 0;
894
+ }
632
895
  function createTrackingClient(opts) {
633
896
  const sink = opts?.sink;
634
897
  const batchSink = opts?.batchSink;
@@ -646,13 +909,14 @@ function createTrackingClient(opts) {
646
909
  let disposed2 = false;
647
910
  return {
648
911
  track: (event) => {
649
- if (disposed2) return;
912
+ if (disposed2) return false;
650
913
  if (sink) {
651
914
  try {
652
915
  invokeTrackingSink(sink, event);
653
916
  } catch {
654
917
  }
655
918
  }
919
+ return true;
656
920
  },
657
921
  deliver: async (event) => {
658
922
  if (disposed2) return false;
@@ -665,19 +929,32 @@ function createTrackingClient(opts) {
665
929
  };
666
930
  }
667
931
  if (!sink && !batchSink) {
668
- return { track: () => {
669
- } };
932
+ return { track: () => true };
670
933
  }
671
934
  const buffer = [];
935
+ const pendingDeliverIds = /* @__PURE__ */ new Set();
672
936
  let flushInFlight = null;
673
- let inflightExitBatch = null;
674
937
  let disposed = false;
675
938
  let disposing = false;
676
939
  let intervalId;
940
+ const clearPendingDeliverIds = (events) => {
941
+ for (const event of events) {
942
+ const key = eventDedupKey(event);
943
+ if (key) pendingDeliverIds.delete(key);
944
+ }
945
+ };
946
+ const isEventBuffered = (event) => {
947
+ const key = eventDedupKey(event);
948
+ if (!key) return false;
949
+ return buffer.some((buffered) => eventDedupKey(buffered) === key);
950
+ };
677
951
  const runFlush = () => {
678
952
  if (!buffer.length) return Promise.resolve(true);
679
953
  const events = buffer.splice(0, buffer.length);
680
- inflightExitBatch = events;
954
+ for (const event of events) {
955
+ const key = eventDedupKey(event);
956
+ if (key) pendingDeliverIds.add(key);
957
+ }
681
958
  let succeeded = false;
682
959
  return Promise.resolve().then(async () => {
683
960
  if (batchSink) {
@@ -687,7 +964,7 @@ function createTrackingClient(opts) {
687
964
  try {
688
965
  await sink?.(events[i]);
689
966
  } catch {
690
- buffer.unshift(...events.slice(i));
967
+ buffer.unshift(...events);
691
968
  return;
692
969
  }
693
970
  }
@@ -698,12 +975,13 @@ function createTrackingClient(opts) {
698
975
  buffer.unshift(...events);
699
976
  }
700
977
  }).then(async () => {
978
+ if (succeeded) {
979
+ clearPendingDeliverIds(events);
980
+ }
701
981
  if (succeeded && buffer.length > 0 && !disposed) {
702
982
  return runFlush();
703
983
  }
704
984
  return succeeded;
705
- }).finally(() => {
706
- inflightExitBatch = null;
707
985
  });
708
986
  };
709
987
  const flush = () => {
@@ -724,18 +1002,27 @@ function createTrackingClient(opts) {
724
1002
  if (!delivered) break;
725
1003
  }
726
1004
  if (buffer.length > 0) {
1005
+ const droppedCount = buffer.length;
727
1006
  if (isDevEnvironment()) {
728
1007
  console.warn(
729
- `[lessonkit] dropped ${buffer.length} buffered telemetry event(s) after dispose flush cap`
1008
+ `[lessonkit] dropped ${droppedCount} buffered telemetry event(s) after dispose flush cap`
730
1009
  );
731
1010
  }
1011
+ for (let i = 0; i < droppedCount; i++) {
1012
+ opts?.onBufferDrop?.();
1013
+ }
732
1014
  buffer.length = 0;
1015
+ pendingDeliverIds.clear();
733
1016
  }
734
1017
  };
735
1018
  intervalId = flushIntervalMs > 0 ? globalThis.setInterval(() => void flush(), flushIntervalMs) : void 0;
736
1019
  intervalId?.unref?.();
737
1020
  const track = (event) => {
738
- if (disposed || disposing) return;
1021
+ if (disposed || disposing) return false;
1022
+ const key = eventDedupKey(event);
1023
+ if (key && (pendingDeliverIds.has(key) || isEventBuffered(event))) {
1024
+ return true;
1025
+ }
739
1026
  if (buffer.length >= maxBufferSize) {
740
1027
  opts?.onBufferDrop?.();
741
1028
  if (!warnedBufferCap && isDevEnvironment()) {
@@ -744,29 +1031,37 @@ function createTrackingClient(opts) {
744
1031
  `[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; new events are dropped until the buffer drains.`
745
1032
  );
746
1033
  }
747
- return;
1034
+ return false;
748
1035
  }
749
1036
  buffer.push(event);
750
1037
  if (buffer.length >= maxBatchSize) void flush();
1038
+ return true;
751
1039
  };
752
1040
  return {
753
1041
  track,
754
1042
  deliver: async (event) => {
755
- track(event);
1043
+ const key = eventDedupKey(event);
1044
+ if (key && (pendingDeliverIds.has(key) || isEventBuffered(event))) {
1045
+ return flush();
1046
+ }
1047
+ if (!track(event)) return false;
1048
+ if (key) pendingDeliverIds.add(key);
756
1049
  return flush();
757
1050
  },
758
1051
  flush,
759
1052
  flushOnExit: opts?.exitBatchSink ? () => {
760
- const fromBuffer = buffer.splice(0, buffer.length);
761
- const fromInflight = inflightExitBatch ? [...inflightExitBatch] : [];
762
- const events = [...fromInflight, ...fromBuffer];
1053
+ const events = buffer.splice(0, buffer.length);
763
1054
  if (!events.length) return;
764
1055
  try {
765
1056
  const result = opts.exitBatchSink(events);
766
1057
  if (result != null && typeof result.catch === "function") {
767
- void result.catch(() => {
1058
+ void result.then(() => {
1059
+ clearPendingDeliverIds(events);
1060
+ }).catch(() => {
768
1061
  buffer.unshift(...events);
769
1062
  });
1063
+ } else {
1064
+ clearPendingDeliverIds(events);
770
1065
  }
771
1066
  } catch {
772
1067
  buffer.unshift(...events);
@@ -788,21 +1083,44 @@ function createTrackingClient(opts) {
788
1083
  }
789
1084
 
790
1085
  // src/telemetryPipeline.ts
791
- function invokeSink(sink, event, emitCtx) {
792
- invokePipelineSink(sink.id, () => sink.emit(event, emitCtx));
1086
+ var LIFECYCLE_TELEMETRY_EVENTS = /* @__PURE__ */ new Set([
1087
+ "course_started",
1088
+ "course_completed",
1089
+ "lesson_started",
1090
+ "lesson_completed",
1091
+ "lesson_time_on_task"
1092
+ ]);
1093
+ function isLifecycleTelemetryEvent(name) {
1094
+ return LIFECYCLE_TELEMETRY_EVENTS.has(name);
1095
+ }
1096
+ async function invokeSink(sink, event, emitCtx) {
1097
+ let result;
1098
+ try {
1099
+ result = sink.emit(event, emitCtx);
1100
+ } catch (err) {
1101
+ warnDev(`[lessonkit] telemetry sink "${sink.id}" failed:`, err);
1102
+ return;
1103
+ }
1104
+ if (result != null && typeof result.then === "function") {
1105
+ try {
1106
+ await result;
1107
+ } catch (err) {
1108
+ warnDev(`[lessonkit] telemetry sink "${sink.id}" failed:`, err);
1109
+ }
1110
+ }
793
1111
  }
794
1112
  function createTelemetryPipeline(sinks) {
795
1113
  const list = [...sinks];
796
1114
  return {
797
1115
  sinks: list,
798
- emit(event, ctx) {
1116
+ async emit(event, ctx) {
799
1117
  const emitCtx = ctx ?? {
800
1118
  courseId: event.courseId,
801
1119
  sessionId: event.sessionId,
802
1120
  attemptId: event.attemptId
803
1121
  };
804
1122
  for (const sink of list) {
805
- invokeSink(sink, event, emitCtx);
1123
+ await invokeSink(sink, event, emitCtx);
806
1124
  }
807
1125
  }
808
1126
  };
@@ -841,6 +1159,11 @@ function createProgressController() {
841
1159
  }
842
1160
  return { didComplete: false };
843
1161
  }
1162
+ if (!lessonStartTimes.has(lessonId) && isDevEnvironment()) {
1163
+ console.warn(
1164
+ `[lessonkit] completeLesson("${lessonId}") called without activating the lesson first`
1165
+ );
1166
+ }
844
1167
  completedLessonIds = new Set(completedLessonIds).add(lessonId);
845
1168
  if (activeLessonId === lessonId) {
846
1169
  activeLessonId = void 0;
@@ -874,6 +1197,20 @@ function warnDuplicatePlugin(id) {
874
1197
  if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
875
1198
  console.warn(`[lessonkit] plugin id "${id}" was registered more than once; using the latest definition`);
876
1199
  }
1200
+ function stableUserHash(user) {
1201
+ if (!user) return "";
1202
+ const keys = Object.keys(user).sort();
1203
+ const normalized = {};
1204
+ for (const key of keys) {
1205
+ normalized[key] = user[key];
1206
+ }
1207
+ let h = 0;
1208
+ const serialized = JSON.stringify(normalized);
1209
+ for (let i = 0; i < serialized.length; i++) {
1210
+ h = Math.imul(31, h) + serialized.charCodeAt(i) >>> 0;
1211
+ }
1212
+ return h.toString(36);
1213
+ }
877
1214
  function createPluginRegistry(plugins = []) {
878
1215
  const registry = /* @__PURE__ */ new Map();
879
1216
  for (const plugin of plugins) {
@@ -915,7 +1252,7 @@ function createPluginRegistry(plugins = []) {
915
1252
  const composeTrackingSink = (sink, ctxSource) => {
916
1253
  if (!sink) return void 0;
917
1254
  const resolveCtx = () => typeof ctxSource === "function" ? ctxSource() : ctxSource;
918
- const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}\0${ctx.user?.id ?? ""}`;
1255
+ const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}\0${stableUserHash(ctx.user)}`;
919
1256
  const layers = [];
920
1257
  let composed = sink;
921
1258
  for (const plugin of list) {
@@ -962,6 +1299,10 @@ function resolvePluginHost(plugins) {
962
1299
  if (Array.isArray(plugins) && plugins.length > 0) return createPluginRegistry(plugins);
963
1300
  return null;
964
1301
  }
1302
+ function pluginListFingerprint(plugins) {
1303
+ if (!plugins || !Array.isArray(plugins)) return null;
1304
+ return plugins.map((p) => `${p.id}\0${p.version ?? ""}`).join("\n");
1305
+ }
965
1306
  function warnRuntimeV1Deprecated() {
966
1307
  const g = globalThis;
967
1308
  if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
@@ -974,12 +1315,20 @@ function createLessonkitRuntime(config, ports = {}) {
974
1315
  const storage = ports.storage ?? createSessionStoragePort();
975
1316
  const clock = ports.clock ?? createDefaultClock();
976
1317
  const configSnapshot = { ...config };
1318
+ const hasExplicitSessionId = Boolean(configSnapshot.session?.sessionId?.trim());
1319
+ let autoSessionId;
977
1320
  let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
1321
+ if (!hasExplicitSessionId) {
1322
+ autoSessionId = sessionId;
1323
+ }
978
1324
  let attemptId = configSnapshot.session?.attemptId;
979
1325
  let user = configSnapshot.session?.user;
980
1326
  let courseId = configSnapshot.courseId;
1327
+ let configuredSessionId = configSnapshot.session?.sessionId;
981
1328
  let progress = createProgressController();
982
1329
  let pluginHost = resolvePluginHost(configSnapshot.plugins);
1330
+ let pluginFingerprint = pluginListFingerprint(configSnapshot.plugins);
1331
+ let disposed = false;
983
1332
  const getPluginCtx = () => buildPluginContext({
984
1333
  courseId,
985
1334
  sessionId,
@@ -990,12 +1339,6 @@ function createLessonkitRuntime(config, ports = {}) {
990
1339
  pluginHost?.setupAll(getPluginCtx());
991
1340
  }
992
1341
  const getSession = () => ({ sessionId, attemptId, user });
993
- const syncSessionFromConfig = (next) => {
994
- sessionId = resolveSessionId(storage, next.session?.sessionId);
995
- attemptId = next.session?.attemptId;
996
- user = next.session?.user;
997
- courseId = next.courseId;
998
- };
999
1342
  const applyPluginsToEvent = (event) => {
1000
1343
  if (!pluginHost) return event;
1001
1344
  return pluginHost.runTelemetry(event, getPluginCtx());
@@ -1013,28 +1356,23 @@ function createLessonkitRuntime(config, ports = {}) {
1013
1356
  if (!event) return null;
1014
1357
  return applyPluginsToEvent(event);
1015
1358
  };
1016
- const wrapEmitFn = (emitFn) => {
1017
- return (name, data, lessonId) => {
1018
- const event = buildAndApply(name, data, lessonId);
1019
- if (event === null) return;
1020
- const eventLessonId = "lessonId" in event ? event.lessonId : lessonId;
1021
- const eventData = "data" in event ? event.data : data;
1022
- emitFn(event.name, eventData, eventLessonId);
1023
- };
1359
+ const emitLifecycleEvent = (emitFn, name, data, lessonId) => {
1360
+ const event = buildAndApply(name, data, lessonId);
1361
+ if (event) emitFn(event);
1024
1362
  };
1025
- syncSessionFromConfig(configSnapshot);
1026
1363
  const track = (name, data, emit, lessonId) => {
1364
+ if (disposed) return;
1027
1365
  const event = buildAndApply(name, data, lessonId);
1028
1366
  if (!event) return;
1029
1367
  emit(event);
1030
1368
  };
1031
1369
  const emitLessonCompletedEvents = (lessonId, durationMs, emitFn) => {
1032
- const wrapped = wrapEmitFn(emitFn);
1033
- wrapped("lesson_completed", { lessonId, durationMs }, lessonId);
1370
+ emitLifecycleEvent(emitFn, "lesson_completed", { lessonId, durationMs }, lessonId);
1034
1371
  if (durationMs !== void 0) {
1035
- wrapped("lesson_time_on_task", { lessonId, durationMs }, lessonId);
1372
+ emitLifecycleEvent(emitFn, "lesson_time_on_task", { lessonId, durationMs }, lessonId);
1036
1373
  }
1037
1374
  };
1375
+ const autoCompleteOnLessonSwitch = () => configSnapshot.autoCompleteOnLessonSwitch ?? true;
1038
1376
  return {
1039
1377
  get config() {
1040
1378
  return configSnapshot;
@@ -1047,7 +1385,12 @@ function createLessonkitRuntime(config, ports = {}) {
1047
1385
  },
1048
1386
  getProgressState: () => progress.getState(),
1049
1387
  getSession,
1388
+ migrateSessionMarks(fromSessionId, toSessionId) {
1389
+ if (disposed) return;
1390
+ migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId);
1391
+ },
1050
1392
  updateConfig(next) {
1393
+ if (disposed) return;
1051
1394
  const previousCourseId = courseId;
1052
1395
  const sessionKeyBefore = JSON.stringify({ sessionId, attemptId, user });
1053
1396
  if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
@@ -1055,35 +1398,69 @@ function createLessonkitRuntime(config, ports = {}) {
1055
1398
  if (next.runtimeVersion === "v1") warnRuntimeV1Deprecated();
1056
1399
  configSnapshot.runtimeVersion = next.runtimeVersion;
1057
1400
  }
1401
+ if (next.autoCompleteOnLessonSwitch !== void 0) {
1402
+ configSnapshot.autoCompleteOnLessonSwitch = next.autoCompleteOnLessonSwitch;
1403
+ }
1404
+ if (next.courseId !== void 0) {
1405
+ courseId = next.courseId;
1406
+ }
1058
1407
  if (next.session !== void 0) {
1408
+ const previousSessionId = sessionId;
1059
1409
  configSnapshot.session = { ...configSnapshot.session, ...next.session };
1410
+ const explicitSessionId = configSnapshot.session?.sessionId?.trim();
1411
+ if (explicitSessionId) {
1412
+ sessionId = resolveSessionId(storage, explicitSessionId);
1413
+ autoSessionId = void 0;
1414
+ } else {
1415
+ sessionId = autoSessionId ?? resolveSessionId(storage, void 0);
1416
+ if (!autoSessionId) autoSessionId = sessionId;
1417
+ }
1418
+ attemptId = configSnapshot.session?.attemptId;
1419
+ user = configSnapshot.session?.user;
1420
+ if (previousSessionId !== sessionId) {
1421
+ const prevExplicit = configuredSessionId?.trim();
1422
+ const nextExplicit = configSnapshot.session?.sessionId?.trim();
1423
+ const isExplicitLearnerSwap = Boolean(prevExplicit) && Boolean(nextExplicit) && prevExplicit !== nextExplicit;
1424
+ if (!isExplicitLearnerSwap) {
1425
+ migrateCourseStartedMark(storage, previousSessionId, sessionId, courseId);
1426
+ }
1427
+ }
1428
+ configuredSessionId = configSnapshot.session?.sessionId;
1060
1429
  }
1061
- syncSessionFromConfig(configSnapshot);
1062
1430
  const sessionKeyAfter = JSON.stringify({ sessionId, attemptId, user });
1063
1431
  if (next.courseId !== void 0 && next.courseId !== previousCourseId) {
1064
1432
  progress = createProgressController();
1065
- }
1066
- if (next.plugins !== void 0 && next.plugins !== configSnapshot.plugins) {
1067
- pluginHost?.disposeAll();
1068
- configSnapshot.plugins = next.plugins;
1069
- pluginHost = resolvePluginHost(configSnapshot.plugins);
1070
1433
  if (!configSnapshot.deferPluginSetup) {
1434
+ pluginHost?.disposeAll();
1071
1435
  pluginHost?.setupAll(getPluginCtx());
1072
1436
  }
1437
+ }
1438
+ if (next.plugins !== void 0) {
1439
+ const nextFingerprint = pluginListFingerprint(next.plugins);
1440
+ const pluginsChanged = next.plugins !== configSnapshot.plugins || nextFingerprint !== null && nextFingerprint !== pluginFingerprint;
1441
+ if (pluginsChanged) {
1442
+ pluginHost?.disposeAll();
1443
+ configSnapshot.plugins = next.plugins;
1444
+ pluginFingerprint = nextFingerprint;
1445
+ pluginHost = resolvePluginHost(configSnapshot.plugins);
1446
+ if (!configSnapshot.deferPluginSetup) {
1447
+ pluginHost?.setupAll(getPluginCtx());
1448
+ }
1449
+ }
1073
1450
  } else if (next.session !== void 0 && sessionKeyBefore !== sessionKeyAfter && pluginHost && !configSnapshot.deferPluginSetup) {
1074
1451
  pluginHost.disposeAll();
1075
1452
  pluginHost.setupAll(getPluginCtx());
1076
1453
  }
1077
1454
  },
1078
1455
  setActiveLesson(lessonId, emitFn) {
1079
- const wrapped = wrapEmitFn(emitFn);
1456
+ if (disposed) return;
1080
1457
  const current = progress.getState();
1081
1458
  if (current.activeLessonId === lessonId) return;
1082
1459
  const previous = current.activeLessonId;
1083
- if (previous && previous !== lessonId && !current.completedLessonIds.has(previous)) {
1460
+ if (autoCompleteOnLessonSwitch() && previous && previous !== lessonId && !current.completedLessonIds.has(previous)) {
1084
1461
  const completed = progress.completeLesson(previous, clock.nowMs());
1085
1462
  if (completed.didComplete) {
1086
- emitLessonCompletedEvents(previous, completed.durationMs, wrapped);
1463
+ emitLessonCompletedEvents(previous, completed.durationMs, emitFn);
1087
1464
  }
1088
1465
  }
1089
1466
  if (current.completedLessonIds.has(lessonId)) {
@@ -1091,39 +1468,50 @@ function createLessonkitRuntime(config, ports = {}) {
1091
1468
  return;
1092
1469
  }
1093
1470
  progress.setActiveLesson(lessonId, clock.nowMs());
1094
- wrapped("lesson_started", { lessonId }, lessonId);
1471
+ emitLifecycleEvent(emitFn, "lesson_started", { lessonId }, lessonId);
1095
1472
  },
1096
1473
  completeLesson(lessonId, emitFn) {
1474
+ if (disposed) return;
1097
1475
  completeLessonWithTelemetry({
1098
1476
  progress,
1099
1477
  lessonId,
1100
1478
  nowMs: clock.nowMs(),
1101
- emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, wrapEmitFn(emitFn))
1479
+ emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, emitFn)
1102
1480
  });
1103
1481
  },
1104
1482
  completeCourse(emitFn) {
1483
+ if (disposed) return;
1105
1484
  completeCourseWithTelemetry({
1106
1485
  progress,
1107
1486
  nowMs: clock.nowMs(),
1108
- emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, wrapEmitFn(emitFn)),
1109
- emitCourseCompleted: () => wrapEmitFn(emitFn)("course_completed")
1487
+ emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, emitFn),
1488
+ emitCourseCompleted: () => emitLifecycleEvent(emitFn, "course_completed")
1110
1489
  });
1111
1490
  },
1112
1491
  track,
1113
1492
  scoreAssessment(input, lessonId) {
1114
- if (!pluginHost) return null;
1493
+ if (disposed || !pluginHost) return null;
1115
1494
  return pluginHost.scoreAssessment(
1116
1495
  { ...input, lessonId: input.lessonId ?? lessonId },
1117
1496
  getPluginCtx()
1118
1497
  );
1119
1498
  },
1120
1499
  resetForCourseChange(nextCourseId) {
1500
+ if (disposed) return;
1121
1501
  configSnapshot.courseId = nextCourseId;
1122
1502
  courseId = nextCourseId;
1123
1503
  progress = createProgressController();
1504
+ if (!configSnapshot.deferPluginSetup) {
1505
+ pluginHost?.disposeAll();
1506
+ pluginHost?.setupAll(getPluginCtx());
1507
+ }
1124
1508
  },
1125
1509
  dispose() {
1126
- pluginHost?.disposeAll();
1510
+ if (disposed) return;
1511
+ disposed = true;
1512
+ if (!configSnapshot.deferPluginSetup) {
1513
+ pluginHost?.disposeAll();
1514
+ }
1127
1515
  }
1128
1516
  };
1129
1517
  }
@@ -1142,12 +1530,16 @@ export {
1142
1530
  ACCORDION_FORBIDDEN_CHILD_TYPES,
1143
1531
  ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
1144
1532
  BLOCKS_14_PAGE_SLIDE,
1533
+ BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES,
1534
+ BRANCH_NODE_ALLOWED_CHILD_TYPES,
1145
1535
  COMPOUND_MAX_NESTING_DEPTH,
1146
1536
  COMPOUND_RESUME_SCHEMA_VERSION,
1537
+ GAME_MAP_ALLOWED_CHILD_TYPES,
1147
1538
  ID_MAX_LENGTH,
1148
1539
  ID_PATTERN,
1149
1540
  INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
1150
1541
  INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
1542
+ MAP_STAGE_ALLOWED_CHILD_TYPES,
1151
1543
  PAGE_ALLOWED_CHILD_TYPES,
1152
1544
  SESSION_STORAGE_KEY,
1153
1545
  SLIDE_ALLOWED_CHILD_TYPES,
@@ -1191,11 +1583,14 @@ export {
1191
1583
  hasCourseStarted,
1192
1584
  hasCourseStartedEmittedToTracking,
1193
1585
  hasCourseStartedPipelineDelivered,
1586
+ hasCourseStartedXapiSent,
1194
1587
  isChildTypeAllowed,
1588
+ isLifecycleTelemetryEvent,
1195
1589
  loadCompoundState,
1196
1590
  markCourseStarted,
1197
1591
  markCourseStartedEmittedToTracking,
1198
1592
  markCourseStartedPipelineDelivered,
1593
+ markCourseStartedXapiSent,
1199
1594
  migrateCourseStartedMark,
1200
1595
  nowIso,
1201
1596
  parseBlockId,
@@ -1214,5 +1609,6 @@ export {
1214
1609
  telemetryCatalogVersion,
1215
1610
  tryBuildTelemetryEvent,
1216
1611
  tryEmitCourseStarted,
1612
+ validateBranchGraph,
1217
1613
  validateId
1218
1614
  };