@lessonkit/core 1.0.2 → 1.2.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
@@ -102,6 +102,122 @@ function buildLessonkitUrn(parts) {
102
102
  return urn;
103
103
  }
104
104
 
105
+ // src/compound.ts
106
+ var COMPOUND_RESUME_SCHEMA_VERSION = 1;
107
+ function createCompoundResumeState(input = {}) {
108
+ const childStates = {};
109
+ if (input.childStates) {
110
+ for (const [key, value] of Object.entries(input.childStates)) {
111
+ childStates[key] = value;
112
+ }
113
+ }
114
+ return {
115
+ schemaVersion: COMPOUND_RESUME_SCHEMA_VERSION,
116
+ activePageIndex: input.activePageIndex ?? 0,
117
+ ...input.activeChapterIndex !== void 0 ? { activeChapterIndex: input.activeChapterIndex } : {},
118
+ childStates
119
+ };
120
+ }
121
+ function clampCompoundPageIndex(index, pageCount) {
122
+ if (pageCount < 1) return 0;
123
+ return Math.min(Math.max(0, Math.floor(index)), pageCount - 1);
124
+ }
125
+ function parseCompoundResumeState(raw) {
126
+ if (!raw || typeof raw !== "object") return null;
127
+ const obj = raw;
128
+ if (obj.schemaVersion !== COMPOUND_RESUME_SCHEMA_VERSION) return null;
129
+ if (typeof obj.activePageIndex !== "number" || !Number.isFinite(obj.activePageIndex)) return null;
130
+ const childStates = {};
131
+ if (obj.childStates && typeof obj.childStates === "object" && !Array.isArray(obj.childStates)) {
132
+ for (const [key, value] of Object.entries(obj.childStates)) {
133
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
134
+ childStates[key] = value;
135
+ }
136
+ }
137
+ }
138
+ const activeChapterIndex = typeof obj.activeChapterIndex === "number" && Number.isFinite(obj.activeChapterIndex) ? obj.activeChapterIndex : void 0;
139
+ return {
140
+ schemaVersion: COMPOUND_RESUME_SCHEMA_VERSION,
141
+ activePageIndex: Math.max(0, Math.floor(obj.activePageIndex)),
142
+ ...activeChapterIndex !== void 0 ? { activeChapterIndex: Math.max(0, Math.floor(activeChapterIndex)) } : {},
143
+ childStates
144
+ };
145
+ }
146
+
147
+ // src/compoundState.ts
148
+ var COMPOUND_STATE_PREFIX = "lessonkit:compound:";
149
+ function compoundStateStorageKey(courseId, compoundId) {
150
+ return `${COMPOUND_STATE_PREFIX}${courseId}:${compoundId}`;
151
+ }
152
+ function loadCompoundState(storage, courseId, compoundId) {
153
+ const raw = storage.getItem(compoundStateStorageKey(courseId, compoundId));
154
+ if (!raw) return null;
155
+ try {
156
+ return parseCompoundResumeState(JSON.parse(raw));
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+ function saveCompoundState(storage, courseId, compoundId, state) {
162
+ storage.setItem(compoundStateStorageKey(courseId, compoundId), JSON.stringify(state));
163
+ }
164
+ function clearCompoundState(storage, courseId, compoundId) {
165
+ storage.removeItem?.(compoundStateStorageKey(courseId, compoundId));
166
+ }
167
+
168
+ // src/compoundAllowlists.ts
169
+ var PAGE_ALLOWED_CHILD_TYPES = [
170
+ "Text",
171
+ "Heading",
172
+ "Image",
173
+ "Scenario",
174
+ "Reflection",
175
+ "Quiz",
176
+ "KnowledgeCheck",
177
+ "TrueFalse",
178
+ "FillInTheBlanks",
179
+ "DragAndDrop",
180
+ "DragTheWords",
181
+ "MarkTheWords",
182
+ "Accordion",
183
+ "DialogCards",
184
+ "Flashcards",
185
+ "ImageHotspots",
186
+ "FindHotspot",
187
+ "FindMultipleHotspots",
188
+ "ImageSlider",
189
+ "ProgressTracker"
190
+ ];
191
+ var INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES = ["Page"];
192
+ var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
193
+ "TrueFalse",
194
+ "FillInTheBlanks",
195
+ "DragAndDrop",
196
+ "DragTheWords",
197
+ "MarkTheWords",
198
+ "Quiz",
199
+ "KnowledgeCheck",
200
+ "FindHotspot",
201
+ "FindMultipleHotspots"
202
+ ];
203
+ var ALLOWLISTS = {
204
+ Page: PAGE_ALLOWED_CHILD_TYPES,
205
+ InteractiveBook: INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
206
+ AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES
207
+ };
208
+ var COMPOUND_MAX_NESTING_DEPTH = {
209
+ Page: 1,
210
+ InteractiveBook: 2,
211
+ AssessmentSequence: 1
212
+ };
213
+ function getAllowedChildTypes(parent) {
214
+ return ALLOWLISTS[parent];
215
+ }
216
+ function isChildTypeAllowed(parent, childType) {
217
+ return ALLOWLISTS[parent].includes(childType);
218
+ }
219
+ var ACCORDION_FORBIDDEN_CHILD_TYPES = ["Accordion"];
220
+
105
221
  // src/telemetryCatalog.ts
106
222
  var telemetryCatalogVersion = 1;
107
223
  var TELEMETRY_EVENT_CATALOG = [
@@ -174,35 +290,125 @@ function buildTelemetryCatalog() {
174
290
  return TELEMETRY_EVENT_CATALOG.map((entry) => ({ ...entry }));
175
291
  }
176
292
 
177
- // src/trackingClient.ts
293
+ // src/telemetryCatalogV2.ts
294
+ var telemetryCatalogV2Version = 2;
295
+ var TELEMETRY_EVENT_CATALOG_V2 = [
296
+ {
297
+ name: "assessment_answered",
298
+ description: "Learner submitted an assessment interaction answer",
299
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
300
+ dataFields: ["checkId", "interactionType", "question", "response", "correct"],
301
+ xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
302
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}"
303
+ },
304
+ {
305
+ name: "assessment_completed",
306
+ description: "Assessment interaction completed (passing criteria met)",
307
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
308
+ dataFields: ["checkId", "interactionType", "score", "maxScore", "passingScore"],
309
+ xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
310
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}"
311
+ }
312
+ ];
313
+ function buildTelemetryCatalogV2() {
314
+ return TELEMETRY_EVENT_CATALOG_V2.map((entry) => ({ ...entry }));
315
+ }
316
+
317
+ // src/telemetryCatalogV3.ts
318
+ var telemetryCatalogV3Version = 3;
319
+ var TELEMETRY_EVENT_CATALOG_V3 = [
320
+ {
321
+ name: "book_page_viewed",
322
+ description: "Learner viewed a page/chapter in an Interactive Book",
323
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
324
+ dataFields: ["blockId", "pageIndex", "pageTitle"],
325
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
326
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
327
+ },
328
+ {
329
+ name: "compound_page_viewed",
330
+ description: "Learner activated a page inside a compound container",
331
+ requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
332
+ dataFields: ["blockId", "pageIndex", "parentType"],
333
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
334
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
335
+ },
336
+ {
337
+ name: "hotspot_opened",
338
+ description: "Learner opened an image hotspot popover",
339
+ requiredFields: ["courseId", "sessionId", "timestamp"],
340
+ dataFields: ["blockId", "hotspotId"],
341
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
342
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
343
+ },
344
+ {
345
+ name: "accordion_section_toggled",
346
+ description: "Learner expanded or collapsed an accordion section",
347
+ requiredFields: ["courseId", "sessionId", "timestamp"],
348
+ dataFields: ["blockId", "sectionId", "expanded"],
349
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
350
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
351
+ },
352
+ {
353
+ name: "flashcard_flipped",
354
+ description: "Learner flipped a flashcard",
355
+ requiredFields: ["courseId", "sessionId", "timestamp"],
356
+ dataFields: ["blockId", "cardIndex", "face"],
357
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
358
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
359
+ },
360
+ {
361
+ name: "image_slider_changed",
362
+ description: "Learner changed the active slide in an image slider",
363
+ requiredFields: ["courseId", "sessionId", "timestamp"],
364
+ dataFields: ["blockId", "slideIndex"],
365
+ xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
366
+ urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
367
+ }
368
+ ];
369
+ function buildTelemetryCatalogV3() {
370
+ return TELEMETRY_EVENT_CATALOG_V3.map((entry) => ({ ...entry }));
371
+ }
372
+
373
+ // src/internal/env.ts
178
374
  function isDevEnvironment() {
179
375
  const g = globalThis;
180
376
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
181
377
  }
