@lessonkit/core 0.9.3 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -22,18 +22,51 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ID_MAX_LENGTH: () => ID_MAX_LENGTH,
24
24
  ID_PATTERN: () => ID_PATTERN,
25
+ SESSION_STORAGE_KEY: () => SESSION_STORAGE_KEY,
25
26
  TELEMETRY_EVENT_CATALOG: () => TELEMETRY_EVENT_CATALOG,
27
+ assertNever: () => assertNever,
26
28
  assertValidId: () => assertValidId,
29
+ buildCourseStartedTelemetryEvent: () => buildCourseStartedTelemetryEvent,
27
30
  buildLessonkitUrn: () => buildLessonkitUrn,
28
31
  buildTelemetryCatalog: () => buildTelemetryCatalog,
29
- createPluginHost: () => createPluginHost,
32
+ buildTelemetryEvent: () => buildTelemetryEvent,
33
+ completeCourseWithTelemetry: () => completeCourseWithTelemetry,
34
+ completeLessonWithTelemetry: () => completeLessonWithTelemetry,
35
+ createDefaultClock: () => createDefaultClock,
36
+ createGlobalTimer: () => createGlobalTimer,
37
+ createLessonkitRuntime: () => createLessonkitRuntime,
38
+ createNoopStorage: () => createNoopStorage,
39
+ createPluginRegistry: () => createPluginRegistry,
40
+ createProgressController: () => createProgressController,
30
41
  createSessionId: () => createSessionId,
42
+ createSessionStoragePort: () => createSessionStoragePort,
43
+ createTelemetryPipeline: () => createTelemetryPipeline,
31
44
  createTrackingClient: () => createTrackingClient,
32
- defineLessonkitPlugin: () => defineLessonkitPlugin,
45
+ createTrackingPipelineSink: () => createTrackingPipelineSink,
46
+ defineAssessmentPlugin: () => defineAssessmentPlugin,
47
+ defineLifecyclePlugin: () => defineLifecyclePlugin,
48
+ defineTelemetryPlugin: () => defineTelemetryPlugin,
33
49
  deriveId: () => deriveId,
50
+ getTabSessionId: () => getTabSessionId,
51
+ hasCourseStarted: () => hasCourseStarted,
52
+ hasCourseStartedEmittedToTracking: () => hasCourseStartedEmittedToTracking,
53
+ hasCourseStartedPipelineDelivered: () => hasCourseStartedPipelineDelivered,
54
+ markCourseStarted: () => markCourseStarted,
55
+ markCourseStartedEmittedToTracking: () => markCourseStartedEmittedToTracking,
56
+ markCourseStartedPipelineDelivered: () => markCourseStartedPipelineDelivered,
57
+ migrateCourseStartedMark: () => migrateCourseStartedMark,
34
58
  nowIso: () => nowIso,
59
+ parseBlockId: () => parseBlockId,
60
+ parseCheckId: () => parseCheckId,
61
+ parseCourseId: () => parseCourseId,
62
+ parseLessonId: () => parseLessonId,
63
+ resetStoragePortForTests: () => resetStoragePortForTests,
64
+ resetTelemetryBuilderWarningsForTests: () => resetTelemetryBuilderWarningsForTests,
65
+ resolveSessionId: () => resolveSessionId,
35
66
  slugifyId: () => slugifyId,
36
67
  telemetryCatalogVersion: () => telemetryCatalogVersion,
68
+ tryBuildTelemetryEvent: () => tryBuildTelemetryEvent,
69
+ tryEmitCourseStarted: () => tryEmitCourseStarted,
37
70
  validateId: () => validateId
38
71
  });
39
72
  module.exports = __toCommonJS(index_exports);
@@ -42,6 +75,11 @@ module.exports = __toCommonJS(index_exports);
42
75
  var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
43
76
  var ID_MAX_LENGTH = 64;
44
77
 
78
+ // src/assertNever.ts
79
+ function assertNever(value, message = "Unexpected value") {
80
+ throw new Error(`${message}: ${String(value)}`);
81
+ }
82
+
45
83
  // src/validateId.ts
