@lessonkit/core 1.3.1 → 1.4.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,3 +1,33 @@
1
+ import {
2
+ SESSION_STORAGE_KEY,
3
+ buildCourseStartedTelemetryEvent,
4
+ buildTelemetryEvent,
5
+ completeCourseWithTelemetry,
6
+ completeLessonWithTelemetry,
7
+ createDefaultClock,
8
+ createGlobalTimer,
9
+ createNoopStorage,
10
+ createSessionId,
11
+ createSessionStoragePort,
12
+ getTabSessionId,
13
+ hasCourseStarted,
14
+ hasCourseStartedEmittedToTracking,
15
+ hasCourseStartedPipelineDelivered,
16
+ isDevEnvironment,
17
+ markCourseStarted,
18
+ markCourseStartedEmittedToTracking,
19
+ markCourseStartedPipelineDelivered,
20
+ migrateCourseStartedMark,
21
+ nowIso,
22
+ resetSharedVolatileSessionIdForTests,
23
+ resetStoragePortForTests,
24
+ resetTelemetryBuilderWarningsForTests,
25
+ resolveSessionId,
26
+ tryBuildTelemetryEvent,
27
+ tryEmitCourseStarted,
28
+ warnDev
29
+ } from "./chunk-PEWFPVQ6.js";
30
+
1
31
  // src/identityTypes.ts
2
32
  var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
3
33
  var ID_MAX_LENGTH = 64;
@@ -184,16 +214,6 @@ function parseCompoundResumeState(raw) {
184
214
  };
185
215
  }
186
216
 
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
-
197
217
  // src/compoundState.ts
198
218
  var COMPOUND_STATE_PREFIX = "lessonkit:compound:";
