@lessonkit/core 1.2.0 → 1.3.1

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
@@ -60,21 +60,40 @@ function assertValidId(input, path = "id") {
60
60
  }
61
61
 
62
62
  // src/slugify.ts
63
+ function shortHash(input) {
64
+ let h = 0;
65
+ for (let i = 0; i < input.length; i++) {
66
+ h = Math.imul(31, h) + input.charCodeAt(i) >>> 0;
67
+ }
68
+ return h.toString(36);
69
+ }
70
+ function uniqueFallbackId(input, usedIds) {
71
+ const hash = shortHash(input);
72
+ for (let n = 0; n < 100; n++) {
73
+ const candidate = (n === 0 ? `id-${hash}` : `id-${hash}-${n}`).slice(0, 64);
74
+ const validated2 = validateId(candidate);
75
+ if (validated2.ok && !usedIds.has(validated2.id)) return validated2.id;
76
+ }
77
+ const timed = `id-${hash}-${Date.now().toString(36)}`.slice(0, 64);
78
+ const validated = validateId(timed);
79
+ return validated.ok ? validated.id : `id-${hash}`;
80
+ }
63
81
  function slugifyId(input) {
64
82
  const slug = input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-").slice(0, 64);
65
- if (!slug.length) return "id";
66
- const candidate = /^[a-z]/.test(slug) ? slug : `id-${slug}`;
83
+ if (!slug.length) return uniqueFallbackId(input, /* @__PURE__ */ new Set());
84
+ const candidate = /^[a-z]/.test(slug) ? slug : `id-${slug}`.slice(0, 64);
67
85
  const validated = validateId(candidate);
68
- return validated.ok ? validated.id : "id";
86
+ return validated.ok ? validated.id : uniqueFallbackId(input, /* @__PURE__ */ new Set());
69
87
  }
70
88
  function deriveId(title, usedIds = /* @__PURE__ */ new Set()) {
71
89
  const base = slugifyId(title);
72
- if (!usedIds.has(base)) return base;
90
+ if (!usedIds.has(base) && validateId(base).ok) return base;
73
91
  for (let n = 2; n < 1e3; n++) {
74
- const candidate = `${base}-${n}`;
75
- if (!usedIds.has(candidate)) return candidate;
92
+ const candidate = `${base}-${n}`.slice(0, 64);
93
+ const validated = validateId(candidate);
94
+ if (validated.ok && !usedIds.has(validated.id)) return validated.id;
76
95
  }
77
- return `${base}-${Date.now()}`;
96
+ return uniqueFallbackId(`${title}-${Date.now()}`, usedIds);
78
97
  }
79
98
 
80
99
  // src/urn.ts
@@ -122,6 +141,27 @@ function clampCompoundPageIndex(index, pageCount) {
122
141
  if (pageCount < 1) return 0;
123
142
  return Math.min(Math.max(0, Math.floor(index)), pageCount - 1);
124
143
  }