46
84
  function validateId(input, path = "id") {
47
85
  if (typeof input !== "string") {
@@ -70,6 +108,22 @@ function validateId(input, path = "id") {
70
108
  }
71
109
  return { ok: true, id };
72
110
  }
111
+ function parseCourseId(input) {
112
+ const result = validateId(input, "courseId");
113
+ return result.ok ? result.id : null;
114
+ }
115
+ function parseLessonId(input) {
116
+ const result = validateId(input, "lessonId");
117
+ return result.ok ? result.id : null;
118
+ }
119
+ function parseCheckId(input) {
120
+ const result = validateId(input, "checkId");
121
+ return result.ok ? result.id : null;
122
+ }
123
+ function parseBlockId(input) {
124
+ const result = validateId(input, "blockId");
125
+ return result.ok ? result.id : null;
126
+ }
73
127
  function assertValidId(input, path = "id") {
74
128
  const result = validateId(input, path);
75
129
  if (!result.ok) {
@@ -194,18 +248,48 @@ function buildTelemetryCatalog() {
194
248
  }
195
249
 
196
250
  // src/trackingClient.ts
251
+ function isDevEnvironment() {
252
+ const g = globalThis;
253
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
254
+ }
255
+ function invokeTrackingSink(sink, event) {
256
+ let result;
257
+ try {
258
+ result = sink(event);
259
+ } catch (err) {
260
+ if (isDevEnvironment()) {
261
+ console.warn(
262
+ "[lessonkit] tracking sink failed:",
263
+ err instanceof Error ? err.message : err
264
+ );
265
+ }
266
+ throw err;
267
+ }
268
+ if (result != null && typeof result.catch === "function") {
269
+ void result.catch((err) => {
270
+ if (isDevEnvironment()) {
271
+ console.warn(
272
+ "[lessonkit] tracking sink failed:",
273
+ err instanceof Error ? err.message : err
274
+ );
275
+ }
276
+ });
277
+ }
278
+ }
197
279
  function createTrackingClient(opts) {
198
280
  const sink = opts?.sink;
199
281
  const batchSink = opts?.batchSink;
200
282
  const batchEnabled = opts?.batch?.enabled ?? Boolean(batchSink);
201
283
  const flushIntervalMs = opts?.batch?.flushIntervalMs ?? 5e3;
202
284
  const maxBatchSize = opts?.batch?.maxBatchSize ?? 25;
285
+ const maxBufferSize = 1e3;
286
+ let warnedBufferCap = false;
203
287
  if (!batchEnabled) {
204
288
  let disposed2 = false;
205
289
  return {
206
290
  track: (event) => {
207
291
  if (disposed2) return;
208
- void sink?.(event);
292
+ if (sink) invokeTrackingSink(sink, event);
209
293
  },
210
294
  dispose: () => {
211
295
  disposed2 = true;
@@ -264,6 +348,15 @@ function createTrackingClient(opts) {
264
348
  return {
265
349
  track: (event) => {
266
350
  if (disposed || disposing) return;
351
+ if (buffer.length >= maxBufferSize) {
352
+ buffer.shift();
353
+ if (!warnedBufferCap && typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
354
+ warnedBufferCap = true;
355
+ console.warn(
356
+ `[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; oldest events are dropped while the sink is unavailable.`
357
+ );
358
+ }
359
+ }
267
360
  buffer.push(event);
268
361
  if (buffer.length >= maxBatchSize) void flush();
269
362
  },
@@ -295,16 +388,476 @@ function nowIso() {
295
388
  return (/* @__PURE__ */ new Date()).toISOString();
296
389
  }
297
390
 
298
- // src/plugins.ts
299
- function defineLessonkitPlugin(plugin) {
300
- return plugin;
391
+ // src/telemetryBuilder.ts
392
+ var warnedMissingQuizLesson = false;
393
+ function isDevEnvironment2() {
394
+ const g = globalThis;
395
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
396
+ }
397
+ function resetTelemetryBuilderWarningsForTests() {
398
+ warnedMissingQuizLesson = false;
399
+ }
400
+ function resolveLessonId(opts, eventName) {
401
+ const lessonId = opts.lessonId ?? opts.data?.lessonId;
402
+ if (!lessonId) throw new Error(`${eventName} requires lessonId`);
403
+ return lessonId;
404
+ }
405
+ function buildTelemetryEvent(opts) {
406
+ const base = {
407
+ timestamp: opts.timestamp ?? nowIso(),
408
+ courseId: opts.courseId,
409
+ sessionId: opts.sessionId,
410
+ attemptId: opts.attemptId,
411
+ user: opts.user
412
+ };
413
+ switch (opts.name) {
414
+ case "course_started":
415
+ return { name: "course_started", ...base };
416
+ case "course_completed":
417
+ return { name: "course_completed", ...base };
418
+ case "lesson_started": {
419
+ const lessonId = resolveLessonId(opts, "lesson_started");
420
+ return {
421
+ name: "lesson_started",
422
+ ...base,
423
+ lessonId,
424
+ data: { ...opts.data, lessonId }
425
+ };
426
+ }
427
+ case "lesson_completed":
428
+ case "lesson_time_on_task": {
429
+ const lessonId = resolveLessonId(opts, opts.name);
430
+ return {
431
+ name: opts.name,
432
+ ...base,
433
+ lessonId,
434
+ data: { ...opts.data, lessonId }
435
+ };
436
+ }
437
+ case "quiz_answered": {
438
+ const lessonId = opts.lessonId;
439
+ if (!lessonId) throw new Error("quiz_answered requires active lessonId");
440
+ return { name: "quiz_answered", ...base, lessonId, data: opts.data };
441
+ }
442
+ case "quiz_completed": {
443
+ const lessonId = opts.lessonId;
444
+ if (!lessonId) throw new Error("quiz_completed requires active lessonId");
445
+ return { name: "quiz_completed", ...base, lessonId, data: opts.data };
446
+ }
447
+ case "interaction":
448
+ return {
449
+ name: "interaction",
450
+ ...base,
451
+ lessonId: opts.lessonId,
452
+ data: opts.data
453
+ };
454
+ default:
455
+ return assertNever(opts);
456
+ }
457
+ }
458
+ function tryBuildTelemetryEvent(opts) {
459
+ const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
460
+ if (isQuiz && !opts.lessonId) {
461
+ if (isDevEnvironment2() && !warnedMissingQuizLesson) {
462
+ warnedMissingQuizLesson = true;
463
+ console.warn(
464
+ `[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
465
+ );
466
+ }
467
+ return null;
468
+ }
469
+ return buildTelemetryEvent(opts);
470
+ }
471
+
472
+ // src/telemetryPipeline.ts
473
+ function isDevEnvironment3() {
474
+ const g = globalThis;
475
+ return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
476
+ }
477
+ function warnSinkFailure(sinkId, err) {
478
+ if (isDevEnvironment3()) {
479
+ console.warn(
480
+ `[lessonkit] telemetry sink "${sinkId}" failed:`,
481
+ err instanceof Error ? err.message : err
482
+ );
483
+ }
484
+ }
485
+ function invokeSink(sink, event, emitCtx) {
486
+ let result;
487
+ try {
488
+ result = sink.emit(event, emitCtx);
489
+ } catch (err) {
490
+ warnSinkFailure(sink.id, err);
491
+ return;
492
+ }
493
+ if (result != null && typeof result.catch === "function") {
494
+ void result.catch((err) => warnSinkFailure(sink.id, err));
495
+ }
496
+ }
497
+ function createTelemetryPipeline(sinks) {
498
+ const list = [...sinks];
499
+ return {
500
+ sinks: list,
501
+ emit(event, ctx) {
502
+ const emitCtx = ctx ?? {
503
+ courseId: event.courseId,
504
+ sessionId: event.sessionId,
505
+ attemptId: event.attemptId
506
+ };
507
+ for (const sink of list) {
508
+ invokeSink(sink, event, emitCtx);
509
+ }
510
+ }
511
+ };
512
+ }
513
+ function createTrackingPipelineSink(id, track) {
514
+ return {
515
+ id,
516
+ emit(event) {
517
+ track(event);
518
+ }
519
+ };
520
+ }
521
+
522
+ // src/ports.ts
523
+ function createDefaultClock() {
524
+ return {
525
+ nowMs: () => Date.now(),
526
+ nowIso: () => (/* @__PURE__ */ new Date()).toISOString()
527
+ };
528
+ }
529
+ function createNoopStorage() {
530
+ return {
531
+ getItem: () => null,
532
+ setItem: () => {
533
+ }
534
+ };
535
+ }
536
+ function createMemoryBackedSessionStorage(session) {
537
+ const memory = /* @__PURE__ */ new Map();
538
+ let warnedPersistFailure = false;
539
+ const warnPersistFailure = () => {
540
+ if (warnedPersistFailure) return;
541
+ warnedPersistFailure = true;
542
+ if (typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
543
+ console.warn(
544
+ "[lessonkit] sessionStorage is unavailable or failed; using in-memory session dedupe for this tab (may reset on full reload)."
545
+ );
546
+ }
547
+ };
548
+ return {
549
+ getItem: (key) => {
550
+ if (memory.has(key)) return memory.get(key);
551
+ try {
552
+ const value = session.getItem(key);
553
+ if (value !== null) memory.set(key, value);
554
+ return value;
555
+ } catch {
556
+ return memory.get(key) ?? null;
557
+ }
558
+ },
559
+ setItem: (key, value) => {
560
+ memory.set(key, value);
561
+ try {
562
+ session.setItem(key, value);
563
+ } catch {
564
+ warnPersistFailure();
565
+ }
566
+ },
567
+ removeItem: (key) => {
568
+ memory.delete(key);
569
+ try {
570
+ session.removeItem(key);
571
+ } catch {
572
+ warnPersistFailure();
573
+ }
574
+ },
575
+ resetForTests: () => {
576
+ memory.clear();
577
+ }
578
+ };
579
+ }
580
+ function resetStoragePortForTests(storage) {
581
+ storage.resetForTests?.();
582
+ }
583
+ function createSessionStoragePort() {
584
+ if (typeof sessionStorage === "undefined") {
585
+ const memory = /* @__PURE__ */ new Map();
586
+ return {
587
+ getItem: (key) => memory.get(key) ?? null,
588
+ setItem: (key, value) => {
589
+ memory.set(key, value);
590
+ },
591
+ removeItem: (key) => {
592
+ memory.delete(key);
593
+ },
594
+ resetForTests: () => {
595
+ memory.clear();
596
+ }
597
+ };
598
+ }
599
+ return createMemoryBackedSessionStorage(sessionStorage);
600
+ }
601
+ function createGlobalTimer() {
602
+ return {
603
+ setInterval: (fn, ms) => globalThis.setInterval(fn, ms),
604
+ clearInterval: (id) => globalThis.clearInterval(id)
605
+ };
606
+ }
607
+
608
+ // src/progress.ts
609
+ function createProgressController() {
610
+ let activeLessonId;
611
+ let completedLessonIds = /* @__PURE__ */ new Set();
612
+ let courseCompleted = false;
613
+ const lessonStartTimes = /* @__PURE__ */ new Map();
614
+ return {
615
+ getState: () => ({
616
+ activeLessonId,
617
+ completedLessonIds: new Set(completedLessonIds),
618
+ courseCompleted
619
+ }),
620
+ setActiveLesson: (lessonId, startedAtMs) => {
621
+ const previousLessonId = activeLessonId;
622
+ activeLessonId = lessonId;
623
+ lessonStartTimes.set(lessonId, startedAtMs);
624
+ return { previousLessonId };
625
+ },
626
+ completeLesson: (lessonId, completedAtMs) => {
627
+ if (completedLessonIds.has(lessonId)) return { didComplete: false };
628
+ completedLessonIds = new Set(completedLessonIds).add(lessonId);
629
+ if (activeLessonId === lessonId) {
630
+ activeLessonId = void 0;
631
+ }
632
+ const startedAt = lessonStartTimes.get(lessonId);
633
+ lessonStartTimes.delete(lessonId);
634
+ const durationMs = typeof startedAt === "number" ? Math.max(0, completedAtMs - startedAt) : void 0;
635
+ return { durationMs, didComplete: true };
636
+ },
637
+ completeCourse: () => {
638
+ if (courseCompleted) return { didComplete: false };
639
+ courseCompleted = true;
640
+ return { didComplete: true };
641
+ }
642
+ };
643
+ }
644
+
645
+ // src/session.ts
646
+ var SESSION_STORAGE_KEY = "lessonkit:sessionId";
647
+ function getTabSessionId(storage) {
648
+ return storage.getItem(SESSION_STORAGE_KEY);
649
+ }
650
+ var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
651
+ var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
652
+ var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
653
+ function resolveSessionId(storage, provided) {
654
+ if (provided) return provided;
655
+ const existing = storage.getItem(SESSION_STORAGE_KEY);
656
+ if (existing) return existing;
657
+ const id = createSessionId();
658
+ storage.setItem(SESSION_STORAGE_KEY, id);
659
+ return id;
660
+ }
661
+ function courseStartedStorageKey(sessionId, courseId) {
662
+ return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
663
+ }
664
+ function courseStartedTrackingStorageKey(sessionId, courseId) {
665
+ return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
666
+ }
667
+ function courseStartedPipelineStorageKey(sessionId, courseId) {
668
+ return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
669
+ }
670
+ function hasCourseStarted(storage, sessionId, courseId) {
671
+ if (!courseId) return false;
672
+ return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
673
+ }
674
+ function markCourseStarted(storage, sessionId, courseId) {
675
+ if (!courseId) return;
676
+ storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
677
+ }
678
+ function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
679
+ if (!courseId) return false;
680
+ return storage.getItem(courseStartedTrackingStorageKey(sessionId, courseId)) === "1";
681
+ }
682
+ function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
683
+ if (!courseId) return;
684
+ storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
685
+ }
686
+ function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
687
+ if (!courseId) return false;
688
+ return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
689
+ }
690
+ function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
691
+ if (!courseId) return;
692
+ storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
693
+ }
694
+ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
695
+ if (!courseId || fromSessionId === toSessionId) return;
696
+ if (hasCourseStarted(storage, fromSessionId, courseId)) {
697
+ markCourseStarted(storage, toSessionId, courseId);
698
+ storage.removeItem?.(courseStartedStorageKey(fromSessionId, courseId));
699
+ }
700
+ if (hasCourseStartedEmittedToTracking(storage, fromSessionId, courseId)) {
701
+ markCourseStartedEmittedToTracking(storage, toSessionId, courseId);
702
+ storage.removeItem?.(courseStartedTrackingStorageKey(fromSessionId, courseId));
703
+ }
704
+ if (hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)) {
705
+ markCourseStartedPipelineDelivered(storage, toSessionId, courseId);
706
+ storage.removeItem?.(courseStartedPipelineStorageKey(fromSessionId, courseId));
707
+ }
708
+ }
709
+
710
+ // src/runtime/courseLifecycle.ts
711
+ function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
712
+ const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
713
+ if (alreadyEmittedToSink) {
714
+ return { emitted: true, marked };
715
+ }
716
+ if (marked) {
717
+ return { emitted: false, marked: true };
718
+ }
719
+ const emitted = deps.emitCourseStartedEvent(ctx);
720
+ if (emitted) {
721
+ markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
722
+ }
723
+ return { emitted, marked: emitted };
724
+ }
725
+ function buildCourseStartedTelemetryEvent(ctx) {
726
+ return buildTelemetryEvent({
727
+ name: "course_started",
728
+ courseId: ctx.courseId,
729
+ sessionId: ctx.sessionId,
730
+ attemptId: ctx.attemptId,
731
+ user: ctx.user
732
+ });
733
+ }
734
+ function completeLessonWithTelemetry(opts) {
735
+ const result = opts.progress.completeLesson(opts.lessonId, opts.nowMs);
736
+ if (!result.didComplete) return false;
737
+ opts.emitLessonCompleted(opts.lessonId, result.durationMs);
738
+ return true;
739
+ }
740
+ function completeCourseWithTelemetry(opts) {
741
+ const current = opts.progress.getState();
742
+ if (current.activeLessonId) {
743
+ completeLessonWithTelemetry({
744
+ progress: opts.progress,
745
+ lessonId: current.activeLessonId,
746
+ nowMs: opts.nowMs,
747
+ emitLessonCompleted: opts.emitLessonCompleted
748
+ });
749
+ }
750
+ const result = opts.progress.completeCourse();
751
+ if (!result.didComplete) return false;
752
+ opts.emitCourseCompleted();
753
+ return true;
301
754
  }
755
+
756
+ // src/runtime/createLessonkitRuntime.ts
757
+ function createLessonkitRuntime(config, ports = {}) {
758
+ const storage = ports.storage ?? createSessionStoragePort();
759
+ const clock = ports.clock ?? createDefaultClock();
760
+ const configSnapshot = { ...config };
761
+ let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
762
+ let attemptId = configSnapshot.session?.attemptId;
763
+ let user = configSnapshot.session?.user;
764
+ let courseId = configSnapshot.courseId;
765
+ let progress = createProgressController();
766
+ const getSession = () => ({ sessionId, attemptId, user });
767
+ const syncSessionFromConfig = (next) => {
768
+ sessionId = resolveSessionId(storage, next.session?.sessionId);
769
+ attemptId = next.session?.attemptId;
770
+ user = next.session?.user;
771
+ courseId = next.courseId;
772
+ };
773
+ syncSessionFromConfig(configSnapshot);
774
+ const track = (name, data, emit, lessonId) => {
775
+ const event = tryBuildTelemetryEvent({
776
+ name,
777
+ courseId,
778
+ lessonId: lessonId ?? progress.getState().activeLessonId,
779
+ sessionId,
780
+ attemptId,
781
+ user,
782
+ data
783
+ });
784
+ if (!event) return;
785
+ emit(event);
786
+ };
787
+ const emitLessonCompleted = (lessonId, durationMs, emitFn) => {
788
+ emitFn("lesson_completed", { lessonId, durationMs }, lessonId);
789
+ if (durationMs !== void 0) {
790
+ emitFn("lesson_time_on_task", { lessonId, durationMs }, lessonId);
791
+ }
792
+ };
793
+ return {
794
+ get config() {
795
+ return configSnapshot;
796
+ },
797
+ get progress() {
798
+ return progress;
799
+ },
800
+ getProgressState: () => progress.getState(),
801
+ getSession,
802
+ updateConfig(next) {
803
+ if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
804
+ if (next.runtimeVersion !== void 0) configSnapshot.runtimeVersion = next.runtimeVersion;
805
+ if (next.plugins !== void 0) configSnapshot.plugins = next.plugins;
806
+ if (next.session !== void 0) {
807
+ configSnapshot.session = { ...configSnapshot.session, ...next.session };
808
+ }
809
+ syncSessionFromConfig(configSnapshot);
810
+ },
811
+ setActiveLesson(lessonId, emitFn) {
812
+ const current = progress.getState();
813
+ if (current.activeLessonId === lessonId) return;
814
+ if (current.completedLessonIds.has(lessonId)) {
815
+ progress.setActiveLesson(lessonId, clock.nowMs());
816
+ return;
817
+ }
818
+ const previous = current.activeLessonId;
819
+ if (previous && previous !== lessonId) {
820
+ const completed = progress.completeLesson(previous, clock.nowMs());
821
+ if (completed.didComplete) {
822
+ emitLessonCompleted(previous, completed.durationMs, emitFn);
823
+ }
824
+ }
825
+ progress.setActiveLesson(lessonId, clock.nowMs());
826
+ emitFn("lesson_started", { lessonId }, lessonId);
827
+ },
828
+ completeLesson(lessonId, emitFn) {
829
+ const result = progress.completeLesson(lessonId, clock.nowMs());
830
+ if (!result.didComplete) return;
831
+ emitLessonCompleted(lessonId, result.durationMs, emitFn);
832
+ },
833
+ completeCourse(emitFn) {
834
+ const current = progress.getState();
835
+ if (current.activeLessonId) {
836
+ const lessonResult = progress.completeLesson(current.activeLessonId, clock.nowMs());
837
+ if (lessonResult.didComplete) {
838
+ emitLessonCompleted(current.activeLessonId, lessonResult.durationMs, emitFn);
839
+ }
840
+ }
841
+ const result = progress.completeCourse();
842
+ if (!result.didComplete) return;
843
+ emitFn("course_completed");
844
+ },
845
+ track,
846
+ resetForCourseChange(nextCourseId) {
847
+ configSnapshot.courseId = nextCourseId;
848
+ courseId = nextCourseId;
849
+ progress = createProgressController();
850
+ }
851
+ };
852
+ }
853
+
854
+ // src/plugins/registry.ts
302
855
  function warnDuplicatePlugin(id) {
303
856
  const g = globalThis;
304
857
  if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
305
858
  console.warn(`[lessonkit] plugin id "${id}" was registered more than once; using the latest definition`);
306
859
  }
307
- function createPluginHost(plugins = []) {
860
+ function createPluginRegistry(plugins = []) {
308
861
  const registry = /* @__PURE__ */ new Map();
309
862
  for (const plugin of plugins) {
310
863
  if (registry.has(plugin.id)) warnDuplicatePlugin(plugin.id);
@@ -384,21 +937,65 @@ function createPluginHost(plugins = []) {
384
937
  scoreAssessment
385
938
  };
386
939
  }
940
+
941
+ // src/plugins/define.ts
942
+ function defineTelemetryPlugin(plugin) {
943
+ return plugin;
944
+ }
945
+ function defineAssessmentPlugin(plugin) {
946
+ return plugin;
947
+ }
948
+ function defineLifecyclePlugin(plugin) {
949
+ return plugin;
950
+ }
387
951
  // Annotate the CommonJS export names for ESM import in node:
388
952
  0 && (module.exports = {
389
953
  ID_MAX_LENGTH,
390
954
  ID_PATTERN,
955
+ SESSION_STORAGE_KEY,
391
956
  TELEMETRY_EVENT_CATALOG,
957
+ assertNever,
392
958
  assertValidId,
959
+ buildCourseStartedTelemetryEvent,
393
960
  buildLessonkitUrn,
394
961
  buildTelemetryCatalog,
395
- createPluginHost,
962
+ buildTelemetryEvent,
963
+ completeCourseWithTelemetry,
964
+ completeLessonWithTelemetry,
965
+ createDefaultClock,
966
+ createGlobalTimer,
967
+ createLessonkitRuntime,
968
+ createNoopStorage,
969
+ createPluginRegistry,
970
+ createProgressController,
396
971
  createSessionId,
972
+ createSessionStoragePort,
973
+ createTelemetryPipeline,
397
974
  createTrackingClient,
398
- defineLessonkitPlugin,
975
+ createTrackingPipelineSink,
976
+ defineAssessmentPlugin,
977
+ defineLifecyclePlugin,
978
+ defineTelemetryPlugin,
399
979
  deriveId,
980
+ getTabSessionId,
981
+ hasCourseStarted,
982
+ hasCourseStartedEmittedToTracking,
983
+ hasCourseStartedPipelineDelivered,
984
+ markCourseStarted,
985
+ markCourseStartedEmittedToTracking,
986
+ markCourseStartedPipelineDelivered,
987
+ migrateCourseStartedMark,
400
988
  nowIso,
989
+ parseBlockId,
990
+ parseCheckId,
991
+ parseCourseId,
992
+ parseLessonId,
993
+ resetStoragePortForTests,
994
+ resetTelemetryBuilderWarningsForTests,
995
+ resolveSessionId,
401
996
  slugifyId,
402
997
  telemetryCatalogVersion,
998
+ tryBuildTelemetryEvent,
999
+ tryEmitCourseStarted,
403
1000
  validateId
404
1001
  });