@lessonkit/core 1.6.0 → 1.7.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/README.md CHANGED
@@ -63,7 +63,7 @@ Machine-readable: `@lessonkit/core/telemetry-catalog.v3.json` (current; v1–v3
63
63
 
64
64
  ## Docs
65
65
 
66
- [Core reference](https://lessonkit.readthedocs.io/en/latest/reference/core.html) · [Identity](https://lessonkit.readthedocs.io/en/latest/reference/identity.html) · [Telemetry](https://lessonkit.readthedocs.io/en/latest/reference/telemetry.html) · [Plugins](https://lessonkit.readthedocs.io/en/latest/reference/plugins.html) · [TypeDoc API index](https://lessonkit.readthedocs.io/en/latest/reference/api.html)
66
+ [5-minute guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/getting-started-in-5-minutes.html) · [LMS Go-Live](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/lms-go-live.html) · [Core reference](https://lessonkit.readthedocs.io/en/latest/reference/core.html) · [TypeDoc API index](https://lessonkit.readthedocs.io/en/latest/reference/api.html)
67
67
 
68
68
  ## License
69
69
 
@@ -588,9 +588,15 @@ function createMemoryBackedSessionStorage(session) {
588
588
  if (!bypassCacheForKey(key) && memory.has(key)) return memory.get(key);
589
589
  try {
590
590
  const value = session.getItem(key);
591
- if (value !== null) memory.set(key, value);
592
- else if (bypassCacheForKey(key)) memory.delete(key);
593
- return value;
591
+ if (value !== null) {
592
+ memory.set(key, value);
593
+ return value;
594
+ }
595
+ if (bypassCacheForKey(key) && memory.has(key)) {
596
+ return memory.get(key);
597
+ }
598
+ if (bypassCacheForKey(key)) memory.delete(key);
599
+ return null;
594
600
  } catch {
595
601
  return memory.get(key) ?? null;
596
602
  }
@@ -603,9 +609,7 @@ function createMemoryBackedSessionStorage(session) {
603
609
  return true;
604
610
  } catch {
605
611
  warnPersistFailure();
606
- if (!bypassCacheForKey(key)) {
607
- memory.set(key, value);
608
- }
612
+ memory.set(key, value);
609
613
  return false;
610
614
  }
611
615
  },
@@ -671,6 +675,29 @@ function createGlobalTimer() {
671
675
  // src/session.ts
672
676
  var SESSION_STORAGE_KEY = "lessonkit:sessionId";
673
677
  var volatileSessionIds = /* @__PURE__ */ new WeakMap();
678
+ var volatileStorageMarks = /* @__PURE__ */ new WeakMap();
679
+ function rememberVolatileMark(storage, key) {
680
+ let keys = volatileStorageMarks.get(storage);
681
+ if (!keys) {
682
+ keys = /* @__PURE__ */ new Set();
683
+ volatileStorageMarks.set(storage, keys);
684
+ }
685
+ keys.add(key);
686
+ }
687
+ function hasVolatileMark(storage, key) {
688
+ return volatileStorageMarks.get(storage)?.has(key) ?? false;
689
+ }
690
+ function clearVolatileMark(storage, key) {
691
+ volatileStorageMarks.get(storage)?.delete(key);
692
+ }
693
+ function storageHasMark(storage, key) {
694
+ return storage.getItem(key) === "1" || hasVolatileMark(storage, key);
695
+ }
696
+ function storageSetMark(storage, key) {
697
+ const persisted = storage.setItem(key, "1");
698
+ if (!persisted) rememberVolatileMark(storage, key);
699
+ return persisted;
700
+ }
674
701
  function isDevEnvironment2() {
675
702
  const g = globalThis;
676
703
  return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
@@ -686,19 +713,23 @@ function sessionKeySegment(sessionId) {
686
713
  const validated = validateId(sessionId);
687
714
  return validated.ok ? validated.id : encodeURIComponent(sessionId);
688
715
  }
689
- function resolveSessionId(storage, provided) {
690
- if (provided !== void 0) {
691
- const trimmed = provided.trim();
692
- if (trimmed.length > 0) {
693
- const validated = validateId(trimmed);
694
- if (validated.ok) return validated.id;
695
- if (isDevEnvironment2()) {
696
- console.warn(
697
- `[lessonkit] Invalid sessionId "${trimmed}"; falling back to tab or generated id.`
698
- );
699
- }
716
+ function resolveGeneratedSessionId(storage) {
717
+ const volatile = volatileSessionIds.get(storage);
718
+ if (volatile) return volatile;
719
+ const id = createSessionId();
720
+ const persisted = storage.setItem(SESSION_STORAGE_KEY, id);
721
+ if (!persisted) {
722
+ volatileSessionIds.set(storage, id);
723
+ if (isDevEnvironment2()) {
724
+ console.warn(
725
+ "[lessonkit] session id could not be persisted; using in-memory id for this storage."
726
+ );
700
727
  }
728
+ return id;
701
729
  }
730
+ return id;
731
+ }
732
+ function resolveFallbackSessionId(storage, options) {
702
733
  const existing = storage.getItem(SESSION_STORAGE_KEY);
703
734
  if (existing) {
704
735
  const trimmedExisting = existing.trim();
@@ -710,21 +741,37 @@ function resolveSessionId(storage, provided) {
710
741
  `[lessonkit] Invalid stored sessionId "${existing}"; generating a new id.`
711
742
  );
712
743
  }
744
+ const fallback = resolveGeneratedSessionId(storage);
745
+ options?.onInvalidSessionId?.({
746
+ invalidId: existing,
747
+ fallbackId: fallback,
748
+ source: "stored"
749
+ });
750
+ return fallback;
713
751
  }
714
- const volatile = volatileSessionIds.get(storage);
715
- if (volatile) return volatile;
716
- const id = createSessionId();
717
- const persisted = storage.setItem(SESSION_STORAGE_KEY, id);
718
- if (!persisted) {
719
- volatileSessionIds.set(storage, id);
720
- if (isDevEnvironment2()) {
721
- console.warn(
722
- "[lessonkit] session id could not be persisted; using in-memory id for this storage."
723
- );
752
+ return resolveGeneratedSessionId(storage);
753
+ }
754
+ function resolveSessionId(storage, provided, options) {
755
+ if (provided !== void 0) {
756
+ const trimmed = provided.trim();
757
+ if (trimmed.length > 0) {
758
+ const validated = validateId(trimmed);
759
+ if (validated.ok) return validated.id;
760
+ if (isDevEnvironment2()) {
761
+ console.warn(
762
+ `[lessonkit] Invalid sessionId "${trimmed}"; falling back to tab or generated id.`
763
+ );
764
+ }
765
+ const fallback = resolveFallbackSessionId(storage, options);
766
+ options?.onInvalidSessionId?.({
767
+ invalidId: trimmed,
768
+ fallbackId: fallback,
769
+ source: "provided"
770
+ });
771
+ return fallback;
724
772
  }
725
- return id;
726
773
  }
727
- return id;
774
+ return resolveFallbackSessionId(storage, options);
728
775
  }
729
776
  function courseStartedStorageKey(sessionId, courseId) {
730
777
  return `${COURSE_STARTED_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
@@ -740,35 +787,35 @@ function courseStartedXapiStorageKey(sessionId, courseId) {
740
787
  }
741
788
  function hasCourseStarted(storage, sessionId, courseId) {
742
789
  if (!courseId) return false;
743
- return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
790
+ return storageHasMark(storage, courseStartedStorageKey(sessionId, courseId));
744
791
  }
745
792
  function markCourseStarted(storage, sessionId, courseId) {
746
793
  if (!courseId) return false;
747
- return storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
794
+ return storageSetMark(storage, courseStartedStorageKey(sessionId, courseId));
748
795
  }
749
796
  function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
750
797
  if (!courseId) return false;
751
- return storage.getItem(courseStartedTrackingStorageKey(sessionId, courseId)) === "1";
798
+ return storageHasMark(storage, courseStartedTrackingStorageKey(sessionId, courseId));
752
799
  }
753
800
  function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
754
801
  if (!courseId) return false;
755
- return storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
802
+ return storageSetMark(storage, courseStartedTrackingStorageKey(sessionId, courseId));
756
803
  }
757
804
  function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
758
805
  if (!courseId) return false;
759
- return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
806
+ return storageHasMark(storage, courseStartedPipelineStorageKey(sessionId, courseId));
760
807
  }
761
808
  function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
762
809
  if (!courseId) return false;
763
- return storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
810
+ return storageSetMark(storage, courseStartedPipelineStorageKey(sessionId, courseId));
764
811
  }
765
812
  function hasCourseStartedXapiSent(storage, sessionId, courseId) {
766
813
  if (!courseId) return false;
767
- return storage.getItem(courseStartedXapiStorageKey(sessionId, courseId)) === "1";
814
+ return storageHasMark(storage, courseStartedXapiStorageKey(sessionId, courseId));
768
815
  }
769
816
  function markCourseStartedXapiSent(storage, sessionId, courseId) {
770
817
  if (!courseId) return false;
771
- return storage.setItem(courseStartedXapiStorageKey(sessionId, courseId), "1");
818
+ return storageSetMark(storage, courseStartedXapiStorageKey(sessionId, courseId));
772
819
  }
773
820
  function resetSharedVolatileSessionIdForTests() {
774
821
  }
@@ -776,6 +823,7 @@ function migrateStorageMark(storage, fromKey, toKey, hasMark) {
776
823
  if (!hasMark) return;
777
824
  if (storage.setItem(toKey, "1")) {
778
825
  storage.removeItem?.(fromKey);
826
+ clearVolatileMark(storage, fromKey);
779
827
  }
780
828
  }
781
829
  function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
@@ -808,12 +856,20 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
808
856
 
809
857
  // src/runtime/courseLifecycle.ts
810
858
  var courseStartedEmitFlights = /* @__PURE__ */ new Map();
859
+ var courseStartedEmittedInTab = /* @__PURE__ */ new Set();
811
860
  function resetCourseStartedEmitFlightForTests() {
812
861
  courseStartedEmitFlights.clear();
862
+ courseStartedEmittedInTab.clear();
813
863
  }
814
864
  function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
815
865
  const flightKey = `${ctx.sessionId}:${ctx.courseId}`;
816
866
  const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
867
+ if (courseStartedEmittedInTab.has(flightKey)) {
868
+ return Promise.resolve({
869
+ emitted: true,
870
+ marked: marked || markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
871
+ });
872
+ }
817
873
  if (alreadyEmittedToSink) {
818
874
  const markPersisted = marked ? true : markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
819
875
  return Promise.resolve({
@@ -834,6 +890,9 @@ function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
834
890
  const flight = Promise.resolve().then(() => {
835
891
  try {
836
892
  const emitted = deps.emitCourseStartedEvent(ctx);
893
+ if (emitted) {
894
+ courseStartedEmittedInTab.add(flightKey);
895
+ }
837
896
  const markPersisted = emitted && !marked ? markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId) : marked;
838
897
  return {
839
898
  emitted,