144
+ function isJsonPrimitive(value) {
145
+ return value === null || typeof value === "boolean" || typeof value === "string" || typeof value === "number" && Number.isFinite(value);
146
+ }
147
+ function isPlainStringKeyMap(value) {
148
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
149
+ return Object.entries(value).every(
150
+ ([key, entry]) => typeof key === "string" && isJsonPrimitive(entry)
151
+ );
152
+ }
153
+ function isValidChildResumeValue(value) {
154
+ if (isJsonPrimitive(value)) return true;
155
+ if (Array.isArray(value)) return value.every((item) => isJsonPrimitive(item));
156
+ if (isPlainStringKeyMap(value)) return true;
157
+ return false;
158
+ }
159
+ function isPlainSerializableChildState(value) {
160
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
161
+ return Object.values(value).every(
162
+ (entry) => isValidChildResumeValue(entry)
163
+ );
164
+ }
125
165
  function parseCompoundResumeState(raw) {
126
166
  if (!raw || typeof raw !== "object") return null;
127
167
  const obj = raw;
@@ -130,7 +170,7 @@ function parseCompoundResumeState(raw) {
130
170
  const childStates = {};
131
171
  if (obj.childStates && typeof obj.childStates === "object" && !Array.isArray(obj.childStates)) {
132
172
  for (const [key, value] of Object.entries(obj.childStates)) {
133
- if (value !== null && typeof value === "object" && !Array.isArray(value)) {
173
+ if (isPlainSerializableChildState(value)) {
134
174
  childStates[key] = value;
135
175
  }
136
176
  }
@@ -144,22 +184,40 @@ function parseCompoundResumeState(raw) {
144
184
  };
145
185
  }
146
186
 
187
+ // src/internal/env.ts
188
+ function isDevEnvironment() {
189
+ const g = globalThis;
190
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
191
+ }
192
+ function warnDev(message, err) {
193
+ if (!isDevEnvironment()) return;
194
+ console.warn(message, err instanceof Error ? err.message : err);
195
+ }
196
+
147
197
  // src/compoundState.ts
148
198
  var COMPOUND_STATE_PREFIX = "lessonkit:compound:";
149
199
  function compoundStateStorageKey(courseId, compoundId) {
150
200
  return `${COMPOUND_STATE_PREFIX}${courseId}:${compoundId}`;
151
201
  }
152
202
  function loadCompoundState(storage, courseId, compoundId) {
153
- const raw = storage.getItem(compoundStateStorageKey(courseId, compoundId));
203
+ const key = compoundStateStorageKey(courseId, compoundId);
204
+ const raw = storage.getItem(key);
154
205
  if (!raw) return null;
155
206
  try {
156
- return parseCompoundResumeState(JSON.parse(raw));
207
+ const parsed = parseCompoundResumeState(JSON.parse(raw));
208
+ if (parsed === null && isDevEnvironment()) {
209
+ console.warn(`[lessonkit] Ignoring corrupt compound resume state at ${key}`);
210
+ }
211
+ return parsed;
157
212
  } catch {
213
+ if (isDevEnvironment()) {
214
+ console.warn(`[lessonkit] Ignoring corrupt compound resume state at ${key}`);
215
+ }
158
216
  return null;
159
217
  }
160
218
  }
161
219
  function saveCompoundState(storage, courseId, compoundId, state) {
162
- storage.setItem(compoundStateStorageKey(courseId, compoundId), JSON.stringify(state));
220
+ return storage.setItem(compoundStateStorageKey(courseId, compoundId), JSON.stringify(state));
163
221
  }
164
222
  function clearCompoundState(storage, courseId, compoundId) {
165
223
  storage.removeItem?.(compoundStateStorageKey(courseId, compoundId));
@@ -189,6 +247,28 @@ var PAGE_ALLOWED_CHILD_TYPES = [
189
247
  "ProgressTracker"
190
248
  ];
191
249
  var INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES = ["Page"];
250
+ var SLIDE_ALLOWED_CHILD_TYPES = [
251
+ "Text",
252
+ "Heading",
253
+ "Image",
254
+ "Scenario",
255
+ "Reflection",
256
+ "Quiz",
257
+ "KnowledgeCheck",
258
+ "TrueFalse",
259
+ "FillInTheBlanks",
260
+ "DragAndDrop",
261
+ "DragTheWords",
262
+ "MarkTheWords",
263
+ "Accordion",
264
+ "DialogCards",
265
+ "Flashcards",
266
+ "ImageHotspots",
267
+ "FindHotspot",
268
+ "FindMultipleHotspots",
269
+ "ImageSlider"
270
+ ];
271
+ var SLIDE_DECK_ALLOWED_CHILD_TYPES = ["Slide"];
192
272
  var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
193
273
  "TrueFalse",
194
274
  "FillInTheBlanks",
@@ -203,11 +283,15 @@ var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
203
283
  var ALLOWLISTS = {
204
284
  Page: PAGE_ALLOWED_CHILD_TYPES,
205
285
  InteractiveBook: INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
286
+ Slide: SLIDE_ALLOWED_CHILD_TYPES,
287
+ SlideDeck: SLIDE_DECK_ALLOWED_CHILD_TYPES,
206
288
  AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES
207
289
  };
208
290
  var COMPOUND_MAX_NESTING_DEPTH = {
209
291
  Page: 1,
210
292
  InteractiveBook: 2,
293
+ Slide: 1,
294
+ SlideDeck: 2,
211
295
  AssessmentSequence: 1
212
296
  };
213
297
  function getAllowedChildTypes(parent) {
@@ -325,6 +409,14 @@ var TELEMETRY_EVENT_CATALOG_V3 = [
325
409
  xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
326
410
  urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
327
411
  },
412
+ {
413
+ name: "slide_viewed",
414
+ description: "Learner viewed a slide in a SlideDeck (Course Presentation)",
415
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
416
+ dataFields: ["blockId", "slideIndex", "slideTitle"],
417
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
418
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
419
+ },
328
420
  {
329
421
  name: "compound_page_viewed",
330
422
  description: "Learner activated a page inside a compound container",
@@ -370,16 +462,6 @@ function buildTelemetryCatalogV3() {
370
462
  return TELEMETRY_EVENT_CATALOG_V3.map((entry) => ({ ...entry }));
371
463
  }
372
464
 
373
- // src/internal/env.ts
374
- function isDevEnvironment() {
375
- const g = globalThis;
376
- return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
377
- }
378
- function warnDev(message, err) {
379
- if (!isDevEnvironment()) return;
380
- console.warn(message, err instanceof Error ? err.message : err);
381
- }
382
-
383
465
  // src/internal/sinkInvoke.ts
384
466
  function invokeTrackingSink(sink, event) {
385
467
  let result;
@@ -440,45 +522,66 @@ function createTrackingClient(opts) {
440
522
  }
441
523
  const buffer = [];
442
524
  let flushInFlight = null;
525
+ let inflightExitBatch = null;
443
526
  let disposed = false;
444
527
  let disposing = false;
445
528
  let intervalId;
446
529
  const runFlush = () => {
447
- if (!buffer.length) return Promise.resolve();
530
+ if (!buffer.length) return Promise.resolve(true);
448
531
  const events = buffer.splice(0, buffer.length);
449
- let sent = 0;
532
+ inflightExitBatch = events;
450
533
  let succeeded = false;
451
534
  return Promise.resolve().then(async () => {
452
535
  if (batchSink) {
453
536
  await batchSink(events);
454
537
  } else {
455
- for (const e of events) {
456
- await sink?.(e);
457
- sent += 1;
538
+ for (let i = 0; i < events.length; i++) {
539
+ try {
540
+ await sink?.(events[i]);
541
+ } catch {
542
+ buffer.unshift(...events.slice(i));
543
+ return;
544
+ }
458
545
  }
459
546
  }
460
547
  succeeded = true;
461
548
  }).catch(() => {
462
- buffer.unshift(...events.slice(sent));
463
- }).then(() => {
549
+ if (batchSink) {
550
+ buffer.unshift(...events);
551
+ }
552
+ }).then(async () => {
464
553
  if (succeeded && buffer.length > 0 && !disposed) {
465
554
  return runFlush();
466
555
  }
556
+ return succeeded;
557
+ }).finally(() => {
558
+ inflightExitBatch = null;
467
559
  });
468
560
  };
469
561
  const flush = () => {
470
- if (disposed) return Promise.resolve();
562
+ if (disposed) return Promise.resolve(true);
471
563
  if (flushInFlight) return flushInFlight;
472
- if (!buffer.length) return Promise.resolve();
564
+ if (!buffer.length) return Promise.resolve(true);
473
565
  flushInFlight = runFlush().finally(() => {
474
566
  flushInFlight = null;
475
567
  });
476
568
  return flushInFlight;
477
569
  };
570
+ const MAX_DISPOSE_FLUSH_ATTEMPTS = 10;
478
571
  const drainAll = async () => {
479
- await flush();
480
- while (buffer.length > 0) {
481
- await flush();
572
+ let attempts = 0;
573
+ while (buffer.length > 0 && attempts < MAX_DISPOSE_FLUSH_ATTEMPTS) {
574
+ const delivered = await flush();
575
+ attempts += 1;
576
+ if (!delivered) break;
577
+ }
578
+ if (buffer.length > 0) {
579
+ if (isDevEnvironment()) {
580
+ console.warn(
581
+ `[lessonkit] dropped ${buffer.length} buffered telemetry event(s) after dispose flush cap`
582
+ );
583
+ }
584
+ buffer.length = 0;
482
585
  }
483
586
  };
484
587
  intervalId = flushIntervalMs > 0 ? globalThis.setInterval(() => void flush(), flushIntervalMs) : void 0;
@@ -487,18 +590,35 @@ function createTrackingClient(opts) {
487
590
  track: (event) => {
488
591
  if (disposed || disposing) return;
489
592
  if (buffer.length >= maxBufferSize) {
490
- buffer.shift();
593
+ opts?.onBufferDrop?.();
491
594
  if (!warnedBufferCap && isDevEnvironment()) {
492
595
  warnedBufferCap = true;
493
596
  console.warn(
494
- `[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; oldest events are dropped while the sink is unavailable.`
597
+ `[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; new events are dropped until the buffer drains.`
495
598
  );
496
599
  }
600
+ return;
497
601
  }
498
602
  buffer.push(event);
499
603
  if (buffer.length >= maxBatchSize) void flush();
500
604
  },
501
605
  flush,
606
+ flushOnExit: opts?.exitBatchSink ? () => {
607
+ const fromBuffer = buffer.splice(0, buffer.length);
608
+ const fromInflight = inflightExitBatch ? [...inflightExitBatch] : [];
609
+ const events = [...fromInflight, ...fromBuffer];
610
+ if (!events.length) return;
611
+ try {
612
+ const result = opts.exitBatchSink(events);
613
+ if (result != null && typeof result.catch === "function") {
614
+ void result.catch(() => {
615
+ buffer.unshift(...events);
616
+ });
617
+ }
618
+ } catch {
619
+ buffer.unshift(...events);
620
+ }
621
+ } : void 0,
502
622
  dispose: () => {
503
623
  if (disposed || disposing) return Promise.resolve();
504
624
  disposing = true;
@@ -651,6 +771,20 @@ var TELEMETRY_EVENT_REGISTRY = {
651
771
  };
652
772
  }
653
773
  },
774
+ slide_viewed: {
775
+ requiresLessonId: true,
776
+ build: (opts, base) => {
777
+ if (opts.name !== "slide_viewed") throw new Error("unexpected event");
778
+ const lessonId = opts.lessonId;
779
+ if (!lessonId) throw new Error("slide_viewed requires active lessonId");
780
+ return {
781
+ name: "slide_viewed",
782
+ ...base,
783
+ lessonId,
784
+ data: opts.data
785
+ };
786
+ }
787
+ },
654
788
  compound_page_viewed: {
655
789
  requiresLessonId: true,
656
790
  build: (opts, base) => {
@@ -799,8 +933,7 @@ function createDefaultClock() {
799
933
  function createNoopStorage() {
800
934
  return {
801
935
  getItem: () => null,
802
- setItem: () => {
803
- }
936
+ setItem: () => true
804
937
  };
805
938
  }
806
939
  function createMemoryBackedSessionStorage(session) {
@@ -831,8 +964,10 @@ function createMemoryBackedSessionStorage(session) {
831
964
  memory.set(key, value);
832
965
  try {
833
966
  session.setItem(key, value);
967
+ return true;
834
968
  } catch {
835
969
  warnPersistFailure();
970
+ return false;
836
971
  }
837
972
  },
838
973
  removeItem: (key) => {
@@ -857,6 +992,7 @@ function createInMemorySessionStoragePort() {
857
992
  getItem: (key) => memory.get(key) ?? null,
858
993
  setItem: (key, value) => {
859
994
  memory.set(key, value);
995
+ return true;
860
996
  },
861
997
  removeItem: (key) => {
862
998
  memory.delete(key);
@@ -909,7 +1045,12 @@ function createProgressController() {
909
1045
  return { previousLessonId };
910
1046
  },
911
1047
  completeLesson: (lessonId, completedAtMs) => {
912
- if (completedLessonIds.has(lessonId)) return { didComplete: false };
1048
+ if (completedLessonIds.has(lessonId)) {
1049
+ if (activeLessonId === lessonId) {
1050
+ activeLessonId = void 0;
1051
+ }
1052
+ return { didComplete: false };
1053
+ }
913
1054
  completedLessonIds = new Set(completedLessonIds).add(lessonId);
914
1055
  if (activeLessonId === lessonId) {
915
1056
  activeLessonId = void 0;
@@ -929,6 +1070,12 @@ function createProgressController() {
929
1070
 
930
1071
  // src/session.ts
931
1072
  var SESSION_STORAGE_KEY = "lessonkit:sessionId";
1073
+ var volatileSessionIds = /* @__PURE__ */ new WeakMap();
1074
+ var sharedVolatileSessionId = null;
1075
+ function isDevEnvironment2() {
1076
+ const g = globalThis;
1077
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
1078
+ }
932
1079
  function getTabSessionId(storage) {
933
1080
  return storage.getItem(SESSION_STORAGE_KEY);
934
1081
  }
@@ -936,11 +1083,28 @@ var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
936
1083
  var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
937
1084
  var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
938
1085
  function resolveSessionId(storage, provided) {
939
- if (provided) return provided;
1086
+ if (provided !== void 0) {
1087
+ const trimmed = provided.trim();
1088
+ if (trimmed.length > 0) return trimmed;
1089
+ }
940
1090
  const existing = storage.getItem(SESSION_STORAGE_KEY);
941
1091
  if (existing) return existing;
1092
+ const volatile = volatileSessionIds.get(storage);
1093
+ if (volatile) return volatile;
942
1094
  const id = createSessionId();
943
- storage.setItem(SESSION_STORAGE_KEY, id);
1095
+ const persisted = storage.setItem(SESSION_STORAGE_KEY, id);
1096
+ if (!persisted) {
1097
+ if (!sharedVolatileSessionId) {
1098
+ sharedVolatileSessionId = id;
1099
+ }
1100
+ volatileSessionIds.set(storage, sharedVolatileSessionId);
1101
+ if (isDevEnvironment2()) {
1102
+ console.warn(
1103
+ "[lessonkit] session id could not be persisted; reusing in-memory id for this tab."
1104
+ );
1105
+ }
1106
+ return sharedVolatileSessionId;
1107
+ }
944
1108
  return id;
945
1109
  }
946
1110
  function courseStartedStorageKey(sessionId, courseId) {
@@ -957,24 +1121,27 @@ function hasCourseStarted(storage, sessionId, courseId) {
957
1121
  return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
958
1122
  }
959
1123
  function markCourseStarted(storage, sessionId, courseId) {
960
- if (!courseId) return;
961
- storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
1124
+ if (!courseId) return false;
1125
+ return storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
962
1126
  }
963
1127
  function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
964
1128
  if (!courseId) return false;
965
1129
  return storage.getItem(courseStartedTrackingStorageKey(sessionId, courseId)) === "1";
966
1130
  }
967
1131
  function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
968
- if (!courseId) return;
969
- storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
1132
+ if (!courseId) return false;
1133
+ return storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
970
1134
  }
971
1135
  function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
972
1136
  if (!courseId) return false;
973
1137
  return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
974
1138
  }
975
1139
  function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
976
- if (!courseId) return;
977
- storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
1140
+ if (!courseId) return false;
1141
+ return storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
1142
+ }
1143
+ function resetSharedVolatileSessionIdForTests() {
1144
+ sharedVolatileSessionId = null;
978
1145
  }
979
1146
  function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
980
1147
  if (!courseId || fromSessionId === toSessionId) return;
@@ -993,19 +1160,29 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
993
1160
  }
994
1161
 
995
1162
  // src/runtime/courseLifecycle.ts
1163
+ var courseStartedEmitFlights = /* @__PURE__ */ new Set();
996
1164
  function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
1165
+ const flightKey = `${ctx.sessionId}:${ctx.courseId}`;
997
1166
  const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
998
1167
  if (alreadyEmittedToSink) {
999
1168
  return { emitted: true, marked };
1000
1169
  }
1001
- if (marked) {
1002
- return { emitted: false, marked: true };
1170
+ if (courseStartedEmitFlights.has(flightKey)) {
1171
+ return { emitted: false, marked };
1003
1172
  }
1004
- const emitted = deps.emitCourseStartedEvent(ctx);
1005
- if (emitted) {
1006
- markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
1173
+ courseStartedEmitFlights.add(flightKey);
1174
+ try {
1175
+ const emitted = deps.emitCourseStartedEvent(ctx);
1176
+ if (emitted && !marked) {
1177
+ markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
1178
+ }
1179
+ return {
1180
+ emitted,
1181
+ marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
1182
+ };
1183
+ } finally {
1184
+ courseStartedEmitFlights.delete(flightKey);
1007
1185
  }
1008
- return { emitted, marked: emitted };
1009
1186
  }
1010
1187
  function buildCourseStartedTelemetryEvent(ctx) {
1011
1188
  return buildTelemetryEvent({
@@ -1095,7 +1272,7 @@ function createPluginRegistry(plugins = []) {
1095
1272
  const composeTrackingSink = (sink, ctxSource) => {
1096
1273
  if (!sink) return void 0;
1097
1274
  const resolveCtx = () => typeof ctxSource === "function" ? ctxSource() : ctxSource;
1098
- const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}`;
1275
+ const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}\0${ctx.user?.id ?? ""}`;
1099
1276
  const layers = [];
1100
1277
  let composed = sink;
1101
1278
  for (const plugin of list) {
@@ -1166,6 +1343,9 @@ function createLessonkitRuntime(config, ports = {}) {
1166
1343
  attemptId,
1167
1344
  user
1168
1345
  });
1346
+ if (!configSnapshot.deferPluginSetup) {
1347
+ pluginHost?.setupAll(getPluginCtx());
1348
+ }
1169
1349
  const getSession = () => ({ sessionId, attemptId, user });
1170
1350
  const syncSessionFromConfig = (next) => {
1171
1351
  sessionId = resolveSessionId(storage, next.session?.sessionId);
@@ -1225,11 +1405,8 @@ function createLessonkitRuntime(config, ports = {}) {
1225
1405
  getProgressState: () => progress.getState(),
1226
1406
  getSession,
1227
1407
  updateConfig(next) {
1228
- if (next.plugins !== void 0 && next.plugins !== pluginHost) {
1229
- pluginHost?.disposeAll();
1230
- configSnapshot.plugins = next.plugins;
1231
- pluginHost = resolvePluginHost(configSnapshot.plugins);
1232
- }
1408
+ const previousCourseId = courseId;
1409
+ const sessionKeyBefore = JSON.stringify({ sessionId, attemptId, user });
1233
1410
  if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
1234
1411
  if (next.runtimeVersion !== void 0) {
1235
1412
  if (next.runtimeVersion === "v1") warnRuntimeV1Deprecated();
@@ -1239,6 +1416,19 @@ function createLessonkitRuntime(config, ports = {}) {
1239
1416
  configSnapshot.session = { ...configSnapshot.session, ...next.session };
1240
1417
  }
1241
1418
  syncSessionFromConfig(configSnapshot);
1419
+ const sessionKeyAfter = JSON.stringify({ sessionId, attemptId, user });
1420
+ if (next.courseId !== void 0 && next.courseId !== previousCourseId) {
1421
+ progress = createProgressController();
1422
+ }
1423
+ if (next.plugins !== void 0 && next.plugins !== pluginHost) {
1424
+ pluginHost?.disposeAll();
1425
+ configSnapshot.plugins = next.plugins;
1426
+ pluginHost = resolvePluginHost(configSnapshot.plugins);
1427
+ pluginHost?.setupAll(getPluginCtx());
1428
+ } else if (next.session !== void 0 && sessionKeyBefore !== sessionKeyAfter && pluginHost && !configSnapshot.deferPluginSetup) {
1429
+ pluginHost.disposeAll();
1430
+ pluginHost.setupAll(getPluginCtx());
1431
+ }
1242
1432
  },
1243
1433
  setActiveLesson(lessonId, emitFn) {
1244
1434
  const wrapped = wrapEmitFn(emitFn);
@@ -1310,6 +1500,8 @@ export {
1310
1500
  INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
1311
1501
  PAGE_ALLOWED_CHILD_TYPES,
1312
1502
  SESSION_STORAGE_KEY,
1503
+ SLIDE_ALLOWED_CHILD_TYPES,
1504
+ SLIDE_DECK_ALLOWED_CHILD_TYPES,
1313
1505
  TELEMETRY_EVENT_CATALOG,
1314
1506
  TELEMETRY_EVENT_CATALOG_V2,
1315
1507
  TELEMETRY_EVENT_CATALOG_V3,
@@ -1360,6 +1552,7 @@ export {
1360
1552
  parseCompoundResumeState,
1361
1553
  parseCourseId,
1362
1554
  parseLessonId,
1555
+ resetSharedVolatileSessionIdForTests,
1363
1556
  resetStoragePortForTests,
1364
1557
  resetTelemetryBuilderWarningsForTests,
1365
1558
  resolveSessionId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/core",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "private": false,
5
5
  "description": "Shared types and telemetry primitives for LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -9,6 +9,14 @@
9
9
  "xapiVerb": "http://adlnet.gov/expapi/verbs/experienced",
10
10
  "urnPattern": "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
11
11
  },
12
+ {
13
+ "name": "slide_viewed",
14
+ "description": "Learner viewed a slide in a SlideDeck (Course Presentation)",
15
+ "requiredFields": ["courseId", "lessonId", "sessionId", "timestamp"],
16
+ "dataFields": ["blockId", "slideIndex", "slideTitle"],
17
+ "xapiVerb": "http://adlnet.gov/expapi/verbs/experienced",
18
+ "urnPattern": "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
19
+ },
12
20
  {
13
21
  "name": "compound_page_viewed",
14
22
  "description": "Learner activated a page inside a compound container",