@lessonkit/core 1.5.0 → 1.6.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.
@@ -60,7 +60,8 @@ function randomSessionIdFallback() {
60
60
  if (g.crypto?.getRandomValues) {
61
61
  const bytes = new Uint8Array(16);
62
62
  g.crypto.getRandomValues(bytes);
63
- return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
63
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
64
+ return `s-${hex}`;
64
65
  }
65
66
  throw new Error(
66
67
  "[lessonkit] createSessionId requires crypto.randomUUID or crypto.getRandomValues"
@@ -68,7 +69,9 @@ function randomSessionIdFallback() {
68
69
  }
69
70
  function createSessionId() {
70
71
  const g = globalThis;
71
- if (g.crypto?.randomUUID) return g.crypto.randomUUID();
72
+ if (g.crypto?.randomUUID) {
73
+ return `s-${g.crypto.randomUUID().replace(/-/g, "")}`;
74
+ }
72
75
  return randomSessionIdFallback();
73
76
  }
74
77
 
@@ -386,6 +389,90 @@ var TELEMETRY_EVENT_REGISTRY = {
386
389
  data: opts.data
387
390
  };
388
391
  }
392
+ },
393
+ image_juxtaposition_changed: {
394
+ build: (opts, base) => ({
395
+ name: "image_juxtaposition_changed",
396
+ ...base,
397
+ lessonId: opts.lessonId,
398
+ data: opts.data
399
+ })
400
+ },
401
+ timeline_event_viewed: {
402
+ build: (opts, base) => ({
403
+ name: "timeline_event_viewed",
404
+ ...base,
405
+ lessonId: opts.lessonId,
406
+ data: opts.data
407
+ })
408
+ },
409
+ image_sequence_changed: {
410
+ build: (opts, base) => ({
411
+ name: "image_sequence_changed",
412
+ ...base,
413
+ lessonId: opts.lessonId,
414
+ data: opts.data
415
+ })
416
+ },
417
+ audio_recording_started: {
418
+ build: (opts, base) => ({
419
+ name: "audio_recording_started",
420
+ ...base,
421
+ lessonId: opts.lessonId,
422
+ data: opts.data
423
+ })
424
+ },
425
+ audio_recording_completed: {
426
+ build: (opts, base) => ({
427
+ name: "audio_recording_completed",
428
+ ...base,
429
+ lessonId: opts.lessonId,
430
+ data: opts.data
431
+ })
432
+ },
433
+ qr_content_revealed: {
434
+ build: (opts, base) => ({
435
+ name: "qr_content_revealed",
436
+ ...base,
437
+ lessonId: opts.lessonId,
438
+ data: opts.data
439
+ })
440
+ },
441
+ advent_door_opened: {
442
+ build: (opts, base) => ({
443
+ name: "advent_door_opened",
444
+ ...base,
445
+ lessonId: opts.lessonId,
446
+ data: opts.data
447
+ })
448
+ },
449
+ map_stage_viewed: {
450
+ requiresLessonId: true,
451
+ build: (opts, base) => {
452
+ if (opts.name !== "map_stage_viewed") throw new Error("unexpected event");
453
+ const lessonId = opts.lessonId;
454
+ if (!lessonId) throw new Error("map_stage_viewed requires active lessonId");
455
+ return {
456
+ name: "map_stage_viewed",
457
+ ...base,
458
+ lessonId,
459
+ data: opts.data
460
+ };
461
+ }
462
+ },
463
+ map_exit_selected: {
464
+ requiresLessonId: true,
465
+ build: (opts, base) => {
466
+ if (opts.name !== "map_exit_selected") throw new Error("unexpected event");
467
+ const lessonId = opts.lessonId;
468
+ if (!lessonId) throw new Error("map_exit_selected requires active lessonId");
469
+ return {
470
+ name: "map_exit_selected",
471
+ ...base,
472
+ lessonId,
473
+ data: opts.data
474
+ };
475
+ }
389
476
  }
390
477
  };
391
478
  function buildTelemetryEventFromRegistry(opts) {
@@ -494,13 +581,15 @@ function createMemoryBackedSessionStorage(session) {
494
581
  );
495
582
  }
496
583
  };
