@lessonkit/core 1.3.0 → 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;
@@ -522,12 +670,14 @@ function createTrackingClient(opts) {
522
670
  }
523
671
  const buffer = [];
524
672
  let flushInFlight = null;
673
+ let inflightExitBatch = null;
525
674
  let disposed = false;
526
675
  let disposing = false;
527
676
  let intervalId;
528
677
  const runFlush = () => {
529
678
  if (!buffer.length) return Promise.resolve(true);
530
679
  const events = buffer.splice(0, buffer.length);
680
+ inflightExitBatch = events;
531
681
  let succeeded = false;
532
682
  return Promise.resolve().then(async () => {
533
683
  if (batchSink) {
@@ -552,6 +702,8 @@ function createTrackingClient(opts) {
552
702
  return runFlush();
553
703
  }
554
704
  return succeeded;
705
+ }).finally(() => {
706
+ inflightExitBatch = null;
555
707
  });
556
708
  };
557
709
  const flush = () => {
@@ -582,23 +734,44 @@ function createTrackingClient(opts) {
582
734
  };
583
735
  intervalId = flushIntervalMs > 0 ? globalThis.setInterval(() => void flush(), flushIntervalMs) : void 0;
584
736
  intervalId?.unref?.();
585
- return {
586
- track: (event) => {
587
- if (disposed || disposing) return;
588
- if (buffer.length >= maxBufferSize) {
589
- opts?.onBufferDrop?.();
590
- if (!warnedBufferCap && isDevEnvironment()) {
591
- warnedBufferCap = true;
592
- console.warn(
593
- `[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; new events are dropped until the buffer drains.`
594
- );
595
- }
596
- 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
+ );
597
746
  }
598
- buffer.push(event);
599
- 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();
600
757
  },
601
758
  flush,
759
+ flushOnExit: opts?.exitBatchSink ? () => {
760
+ const fromBuffer = buffer.splice(0, buffer.length);
761
+ const fromInflight = inflightExitBatch ? [...inflightExitBatch] : [];
762
+ const events = [...fromInflight, ...fromBuffer];
763
+ if (!events.length) return;
764
+ try {
765
+ const result = opts.exitBatchSink(events);
766
+ if (result != null && typeof result.catch === "function") {
767
+ void result.catch(() => {
768
+ buffer.unshift(...events);
769
+ });
770
+ }
771
+ } catch {
772
+ buffer.unshift(...events);
773
+ }
774
+ } : void 0,
602
775
  dispose: () => {
603
776
  if (disposed || disposing) return Promise.resolve();
604
777
  disposing = true;
@@ -614,266 +787,6 @@ function createTrackingClient(opts) {
614
787
  };
615
788
  }
616
789
 
617
- // src/ids.ts
618
- function createSessionId() {
619
- const g = globalThis;
620
- if (g.crypto?.randomUUID) return g.crypto.randomUUID();
621
- return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
622
- }
623
-
624
- // src/time.ts
625
- function nowIso() {
626
- return (/* @__PURE__ */ new Date()).toISOString();
627
- }
628
-
629
- // src/telemetry/eventRegistry.ts
630
- function resolveLessonId(opts, eventName) {
631
- const lessonId = opts.lessonId ?? opts.data?.lessonId;
632
- if (!lessonId) throw new Error(`${eventName} requires lessonId`);
633
- return lessonId;
634
- }
635
- function withLessonScopedData(name, base, lessonId, data) {
636
- return { name, ...base, lessonId, data: { ...data, lessonId } };
637
- }
638
- var TELEMETRY_EVENT_REGISTRY = {
639
- course_started: {
640
- build: (_opts, base) => ({ name: "course_started", ...base })
641
- },
642
- course_completed: {
643
- build: (_opts, base) => ({ name: "course_completed", ...base })
644
- },
645
- lesson_started: {
646
- requiresLessonId: true,
647
- build: (opts, base) => {
648
- if (opts.name !== "lesson_started") throw new Error("unexpected event");
649
- const lessonId = resolveLessonId(opts, "lesson_started");
650
- return withLessonScopedData("lesson_started", base, lessonId, opts.data);
651
- }
652
- },
653
- lesson_completed: {
654
- requiresLessonId: true,
655
- build: (opts, base) => {
656
- if (opts.name !== "lesson_completed") throw new Error("unexpected event");
657
- const lessonId = resolveLessonId(opts, opts.name);
658
- return withLessonScopedData(opts.name, base, lessonId, opts.data);
659
- }
660
- },
661
- lesson_time_on_task: {
662
- requiresLessonId: true,
663
- build: (opts, base) => {
664
- if (opts.name !== "lesson_time_on_task") throw new Error("unexpected event");
665
- const lessonId = resolveLessonId(opts, opts.name);
666
- return withLessonScopedData(opts.name, base, lessonId, opts.data);
667
- }
668
- },
669
- quiz_answered: {
670
- requiresLessonId: true,
671
- tryBuildMissingLessonWarning: "quiz",
672
- build: (opts, base) => {
673
- if (opts.name !== "quiz_answered") throw new Error("unexpected event");
674
- const lessonId = opts.lessonId;
675
- if (!lessonId) throw new Error("quiz_answered requires active lessonId");
676
- return {
677
- name: "quiz_answered",
678
- ...base,
679
- lessonId,
680
- data: opts.data
681
- };
682
- }
683
- },
684
- quiz_completed: {
685
- requiresLessonId: true,
686
- tryBuildMissingLessonWarning: "quiz",
687
- build: (opts, base) => {
688
- if (opts.name !== "quiz_completed") throw new Error("unexpected event");
689
- const lessonId = opts.lessonId;
690
- if (!lessonId) throw new Error("quiz_completed requires active lessonId");
691
- return {
692
- name: "quiz_completed",
693
- ...base,
694
- lessonId,
695
- data: opts.data
696
- };
697
- }
698
- },
699
- assessment_answered: {
700
- requiresLessonId: true,
701
- tryBuildMissingLessonWarning: "assessment",
702
- build: (opts, base) => {
703
- if (opts.name !== "assessment_answered") throw new Error("unexpected event");
704
- const lessonId = opts.lessonId;
705
- if (!lessonId) throw new Error("assessment_answered requires active lessonId");
706
- return {
707
- name: "assessment_answered",
708
- ...base,
709
- lessonId,
710
- data: opts.data
711
- };
712
- }
713
- },
714
- assessment_completed: {
715
- requiresLessonId: true,
716
- tryBuildMissingLessonWarning: "assessment",
717
- build: (opts, base) => {
718
- if (opts.name !== "assessment_completed") throw new Error("unexpected event");
719
- const lessonId = opts.lessonId;
720
- if (!lessonId) throw new Error("assessment_completed requires active lessonId");
721
- return {
722
- name: "assessment_completed",
723
- ...base,
724
- lessonId,
725
- data: opts.data
726
- };
727
- }
728
- },
729
- interaction: {
730
- build: (opts, base) => {
731
- if (opts.name !== "interaction") throw new Error("unexpected event");
732
- return {
733
- name: "interaction",
734
- ...base,
735
- lessonId: opts.lessonId,
736
- data: opts.data
737
- };
738
- }
739
- },
740
- book_page_viewed: {
741
- requiresLessonId: true,
742
- build: (opts, base) => {
743
- if (opts.name !== "book_page_viewed") throw new Error("unexpected event");
744
- const lessonId = opts.lessonId;
745
- if (!lessonId) throw new Error("book_page_viewed requires active lessonId");
746
- return {
747
- name: "book_page_viewed",
748
- ...base,
749
- lessonId,
750
- data: opts.data
751
- };
752
- }
753
- },
754
- slide_viewed: {
755
- requiresLessonId: true,
756
- build: (opts, base) => {
757
- if (opts.name !== "slide_viewed") throw new Error("unexpected event");
758
- const lessonId = opts.lessonId;
759
- if (!lessonId) throw new Error("slide_viewed requires active lessonId");
760
- return {
761
- name: "slide_viewed",
762
- ...base,
763
- lessonId,
764
- data: opts.data
765
- };
766
- }
767
- },
768
- compound_page_viewed: {
769
- requiresLessonId: true,
770
- build: (opts, base) => {
771
- if (opts.name !== "compound_page_viewed") throw new Error("unexpected event");
772
- const lessonId = opts.lessonId;
773
- if (!lessonId) throw new Error("compound_page_viewed requires active lessonId");
774
- return {
775
- name: "compound_page_viewed",
776
- ...base,
777
- lessonId,
778
- data: opts.data
779
- };
780
- }
781
- },
782
- hotspot_opened: {
783
- build: (opts, base) => {
784
- if (opts.name !== "hotspot_opened") throw new Error("unexpected event");
785
- return {
786
- name: "hotspot_opened",
787
- ...base,
788
- lessonId: opts.lessonId,
789
- data: opts.data
790
- };
791
- }
792
- },
793
- accordion_section_toggled: {
794
- build: (opts, base) => {
795
- if (opts.name !== "accordion_section_toggled") throw new Error("unexpected event");
796
- return {
797
- name: "accordion_section_toggled",
798
- ...base,
799
- lessonId: opts.lessonId,
800
- data: opts.data
801
- };
802
- }
803
- },
804
- flashcard_flipped: {
805
- build: (opts, base) => {
806
- if (opts.name !== "flashcard_flipped") throw new Error("unexpected event");
807
- return {
808
- name: "flashcard_flipped",
809
- ...base,
810
- lessonId: opts.lessonId,
811
- data: opts.data
812
- };
813
- }
814
- },
815
- image_slider_changed: {
816
- build: (opts, base) => {
817
- if (opts.name !== "image_slider_changed") throw new Error("unexpected event");
818
- return {
819
- name: "image_slider_changed",
820
- ...base,
821
- lessonId: opts.lessonId,
822
- data: opts.data
823
- };
824
- }
825
- }
826
- };
827
- function buildTelemetryEventFromRegistry(opts) {
828
- const entry = TELEMETRY_EVENT_REGISTRY[opts.name];
829
- if (!entry) {
830
- throw new Error("Unexpected value");
831
- }
832
- const base = {
833
- timestamp: opts.timestamp ?? nowIso(),
834
- courseId: opts.courseId,
835
- sessionId: opts.sessionId,
836
- attemptId: opts.attemptId,
837
- user: opts.user
838
- };
839
- return entry.build(opts, base);
840
- }
841
- function getTelemetryEventRegistryEntry(name) {
842
- return TELEMETRY_EVENT_REGISTRY[name];
843
- }
844
-
845
- // src/telemetryBuilder.ts
846
- var warnedMissingQuizLesson = false;
847
- var warnedMissingAssessmentLesson = false;
848
- function resetTelemetryBuilderWarningsForTests() {
849
- warnedMissingQuizLesson = false;
850
- warnedMissingAssessmentLesson = false;
851
- }
852
- function buildTelemetryEvent(opts) {
853
- return buildTelemetryEventFromRegistry(opts);
854
- }
855
- function tryBuildTelemetryEvent(opts) {
856
- const entry = getTelemetryEventRegistryEntry(opts.name);
857
- if (entry.requiresLessonId && !opts.lessonId && entry.tryBuildMissingLessonWarning) {
858
- if (isDevEnvironment()) {
859
- if (entry.tryBuildMissingLessonWarning === "quiz" && !warnedMissingQuizLesson) {
860
- warnedMissingQuizLesson = true;
861
- console.warn(
862
- `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
863
- );
864
- }
865
- if (entry.tryBuildMissingLessonWarning === "assessment" && !warnedMissingAssessmentLesson) {
866
- warnedMissingAssessmentLesson = true;
867
- console.warn(
868
- `[lessonkit] ${opts.name} skipped: wrap assessment blocks in <Lesson> so an active lessonId is available`
869
- );
870
- }
871
- }
872
- return null;
873
- }
874
- return buildTelemetryEvent(opts);
875
- }
876
-
877
790
  // src/telemetryPipeline.ts
878
791
  function invokeSink(sink, event, emitCtx) {
879
792
  invokePipelineSink(sink.id, () => sink.emit(event, emitCtx));
@@ -903,109 +816,6 @@ function createTrackingPipelineSink(id, track) {
903
816
  };
904
817
  }
905
818
 
906
- // src/ports.ts
907
- function createDefaultClock() {
908
- return {
909
- nowMs: () => Date.now(),
910
- nowIso: () => (/* @__PURE__ */ new Date()).toISOString()
911
- };
912
- }
913
- function createNoopStorage() {
914
- return {
915
- getItem: () => null,
916
- setItem: () => true
917
- };
918
- }
919
- function createMemoryBackedSessionStorage(session) {
920
- const memory = /* @__PURE__ */ new Map();
921
- let warnedPersistFailure = false;
922
- const warnPersistFailure = () => {
923
- if (warnedPersistFailure) return;
924
- warnedPersistFailure = true;
925
- const g = globalThis;
926
- if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "development") {
927
- console.warn(
928
- "[lessonkit] sessionStorage is unavailable or failed; using in-memory session dedupe for this tab (may reset on full reload)."
929
- );
930
- }
931
- };
932
- return {
933
- getItem: (key) => {
934
- if (memory.has(key)) return memory.get(key);
935
- try {
936
- const value = session.getItem(key);
937
- if (value !== null) memory.set(key, value);
938
- return value;
939
- } catch {
940
- return memory.get(key) ?? null;
941
- }
942
- },
943
- setItem: (key, value) => {
944
- memory.set(key, value);
945
- try {
946
- session.setItem(key, value);
947
- return true;
948
- } catch {
949
- warnPersistFailure();
950
- return false;
951
- }
952
- },
953
- removeItem: (key) => {
954
- memory.delete(key);
955
- try {
956
- session.removeItem(key);
957
- } catch {
958
- warnPersistFailure();
959
- }
960
- },
961
- resetForTests: () => {
962
- memory.clear();
963
- }
964
- };
965
- }
966
- function resetStoragePortForTests(storage) {
967
- storage.resetForTests?.();
968
- }
969
- function createInMemorySessionStoragePort() {
970
- const memory = /* @__PURE__ */ new Map();
971
- return {
972
- getItem: (key) => memory.get(key) ?? null,
973
- setItem: (key, value) => {
974
- memory.set(key, value);
975
- return true;
976
- },
977
- removeItem: (key) => {
978
- memory.delete(key);
979
- },
980
- resetForTests: () => {
981
- memory.clear();
982
- }
983
- };
984
- }
985
- function resolveBrowserSessionStorage() {
986
- try {
987
- if (typeof sessionStorage === "undefined" || sessionStorage == null) {
988
- return null;
989
- }
990
- return sessionStorage;
991
- } catch {
992
- return null;
993
- }
994
- }
995
- function createSessionStoragePort() {
996
- const session = resolveBrowserSessionStorage();
997
- if (!session) {
998
- return createInMemorySessionStoragePort();
999
- }
1000
- return createMemoryBackedSessionStorage(session);
1001
- }
1002
- function createGlobalTimer() {
1003
- return {
1004
- setInterval: (fn, ms) => globalThis.setInterval(fn, ms),
1005
- clearInterval: (id) => globalThis.clearInterval(id)
1006
- };
1007
- }
1008
-
1009
819
  // src/progress.ts
1010
820
  function createProgressController() {
1011
821
  let activeLessonId;
@@ -1048,143 +858,6 @@ function createProgressController() {
1048
858
  };
1049
859
  }
1050
860
 
1051
- // src/session.ts
1052
- var SESSION_STORAGE_KEY = "lessonkit:sessionId";
1053
- var volatileSessionIds = /* @__PURE__ */ new WeakMap();
1054
- var sharedVolatileSessionId = null;
1055
- function isDevEnvironment2() {
1056
- const g = globalThis;
1057
- return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
1058
- }
1059
- function getTabSessionId(storage) {
1060
- return storage.getItem(SESSION_STORAGE_KEY);
1061
- }
1062
- var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
1063
- var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
1064
- var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
1065
- function resolveSessionId(storage, provided) {
1066
- if (provided !== void 0) {
1067
- const trimmed = provided.trim();
1068
- if (trimmed.length > 0) return trimmed;
1069
- }
1070
- const existing = storage.getItem(SESSION_STORAGE_KEY);
1071
- if (existing) return existing;
1072
- const volatile = volatileSessionIds.get(storage);
1073
- if (volatile) return volatile;
1074
- const id = createSessionId();
1075
- const persisted = storage.setItem(SESSION_STORAGE_KEY, id);
1076
- if (!persisted) {
1077
- if (!sharedVolatileSessionId) {
1078
- sharedVolatileSessionId = id;
1079
- }
1080
- volatileSessionIds.set(storage, sharedVolatileSessionId);
1081
- if (isDevEnvironment2()) {
1082
- console.warn(
1083
- "[lessonkit] session id could not be persisted; reusing in-memory id for this tab."
1084
- );
1085
- }
1086
- return sharedVolatileSessionId;
1087
- }
1088
- return id;
1089
- }
1090
- function courseStartedStorageKey(sessionId, courseId) {
1091
- return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
1092
- }
1093
- function courseStartedTrackingStorageKey(sessionId, courseId) {
1094
- return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
1095
- }
1096
- function courseStartedPipelineStorageKey(sessionId, courseId) {
1097
- return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
1098
- }
1099
- function hasCourseStarted(storage, sessionId, courseId) {
1100
- if (!courseId) return false;
1101
- return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
1102
- }
1103
- function markCourseStarted(storage, sessionId, courseId) {
1104
- if (!courseId) return false;
1105
- return storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
1106
- }
1107
- function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
1108
- if (!courseId) return false;
1109
- return storage.getItem(courseStartedTrackingStorageKey(sessionId, courseId)) === "1";
1110
- }
1111
- function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
1112
- if (!courseId) return;
1113
- storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
1114
- }
1115
- function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
1116
- if (!courseId) return false;
1117
- return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
1118
- }
1119
- function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
1120
- if (!courseId) return;
1121
- storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
1122
- }
1123
- function resetSharedVolatileSessionIdForTests() {
1124
- sharedVolatileSessionId = null;
1125
- }
1126
- function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
1127
- if (!courseId || fromSessionId === toSessionId) return;
1128
- if (hasCourseStarted(storage, fromSessionId, courseId)) {
1129
- markCourseStarted(storage, toSessionId, courseId);
1130
- storage.removeItem?.(courseStartedStorageKey(fromSessionId, courseId));
1131
- }
1132
- if (hasCourseStartedEmittedToTracking(storage, fromSessionId, courseId)) {
1133
- markCourseStartedEmittedToTracking(storage, toSessionId, courseId);
1134
- storage.removeItem?.(courseStartedTrackingStorageKey(fromSessionId, courseId));
1135
- }
1136
- if (hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)) {
1137
- markCourseStartedPipelineDelivered(storage, toSessionId, courseId);
1138
- storage.removeItem?.(courseStartedPipelineStorageKey(fromSessionId, courseId));
1139
- }
1140
- }
1141
-
1142
- // src/runtime/courseLifecycle.ts
1143
- function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
1144
- const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
1145
- if (alreadyEmittedToSink) {
1146
- return { emitted: true, marked };
1147
- }
1148
- const emitted = deps.emitCourseStartedEvent(ctx);
1149
- if (emitted && !marked) {
1150
- markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
1151
- }
1152
- return {
1153
- emitted,
1154
- marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
1155
- };
1156
- }
1157
- function buildCourseStartedTelemetryEvent(ctx) {
1158
- return buildTelemetryEvent({
1159
- name: "course_started",
1160
- courseId: ctx.courseId,
1161
- sessionId: ctx.sessionId,
1162
- attemptId: ctx.attemptId,
1163
- user: ctx.user
1164
- });
1165
- }
1166
- function completeLessonWithTelemetry(opts) {
1167
- const result = opts.progress.completeLesson(opts.lessonId, opts.nowMs);
1168
- if (!result.didComplete) return false;
1169
- opts.emitLessonCompleted(opts.lessonId, result.durationMs);
1170
- return true;
1171
- }
1172
- function completeCourseWithTelemetry(opts) {
1173
- const current = opts.progress.getState();
1174
- if (current.activeLessonId) {
1175
- completeLessonWithTelemetry({
1176
- progress: opts.progress,
1177
- lessonId: current.activeLessonId,
1178
- nowMs: opts.nowMs,
1179
- emitLessonCompleted: opts.emitLessonCompleted
1180
- });
1181
- }
1182
- const result = opts.progress.completeCourse();
1183
- if (!result.didComplete) return false;
1184
- opts.emitCourseCompleted();
1185
- return true;
1186
- }
1187
-
1188
861
  // src/plugins/context.ts
1189
862
  function buildPluginContext(opts) {
1190
863
  return {
@@ -1390,11 +1063,13 @@ function createLessonkitRuntime(config, ports = {}) {
1390
1063
  if (next.courseId !== void 0 && next.courseId !== previousCourseId) {
1391
1064
  progress = createProgressController();
1392
1065
  }
1393
- if (next.plugins !== void 0 && next.plugins !== pluginHost) {
1066
+ if (next.plugins !== void 0 && next.plugins !== configSnapshot.plugins) {
1394
1067
  pluginHost?.disposeAll();
1395
1068
  configSnapshot.plugins = next.plugins;
1396
1069
  pluginHost = resolvePluginHost(configSnapshot.plugins);
1397
- pluginHost?.setupAll(getPluginCtx());
1070
+ if (!configSnapshot.deferPluginSetup) {
1071
+ pluginHost?.setupAll(getPluginCtx());
1072
+ }
1398
1073
  } else if (next.session !== void 0 && sessionKeyBefore !== sessionKeyAfter && pluginHost && !configSnapshot.deferPluginSetup) {
1399
1074
  pluginHost.disposeAll();
1400
1075
  pluginHost.setupAll(getPluginCtx());
@@ -1404,17 +1079,17 @@ function createLessonkitRuntime(config, ports = {}) {
1404
1079
  const wrapped = wrapEmitFn(emitFn);
1405
1080
  const current = progress.getState();
1406
1081
  if (current.activeLessonId === lessonId) return;
1407
- if (current.completedLessonIds.has(lessonId)) {
1408
- progress.setActiveLesson(lessonId, clock.nowMs());
1409
- return;
1410
- }
1411
1082
  const previous = current.activeLessonId;
1412
- if (previous && previous !== lessonId) {
1083
+ if (previous && previous !== lessonId && !current.completedLessonIds.has(previous)) {
1413
1084
  const completed = progress.completeLesson(previous, clock.nowMs());
1414
1085
  if (completed.didComplete) {
1415
1086
  emitLessonCompletedEvents(previous, completed.durationMs, wrapped);
1416
1087
  }
1417
1088
  }
1089
+ if (current.completedLessonIds.has(lessonId)) {
1090
+ progress.setActiveLesson(lessonId, clock.nowMs());
1091
+ return;
1092
+ }
1418
1093
  progress.setActiveLesson(lessonId, clock.nowMs());
1419
1094
  wrapped("lesson_started", { lessonId }, lessonId);
1420
1095
  },
@@ -1435,9 +1110,12 @@ function createLessonkitRuntime(config, ports = {}) {
1435
1110
  });
1436
1111
  },
1437
1112
  track,
1438
- scoreAssessment(input, _lessonId) {
1113
+ scoreAssessment(input, lessonId) {
1439
1114
  if (!pluginHost) return null;
1440
- return pluginHost.scoreAssessment(input, getPluginCtx());
1115
+ return pluginHost.scoreAssessment(
1116
+ { ...input, lessonId: input.lessonId ?? lessonId },
1117
+ getPluginCtx()
1118
+ );
1441
1119
  },
1442
1120
  resetForCourseChange(nextCourseId) {
1443
1121
  configSnapshot.courseId = nextCourseId;
@@ -1463,11 +1141,13 @@ function defineLifecyclePlugin(plugin) {
1463
1141
  export {
1464
1142
  ACCORDION_FORBIDDEN_CHILD_TYPES,
1465
1143
  ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
1144
+ BLOCKS_14_PAGE_SLIDE,
1466
1145
  COMPOUND_MAX_NESTING_DEPTH,
1467
1146
  COMPOUND_RESUME_SCHEMA_VERSION,
1468
1147
  ID_MAX_LENGTH,
1469
1148
  ID_PATTERN,
1470
1149
  INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
1150
+ INTERACTIVE_VIDEO_ALLOWED_CHILD_TYPES,
1471
1151
  PAGE_ALLOWED_CHILD_TYPES,
1472
1152
  SESSION_STORAGE_KEY,
1473
1153
  SLIDE_ALLOWED_CHILD_TYPES,
@@ -1475,6 +1155,7 @@ export {
1475
1155
  TELEMETRY_EVENT_CATALOG,
1476
1156
  TELEMETRY_EVENT_CATALOG_V2,
1477
1157
  TELEMETRY_EVENT_CATALOG_V3,
1158
+ TIMED_CUE_ALLOWED_CHILD_TYPES,
1478
1159
  assertNever,
1479
1160
  assertValidId,
1480
1161
  buildCourseStartedTelemetryEvent,