199
219
  function compoundStateStorageKey(courseId, compoundId) {
@@ -224,10 +244,23 @@ function clearCompoundState(storage, courseId, compoundId) {
224
244
  }
225
245
 
226
246
  // src/compoundAllowlists.ts
247
+ var PAGE_AND_SLIDE_14_BLOCKS = [
248
+ "Video",
249
+ "Summary",
250
+ "ImagePairing",
251
+ "ImageSequencing",
252
+ "MemoryGame",
253
+ "InformationWall",
254
+ "ParallaxSlideshow",
255
+ "Questionnaire",
256
+ "Essay",
257
+ "ArithmeticQuiz"
258
+ ];
227
259
  var PAGE_ALLOWED_CHILD_TYPES = [
228
260
  "Text",
229
261
  "Heading",
230
262
  "Image",
263
+ "Video",
231
264
  "Scenario",
232
265
  "Reflection",
233
266
  "Quiz",
@@ -237,6 +270,15 @@ var PAGE_ALLOWED_CHILD_TYPES = [
237
270
  "DragAndDrop",
238
271
  "DragTheWords",
239
272
  "MarkTheWords",
273
+ "Summary",
274
+ "ImagePairing",
275
+ "ImageSequencing",
276
+ "MemoryGame",
277
+ "InformationWall",
278
+ "ParallaxSlideshow",
279
+ "Questionnaire",
280
+ "Essay",
281
+ "ArithmeticQuiz",
240
282
  "Accordion",
241
283
  "DialogCards",
242
284
  "Flashcards",
@@ -251,6 +293,7 @@ var SLIDE_ALLOWED_CHILD_TYPES = [
251
293
  "Text",
252
294
  "Heading",
253
295
  "Image",
296
+ "Video",
254
297
  "Scenario",
255
298
  "Reflection",
256
299
  "Quiz",
@@ -260,6 +303,15 @@ var SLIDE_ALLOWED_CHILD_TYPES = [
260
303
  "DragAndDrop",
261
304
  "DragTheWords",
262
305
  "MarkTheWords",
306
+ "Summary",
307
+ "ImagePairing",
308
+ "ImageSequencing",
309
+ "MemoryGame",
310
+ "InformationWall",
311
+ "ParallaxSlideshow",
312
+ "Questionnaire",
313
+ "Essay",
314
+ "ArithmeticQuiz",
263
315
  "Accordion",
264
316
  "DialogCards",
265
317
  "Flashcards",
@@ -269,6 +321,22 @@ var SLIDE_ALLOWED_CHILD_TYPES = [
269
321
  "ImageSlider"
270
322
  ];
271
323
  var SLIDE_DECK_ALLOWED_CHILD_TYPES = ["Slide"];
324
+ var TIMED_CUE_ALLOWED_CHILD_TYPES = [
325
+ "Text",
326
+ "Heading",
327
+ "Image",
328
+ "Quiz",
329
+ "TrueFalse",
330
+ "FillInTheBlanks",
331
+ "Summary",
332
+ "ImagePairing",
333
+ "ImageSequencing",
334
+ "MemoryGame",
335
+ "Questionnaire",
336
+ "Essay",
337
+ "ArithmeticQuiz"
338
+ ];
339
+ var INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES = ["TimedCue"];
272
340
  var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
273
341
  "TrueFalse",
274
342
  "FillInTheBlanks",
@@ -278,13 +346,20 @@ var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
278
346
  "Quiz",
279
347
  "KnowledgeCheck",
280
348
  "FindHotspot",
281
- "FindMultipleHotspots"
349
+ "FindMultipleHotspots",
350
+ "Summary",
351
+ "ImagePairing",
352
+ "ImageSequencing",
353
+ "ArithmeticQuiz",
354
+ "Essay"
282
355
  ];
283
356
  var ALLOWLISTS = {
284
357
  Page: PAGE_ALLOWED_CHILD_TYPES,
285
358
  InteractiveBook: INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
286
359
  Slide: SLIDE_ALLOWED_CHILD_TYPES,
287
360
  SlideDeck: SLIDE_DECK_ALLOWED_CHILD_TYPES,
361
+ TimedCue: TIMED_CUE_ALLOWED_CHILD_TYPES,
362
+ InteractiveVideo: INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
288
363
  AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES
289
364
  };
290
365
  var COMPOUND_MAX_NESTING_DEPTH = {
@@ -292,6 +367,8 @@ var COMPOUND_MAX_NESTING_DEPTH = {
292
367
  InteractiveBook: 2,
293
368
  Slide: 1,
294
369
  SlideDeck: 2,
370
+ TimedCue: 1,
371
+ InteractiveVideo: 2,
295
372
  AssessmentSequence: 1
296
373
  };
297
374
  function getAllowedChildTypes(parent) {
@@ -301,6 +378,7 @@ function isChildTypeAllowed(parent, childType) {
301
378
  return ALLOWLISTS[parent].includes(childType);
302
379
  }
303
380
  var ACCORDION_FORBIDDEN_CHILD_TYPES = ["Accordion"];
381
+ var BLOCKS_14_PAGE_SLIDE = PAGE_AND_SLIDE_14_BLOCKS;
304
382
 
305
383
  // src/telemetryCatalog.ts
306
384
  var telemetryCatalogVersion = 1;
@@ -456,6 +534,54 @@ var TELEMETRY_EVENT_CATALOG_V3 = [
456
534
  dataFields: ["blockId", "slideIndex"],
457
535
  xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
458
536
  urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
537
+ },
538
+ {
539
+ name: "video_cue_reached",
540
+ description: "Learner reached a timed cue in an Interactive Video",
541
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
542
+ dataFields: ["blockId", "cueIndex", "atSeconds", "cueLabel"],
543
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
544
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
545
+ },
546
+ {
547
+ name: "video_segment_completed",
548
+ description: "Learner completed a timed segment in an Interactive Video",
549
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
550
+ dataFields: ["blockId", "segmentIndex", "atSeconds", "segmentLabel"],
551
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
552
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
553
+ },
554
+ {
555
+ name: "memory_card_flipped",
556
+ description: "Learner flipped a memory game card",
557
+ requiredFields: ["courseId", "sessionId", "timestamp"],
558
+ dataFields: ["blockId", "cardIndex", "face"],
559
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
560
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
561
+ },
562
+ {
563
+ name: "information_wall_search",
564
+ description: "Learner searched an information wall",
565
+ requiredFields: ["courseId", "sessionId", "timestamp"],
566
+ dataFields: ["blockId", "query", "resultCount"],
567
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
568
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
569
+ },
570
+ {
571
+ name: "parallax_slide_viewed",
572
+ description: "Learner viewed a slide in a parallax slideshow",
573
+ requiredFields: ["courseId", "sessionId", "timestamp"],
574
+ dataFields: ["blockId", "slideIndex"],
575
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
576
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
577
+ },
578
+ {
579
+ name: "questionnaire_submitted",
580
+ description: "Learner submitted an unscored questionnaire",
581
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
582
+ dataFields: ["blockId", "fieldCount"],
583
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
584
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
459
585
  }
460
586
  ];
461
587
  function buildTelemetryCatalogV3() {
@@ -463,6 +589,18 @@ function buildTelemetryCatalogV3() {
463
589
  }
464
590
 
465
591
  // src/internal/sinkInvoke.ts
592
+ async function invokeTrackingSinkWithResult(sink, event) {
593
+ try {
594
+ const result = sink(event);
595
+ if (result != null && typeof result.then === "function") {
596
+ await result;
597
+ }
598
+ return true;
599
+ } catch (err) {
600
+ warnDev("[lessonkit] tracking sink failed:", err);
601
+ return false;
602
+ }
603
+ }
466
604
  function invokeTrackingSink(sink, event) {
467
605
  let result;
468
606
  try {
@@ -509,7 +647,17 @@ function createTrackingClient(opts) {
509
647
  return {
510
648
  track: (event) => {
511
649
  if (disposed2) return;
512
- if (sink) invokeTrackingSink(sink, event);
650
+ if (sink) {
651
+ try {
652
+ invokeTrackingSink(sink, event);
653
+ } catch {
654
+ }
655
+ }
656
+ },
657
+ deliver: async (event) => {
658
+ if (disposed2) return false;
659
+ if (!sink) return true;
660
+ return invokeTrackingSinkWithResult(sink, event);
513
661
  },
514
662
  dispose: () => {
515
663
  disposed2 = true;
@@ -586,21 +734,26 @@ function createTrackingClient(opts) {
586
734
  };
587
735
  intervalId = flushIntervalMs > 0 ? globalThis.setInterval(() => void flush(), flushIntervalMs) : void 0;
588
736
  intervalId?.unref?.();
589
- return {
590
- track: (event) => {
591
- if (disposed || disposing) return;
592
- if (buffer.length >= maxBufferSize) {
593
- opts?.onBufferDrop?.();
594
- if (!warnedBufferCap && isDevEnvironment()) {
595
- warnedBufferCap = true;
596
- console.warn(
597
- `[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; new events are dropped until the buffer drains.`
598
- );
599
- }
600
- return;
737
+ const track = (event) => {
738
+ if (disposed || disposing) return;
739
+ if (buffer.length >= maxBufferSize) {
740
+ opts?.onBufferDrop?.();
741
+ if (!warnedBufferCap && isDevEnvironment()) {
742
+ warnedBufferCap = true;
743
+ console.warn(
744
+ `[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; new events are dropped until the buffer drains.`
745
+ );
601
746
  }
602
- buffer.push(event);
603
- if (buffer.length >= maxBatchSize) void flush();
747
+ return;
748
+ }
749
+ buffer.push(event);
750
+ if (buffer.length >= maxBatchSize) void flush();
751
+ };
752
+ return {
753
+ track,
754
+ deliver: async (event) => {
755
+ track(event);
756
+ return flush();
604
757
  },
605
758
  flush,
606
759
  flushOnExit: opts?.exitBatchSink ? () => {
@@ -634,266 +787,6 @@ function createTrackingClient(opts) {
634
787
  };
635
788
  }
636
789
 
637
- // src/ids.ts
638
- function createSessionId() {
639
- const g = globalThis;
640
- if (g.crypto?.randomUUID) return g.crypto.randomUUID();
641
- return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
642
- }
643
-
644
- // src/time.ts
645
- function nowIso() {
646
- return (/* @__PURE__ */ new Date()).toISOString();
647
- }
648
-
649
- // src/telemetry/eventRegistry.ts
650
- function resolveLessonId(opts, eventName) {
651
- const lessonId = opts.lessonId ?? opts.data?.lessonId;
652
- if (!lessonId) throw new Error(`${eventName} requires lessonId`);
653
- return lessonId;
654
- }
655
- function withLessonScopedData(name, base, lessonId, data) {
656
- return { name, ...base, lessonId, data: { ...data, lessonId } };
657
- }
658
- var TELEMETRY_EVENT_REGISTRY = {
659
- course_started: {
660
- build: (_opts, base) => ({ name: "course_started", ...base })
661
- },
662
- course_completed: {
663
- build: (_opts, base) => ({ name: "course_completed", ...base })
664
- },
665
- lesson_started: {
666
- requiresLessonId: true,
667
- build: (opts, base) => {
668
- if (opts.name !== "lesson_started") throw new Error("unexpected event");
669
- const lessonId = resolveLessonId(opts, "lesson_started");
670
- return withLessonScopedData("lesson_started", base, lessonId, opts.data);
671
- }
672
- },
673
- lesson_completed: {
674
- requiresLessonId: true,
675
- build: (opts, base) => {
676
- if (opts.name !== "lesson_completed") throw new Error("unexpected event");
677
- const lessonId = resolveLessonId(opts, opts.name);
678
- return withLessonScopedData(opts.name, base, lessonId, opts.data);
679
- }
680
- },
681
- lesson_time_on_task: {
682
- requiresLessonId: true,
683
- build: (opts, base) => {
684
- if (opts.name !== "lesson_time_on_task") throw new Error("unexpected event");
685
- const lessonId = resolveLessonId(opts, opts.name);
686
- return withLessonScopedData(opts.name, base, lessonId, opts.data);
687
- }
688
- },
689
- quiz_answered: {
690
- requiresLessonId: true,
691
- tryBuildMissingLessonWarning: "quiz",
692
- build: (opts, base) => {
693
- if (opts.name !== "quiz_answered") throw new Error("unexpected event");
694
- const lessonId = opts.lessonId;
695
- if (!lessonId) throw new Error("quiz_answered requires active lessonId");
696
- return {
697
- name: "quiz_answered",
698
- ...base,
699
- lessonId,
700
- data: opts.data
701
- };
702
- }
703
- },
704
- quiz_completed: {
705
- requiresLessonId: true,
706
- tryBuildMissingLessonWarning: "quiz",
707
- build: (opts, base) => {
708
- if (opts.name !== "quiz_completed") throw new Error("unexpected event");
709
- const lessonId = opts.lessonId;
710
- if (!lessonId) throw new Error("quiz_completed requires active lessonId");
711
- return {
712
- name: "quiz_completed",
713
- ...base,
714
- lessonId,
715
- data: opts.data
716
- };
717
- }
718
- },
719
- assessment_answered: {
720
- requiresLessonId: true,
721
- tryBuildMissingLessonWarning: "assessment",
722
- build: (opts, base) => {
723
- if (opts.name !== "assessment_answered") throw new Error("unexpected event");
724
- const lessonId = opts.lessonId;
725
- if (!lessonId) throw new Error("assessment_answered requires active lessonId");
726
- return {
727
- name: "assessment_answered",
728
- ...base,
729
- lessonId,
730
- data: opts.data
731
- };
732
- }
733
- },
734
- assessment_completed: {
735
- requiresLessonId: true,
736
- tryBuildMissingLessonWarning: "assessment",
737
- build: (opts, base) => {
738
- if (opts.name !== "assessment_completed") throw new Error("unexpected event");
739
- const lessonId = opts.lessonId;
740
- if (!lessonId) throw new Error("assessment_completed requires active lessonId");
741
- return {
742
- name: "assessment_completed",
743
- ...base,
744
- lessonId,
745
- data: opts.data
746
- };
747
- }
748
- },
749
- interaction: {
750
- build: (opts, base) => {
751
- if (opts.name !== "interaction") throw new Error("unexpected event");
752
- return {
753
- name: "interaction",
754
- ...base,
755
- lessonId: opts.lessonId,
756
- data: opts.data
757
- };
758
- }
759
- },
760
- book_page_viewed: {
761
- requiresLessonId: true,
762
- build: (opts, base) => {
763
- if (opts.name !== "book_page_viewed") throw new Error("unexpected event");
764
- const lessonId = opts.lessonId;
765
- if (!lessonId) throw new Error("book_page_viewed requires active lessonId");
766
- return {
767
- name: "book_page_viewed",
768
- ...base,
769
- lessonId,
770
- data: opts.data
771
- };
772
- }
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
- },
788
- compound_page_viewed: {
789
- requiresLessonId: true,
790
- build: (opts, base) => {
791
- if (opts.name !== "compound_page_viewed") throw new Error("unexpected event");
792
- const lessonId = opts.lessonId;
793
- if (!lessonId) throw new Error("compound_page_viewed requires active lessonId");
794
- return {
795
- name: "compound_page_viewed",
796
- ...base,
797
- lessonId,
798
- data: opts.data
799
- };
800
- }
801
- },
802
- hotspot_opened: {
803
- build: (opts, base) => {
804
- if (opts.name !== "hotspot_opened") throw new Error("unexpected event");
805
- return {
806
- name: "hotspot_opened",
807
- ...base,
808
- lessonId: opts.lessonId,
809
- data: opts.data
810
- };
811
- }
812
- },
813
- accordion_section_toggled: {
814
- build: (opts, base) => {
815
- if (opts.name !== "accordion_section_toggled") throw new Error("unexpected event");
816
- return {
817
- name: "accordion_section_toggled",
818
- ...base,
819
- lessonId: opts.lessonId,
820
- data: opts.data
821
- };
822
- }
823
- },
824
- flashcard_flipped: {
825
- build: (opts, base) => {
826
- if (opts.name !== "flashcard_flipped") throw new Error("unexpected event");
827
- return {
828
- name: "flashcard_flipped",
829
- ...base,
830
- lessonId: opts.lessonId,
831
- data: opts.data
832
- };
833
- }
834
- },
835
- image_slider_changed: {
836
- build: (opts, base) => {
837
- if (opts.name !== "image_slider_changed") throw new Error("unexpected event");
838
- return {
839
- name: "image_slider_changed",
840
- ...base,
841
- lessonId: opts.lessonId,
842
- data: opts.data
843
- };
844
- }
845
- }
846
- };
847
- function buildTelemetryEventFromRegistry(opts) {
848
- const entry = TELEMETRY_EVENT_REGISTRY[opts.name];
849
- if (!entry) {
850
- throw new Error("Unexpected value");
851
- }
852
- const base = {
853
- timestamp: opts.timestamp ?? nowIso(),
854
- courseId: opts.courseId,
855
- sessionId: opts.sessionId,
856
- attemptId: opts.attemptId,
857
- user: opts.user
858
- };
859
- return entry.build(opts, base);
860
- }
861
- function getTelemetryEventRegistryEntry(name) {
862
- return TELEMETRY_EVENT_REGISTRY[name];
863
- }
864
-
865
- // src/telemetryBuilder.ts
866
- var warnedMissingQuizLesson = false;
867
- var warnedMissingAssessmentLesson = false;
868
- function resetTelemetryBuilderWarningsForTests() {
869
- warnedMissingQuizLesson = false;
870
- warnedMissingAssessmentLesson = false;
871
- }
872
- function buildTelemetryEvent(opts) {
873
- return buildTelemetryEventFromRegistry(opts);
874
- }
875
- function tryBuildTelemetryEvent(opts) {
876
- const entry = getTelemetryEventRegistryEntry(opts.name);
877
- if (entry.requiresLessonId && !opts.lessonId && entry.tryBuildMissingLessonWarning) {
878
- if (isDevEnvironment()) {
879
- if (entry.tryBuildMissingLessonWarning === "quiz" && !warnedMissingQuizLesson) {
880
- warnedMissingQuizLesson = true;
881
- console.warn(
882
- `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
883
- );
884
- }
885
- if (entry.tryBuildMissingLessonWarning === "assessment" && !warnedMissingAssessmentLesson) {
886
- warnedMissingAssessmentLesson = true;
887
- console.warn(
888
- `[lessonkit] ${opts.name} skipped: wrap assessment blocks in <Lesson> so an active lessonId is available`
889
- );
890
- }
891
- }
892
- return null;
893
- }
894
- return buildTelemetryEvent(opts);
895
- }
896
-
897
790
  // src/telemetryPipeline.ts
898
791
  function invokeSink(sink, event, emitCtx) {
899
792
  invokePipelineSink(sink.id, () => sink.emit(event, emitCtx));
@@ -923,109 +816,6 @@ function createTrackingPipelineSink(id, track) {
923
816
  };
924
817
  }
925
818
 
926
- // src/ports.ts
927
- function createDefaultClock() {
928
- return {
929
- nowMs: () => Date.now(),
930
- nowIso: () => (/* @__PURE__ */ new Date()).toISOString()
931
- };
932
- }
933
- function createNoopStorage() {
934
- return {
935
- getItem: () => null,
936
- setItem: () => true
937
- };
938
- }
939
- function createMemoryBackedSessionStorage(session) {
940
- const memory = /* @__PURE__ */ new Map();
941
- let warnedPersistFailure = false;
942
- const warnPersistFailure = () => {
943
- if (warnedPersistFailure) return;
944
- warnedPersistFailure = true;
945
- const g = globalThis;
946
- if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "development") {
947
- console.warn(
948
- "[lessonkit] sessionStorage is unavailable or failed; using in-memory session dedupe for this tab (may reset on full reload)."
949
- );
950
- }
951
- };
952
- return {
953
- getItem: (key) => {
954
- if (memory.has(key)) return memory.get(key);
955
- try {
956
- const value = session.getItem(key);
957
- if (value !== null) memory.set(key, value);
958
- return value;
959
- } catch {
960
- return memory.get(key) ?? null;
961
- }
962
- },
963
- setItem: (key, value) => {
964
- memory.set(key, value);
965
- try {
966
- session.setItem(key, value);
967
- return true;
968
- } catch {
969
- warnPersistFailure();
970
- return false;
971
- }
972
- },
973
- removeItem: (key) => {
974
- memory.delete(key);
975
- try {
976
- session.removeItem(key);
977
- } catch {
978
- warnPersistFailure();
979
- }
980
- },
981
- resetForTests: () => {
982
- memory.clear();
983
- }
984
- };
985
- }
986
- function resetStoragePortForTests(storage) {
987
- storage.resetForTests?.();
988
- }
989
- function createInMemorySessionStoragePort() {
990
- const memory = /* @__PURE__ */ new Map();
991
- return {
992
- getItem: (key) => memory.get(key) ?? null,
993
- setItem: (key, value) => {
994
- memory.set(key, value);
995
- return true;
996
- },
997
- removeItem: (key) => {
998
- memory.delete(key);
999
- },
1000
- resetForTests: () => {
1001
- memory.clear();
1002
- }
1003
- };
1004
- }
1005
- function resolveBrowserSessionStorage() {
1006
- try {
1007
- if (typeof sessionStorage === "undefined" || sessionStorage == null) {
1008
- return null;
1009
- }
1010
- return sessionStorage;
1011
- } catch {
1012
- return null;
1013
- }
1014
- }
1015
- function createSessionStoragePort() {
1016
- const session = resolveBrowserSessionStorage();
1017
- if (!session) {
1018
- return createInMemorySessionStoragePort();
1019
- }
1020
- return createMemoryBackedSessionStorage(session);
1021
- }
1022
- function createGlobalTimer() {
1023
- return {
1024
- setInterval: (fn, ms) => globalThis.setInterval(fn, ms),
1025
- clearInterval: (id) => globalThis.clearInterval(id)
1026
- };
1027
- }
1028
-
1029
819
  // src/progress.ts
1030
820
  function createProgressController() {
1031
821
  let activeLessonId;
@@ -1068,153 +858,6 @@ function createProgressController() {
1068
858
  };
1069
859
  }
1070
860
 
1071
- // src/session.ts
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
- }
1079
- function getTabSessionId(storage) {
1080
- return storage.getItem(SESSION_STORAGE_KEY);
1081
- }
1082
- var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
1083
- var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
1084
- var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
1085
- function resolveSessionId(storage, provided) {
1086
- if (provided !== void 0) {
1087
- const trimmed = provided.trim();
1088
- if (trimmed.length > 0) return trimmed;
1089
- }
1090
- const existing = storage.getItem(SESSION_STORAGE_KEY);
1091
- if (existing) return existing;
1092
- const volatile = volatileSessionIds.get(storage);
1093
- if (volatile) return volatile;
1094
- const id = createSessionId();
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
- }
1108
- return id;
1109
- }
1110
- function courseStartedStorageKey(sessionId, courseId) {
1111
- return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
1112
- }
1113
- function courseStartedTrackingStorageKey(sessionId, courseId) {
1114
- return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
1115
- }
1116
- function courseStartedPipelineStorageKey(sessionId, courseId) {
1117
- return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
1118
- }
1119
- function hasCourseStarted(storage, sessionId, courseId) {
1120
- if (!courseId) return false;
1121
- return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
1122
- }
1123
- function markCourseStarted(storage, sessionId, courseId) {
1124
- if (!courseId) return false;
1125
- return storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
1126
- }
1127
- function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
1128
- if (!courseId) return false;
1129
- return storage.getItem(courseStartedTrackingStorageKey(sessionId, courseId)) === "1";
1130
- }
1131
- function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
1132
- if (!courseId) return false;
1133
- return storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
1134
- }
1135
- function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
1136
- if (!courseId) return false;
1137
- return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
1138
- }
1139
- function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
1140
- if (!courseId) return false;
1141
- return storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
1142
- }
1143
- function resetSharedVolatileSessionIdForTests() {
1144
- sharedVolatileSessionId = null;
1145
- }
1146
- function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
1147
- if (!courseId || fromSessionId === toSessionId) return;
1148
- if (hasCourseStarted(storage, fromSessionId, courseId)) {
1149
- markCourseStarted(storage, toSessionId, courseId);
1150
- storage.removeItem?.(courseStartedStorageKey(fromSessionId, courseId));
1151
- }
1152
- if (hasCourseStartedEmittedToTracking(storage, fromSessionId, courseId)) {
1153
- markCourseStartedEmittedToTracking(storage, toSessionId, courseId);
1154
- storage.removeItem?.(courseStartedTrackingStorageKey(fromSessionId, courseId));
1155
- }
1156
- if (hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)) {
1157
- markCourseStartedPipelineDelivered(storage, toSessionId, courseId);
1158
- storage.removeItem?.(courseStartedPipelineStorageKey(fromSessionId, courseId));
1159
- }
1160
- }
1161
-
1162
- // src/runtime/courseLifecycle.ts
1163
- var courseStartedEmitFlights = /* @__PURE__ */ new Set();
1164
- function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
1165
- const flightKey = `${ctx.sessionId}:${ctx.courseId}`;
1166
- const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
1167
- if (alreadyEmittedToSink) {
1168
- return { emitted: true, marked };
1169
- }
1170
- if (courseStartedEmitFlights.has(flightKey)) {
1171
- return { emitted: false, marked };
1172
- }
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);
1185
- }
1186
- }
1187
- function buildCourseStartedTelemetryEvent(ctx) {
1188
- return buildTelemetryEvent({
1189
- name: "course_started",
1190
- courseId: ctx.courseId,
1191
- sessionId: ctx.sessionId,
1192
- attemptId: ctx.attemptId,
1193
- user: ctx.user
1194
- });
1195
- }
1196
- function completeLessonWithTelemetry(opts) {
1197
- const result = opts.progress.completeLesson(opts.lessonId, opts.nowMs);
1198
- if (!result.didComplete) return false;
1199
- opts.emitLessonCompleted(opts.lessonId, result.durationMs);
1200
- return true;
1201
- }
1202
- function completeCourseWithTelemetry(opts) {
1203
- const current = opts.progress.getState();
1204
- if (current.activeLessonId) {
1205
- completeLessonWithTelemetry({
1206
- progress: opts.progress,
1207
- lessonId: current.activeLessonId,
1208
- nowMs: opts.nowMs,
1209
- emitLessonCompleted: opts.emitLessonCompleted
1210
- });
1211
- }
1212
- const result = opts.progress.completeCourse();
1213
- if (!result.didComplete) return false;
1214
- opts.emitCourseCompleted();
1215
- return true;
1216
- }
1217
-
1218
861
  // src/plugins/context.ts
1219
862
  function buildPluginContext(opts) {
1220
863
  return {
@@ -1420,11 +1063,13 @@ function createLessonkitRuntime(config, ports = {}) {
1420
1063
  if (next.courseId !== void 0 && next.courseId !== previousCourseId) {
1421
1064
  progress = createProgressController();
1422
1065
  }
1423
- if (next.plugins !== void 0 && next.plugins !== pluginHost) {
1066
+ if (next.plugins !== void 0 && next.plugins !== configSnapshot.plugins) {
1424
1067
  pluginHost?.disposeAll();
1425
1068
  configSnapshot.plugins = next.plugins;
1426
1069
  pluginHost = resolvePluginHost(configSnapshot.plugins);
1427
- pluginHost?.setupAll(getPluginCtx());
1070
+ if (!configSnapshot.deferPluginSetup) {
1071
+ pluginHost?.setupAll(getPluginCtx());
1072
+ }
1428
1073
  } else if (next.session !== void 0 && sessionKeyBefore !== sessionKeyAfter && pluginHost && !configSnapshot.deferPluginSetup) {
1429
1074
  pluginHost.disposeAll();
1430
1075
  pluginHost.setupAll(getPluginCtx());
@@ -1434,17 +1079,17 @@ function createLessonkitRuntime(config, ports = {}) {
1434
1079
  const wrapped = wrapEmitFn(emitFn);
1435
1080
  const current = progress.getState();
1436
1081
  if (current.activeLessonId === lessonId) return;
1437
- if (current.completedLessonIds.has(lessonId)) {
1438
- progress.setActiveLesson(lessonId, clock.nowMs());
1439
- return;
1440
- }
1441
1082
  const previous = current.activeLessonId;
1442
- if (previous && previous !== lessonId) {
1083
+ if (previous && previous !== lessonId && !current.completedLessonIds.has(previous)) {
1443
1084
  const completed = progress.completeLesson(previous, clock.nowMs());
1444
1085
  if (completed.didComplete) {
1445
1086
  emitLessonCompletedEvents(previous, completed.durationMs, wrapped);
1446
1087
  }
1447
1088
  }
1089
+ if (current.completedLessonIds.has(lessonId)) {
1090
+ progress.setActiveLesson(lessonId, clock.nowMs());
1091
+ return;
1092
+ }
1448
1093
  progress.setActiveLesson(lessonId, clock.nowMs());
1449
1094
  wrapped("lesson_started", { lessonId }, lessonId);
1450
1095
  },
@@ -1465,9 +1110,12 @@ function createLessonkitRuntime(config, ports = {}) {
1465
1110
  });
1466
1111
  },
1467
1112
  track,
1468
- scoreAssessment(input, _lessonId) {
1113
+ scoreAssessment(input, lessonId) {
1469
1114
  if (!pluginHost) return null;
1470
- return pluginHost.scoreAssessment(input, getPluginCtx());
1115
+ return pluginHost.scoreAssessment(
1116
+ { ...input, lessonId: input.lessonId ?? lessonId },
1117
+ getPluginCtx()
1118
+ );
1471
1119
  },
1472
1120
  resetForCourseChange(nextCourseId) {
1473
1121
  configSnapshot.courseId = nextCourseId;
@@ -1493,11 +1141,13 @@ function defineLifecyclePlugin(plugin) {
1493
1141
  export {
1494
1142
  ACCORDION_FORBIDDEN_CHILD_TYPES,
1495
1143
  ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
1144
+ BLOCKS_14_PAGE_SLIDE,
1496
1145
  COMPOUND_MAX_NESTING_DEPTH,
1497
1146
  COMPOUND_RESUME_SCHEMA_VERSION,
1498
1147
  ID_MAX_LENGTH,
1499
1148
  ID_PATTERN,
1500
1149
  INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
1150
+ INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
1501
1151
  PAGE_ALLOWED_CHILD_TYPES,
1502
1152
  SESSION_STORAGE_KEY,
1503
1153
  SLIDE_ALLOWED_CHILD_TYPES,
@@ -1505,6 +1155,7 @@ export {
1505
1155
  TELEMETRY_EVENT_CATALOG,
1506
1156
  TELEMETRY_EVENT_CATALOG_V2,
1507
1157
  TELEMETRY_EVENT_CATALOG_V3,
1158
+ TIMED_CUE_ALLOWED_CHILD_TYPES,
1508
1159
  assertNever,
1509
1160
  assertValidId,
1510
1161
  buildCourseStartedTelemetryEvent,