378
+ function warnDev(message, err) {
379
+ if (!isDevEnvironment()) return;
380
+ console.warn(message, err instanceof Error ? err.message : err);
381
+ }
382
+
383
+ // src/internal/sinkInvoke.ts
182
384
  function invokeTrackingSink(sink, event) {
183
385
  let result;
184
386
  try {
185
387
  result = sink(event);
186
388
  } catch (err) {
187
- if (isDevEnvironment()) {
188
- console.warn(
189
- "[lessonkit] tracking sink failed:",
190
- err instanceof Error ? err.message : err
191
- );
192
- }
389
+ warnDev("[lessonkit] tracking sink failed:", err);
193
390
  throw err;
194
391
  }
195
392
  if (result != null && typeof result.catch === "function") {
196
- void result.catch((err) => {
197
- if (isDevEnvironment()) {
198
- console.warn(
199
- "[lessonkit] tracking sink failed:",
200
- err instanceof Error ? err.message : err
201
- );
202
- }
203
- });
393
+ void result.catch((err) => warnDev("[lessonkit] tracking sink failed:", err));
204
394
  }
205
395
  }
396
+ function invokePipelineSink(sinkId, emit) {
397
+ let result;
398
+ try {
399
+ result = emit();
400
+ } catch (err) {
401
+ warnDev(`[lessonkit] telemetry sink "${sinkId}" failed:`, err);
402
+ return;
403
+ }
404
+ if (result != null && typeof result.catch === "function") {
405
+ void result.catch(
406
+ (err) => warnDev(`[lessonkit] telemetry sink "${sinkId}" failed:`, err)
407
+ );
408
+ }
409
+ }
410
+
411
+ // src/trackingClient.ts
206
412
  function createTrackingClient(opts) {
207
413
  const sink = opts?.sink;
208
414
  const batchSink = opts?.batchSink;
@@ -320,81 +526,234 @@ function nowIso() {
320
526
  return (/* @__PURE__ */ new Date()).toISOString();
321
527
  }
322
528
 
323
- // src/telemetryBuilder.ts
324
- var warnedMissingQuizLesson = false;
325
- function isDevEnvironment2() {
326
- const g = globalThis;
327
- return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
328
- }
329
- function resetTelemetryBuilderWarningsForTests() {
330
- warnedMissingQuizLesson = false;
331
- }
529
+ // src/telemetry/eventRegistry.ts
332
530
  function resolveLessonId(opts, eventName) {
333
531
  const lessonId = opts.lessonId ?? opts.data?.lessonId;
334
532
  if (!lessonId) throw new Error(`${eventName} requires lessonId`);
335
533
  return lessonId;
336
534
  }
337
- function buildTelemetryEvent(opts) {
338
- const base = {
339
- timestamp: opts.timestamp ?? nowIso(),
340
- courseId: opts.courseId,
341
- sessionId: opts.sessionId,
342
- attemptId: opts.attemptId,
343
- user: opts.user
344
- };
345
- switch (opts.name) {
346
- case "course_started":
347
- return { name: "course_started", ...base };
348
- case "course_completed":
349
- return { name: "course_completed", ...base };
350
- case "lesson_started": {
535
+ function withLessonScopedData(name, base, lessonId, data) {
536
+ return { name, ...base, lessonId, data: { ...data, lessonId } };
537
+ }
538
+ var TELEMETRY_EVENT_REGISTRY = {
539
+ course_started: {
540
+ build: (_opts, base) => ({ name: "course_started", ...base })
541
+ },
542
+ course_completed: {
543
+ build: (_opts, base) => ({ name: "course_completed", ...base })
544
+ },
545
+ lesson_started: {
546
+ requiresLessonId: true,
547
+ build: (opts, base) => {
548
+ if (opts.name !== "lesson_started") throw new Error("unexpected event");
351
549
  const lessonId = resolveLessonId(opts, "lesson_started");
550
+ return withLessonScopedData("lesson_started", base, lessonId, opts.data);
551
+ }
552
+ },
553
+ lesson_completed: {
554
+ requiresLessonId: true,
555
+ build: (opts, base) => {
556
+ if (opts.name !== "lesson_completed") throw new Error("unexpected event");
557
+ const lessonId = resolveLessonId(opts, opts.name);
558
+ return withLessonScopedData(opts.name, base, lessonId, opts.data);
559
+ }
560
+ },
561
+ lesson_time_on_task: {
562
+ requiresLessonId: true,
563
+ build: (opts, base) => {
564
+ if (opts.name !== "lesson_time_on_task") throw new Error("unexpected event");
565
+ const lessonId = resolveLessonId(opts, opts.name);
566
+ return withLessonScopedData(opts.name, base, lessonId, opts.data);
567
+ }
568
+ },
569
+ quiz_answered: {
570
+ requiresLessonId: true,
571
+ tryBuildMissingLessonWarning: "quiz",
572
+ build: (opts, base) => {
573
+ if (opts.name !== "quiz_answered") throw new Error("unexpected event");
574
+ const lessonId = opts.lessonId;
575
+ if (!lessonId) throw new Error("quiz_answered requires active lessonId");
352
576
  return {
353
- name: "lesson_started",
577
+ name: "quiz_answered",
354
578
  ...base,
355
579
  lessonId,
356
- data: { ...opts.data, lessonId }
580
+ data: opts.data
357
581
  };
358
582
  }
359
- case "lesson_completed":
360
- case "lesson_time_on_task": {
361
- const lessonId = resolveLessonId(opts, opts.name);
583
+ },
584
+ quiz_completed: {
585
+ requiresLessonId: true,
586
+ tryBuildMissingLessonWarning: "quiz",
587
+ build: (opts, base) => {
588
+ if (opts.name !== "quiz_completed") throw new Error("unexpected event");
589
+ const lessonId = opts.lessonId;
590
+ if (!lessonId) throw new Error("quiz_completed requires active lessonId");
362
591
  return {
363
- name: opts.name,
592
+ name: "quiz_completed",
364
593
  ...base,
365
594
  lessonId,
366
- data: { ...opts.data, lessonId }
595
+ data: opts.data
367
596
  };
368
597
  }
369
- case "quiz_answered": {
598
+ },
599
+ assessment_answered: {
600
+ requiresLessonId: true,
601
+ tryBuildMissingLessonWarning: "assessment",
602
+ build: (opts, base) => {
603
+ if (opts.name !== "assessment_answered") throw new Error("unexpected event");
370
604
  const lessonId = opts.lessonId;
371
- if (!lessonId) throw new Error("quiz_answered requires active lessonId");
372
- return { name: "quiz_answered", ...base, lessonId, data: opts.data };
605
+ if (!lessonId) throw new Error("assessment_answered requires active lessonId");
606
+ return {
607
+ name: "assessment_answered",
608
+ ...base,
609
+ lessonId,
610
+ data: opts.data
611
+ };
373
612
  }
374
- case "quiz_completed": {
613
+ },
614
+ assessment_completed: {
615
+ requiresLessonId: true,
616
+ tryBuildMissingLessonWarning: "assessment",
617
+ build: (opts, base) => {
618
+ if (opts.name !== "assessment_completed") throw new Error("unexpected event");
375
619
  const lessonId = opts.lessonId;
376
- if (!lessonId) throw new Error("quiz_completed requires active lessonId");
377
- return { name: "quiz_completed", ...base, lessonId, data: opts.data };
620
+ if (!lessonId) throw new Error("assessment_completed requires active lessonId");
621
+ return {
622
+ name: "assessment_completed",
623
+ ...base,
624
+ lessonId,
625
+ data: opts.data
626
+ };
378
627
  }
379
- case "interaction":
628
+ },
629
+ interaction: {
630
+ build: (opts, base) => {
631
+ if (opts.name !== "interaction") throw new Error("unexpected event");
380
632
  return {
381
633
  name: "interaction",
382
634
  ...base,
383
635
  lessonId: opts.lessonId,
384
636
  data: opts.data
385
637
  };
386
- default:
387
- return assertNever(opts);
638
+ }
639
+ },
640
+ book_page_viewed: {
641
+ requiresLessonId: true,
642
+ build: (opts, base) => {
643
+ if (opts.name !== "book_page_viewed") throw new Error("unexpected event");
644
+ const lessonId = opts.lessonId;
645
+ if (!lessonId) throw new Error("book_page_viewed requires active lessonId");
646
+ return {
647
+ name: "book_page_viewed",
648
+ ...base,
649
+ lessonId,
650
+ data: opts.data
651
+ };
652
+ }
653
+ },
654
+ compound_page_viewed: {
655
+ requiresLessonId: true,
656
+ build: (opts, base) => {
657
+ if (opts.name !== "compound_page_viewed") throw new Error("unexpected event");
658
+ const lessonId = opts.lessonId;
659
+ if (!lessonId) throw new Error("compound_page_viewed requires active lessonId");
660
+ return {
661
+ name: "compound_page_viewed",
662
+ ...base,
663
+ lessonId,
664
+ data: opts.data
665
+ };
666
+ }
667
+ },
668
+ hotspot_opened: {
669
+ build: (opts, base) => {
670
+ if (opts.name !== "hotspot_opened") throw new Error("unexpected event");
671
+ return {
672
+ name: "hotspot_opened",
673
+ ...base,
674
+ lessonId: opts.lessonId,
675
+ data: opts.data
676
+ };
677
+ }
678
+ },
679
+ accordion_section_toggled: {
680
+ build: (opts, base) => {
681
+ if (opts.name !== "accordion_section_toggled") throw new Error("unexpected event");
682
+ return {
683
+ name: "accordion_section_toggled",
684
+ ...base,
685
+ lessonId: opts.lessonId,
686
+ data: opts.data
687
+ };
688
+ }
689
+ },
690
+ flashcard_flipped: {
691
+ build: (opts, base) => {
692
+ if (opts.name !== "flashcard_flipped") throw new Error("unexpected event");
693
+ return {
694
+ name: "flashcard_flipped",
695
+ ...base,
696
+ lessonId: opts.lessonId,
697
+ data: opts.data
698
+ };
699
+ }
700
+ },
701
+ image_slider_changed: {
702
+ build: (opts, base) => {
703
+ if (opts.name !== "image_slider_changed") throw new Error("unexpected event");
704
+ return {
705
+ name: "image_slider_changed",
706
+ ...base,
707
+ lessonId: opts.lessonId,
708
+ data: opts.data
709
+ };
710
+ }
388
711
  }
712
+ };
713
+ function buildTelemetryEventFromRegistry(opts) {
714
+ const entry = TELEMETRY_EVENT_REGISTRY[opts.name];
715
+ if (!entry) {
716
+ throw new Error("Unexpected value");
717
+ }
718
+ const base = {
719
+ timestamp: opts.timestamp ?? nowIso(),
720
+ courseId: opts.courseId,
721
+ sessionId: opts.sessionId,
722
+ attemptId: opts.attemptId,
723
+ user: opts.user
724
+ };
725
+ return entry.build(opts, base);
726
+ }
727
+ function getTelemetryEventRegistryEntry(name) {
728
+ return TELEMETRY_EVENT_REGISTRY[name];
729
+ }
730
+
731
+ // src/telemetryBuilder.ts
732
+ var warnedMissingQuizLesson = false;
733
+ var warnedMissingAssessmentLesson = false;
734
+ function resetTelemetryBuilderWarningsForTests() {
735
+ warnedMissingQuizLesson = false;
736
+ warnedMissingAssessmentLesson = false;
737
+ }
738
+ function buildTelemetryEvent(opts) {
739
+ return buildTelemetryEventFromRegistry(opts);
389
740
  }
390
741
  function tryBuildTelemetryEvent(opts) {
391
- const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
392
- if (isQuiz && !opts.lessonId) {
393
- if (isDevEnvironment2() && !warnedMissingQuizLesson) {
394
- warnedMissingQuizLesson = true;
395
- console.warn(
396
- `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
397
- );
742
+ const entry = getTelemetryEventRegistryEntry(opts.name);
743
+ if (entry.requiresLessonId && !opts.lessonId && entry.tryBuildMissingLessonWarning) {
744
+ if (isDevEnvironment()) {
745
+ if (entry.tryBuildMissingLessonWarning === "quiz" && !warnedMissingQuizLesson) {
746
+ warnedMissingQuizLesson = true;
747
+ console.warn(
748
+ `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
749
+ );
750
+ }
751
+ if (entry.tryBuildMissingLessonWarning === "assessment" && !warnedMissingAssessmentLesson) {
752
+ warnedMissingAssessmentLesson = true;
753
+ console.warn(
754
+ `[lessonkit] ${opts.name} skipped: wrap assessment blocks in <Lesson> so an active lessonId is available`
755
+ );
756
+ }
398
757
  }
399
758
  return null;
400
759
  }
@@ -402,29 +761,8 @@ function tryBuildTelemetryEvent(opts) {
402
761
  }
403
762
 
404
763
  // src/telemetryPipeline.ts
405
- function isDevEnvironment3() {
406
- const g = globalThis;
407
- return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
408
- }
409
- function warnSinkFailure(sinkId, err) {
410
- if (isDevEnvironment3()) {
411
- console.warn(
412
- `[lessonkit] telemetry sink "${sinkId}" failed:`,
413
- err instanceof Error ? err.message : err
414
- );
415
- }
416
- }
417
764
  function invokeSink(sink, event, emitCtx) {
418
- let result;
419
- try {
420
- result = sink.emit(event, emitCtx);
421
- } catch (err) {
422
- warnSinkFailure(sink.id, err);
423
- return;
424
- }
425
- if (result != null && typeof result.catch === "function") {
426
- void result.catch((err) => warnSinkFailure(sink.id, err));
427
- }
765
+ invokePipelineSink(sink.id, () => sink.emit(event, emitCtx));
428
766
  }
429
767
  function createTelemetryPipeline(sinks) {
430
768
  const list = [...sinks];
@@ -700,101 +1038,13 @@ function completeCourseWithTelemetry(opts) {
700
1038
  return true;
701
1039
  }
702
1040
 
703
- // src/runtime/createLessonkitRuntime.ts
704
- function createLessonkitRuntime(config, ports = {}) {
705
- const storage = ports.storage ?? createSessionStoragePort();
706
- const clock = ports.clock ?? createDefaultClock();
707
- const configSnapshot = { ...config };
708
- let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
709
- let attemptId = configSnapshot.session?.attemptId;
710
- let user = configSnapshot.session?.user;
711
- let courseId = configSnapshot.courseId;
712
- let progress = createProgressController();
713
- const getSession = () => ({ sessionId, attemptId, user });
714
- const syncSessionFromConfig = (next) => {
715
- sessionId = resolveSessionId(storage, next.session?.sessionId);
716
- attemptId = next.session?.attemptId;
717
- user = next.session?.user;
718
- courseId = next.courseId;
719
- };
720
- syncSessionFromConfig(configSnapshot);
721
- const track = (name, data, emit, lessonId) => {
722
- const event = tryBuildTelemetryEvent({
723
- name,
724
- courseId,
725
- lessonId: lessonId ?? progress.getState().activeLessonId,
726
- sessionId,
727
- attemptId,
728
- user,
729
- data
730
- });
731
- if (!event) return;
732
- emit(event);
733
- };
734
- const emitLessonCompleted = (lessonId, durationMs, emitFn) => {
735
- emitFn("lesson_completed", { lessonId, durationMs }, lessonId);
736
- if (durationMs !== void 0) {
737
- emitFn("lesson_time_on_task", { lessonId, durationMs }, lessonId);
738
- }
739
- };
1041
+ // src/plugins/context.ts
1042
+ function buildPluginContext(opts) {
740
1043
  return {
741
- get config() {
742
- return configSnapshot;
743
- },
744
- get progress() {
745
- return progress;
746
- },
747
- getProgressState: () => progress.getState(),
748
- getSession,
749
- updateConfig(next) {
750
- if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
751
- if (next.runtimeVersion !== void 0) configSnapshot.runtimeVersion = next.runtimeVersion;
752
- if (next.plugins !== void 0) configSnapshot.plugins = next.plugins;
753
- if (next.session !== void 0) {
754
- configSnapshot.session = { ...configSnapshot.session, ...next.session };
755
- }
756
- syncSessionFromConfig(configSnapshot);
757
- },
758
- setActiveLesson(lessonId, emitFn) {
759
- const current = progress.getState();
760
- if (current.activeLessonId === lessonId) return;
761
- if (current.completedLessonIds.has(lessonId)) {
762
- progress.setActiveLesson(lessonId, clock.nowMs());
763
- return;
764
- }
765
- const previous = current.activeLessonId;
766
- if (previous && previous !== lessonId) {
767
- const completed = progress.completeLesson(previous, clock.nowMs());
768
- if (completed.didComplete) {
769
- emitLessonCompleted(previous, completed.durationMs, emitFn);
770
- }
771
- }
772
- progress.setActiveLesson(lessonId, clock.nowMs());
773
- emitFn("lesson_started", { lessonId }, lessonId);
774
- },
775
- completeLesson(lessonId, emitFn) {
776
- const result = progress.completeLesson(lessonId, clock.nowMs());
777
- if (!result.didComplete) return;
778
- emitLessonCompleted(lessonId, result.durationMs, emitFn);
779
- },
780
- completeCourse(emitFn) {
781
- const current = progress.getState();
782
- if (current.activeLessonId) {
783
- const lessonResult = progress.completeLesson(current.activeLessonId, clock.nowMs());
784
- if (lessonResult.didComplete) {
785
- emitLessonCompleted(current.activeLessonId, lessonResult.durationMs, emitFn);
786
- }
787
- }
788
- const result = progress.completeCourse();
789
- if (!result.didComplete) return;
790
- emitFn("course_completed");
791
- },
792
- track,
793
- resetForCourseChange(nextCourseId) {
794
- configSnapshot.courseId = nextCourseId;
795
- courseId = nextCourseId;
796
- progress = createProgressController();
797
- }
1044
+ courseId: opts.courseId,
1045
+ sessionId: opts.sessionId,
1046
+ attemptId: opts.attemptId,
1047
+ user: opts.user
798
1048
  };
799
1049
  }
800
1050
 
@@ -885,6 +1135,161 @@ function createPluginRegistry(plugins = []) {
885
1135
  };
886
1136
  }
887
1137
 
1138
+ // src/runtime/createLessonkitRuntime.ts
1139
+ function resolvePluginHost(plugins) {
1140
+ if (!plugins) return null;
1141
+ if (typeof plugins === "object" && "runTelemetry" in plugins) return plugins;
1142
+ if (Array.isArray(plugins) && plugins.length > 0) return createPluginRegistry(plugins);
1143
+ return null;
1144
+ }
1145
+ function warnRuntimeV1Deprecated() {
1146
+ const g = globalThis;
1147
+ if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
1148
+ console.warn(
1149
+ '[lessonkit] runtimeVersion "v1" is deprecated; use "v2" (default). v1 will be removed in LessonKit 2.0.'
1150
+ );
1151
+ }
1152
+ function createLessonkitRuntime(config, ports = {}) {
1153
+ if (config.runtimeVersion === "v1") warnRuntimeV1Deprecated();
1154
+ const storage = ports.storage ?? createSessionStoragePort();
1155
+ const clock = ports.clock ?? createDefaultClock();
1156
+ const configSnapshot = { ...config };
1157
+ let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
1158
+ let attemptId = configSnapshot.session?.attemptId;
1159
+ let user = configSnapshot.session?.user;
1160
+ let courseId = configSnapshot.courseId;
1161
+ let progress = createProgressController();
1162
+ let pluginHost = resolvePluginHost(configSnapshot.plugins);
1163
+ const getPluginCtx = () => buildPluginContext({
1164
+ courseId,
1165
+ sessionId,
1166
+ attemptId,
1167
+ user
1168
+ });
1169
+ const getSession = () => ({ sessionId, attemptId, user });
1170
+ const syncSessionFromConfig = (next) => {
1171
+ sessionId = resolveSessionId(storage, next.session?.sessionId);
1172
+ attemptId = next.session?.attemptId;
1173
+ user = next.session?.user;
1174
+ courseId = next.courseId;
1175
+ };
1176
+ const applyPluginsToEvent = (event) => {
1177
+ if (!pluginHost) return event;
1178
+ return pluginHost.runTelemetry(event, getPluginCtx());
1179
+ };
1180
+ const buildAndApply = (name, data, lessonId) => {
1181
+ const event = tryBuildTelemetryEvent({
1182
+ name,
1183
+ courseId,
1184
+ lessonId: lessonId ?? progress.getState().activeLessonId,
1185
+ sessionId,
1186
+ attemptId,
1187
+ user,
1188
+ data
1189
+ });
1190
+ if (!event) return null;
1191
+ return applyPluginsToEvent(event);
1192
+ };
1193
+ const wrapEmitFn = (emitFn) => {
1194
+ return (name, data, lessonId) => {
1195
+ const event = buildAndApply(name, data, lessonId);
1196
+ if (event === null) return;
1197
+ const eventLessonId = "lessonId" in event ? event.lessonId : lessonId;
1198
+ const eventData = "data" in event ? event.data : data;
1199
+ emitFn(event.name, eventData, eventLessonId);
1200
+ };
1201
+ };
1202
+ syncSessionFromConfig(configSnapshot);
1203
+ const track = (name, data, emit, lessonId) => {
1204
+ const event = buildAndApply(name, data, lessonId);
1205
+ if (!event) return;
1206
+ emit(event);
1207
+ };
1208
+ const emitLessonCompletedEvents = (lessonId, durationMs, emitFn) => {
1209
+ const wrapped = wrapEmitFn(emitFn);
1210
+ wrapped("lesson_completed", { lessonId, durationMs }, lessonId);
1211
+ if (durationMs !== void 0) {
1212
+ wrapped("lesson_time_on_task", { lessonId, durationMs }, lessonId);
1213
+ }
1214
+ };
1215
+ return {
1216
+ get config() {
1217
+ return configSnapshot;
1218
+ },
1219
+ get progress() {
1220
+ return progress;
1221
+ },
1222
+ get pluginHost() {
1223
+ return pluginHost;
1224
+ },
1225
+ getProgressState: () => progress.getState(),
1226
+ getSession,
1227
+ 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
+ }
1233
+ if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
1234
+ if (next.runtimeVersion !== void 0) {
1235
+ if (next.runtimeVersion === "v1") warnRuntimeV1Deprecated();
1236
+ configSnapshot.runtimeVersion = next.runtimeVersion;
1237
+ }
1238
+ if (next.session !== void 0) {
1239
+ configSnapshot.session = { ...configSnapshot.session, ...next.session };
1240
+ }
1241
+ syncSessionFromConfig(configSnapshot);
1242
+ },
1243
+ setActiveLesson(lessonId, emitFn) {
1244
+ const wrapped = wrapEmitFn(emitFn);
1245
+ const current = progress.getState();
1246
+ if (current.activeLessonId === lessonId) return;
1247
+ if (current.completedLessonIds.has(lessonId)) {
1248
+ progress.setActiveLesson(lessonId, clock.nowMs());
1249
+ return;
1250
+ }
1251
+ const previous = current.activeLessonId;
1252
+ if (previous && previous !== lessonId) {
1253
+ const completed = progress.completeLesson(previous, clock.nowMs());
1254
+ if (completed.didComplete) {
1255
+ emitLessonCompletedEvents(previous, completed.durationMs, wrapped);
1256
+ }
1257
+ }
1258
+ progress.setActiveLesson(lessonId, clock.nowMs());
1259
+ wrapped("lesson_started", { lessonId }, lessonId);
1260
+ },
1261
+ completeLesson(lessonId, emitFn) {
1262
+ completeLessonWithTelemetry({
1263
+ progress,
1264
+ lessonId,
1265
+ nowMs: clock.nowMs(),
1266
+ emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, wrapEmitFn(emitFn))
1267
+ });
1268
+ },
1269
+ completeCourse(emitFn) {
1270
+ completeCourseWithTelemetry({
1271
+ progress,
1272
+ nowMs: clock.nowMs(),
1273
+ emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, wrapEmitFn(emitFn)),
1274
+ emitCourseCompleted: () => wrapEmitFn(emitFn)("course_completed")
1275
+ });
1276
+ },
1277
+ track,
1278
+ scoreAssessment(input, _lessonId) {
1279
+ if (!pluginHost) return null;
1280
+ return pluginHost.scoreAssessment(input, getPluginCtx());
1281
+ },
1282
+ resetForCourseChange(nextCourseId) {
1283
+ configSnapshot.courseId = nextCourseId;
1284
+ courseId = nextCourseId;
1285
+ progress = createProgressController();
1286
+ },
1287
+ dispose() {
1288
+ pluginHost?.disposeAll();
1289
+ }
1290
+ };
1291
+ }
1292
+
888
1293
  // src/plugins/define.ts
889
1294
  function defineTelemetryPlugin(plugin) {
890
1295
  return plugin;
@@ -896,18 +1301,33 @@ function defineLifecyclePlugin(plugin) {
896
1301
  return plugin;
897
1302
  }
898
1303
  export {
1304
+ ACCORDION_FORBIDDEN_CHILD_TYPES,
1305
+ ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
1306
+ COMPOUND_MAX_NESTING_DEPTH,
1307
+ COMPOUND_RESUME_SCHEMA_VERSION,
899
1308
  ID_MAX_LENGTH,
900
1309
  ID_PATTERN,
1310
+ INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
1311
+ PAGE_ALLOWED_CHILD_TYPES,
901
1312
  SESSION_STORAGE_KEY,
902
1313
  TELEMETRY_EVENT_CATALOG,
1314
+ TELEMETRY_EVENT_CATALOG_V2,
1315
+ TELEMETRY_EVENT_CATALOG_V3,
903
1316
  assertNever,
904
1317
  assertValidId,
905
1318
  buildCourseStartedTelemetryEvent,
906
1319
  buildLessonkitUrn,
1320
+ buildPluginContext,
907
1321
  buildTelemetryCatalog,
1322
+ buildTelemetryCatalogV2,
1323
+ buildTelemetryCatalogV3,
908
1324
  buildTelemetryEvent,
1325
+ clampCompoundPageIndex,
1326
+ clearCompoundState,
909
1327
  completeCourseWithTelemetry,
910
1328
  completeLessonWithTelemetry,
1329
+ compoundStateStorageKey,
1330
+ createCompoundResumeState,
911
1331
  createDefaultClock,
912
1332
  createGlobalTimer,
913
1333
  createLessonkitRuntime,
@@ -923,10 +1343,13 @@ export {
923
1343
  defineLifecyclePlugin,
924
1344
  defineTelemetryPlugin,
925
1345
  deriveId,
1346
+ getAllowedChildTypes,
926
1347
  getTabSessionId,
927
1348
  hasCourseStarted,
928
1349
  hasCourseStartedEmittedToTracking,
929
1350
  hasCourseStartedPipelineDelivered,
1351
+ isChildTypeAllowed,
1352
+ loadCompoundState,
930
1353
  markCourseStarted,
931
1354
  markCourseStartedEmittedToTracking,
932
1355
  markCourseStartedPipelineDelivered,
@@ -934,12 +1357,16 @@ export {
934
1357
  nowIso,
935
1358
  parseBlockId,
936
1359
  parseCheckId,
1360
+ parseCompoundResumeState,
937
1361
  parseCourseId,
938
1362
  parseLessonId,
939
1363
  resetStoragePortForTests,
940
1364
  resetTelemetryBuilderWarningsForTests,
941
1365
  resolveSessionId,
1366
+ saveCompoundState,
942
1367
  slugifyId,
1368
+ telemetryCatalogV2Version,
1369
+ telemetryCatalogV3Version,
943
1370
  telemetryCatalogVersion,
944
1371
  tryBuildTelemetryEvent,
945
1372
  tryEmitCourseStarted,