@lessonkit/core 1.5.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 +1 -1
- package/dist/{chunk-KFXFQ6B2.js → chunk-DTJGIMUU.js} +216 -35
- package/dist/index.cjs +532 -60
- package/dist/index.d.cts +129 -13
- package/dist/index.d.ts +129 -13
- package/dist/index.js +308 -26
- package/dist/{testing-BFr8oEfw.d.cts → testing-CQ-ZsT7D.d.cts} +185 -5
- package/dist/{testing-BFr8oEfw.d.ts → testing-CQ-ZsT7D.d.ts} +185 -5
- package/dist/testing.cjs +2 -0
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/package.json +3 -3
- package/telemetry-catalog.v3.json +140 -0
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
|
-
[
|
|
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
|
|
|
@@ -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
|
-
|
|
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)
|
|
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,26 +581,35 @@ 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
|
-
if (value !== null)
|
|
504
|
-
|
|
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;
|
|
505
600
|
} catch {
|
|
506
601
|
return memory.get(key) ?? null;
|
|
507
602
|
}
|
|
508
603
|
},
|
|
509
604
|
setItem: (key, value) => {
|
|
510
605
|
tombstones.delete(key);
|
|
511
|
-
memory.set(key, value);
|
|
512
606
|
try {
|
|
513
607
|
session.setItem(key, value);
|
|
608
|
+
memory.set(key, value);
|
|
514
609
|
return true;
|
|
515
610
|
} catch {
|
|
516
611
|
warnPersistFailure();
|
|
612
|
+
memory.set(key, value);
|
|
517
613
|
return false;
|
|
518
614
|
}
|
|
519
615
|
},
|
|
@@ -579,6 +675,29 @@ function createGlobalTimer() {
|
|
|
579
675
|
// src/session.ts
|
|
580
676
|
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
581
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
|
+
}
|
|
582
701
|
function isDevEnvironment2() {
|
|
583
702
|
const g = globalThis;
|
|
584
703
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
@@ -594,21 +713,7 @@ function sessionKeySegment(sessionId) {
|
|
|
594
713
|
const validated = validateId(sessionId);
|
|
595
714
|
return validated.ok ? validated.id : encodeURIComponent(sessionId);
|
|
596
715
|
}
|
|
597
|
-
function
|
|
598
|
-
if (provided !== void 0) {
|
|
599
|
-
const trimmed = provided.trim();
|
|
600
|
-
if (trimmed.length > 0) {
|
|
601
|
-
const validated = validateId(trimmed);
|
|
602
|
-
if (validated.ok) return validated.id;
|
|
603
|
-
if (isDevEnvironment2()) {
|
|
604
|
-
console.warn(
|
|
605
|
-
`[lessonkit] Invalid sessionId "${trimmed}"; falling back to tab or generated id.`
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
611
|
-
if (existing) return existing;
|
|
716
|
+
function resolveGeneratedSessionId(storage) {
|
|
612
717
|
const volatile = volatileSessionIds.get(storage);
|
|
613
718
|
if (volatile) return volatile;
|
|
614
719
|
const id = createSessionId();
|
|
@@ -624,6 +729,50 @@ function resolveSessionId(storage, provided) {
|
|
|
624
729
|
}
|
|
625
730
|
return id;
|
|
626
731
|
}
|
|
732
|
+
function resolveFallbackSessionId(storage, options) {
|
|
733
|
+
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
734
|
+
if (existing) {
|
|
735
|
+
const trimmedExisting = existing.trim();
|
|
736
|
+
const validatedExisting = validateId(trimmedExisting);
|
|
737
|
+
if (validatedExisting.ok) return validatedExisting.id;
|
|
738
|
+
storage.removeItem?.(SESSION_STORAGE_KEY);
|
|
739
|
+
if (isDevEnvironment2()) {
|
|
740
|
+
console.warn(
|
|
741
|
+
`[lessonkit] Invalid stored sessionId "${existing}"; generating a new id.`
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
const fallback = resolveGeneratedSessionId(storage);
|
|
745
|
+
options?.onInvalidSessionId?.({
|
|
746
|
+
invalidId: existing,
|
|
747
|
+
fallbackId: fallback,
|
|
748
|
+
source: "stored"
|
|
749
|
+
});
|
|
750
|
+
return fallback;
|
|
751
|
+
}
|
|
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;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return resolveFallbackSessionId(storage, options);
|
|
775
|
+
}
|
|
627
776
|
function courseStartedStorageKey(sessionId, courseId) {
|
|
628
777
|
return `${COURSE_STARTED_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
629
778
|
}
|
|
@@ -638,35 +787,35 @@ function courseStartedXapiStorageKey(sessionId, courseId) {
|
|
|
638
787
|
}
|
|
639
788
|
function hasCourseStarted(storage, sessionId, courseId) {
|
|
640
789
|
if (!courseId) return false;
|
|
641
|
-
return storage
|
|
790
|
+
return storageHasMark(storage, courseStartedStorageKey(sessionId, courseId));
|
|
642
791
|
}
|
|
643
792
|
function markCourseStarted(storage, sessionId, courseId) {
|
|
644
793
|
if (!courseId) return false;
|
|
645
|
-
return storage
|
|
794
|
+
return storageSetMark(storage, courseStartedStorageKey(sessionId, courseId));
|
|
646
795
|
}
|
|
647
796
|
function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
648
797
|
if (!courseId) return false;
|
|
649
|
-
return storage
|
|
798
|
+
return storageHasMark(storage, courseStartedTrackingStorageKey(sessionId, courseId));
|
|
650
799
|
}
|
|
651
800
|
function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
652
801
|
if (!courseId) return false;
|
|
653
|
-
return storage
|
|
802
|
+
return storageSetMark(storage, courseStartedTrackingStorageKey(sessionId, courseId));
|
|
654
803
|
}
|
|
655
804
|
function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
656
805
|
if (!courseId) return false;
|
|
657
|
-
return storage
|
|
806
|
+
return storageHasMark(storage, courseStartedPipelineStorageKey(sessionId, courseId));
|
|
658
807
|
}
|
|
659
808
|
function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
660
809
|
if (!courseId) return false;
|
|
661
|
-
return storage
|
|
810
|
+
return storageSetMark(storage, courseStartedPipelineStorageKey(sessionId, courseId));
|
|
662
811
|
}
|
|
663
812
|
function hasCourseStartedXapiSent(storage, sessionId, courseId) {
|
|
664
813
|
if (!courseId) return false;
|
|
665
|
-
return storage
|
|
814
|
+
return storageHasMark(storage, courseStartedXapiStorageKey(sessionId, courseId));
|
|
666
815
|
}
|
|
667
816
|
function markCourseStartedXapiSent(storage, sessionId, courseId) {
|
|
668
817
|
if (!courseId) return false;
|
|
669
|
-
return storage
|
|
818
|
+
return storageSetMark(storage, courseStartedXapiStorageKey(sessionId, courseId));
|
|
670
819
|
}
|
|
671
820
|
function resetSharedVolatileSessionIdForTests() {
|
|
672
821
|
}
|
|
@@ -674,6 +823,7 @@ function migrateStorageMark(storage, fromKey, toKey, hasMark) {
|
|
|
674
823
|
if (!hasMark) return;
|
|
675
824
|
if (storage.setItem(toKey, "1")) {
|
|
676
825
|
storage.removeItem?.(fromKey);
|
|
826
|
+
clearVolatileMark(storage, fromKey);
|
|
677
827
|
}
|
|
678
828
|
}
|
|
679
829
|
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
@@ -706,14 +856,32 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
|
|
|
706
856
|
|
|
707
857
|
// src/runtime/courseLifecycle.ts
|
|
708
858
|
var courseStartedEmitFlights = /* @__PURE__ */ new Map();
|
|
859
|
+
var courseStartedEmittedInTab = /* @__PURE__ */ new Set();
|
|
709
860
|
function resetCourseStartedEmitFlightForTests() {
|
|
710
861
|
courseStartedEmitFlights.clear();
|
|
862
|
+
courseStartedEmittedInTab.clear();
|
|
711
863
|
}
|
|
712
864
|
function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
|
|
713
865
|
const flightKey = `${ctx.sessionId}:${ctx.courseId}`;
|
|
714
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
|
+
}
|
|
715
873
|
if (alreadyEmittedToSink) {
|
|
716
|
-
|
|
874
|
+
const markPersisted = marked ? true : markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
875
|
+
return Promise.resolve({
|
|
876
|
+
emitted: true,
|
|
877
|
+
marked: markPersisted
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
if (marked && hasCourseStartedEmittedToTracking(ctx.storage, ctx.sessionId, ctx.courseId)) {
|
|
881
|
+
return Promise.resolve({
|
|
882
|
+
emitted: true,
|
|
883
|
+
marked: true
|
|
884
|
+
});
|
|
717
885
|
}
|
|
718
886
|
const existing = courseStartedEmitFlights.get(flightKey);
|
|
719
887
|
if (existing) {
|
|
@@ -722,15 +890,19 @@ function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
|
|
|
722
890
|
const flight = Promise.resolve().then(() => {
|
|
723
891
|
try {
|
|
724
892
|
const emitted = deps.emitCourseStartedEvent(ctx);
|
|
725
|
-
if (emitted
|
|
726
|
-
|
|
893
|
+
if (emitted) {
|
|
894
|
+
courseStartedEmittedInTab.add(flightKey);
|
|
727
895
|
}
|
|
896
|
+
const markPersisted = emitted && !marked ? markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId) : marked;
|
|
728
897
|
return {
|
|
729
898
|
emitted,
|
|
730
|
-
marked:
|
|
899
|
+
marked: markPersisted
|
|
731
900
|
};
|
|
732
901
|
} catch {
|
|
733
|
-
return {
|
|
902
|
+
return {
|
|
903
|
+
emitted: false,
|
|
904
|
+
marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
|
|
905
|
+
};
|
|
734
906
|
} finally {
|
|
735
907
|
if (courseStartedEmitFlights.get(flightKey) === flight) {
|
|
736
908
|
courseStartedEmitFlights.delete(flightKey);
|
|
@@ -766,7 +938,16 @@ function completeCourseWithTelemetry(opts) {
|
|
|
766
938
|
});
|
|
767
939
|
}
|
|
768
940
|
const result = opts.progress.completeCourse();
|
|
769
|
-
if (!result.didComplete)
|
|
941
|
+
if (!result.didComplete) {
|
|
942
|
+
const after = opts.progress.getState();
|
|
943
|
+
if (after.activeLessonId) {
|
|
944
|
+
const lessonResult = opts.progress.completeLesson(after.activeLessonId, opts.nowMs);
|
|
945
|
+
if (lessonResult.didComplete) {
|
|
946
|
+
opts.emitLessonCompleted(after.activeLessonId, lessonResult.durationMs);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
770
951
|
opts.emitCourseCompleted();
|
|
771
952
|
return true;
|
|
772
953
|
}
|