584
+ const bypassCacheForKey = (key) => key === "lessonkit:sessionId" || key.startsWith("lessonkit:course_started");
497
585
  return {
498
586
  getItem: (key) => {
499
587
  if (tombstones.has(key)) return null;
500
- if (memory.has(key)) return memory.get(key);
588
+ if (!bypassCacheForKey(key) && memory.has(key)) return memory.get(key);
501
589
  try {
502
590
  const value = session.getItem(key);
503
591
  if (value !== null) memory.set(key, value);
592
+ else if (bypassCacheForKey(key)) memory.delete(key);
504
593
  return value;
505
594
  } catch {
506
595
  return memory.get(key) ?? null;
@@ -508,12 +597,15 @@ function createMemoryBackedSessionStorage(session) {
508
597
  },
509
598
  setItem: (key, value) => {
510
599
  tombstones.delete(key);
511
- memory.set(key, value);
512
600
  try {
513
601
  session.setItem(key, value);
602
+ memory.set(key, value);
514
603
  return true;
515
604
  } catch {
516
605
  warnPersistFailure();
606
+ if (!bypassCacheForKey(key)) {
607
+ memory.set(key, value);
608
+ }
517
609
  return false;
518
610
  }
519
611
  },
@@ -608,7 +700,17 @@ function resolveSessionId(storage, provided) {
608
700
  }
609
701
  }
610
702
  const existing = storage.getItem(SESSION_STORAGE_KEY);
611
- if (existing) return existing;
703
+ if (existing) {
704
+ const trimmedExisting = existing.trim();
705
+ const validatedExisting = validateId(trimmedExisting);
706
+ if (validatedExisting.ok) return validatedExisting.id;
707
+ storage.removeItem?.(SESSION_STORAGE_KEY);
708
+ if (isDevEnvironment2()) {
709
+ console.warn(
710
+ `[lessonkit] Invalid stored sessionId "${existing}"; generating a new id.`
711
+ );
712
+ }
713
+ }
612
714
  const volatile = volatileSessionIds.get(storage);
613
715
  if (volatile) return volatile;
614
716
  const id = createSessionId();
@@ -713,7 +815,17 @@ function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
713
815
  const flightKey = `${ctx.sessionId}:${ctx.courseId}`;
714
816
  const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
715
817
  if (alreadyEmittedToSink) {
716
- return Promise.resolve({ emitted: true, marked });
818
+ const markPersisted = marked ? true : markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
819
+ return Promise.resolve({
820
+ emitted: true,
821
+ marked: markPersisted
822
+ });
823
+ }
824
+ if (marked && hasCourseStartedEmittedToTracking(ctx.storage, ctx.sessionId, ctx.courseId)) {
825
+ return Promise.resolve({
826
+ emitted: true,
827
+ marked: true
828
+ });
717
829
  }
718
830
  const existing = courseStartedEmitFlights.get(flightKey);
719
831
  if (existing) {
@@ -722,15 +834,16 @@ function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
722
834
  const flight = Promise.resolve().then(() => {
723
835
  try {
724
836
  const emitted = deps.emitCourseStartedEvent(ctx);
725
- if (emitted && !marked) {
726
- markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
727
- }
837
+ const markPersisted = emitted && !marked ? markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId) : marked;
728
838
  return {
729
839
  emitted,
730
- marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
840
+ marked: markPersisted
731
841
  };
732
842
  } catch {
733
- return { emitted: false, marked };
843
+ return {
844
+ emitted: false,
845
+ marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
846
+ };
734
847
  } finally {
735
848
  if (courseStartedEmitFlights.get(flightKey) === flight) {
736
849
  courseStartedEmitFlights.delete(flightKey);
@@ -766,7 +879,16 @@ function completeCourseWithTelemetry(opts) {
766
879
  });
767
880
  }
768
881
  const result = opts.progress.completeCourse();
769
- if (!result.didComplete) return false;
882
+ if (!result.didComplete) {
883
+ const after = opts.progress.getState();
884
+ if (after.activeLessonId) {
885
+ const lessonResult = opts.progress.completeLesson(after.activeLessonId, opts.nowMs);
886
+ if (lessonResult.didComplete) {
887
+ opts.emitLessonCompleted(after.activeLessonId, lessonResult.durationMs);
888
+ }
889
+ }
890
+ return false;
891
+ }
770
892
  opts.emitCourseCompleted();
771
893
  return true;
772
894
  }