@lessonkit/core 1.5.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.cjs CHANGED
@@ -27,10 +27,12 @@ __export(index_exports, {
27
27
  BRANCH_NODE_ALLOWED_CHILD_TYPES: () => BRANCH_NODE_ALLOWED_CHILD_TYPES,
28
28
  COMPOUND_MAX_NESTING_DEPTH: () => COMPOUND_MAX_NESTING_DEPTH,
29
29
  COMPOUND_RESUME_SCHEMA_VERSION: () => COMPOUND_RESUME_SCHEMA_VERSION,
30
+ GAME_MAP_ALLOWED_CHILD_TYPES: () => GAME_MAP_ALLOWED_CHILD_TYPES,
30
31
  ID_MAX_LENGTH: () => ID_MAX_LENGTH,
31
32
  ID_PATTERN: () => ID_PATTERN,
32
33
  INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES: () => INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
33
34
  INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES: () => INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
35
+ MAP_STAGE_ALLOWED_CHILD_TYPES: () => MAP_STAGE_ALLOWED_CHILD_TYPES,
34
36
  PAGE_ALLOWED_CHILD_TYPES: () => PAGE_ALLOWED_CHILD_TYPES,
35
37
  SESSION_STORAGE_KEY: () => SESSION_STORAGE_KEY,
36
38
  SLIDE_ALLOWED_CHILD_TYPES: () => SLIDE_ALLOWED_CHILD_TYPES,
@@ -240,7 +242,7 @@ function buildLessonkitUrn(parts) {
240
242
  urn += `:block:${blockId}`;
241
243
  }
242
244
  if (parts.nodeId !== void 0) {
243
- const nodeId = assertValidId(parts.nodeId, "blockId");
245
+ const nodeId = assertValidId(parts.nodeId, "nodeId");
244
246
  if (parts.blockId === void 0) {
245
247
  throw new Error("buildLessonkitUrn: nodeId requires blockId");
246
248
  }
@@ -310,9 +312,11 @@ function parseCompoundResumeState(raw, opts) {
310
312
  opts?.onDroppedChildKeys?.(droppedChildKeys);
311
313
  }
312
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;
313
317
  return {
314
318
  schemaVersion: COMPOUND_RESUME_SCHEMA_VERSION,
315
- activePageIndex: Math.max(0, Math.floor(obj.activePageIndex)),
319
+ activePageIndex,
316
320
  ...activeChapterIndex !== void 0 ? { activeChapterIndex: Math.max(0, Math.floor(activeChapterIndex)) } : {},
317
321
  childStates
318
322
  };
@@ -406,6 +410,16 @@ var PAGE_ALLOWED_CHILD_TYPES = [
406
410
  "ImageSlider",
407
411
  "Embed",
408
412
  "Chart",
413
+ "Table",
414
+ "ImageJuxtaposition",
415
+ "Timeline",
416
+ "ImageSequence",
417
+ "Collage",
418
+ "AudioRecorder",
419
+ "CombinationLock",
420
+ "QrContent",
421
+ "Crossword",
422
+ "AdventCalendar",
409
423
  "ProgressTracker"
410
424
  ];
411
425
  var BRANCH_NODE_ALLOWED_CHILD_TYPES = [
@@ -440,9 +454,64 @@ var BRANCH_NODE_ALLOWED_CHILD_TYPES = [
440
454
  "ImageSlider",
441
455
  "Embed",
442
456
  "Chart",
457
+ "Table",
458
+ "ImageJuxtaposition",
459
+ "Timeline",
460
+ "ImageSequence",
461
+ "Collage",
462
+ "AudioRecorder",
463
+ "CombinationLock",
464
+ "QrContent",
465
+ "Crossword",
466
+ "AdventCalendar",
443
467
  "BranchChoice"
444
468
  ];
445
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
+ ];
446
515
  var INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES = ["Page"];
447
516
  var SLIDE_ALLOWED_CHILD_TYPES = [
448
517
  "Text",
@@ -475,7 +544,17 @@ var SLIDE_ALLOWED_CHILD_TYPES = [
475
544
  "FindMultipleHotspots",
476
545
  "ImageSlider",
477
546
  "Embed",
478
- "Chart"
547
+ "Chart",
548
+ "Table",
549
+ "ImageJuxtaposition",
550
+ "Timeline",
551
+ "ImageSequence",
552
+ "Collage",
553
+ "AudioRecorder",
554
+ "CombinationLock",
555
+ "QrContent",
556
+ "Crossword",
557
+ "AdventCalendar"
479
558
  ];
480
559
  var SLIDE_DECK_ALLOWED_CHILD_TYPES = ["Slide"];
481
560
  var TIMED_CUE_ALLOWED_CHILD_TYPES = [
@@ -519,7 +598,9 @@ var ALLOWLISTS = {
519
598
  InteractiveVideo: INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
520
599
  AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
521
600
  BranchingScenario: BRANCHING_SCENARIO_ALLOWED_CHILD_TYPES,
522
- BranchNode: BRANCH_NODE_ALLOWED_CHILD_TYPES
601
+ BranchNode: BRANCH_NODE_ALLOWED_CHILD_TYPES,
602
+ GameMap: GAME_MAP_ALLOWED_CHILD_TYPES,
603
+ MapStage: MAP_STAGE_ALLOWED_CHILD_TYPES
523
604
  };
524
605
  var COMPOUND_MAX_NESTING_DEPTH = {
525
606
  Page: 1,
@@ -530,7 +611,9 @@ var COMPOUND_MAX_NESTING_DEPTH = {
530
611
  InteractiveVideo: 2,
531
612
  AssessmentSequence: 1,
532
613
  BranchingScenario: 2,
533
- BranchNode: 1
614
+ BranchNode: 1,
615
+ GameMap: 2,
616
+ MapStage: 1
534
617
  };
535
618
  function getAllowedChildTypes(parent) {
536
619
  return ALLOWLISTS[parent];
@@ -835,6 +918,78 @@ var TELEMETRY_EVENT_CATALOG_V3 = [
835
918
  dataFields: ["blockId", "fromNodeId", "toNodeId", "label", "scoreWeight"],
836
919
  xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
837
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}"
838
993
  }
839
994
  ];
840
995
  function buildTelemetryCatalogV3() {
@@ -931,6 +1086,10 @@ function createTrackingClient(opts) {
931
1086
  const runFlush = () => {
932
1087
  if (!buffer.length) return Promise.resolve(true);
933
1088
  const events = buffer.splice(0, buffer.length);
1089
+ for (const event of events) {
1090
+ const key = eventDedupKey(event);
1091
+ if (key) pendingDeliverIds.add(key);
1092
+ }
934
1093
  let succeeded = false;
935
1094
  return Promise.resolve().then(async () => {
936
1095
  if (batchSink) {
@@ -940,7 +1099,7 @@ function createTrackingClient(opts) {
940
1099
  try {
941
1100
  await sink?.(events[i]);
942
1101
  } catch {
943
- buffer.unshift(...events.slice(i));
1102
+ buffer.unshift(...events);
944
1103
  return;
945
1104
  }
946
1105
  }
@@ -1064,7 +1223,8 @@ function randomSessionIdFallback() {
1064
1223
  if (g.crypto?.getRandomValues) {
1065
1224
  const bytes = new Uint8Array(16);
1066
1225
  g.crypto.getRandomValues(bytes);
1067
- return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1226
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1227
+ return `s-${hex}`;
1068
1228
  }
1069
1229
  throw new Error(
1070
1230
  "[lessonkit] createSessionId requires crypto.randomUUID or crypto.getRandomValues"
@@ -1072,7 +1232,9 @@ function randomSessionIdFallback() {
1072
1232
  }
1073
1233
  function createSessionId() {
1074
1234
  const g = globalThis;
1075
- if (g.crypto?.randomUUID) return g.crypto.randomUUID();
1235
+ if (g.crypto?.randomUUID) {
1236
+ return `s-${g.crypto.randomUUID().replace(/-/g, "")}`;
1237
+ }
1076
1238
  return randomSessionIdFallback();
1077
1239
  }
1078
1240
 
@@ -1380,6 +1542,90 @@ var TELEMETRY_EVENT_REGISTRY = {
1380
1542
  data: opts.data
1381
1543
  };
1382
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
+ }
1383
1629
  }
1384
1630
  };
1385
1631
  function buildTelemetryEventFromRegistry(opts) {
@@ -1540,13 +1786,15 @@ function createMemoryBackedSessionStorage(session) {
1540
1786
  );
1541
1787
  }
1542
1788
  };
1789
+ const bypassCacheForKey = (key) => key === "lessonkit:sessionId" || key.startsWith("lessonkit:course_started");
1543
1790
  return {
1544
1791
  getItem: (key) => {
1545
1792
  if (tombstones.has(key)) return null;
1546
- if (memory.has(key)) return memory.get(key);
1793
+ if (!bypassCacheForKey(key) && memory.has(key)) return memory.get(key);
1547
1794
  try {
1548
1795
  const value = session.getItem(key);
1549
1796
  if (value !== null) memory.set(key, value);
1797
+ else if (bypassCacheForKey(key)) memory.delete(key);
1550
1798
  return value;
1551
1799
  } catch {
1552
1800
  return memory.get(key) ?? null;
@@ -1554,12 +1802,15 @@ function createMemoryBackedSessionStorage(session) {
1554
1802
  },
1555
1803
  setItem: (key, value) => {
1556
1804
  tombstones.delete(key);
1557
- memory.set(key, value);
1558
1805
  try {
1559
1806
  session.setItem(key, value);
1807
+ memory.set(key, value);
1560
1808
  return true;
1561
1809
  } catch {
1562
1810
  warnPersistFailure();
1811
+ if (!bypassCacheForKey(key)) {
1812
+ memory.set(key, value);
1813
+ }
1563
1814
  return false;
1564
1815
  }
1565
1816
  },
@@ -1701,7 +1952,17 @@ function resolveSessionId(storage, provided) {
1701
1952
  }
1702
1953
  }
1703
1954
  const existing = storage.getItem(SESSION_STORAGE_KEY);
1704
- if (existing) return 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
+ }
1705
1966
  const volatile = volatileSessionIds.get(storage);
1706
1967
  if (volatile) return volatile;
1707
1968
  const id = createSessionId();
@@ -1803,7 +2064,17 @@ function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
1803
2064
  const flightKey = `${ctx.sessionId}:${ctx.courseId}`;
1804
2065
  const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
1805
2066
  if (alreadyEmittedToSink) {
1806
- return Promise.resolve({ emitted: true, marked });
2067
+ const markPersisted = marked ? true : markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
2068
+ return Promise.resolve({
2069
+ emitted: true,
2070
+ marked: markPersisted
2071
+ });
2072
+ }
2073
+ if (marked && hasCourseStartedEmittedToTracking(ctx.storage, ctx.sessionId, ctx.courseId)) {
2074
+ return Promise.resolve({
2075
+ emitted: true,
2076
+ marked: true
2077
+ });
1807
2078
  }
1808
2079
  const existing = courseStartedEmitFlights.get(flightKey);
1809
2080
  if (existing) {
@@ -1812,15 +2083,16 @@ function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
1812
2083
  const flight = Promise.resolve().then(() => {
1813
2084
  try {
1814
2085
  const emitted = deps.emitCourseStartedEvent(ctx);
1815
- if (emitted && !marked) {
1816
- markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
1817
- }
2086
+ const markPersisted = emitted && !marked ? markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId) : marked;
1818
2087
  return {
1819
2088
  emitted,
1820
- marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
2089
+ marked: markPersisted
1821
2090
  };
1822
2091
  } catch {
1823
- return { emitted: false, marked };
2092
+ return {
2093
+ emitted: false,
2094
+ marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
2095
+ };
1824
2096
  } finally {
1825
2097
  if (courseStartedEmitFlights.get(flightKey) === flight) {
1826
2098
  courseStartedEmitFlights.delete(flightKey);
@@ -1856,7 +2128,16 @@ function completeCourseWithTelemetry(opts) {
1856
2128
  });
1857
2129
  }
1858
2130
  const result = opts.progress.completeCourse();
1859
- if (!result.didComplete) return false;
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
+ }
1860
2141
  opts.emitCourseCompleted();
1861
2142
  return true;
1862
2143
  }
@@ -1979,6 +2260,10 @@ function resolvePluginHost(plugins) {
1979
2260
  if (Array.isArray(plugins) && plugins.length > 0) return createPluginRegistry(plugins);
1980
2261
  return null;
1981
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
+ }
1982
2267
  function warnRuntimeV1Deprecated() {
1983
2268
  const g = globalThis;
1984
2269
  if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
@@ -1991,12 +2276,19 @@ function createLessonkitRuntime(config, ports = {}) {
1991
2276
  const storage = ports.storage ?? createSessionStoragePort();
1992
2277
  const clock = ports.clock ?? createDefaultClock();
1993
2278
  const configSnapshot = { ...config };
2279
+ const hasExplicitSessionId = Boolean(configSnapshot.session?.sessionId?.trim());
2280
+ let autoSessionId;
1994
2281
  let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
2282
+ if (!hasExplicitSessionId) {
2283
+ autoSessionId = sessionId;
2284
+ }
1995
2285
  let attemptId = configSnapshot.session?.attemptId;
1996
2286
  let user = configSnapshot.session?.user;
1997
2287
  let courseId = configSnapshot.courseId;
2288
+ let configuredSessionId = configSnapshot.session?.sessionId;
1998
2289
  let progress = createProgressController();
1999
2290
  let pluginHost = resolvePluginHost(configSnapshot.plugins);
2291
+ let pluginFingerprint = pluginListFingerprint(configSnapshot.plugins);
2000
2292
  let disposed = false;
2001
2293
  const getPluginCtx = () => buildPluginContext({
2002
2294
  courseId,
@@ -2008,12 +2300,6 @@ function createLessonkitRuntime(config, ports = {}) {
2008
2300
  pluginHost?.setupAll(getPluginCtx());
2009
2301
  }
2010
2302
  const getSession = () => ({ sessionId, attemptId, user });
2011
- const syncSessionFromConfig = (next) => {
2012
- sessionId = resolveSessionId(storage, next.session?.sessionId);
2013
- attemptId = next.session?.attemptId;
2014
- user = next.session?.user;
2015
- courseId = next.courseId;
2016
- };
2017
2303
  const applyPluginsToEvent = (event) => {
2018
2304
  if (!pluginHost) return event;
2019
2305
  return pluginHost.runTelemetry(event, getPluginCtx());
@@ -2035,7 +2321,6 @@ function createLessonkitRuntime(config, ports = {}) {
2035
2321
  const event = buildAndApply(name, data, lessonId);
2036
2322
  if (event) emitFn(event);
2037
2323
  };
2038
- syncSessionFromConfig(configSnapshot);
2039
2324
  const track = (name, data, emit, lessonId) => {
2040
2325
  if (disposed) return;
2041
2326
  const event = buildAndApply(name, data, lessonId);
@@ -2077,21 +2362,52 @@ function createLessonkitRuntime(config, ports = {}) {
2077
2362
  if (next.autoCompleteOnLessonSwitch !== void 0) {
2078
2363
  configSnapshot.autoCompleteOnLessonSwitch = next.autoCompleteOnLessonSwitch;
2079
2364
  }
2365
+ if (next.courseId !== void 0) {
2366
+ courseId = next.courseId;
2367
+ }
2080
2368
  if (next.session !== void 0) {
2369
+ const previousSessionId = sessionId;
2081
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;
2082
2390
  }
2083
- syncSessionFromConfig(configSnapshot);
2084
2391
  const sessionKeyAfter = JSON.stringify({ sessionId, attemptId, user });
2085
2392
  if (next.courseId !== void 0 && next.courseId !== previousCourseId) {
2086
2393
  progress = createProgressController();
2087
- }
2088
- if (next.plugins !== void 0 && next.plugins !== configSnapshot.plugins) {
2089
- pluginHost?.disposeAll();
2090
- configSnapshot.plugins = next.plugins;
2091
- pluginHost = resolvePluginHost(configSnapshot.plugins);
2092
2394
  if (!configSnapshot.deferPluginSetup) {
2395
+ pluginHost?.disposeAll();
2093
2396
  pluginHost?.setupAll(getPluginCtx());
2094
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
+ }
2095
2411
  } else if (next.session !== void 0 && sessionKeyBefore !== sessionKeyAfter && pluginHost && !configSnapshot.deferPluginSetup) {
2096
2412
  pluginHost.disposeAll();
2097
2413
  pluginHost.setupAll(getPluginCtx());
@@ -2146,15 +2462,17 @@ function createLessonkitRuntime(config, ports = {}) {
2146
2462
  configSnapshot.courseId = nextCourseId;
2147
2463
  courseId = nextCourseId;
2148
2464
  progress = createProgressController();
2149
- pluginHost?.disposeAll();
2150
2465
  if (!configSnapshot.deferPluginSetup) {
2466
+ pluginHost?.disposeAll();
2151
2467
  pluginHost?.setupAll(getPluginCtx());
2152
2468
  }
2153
2469
  },
2154
2470
  dispose() {
2155
2471
  if (disposed) return;
2156
2472
  disposed = true;
2157
- pluginHost?.disposeAll();
2473
+ if (!configSnapshot.deferPluginSetup) {
2474
+ pluginHost?.disposeAll();
2475
+ }
2158
2476
  }
2159
2477
  };
2160
2478
  }
@@ -2178,10 +2496,12 @@ function defineLifecyclePlugin(plugin) {
2178
2496
  BRANCH_NODE_ALLOWED_CHILD_TYPES,
2179
2497
  COMPOUND_MAX_NESTING_DEPTH,
2180
2498
  COMPOUND_RESUME_SCHEMA_VERSION,
2499
+ GAME_MAP_ALLOWED_CHILD_TYPES,
2181
2500
  ID_MAX_LENGTH,
2182
2501
  ID_PATTERN,
2183
2502
  INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
2184
2503
  INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
2504
+ MAP_STAGE_ALLOWED_CHILD_TYPES,
2185
2505
  PAGE_ALLOWED_CHILD_TYPES,
2186
2506
  SESSION_STORAGE_KEY,
2187
2507
  SLIDE_ALLOWED_CHILD_TYPES,