@lessonkit/core 1.2.0 → 1.3.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 +254 -58
- package/dist/index.d.cts +38 -10
- package/dist/index.d.ts +38 -10
- package/dist/index.js +251 -58
- package/package.json +1 -1
- package/telemetry-catalog.v3.json +8 -0
package/dist/index.js
CHANGED
|
@@ -60,21 +60,40 @@ function assertValidId(input, path = "id") {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// src/slugify.ts
|
|
63
|
+
function shortHash(input) {
|
|
64
|
+
let h = 0;
|
|
65
|
+
for (let i = 0; i < input.length; i++) {
|
|
66
|
+
h = Math.imul(31, h) + input.charCodeAt(i) >>> 0;
|
|
67
|
+
}
|
|
68
|
+
return h.toString(36);
|
|
69
|
+
}
|
|
70
|
+
function uniqueFallbackId(input, usedIds) {
|
|
71
|
+
const hash = shortHash(input);
|
|
72
|
+
for (let n = 0; n < 100; n++) {
|
|
73
|
+
const candidate = (n === 0 ? `id-${hash}` : `id-${hash}-${n}`).slice(0, 64);
|
|
74
|
+
const validated2 = validateId(candidate);
|
|
75
|
+
if (validated2.ok && !usedIds.has(validated2.id)) return validated2.id;
|
|
76
|
+
}
|
|
77
|
+
const timed = `id-${hash}-${Date.now().toString(36)}`.slice(0, 64);
|
|
78
|
+
const validated = validateId(timed);
|
|
79
|
+
return validated.ok ? validated.id : `id-${hash}`;
|
|
80
|
+
}
|
|
63
81
|
function slugifyId(input) {
|
|
64
82
|
const slug = input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-").slice(0, 64);
|
|
65
|
-
if (!slug.length) return
|
|
66
|
-
const candidate = /^[a-z]/.test(slug) ? slug : `id-${slug}
|
|
83
|
+
if (!slug.length) return uniqueFallbackId(input, /* @__PURE__ */ new Set());
|
|
84
|
+
const candidate = /^[a-z]/.test(slug) ? slug : `id-${slug}`.slice(0, 64);
|
|
67
85
|
const validated = validateId(candidate);
|
|
68
|
-
return validated.ok ? validated.id :
|
|
86
|
+
return validated.ok ? validated.id : uniqueFallbackId(input, /* @__PURE__ */ new Set());
|
|
69
87
|
}
|
|
70
88
|
function deriveId(title, usedIds = /* @__PURE__ */ new Set()) {
|
|
71
89
|
const base = slugifyId(title);
|
|
72
|
-
if (!usedIds.has(base)) return base;
|
|
90
|
+
if (!usedIds.has(base) && validateId(base).ok) return base;
|
|
73
91
|
for (let n = 2; n < 1e3; n++) {
|
|
74
|
-
const candidate = `${base}-${n}
|
|
75
|
-
|
|
92
|
+
const candidate = `${base}-${n}`.slice(0, 64);
|
|
93
|
+
const validated = validateId(candidate);
|
|
94
|
+
if (validated.ok && !usedIds.has(validated.id)) return validated.id;
|
|
76
95
|
}
|
|
77
|
-
return `${
|
|
96
|
+
return uniqueFallbackId(`${title}-${Date.now()}`, usedIds);
|
|
78
97
|
}
|
|
79
98
|
|
|
80
99
|
// src/urn.ts
|
|
@@ -122,6 +141,27 @@ function clampCompoundPageIndex(index, pageCount) {
|
|
|
122
141
|
if (pageCount < 1) return 0;
|
|
123
142
|
return Math.min(Math.max(0, Math.floor(index)), pageCount - 1);
|
|
124
143
|
}
|
|
144
|
+
function isJsonPrimitive(value) {
|
|
145
|
+
return value === null || typeof value === "boolean" || typeof value === "string" || typeof value === "number" && Number.isFinite(value);
|
|
146
|
+
}
|
|
147
|
+
function isPlainStringKeyMap(value) {
|
|
148
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
149
|
+
return Object.entries(value).every(
|
|
150
|
+
([key, entry]) => typeof key === "string" && isJsonPrimitive(entry)
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
function isValidChildResumeValue(value) {
|
|
154
|
+
if (isJsonPrimitive(value)) return true;
|
|
155
|
+
if (Array.isArray(value)) return value.every((item) => isJsonPrimitive(item));
|
|
156
|
+
if (isPlainStringKeyMap(value)) return true;
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
function isPlainSerializableChildState(value) {
|
|
160
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
161
|
+
return Object.values(value).every(
|
|
162
|
+
(entry) => isValidChildResumeValue(entry)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
125
165
|
function parseCompoundResumeState(raw) {
|
|
126
166
|
if (!raw || typeof raw !== "object") return null;
|
|
127
167
|
const obj = raw;
|
|
@@ -130,7 +170,7 @@ function parseCompoundResumeState(raw) {
|
|
|
130
170
|
const childStates = {};
|
|
131
171
|
if (obj.childStates && typeof obj.childStates === "object" && !Array.isArray(obj.childStates)) {
|
|
132
172
|
for (const [key, value] of Object.entries(obj.childStates)) {
|
|
133
|
-
if (
|
|
173
|
+
if (isPlainSerializableChildState(value)) {
|
|
134
174
|
childStates[key] = value;
|
|
135
175
|
}
|
|
136
176
|
}
|
|
@@ -144,22 +184,40 @@ function parseCompoundResumeState(raw) {
|
|
|
144
184
|
};
|
|
145
185
|
}
|
|
146
186
|
|
|
187
|
+
// src/internal/env.ts
|
|
188
|
+
function isDevEnvironment() {
|
|
189
|
+
const g = globalThis;
|
|
190
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
191
|
+
}
|
|
192
|
+
function warnDev(message, err) {
|
|
193
|
+
if (!isDevEnvironment()) return;
|
|
194
|
+
console.warn(message, err instanceof Error ? err.message : err);
|
|
195
|
+
}
|
|
196
|
+
|
|
147
197
|
// src/compoundState.ts
|
|
148
198
|
var COMPOUND_STATE_PREFIX = "lessonkit:compound:";
|
|
149
199
|
function compoundStateStorageKey(courseId, compoundId) {
|
|
150
200
|
return `${COMPOUND_STATE_PREFIX}${courseId}:${compoundId}`;
|
|
151
201
|
}
|
|
152
202
|
function loadCompoundState(storage, courseId, compoundId) {
|
|
153
|
-
const
|
|
203
|
+
const key = compoundStateStorageKey(courseId, compoundId);
|
|
204
|
+
const raw = storage.getItem(key);
|
|
154
205
|
if (!raw) return null;
|
|
155
206
|
try {
|
|
156
|
-
|
|
207
|
+
const parsed = parseCompoundResumeState(JSON.parse(raw));
|
|
208
|
+
if (parsed === null && isDevEnvironment()) {
|
|
209
|
+
console.warn(`[lessonkit] Ignoring corrupt compound resume state at ${key}`);
|
|
210
|
+
}
|
|
211
|
+
return parsed;
|
|
157
212
|
} catch {
|
|
213
|
+
if (isDevEnvironment()) {
|
|
214
|
+
console.warn(`[lessonkit] Ignoring corrupt compound resume state at ${key}`);
|
|
215
|
+
}
|
|
158
216
|
return null;
|
|
159
217
|
}
|
|
160
218
|
}
|
|
161
219
|
function saveCompoundState(storage, courseId, compoundId, state) {
|
|
162
|
-
storage.setItem(compoundStateStorageKey(courseId, compoundId), JSON.stringify(state));
|
|
220
|
+
return storage.setItem(compoundStateStorageKey(courseId, compoundId), JSON.stringify(state));
|
|
163
221
|
}
|
|
164
222
|
function clearCompoundState(storage, courseId, compoundId) {
|
|
165
223
|
storage.removeItem?.(compoundStateStorageKey(courseId, compoundId));
|
|
@@ -189,6 +247,28 @@ var PAGE_ALLOWED_CHILD_TYPES = [
|
|
|
189
247
|
"ProgressTracker"
|
|
190
248
|
];
|
|
191
249
|
var INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES = ["Page"];
|
|
250
|
+
var SLIDE_ALLOWED_CHILD_TYPES = [
|
|
251
|
+
"Text",
|
|
252
|
+
"Heading",
|
|
253
|
+
"Image",
|
|
254
|
+
"Scenario",
|
|
255
|
+
"Reflection",
|
|
256
|
+
"Quiz",
|
|
257
|
+
"KnowledgeCheck",
|
|
258
|
+
"TrueFalse",
|
|
259
|
+
"FillInTheBlanks",
|
|
260
|
+
"DragAndDrop",
|
|
261
|
+
"DragTheWords",
|
|
262
|
+
"MarkTheWords",
|
|
263
|
+
"Accordion",
|
|
264
|
+
"DialogCards",
|
|
265
|
+
"Flashcards",
|
|
266
|
+
"ImageHotspots",
|
|
267
|
+
"FindHotspot",
|
|
268
|
+
"FindMultipleHotspots",
|
|
269
|
+
"ImageSlider"
|
|
270
|
+
];
|
|
271
|
+
var SLIDE_DECK_ALLOWED_CHILD_TYPES = ["Slide"];
|
|
192
272
|
var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
|
|
193
273
|
"TrueFalse",
|
|
194
274
|
"FillInTheBlanks",
|
|
@@ -203,11 +283,15 @@ var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
|
|
|
203
283
|
var ALLOWLISTS = {
|
|
204
284
|
Page: PAGE_ALLOWED_CHILD_TYPES,
|
|
205
285
|
InteractiveBook: INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
286
|
+
Slide: SLIDE_ALLOWED_CHILD_TYPES,
|
|
287
|
+
SlideDeck: SLIDE_DECK_ALLOWED_CHILD_TYPES,
|
|
206
288
|
AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES
|
|
207
289
|
};
|
|
208
290
|
var COMPOUND_MAX_NESTING_DEPTH = {
|
|
209
291
|
Page: 1,
|
|
210
292
|
InteractiveBook: 2,
|
|
293
|
+
Slide: 1,
|
|
294
|
+
SlideDeck: 2,
|
|
211
295
|
AssessmentSequence: 1
|
|
212
296
|
};
|
|
213
297
|
function getAllowedChildTypes(parent) {
|
|
@@ -325,6 +409,14 @@ var TELEMETRY_EVENT_CATALOG_V3 = [
|
|
|
325
409
|
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
326
410
|
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
327
411
|
},
|
|
412
|
+
{
|
|
413
|
+
name: "slide_viewed",
|
|
414
|
+
description: "Learner viewed a slide in a SlideDeck (Course Presentation)",
|
|
415
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
416
|
+
dataFields: ["blockId", "slideIndex", "slideTitle"],
|
|
417
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
418
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
419
|
+
},
|
|
328
420
|
{
|
|
329
421
|
name: "compound_page_viewed",
|
|
330
422
|
description: "Learner activated a page inside a compound container",
|
|
@@ -370,16 +462,6 @@ function buildTelemetryCatalogV3() {
|
|
|
370
462
|
return TELEMETRY_EVENT_CATALOG_V3.map((entry) => ({ ...entry }));
|
|
371
463
|
}
|
|
372
464
|
|
|
373
|
-
// src/internal/env.ts
|
|
374
|
-
function isDevEnvironment() {
|
|
375
|
-
const g = globalThis;
|
|
376
|
-
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
377
|
-
}
|
|
378
|
-
function warnDev(message, err) {
|
|
379
|
-
if (!isDevEnvironment()) return;
|
|
380
|
-
console.warn(message, err instanceof Error ? err.message : err);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
465
|
// src/internal/sinkInvoke.ts
|
|
384
466
|
function invokeTrackingSink(sink, event) {
|
|
385
467
|
let result;
|
|
@@ -440,45 +522,66 @@ function createTrackingClient(opts) {
|
|
|
440
522
|
}
|
|
441
523
|
const buffer = [];
|
|
442
524
|
let flushInFlight = null;
|
|
525
|
+
let inflightExitBatch = null;
|
|
443
526
|
let disposed = false;
|
|
444
527
|
let disposing = false;
|
|
445
528
|
let intervalId;
|
|
446
529
|
const runFlush = () => {
|
|
447
|
-
if (!buffer.length) return Promise.resolve();
|
|
530
|
+
if (!buffer.length) return Promise.resolve(true);
|
|
448
531
|
const events = buffer.splice(0, buffer.length);
|
|
449
|
-
|
|
532
|
+
inflightExitBatch = events;
|
|
450
533
|
let succeeded = false;
|
|
451
534
|
return Promise.resolve().then(async () => {
|
|
452
535
|
if (batchSink) {
|
|
453
536
|
await batchSink(events);
|
|
454
537
|
} else {
|
|
455
|
-
for (
|
|
456
|
-
|
|
457
|
-
|
|
538
|
+
for (let i = 0; i < events.length; i++) {
|
|
539
|
+
try {
|
|
540
|
+
await sink?.(events[i]);
|
|
541
|
+
} catch {
|
|
542
|
+
buffer.unshift(...events.slice(i));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
458
545
|
}
|
|
459
546
|
}
|
|
460
547
|
succeeded = true;
|
|
461
548
|
}).catch(() => {
|
|
462
|
-
|
|
463
|
-
|
|
549
|
+
if (batchSink) {
|
|
550
|
+
buffer.unshift(...events);
|
|
551
|
+
}
|
|
552
|
+
}).then(async () => {
|
|
464
553
|
if (succeeded && buffer.length > 0 && !disposed) {
|
|
465
554
|
return runFlush();
|
|
466
555
|
}
|
|
556
|
+
return succeeded;
|
|
557
|
+
}).finally(() => {
|
|
558
|
+
inflightExitBatch = null;
|
|
467
559
|
});
|
|
468
560
|
};
|
|
469
561
|
const flush = () => {
|
|
470
|
-
if (disposed) return Promise.resolve();
|
|
562
|
+
if (disposed) return Promise.resolve(true);
|
|
471
563
|
if (flushInFlight) return flushInFlight;
|
|
472
|
-
if (!buffer.length) return Promise.resolve();
|
|
564
|
+
if (!buffer.length) return Promise.resolve(true);
|
|
473
565
|
flushInFlight = runFlush().finally(() => {
|
|
474
566
|
flushInFlight = null;
|
|
475
567
|
});
|
|
476
568
|
return flushInFlight;
|
|
477
569
|
};
|
|
570
|
+
const MAX_DISPOSE_FLUSH_ATTEMPTS = 10;
|
|
478
571
|
const drainAll = async () => {
|
|
479
|
-
|
|
480
|
-
while (buffer.length > 0) {
|
|
481
|
-
await flush();
|
|
572
|
+
let attempts = 0;
|
|
573
|
+
while (buffer.length > 0 && attempts < MAX_DISPOSE_FLUSH_ATTEMPTS) {
|
|
574
|
+
const delivered = await flush();
|
|
575
|
+
attempts += 1;
|
|
576
|
+
if (!delivered) break;
|
|
577
|
+
}
|
|
578
|
+
if (buffer.length > 0) {
|
|
579
|
+
if (isDevEnvironment()) {
|
|
580
|
+
console.warn(
|
|
581
|
+
`[lessonkit] dropped ${buffer.length} buffered telemetry event(s) after dispose flush cap`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
buffer.length = 0;
|
|
482
585
|
}
|
|
483
586
|
};
|
|
484
587
|
intervalId = flushIntervalMs > 0 ? globalThis.setInterval(() => void flush(), flushIntervalMs) : void 0;
|
|
@@ -487,18 +590,35 @@ function createTrackingClient(opts) {
|
|
|
487
590
|
track: (event) => {
|
|
488
591
|
if (disposed || disposing) return;
|
|
489
592
|
if (buffer.length >= maxBufferSize) {
|
|
490
|
-
|
|
593
|
+
opts?.onBufferDrop?.();
|
|
491
594
|
if (!warnedBufferCap && isDevEnvironment()) {
|
|
492
595
|
warnedBufferCap = true;
|
|
493
596
|
console.warn(
|
|
494
|
-
`[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events;
|
|
597
|
+
`[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; new events are dropped until the buffer drains.`
|
|
495
598
|
);
|
|
496
599
|
}
|
|
600
|
+
return;
|
|
497
601
|
}
|
|
498
602
|
buffer.push(event);
|
|
499
603
|
if (buffer.length >= maxBatchSize) void flush();
|
|
500
604
|
},
|
|
501
605
|
flush,
|
|
606
|
+
flushOnExit: opts?.exitBatchSink ? () => {
|
|
607
|
+
const fromBuffer = buffer.splice(0, buffer.length);
|
|
608
|
+
const fromInflight = inflightExitBatch ? [...inflightExitBatch] : [];
|
|
609
|
+
const events = [...fromInflight, ...fromBuffer];
|
|
610
|
+
if (!events.length) return;
|
|
611
|
+
try {
|
|
612
|
+
const result = opts.exitBatchSink(events);
|
|
613
|
+
if (result != null && typeof result.catch === "function") {
|
|
614
|
+
void result.catch(() => {
|
|
615
|
+
buffer.unshift(...events);
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
} catch {
|
|
619
|
+
buffer.unshift(...events);
|
|
620
|
+
}
|
|
621
|
+
} : void 0,
|
|
502
622
|
dispose: () => {
|
|
503
623
|
if (disposed || disposing) return Promise.resolve();
|
|
504
624
|
disposing = true;
|
|
@@ -651,6 +771,20 @@ var TELEMETRY_EVENT_REGISTRY = {
|
|
|
651
771
|
};
|
|
652
772
|
}
|
|
653
773
|
},
|
|
774
|
+
slide_viewed: {
|
|
775
|
+
requiresLessonId: true,
|
|
776
|
+
build: (opts, base) => {
|
|
777
|
+
if (opts.name !== "slide_viewed") throw new Error("unexpected event");
|
|
778
|
+
const lessonId = opts.lessonId;
|
|
779
|
+
if (!lessonId) throw new Error("slide_viewed requires active lessonId");
|
|
780
|
+
return {
|
|
781
|
+
name: "slide_viewed",
|
|
782
|
+
...base,
|
|
783
|
+
lessonId,
|
|
784
|
+
data: opts.data
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
},
|
|
654
788
|
compound_page_viewed: {
|
|
655
789
|
requiresLessonId: true,
|
|
656
790
|
build: (opts, base) => {
|
|
@@ -799,8 +933,7 @@ function createDefaultClock() {
|
|
|
799
933
|
function createNoopStorage() {
|
|
800
934
|
return {
|
|
801
935
|
getItem: () => null,
|
|
802
|
-
setItem: () =>
|
|
803
|
-
}
|
|
936
|
+
setItem: () => true
|
|
804
937
|
};
|
|
805
938
|
}
|
|
806
939
|
function createMemoryBackedSessionStorage(session) {
|
|
@@ -831,8 +964,10 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
831
964
|
memory.set(key, value);
|
|
832
965
|
try {
|
|
833
966
|
session.setItem(key, value);
|
|
967
|
+
return true;
|
|
834
968
|
} catch {
|
|
835
969
|
warnPersistFailure();
|
|
970
|
+
return false;
|
|
836
971
|
}
|
|
837
972
|
},
|
|
838
973
|
removeItem: (key) => {
|
|
@@ -857,6 +992,7 @@ function createInMemorySessionStoragePort() {
|
|
|
857
992
|
getItem: (key) => memory.get(key) ?? null,
|
|
858
993
|
setItem: (key, value) => {
|
|
859
994
|
memory.set(key, value);
|
|
995
|
+
return true;
|
|
860
996
|
},
|
|
861
997
|
removeItem: (key) => {
|
|
862
998
|
memory.delete(key);
|
|
@@ -909,7 +1045,12 @@ function createProgressController() {
|
|
|
909
1045
|
return { previousLessonId };
|
|
910
1046
|
},
|
|
911
1047
|
completeLesson: (lessonId, completedAtMs) => {
|
|
912
|
-
if (completedLessonIds.has(lessonId))
|
|
1048
|
+
if (completedLessonIds.has(lessonId)) {
|
|
1049
|
+
if (activeLessonId === lessonId) {
|
|
1050
|
+
activeLessonId = void 0;
|
|
1051
|
+
}
|
|
1052
|
+
return { didComplete: false };
|
|
1053
|
+
}
|
|
913
1054
|
completedLessonIds = new Set(completedLessonIds).add(lessonId);
|
|
914
1055
|
if (activeLessonId === lessonId) {
|
|
915
1056
|
activeLessonId = void 0;
|
|
@@ -929,6 +1070,12 @@ function createProgressController() {
|
|
|
929
1070
|
|
|
930
1071
|
// src/session.ts
|
|
931
1072
|
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
1073
|
+
var volatileSessionIds = /* @__PURE__ */ new WeakMap();
|
|
1074
|
+
var sharedVolatileSessionId = null;
|
|
1075
|
+
function isDevEnvironment2() {
|
|
1076
|
+
const g = globalThis;
|
|
1077
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
1078
|
+
}
|
|
932
1079
|
function getTabSessionId(storage) {
|
|
933
1080
|
return storage.getItem(SESSION_STORAGE_KEY);
|
|
934
1081
|
}
|
|
@@ -936,11 +1083,28 @@ var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
|
936
1083
|
var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
|
|
937
1084
|
var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
|
|
938
1085
|
function resolveSessionId(storage, provided) {
|
|
939
|
-
if (provided)
|
|
1086
|
+
if (provided !== void 0) {
|
|
1087
|
+
const trimmed = provided.trim();
|
|
1088
|
+
if (trimmed.length > 0) return trimmed;
|
|
1089
|
+
}
|
|
940
1090
|
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
941
1091
|
if (existing) return existing;
|
|
1092
|
+
const volatile = volatileSessionIds.get(storage);
|
|
1093
|
+
if (volatile) return volatile;
|
|
942
1094
|
const id = createSessionId();
|
|
943
|
-
storage.setItem(SESSION_STORAGE_KEY, id);
|
|
1095
|
+
const persisted = storage.setItem(SESSION_STORAGE_KEY, id);
|
|
1096
|
+
if (!persisted) {
|
|
1097
|
+
if (!sharedVolatileSessionId) {
|
|
1098
|
+
sharedVolatileSessionId = id;
|
|
1099
|
+
}
|
|
1100
|
+
volatileSessionIds.set(storage, sharedVolatileSessionId);
|
|
1101
|
+
if (isDevEnvironment2()) {
|
|
1102
|
+
console.warn(
|
|
1103
|
+
"[lessonkit] session id could not be persisted; reusing in-memory id for this tab."
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
return sharedVolatileSessionId;
|
|
1107
|
+
}
|
|
944
1108
|
return id;
|
|
945
1109
|
}
|
|
946
1110
|
function courseStartedStorageKey(sessionId, courseId) {
|
|
@@ -957,24 +1121,27 @@ function hasCourseStarted(storage, sessionId, courseId) {
|
|
|
957
1121
|
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
958
1122
|
}
|
|
959
1123
|
function markCourseStarted(storage, sessionId, courseId) {
|
|
960
|
-
if (!courseId) return;
|
|
961
|
-
storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
1124
|
+
if (!courseId) return false;
|
|
1125
|
+
return storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
962
1126
|
}
|
|
963
1127
|
function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
964
1128
|
if (!courseId) return false;
|
|
965
1129
|
return storage.getItem(courseStartedTrackingStorageKey(sessionId, courseId)) === "1";
|
|
966
1130
|
}
|
|
967
1131
|
function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
968
|
-
if (!courseId) return;
|
|
969
|
-
storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
|
|
1132
|
+
if (!courseId) return false;
|
|
1133
|
+
return storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
|
|
970
1134
|
}
|
|
971
1135
|
function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
972
1136
|
if (!courseId) return false;
|
|
973
1137
|
return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
|
|
974
1138
|
}
|
|
975
1139
|
function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
976
|
-
if (!courseId) return;
|
|
977
|
-
storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
|
|
1140
|
+
if (!courseId) return false;
|
|
1141
|
+
return storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
|
|
1142
|
+
}
|
|
1143
|
+
function resetSharedVolatileSessionIdForTests() {
|
|
1144
|
+
sharedVolatileSessionId = null;
|
|
978
1145
|
}
|
|
979
1146
|
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
980
1147
|
if (!courseId || fromSessionId === toSessionId) return;
|
|
@@ -993,19 +1160,29 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
|
|
|
993
1160
|
}
|
|
994
1161
|
|
|
995
1162
|
// src/runtime/courseLifecycle.ts
|
|
1163
|
+
var courseStartedEmitFlights = /* @__PURE__ */ new Set();
|
|
996
1164
|
function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
|
|
1165
|
+
const flightKey = `${ctx.sessionId}:${ctx.courseId}`;
|
|
997
1166
|
const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
998
1167
|
if (alreadyEmittedToSink) {
|
|
999
1168
|
return { emitted: true, marked };
|
|
1000
1169
|
}
|
|
1001
|
-
if (
|
|
1002
|
-
return { emitted: false, marked
|
|
1170
|
+
if (courseStartedEmitFlights.has(flightKey)) {
|
|
1171
|
+
return { emitted: false, marked };
|
|
1003
1172
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1173
|
+
courseStartedEmitFlights.add(flightKey);
|
|
1174
|
+
try {
|
|
1175
|
+
const emitted = deps.emitCourseStartedEvent(ctx);
|
|
1176
|
+
if (emitted && !marked) {
|
|
1177
|
+
markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
1178
|
+
}
|
|
1179
|
+
return {
|
|
1180
|
+
emitted,
|
|
1181
|
+
marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
|
|
1182
|
+
};
|
|
1183
|
+
} finally {
|
|
1184
|
+
courseStartedEmitFlights.delete(flightKey);
|
|
1007
1185
|
}
|
|
1008
|
-
return { emitted, marked: emitted };
|
|
1009
1186
|
}
|
|
1010
1187
|
function buildCourseStartedTelemetryEvent(ctx) {
|
|
1011
1188
|
return buildTelemetryEvent({
|
|
@@ -1095,7 +1272,7 @@ function createPluginRegistry(plugins = []) {
|
|
|
1095
1272
|
const composeTrackingSink = (sink, ctxSource) => {
|
|
1096
1273
|
if (!sink) return void 0;
|
|
1097
1274
|
const resolveCtx = () => typeof ctxSource === "function" ? ctxSource() : ctxSource;
|
|
1098
|
-
const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}`;
|
|
1275
|
+
const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}\0${ctx.user?.id ?? ""}`;
|
|
1099
1276
|
const layers = [];
|
|
1100
1277
|
let composed = sink;
|
|
1101
1278
|
for (const plugin of list) {
|
|
@@ -1166,6 +1343,9 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1166
1343
|
attemptId,
|
|
1167
1344
|
user
|
|
1168
1345
|
});
|
|
1346
|
+
if (!configSnapshot.deferPluginSetup) {
|
|
1347
|
+
pluginHost?.setupAll(getPluginCtx());
|
|
1348
|
+
}
|
|
1169
1349
|
const getSession = () => ({ sessionId, attemptId, user });
|
|
1170
1350
|
const syncSessionFromConfig = (next) => {
|
|
1171
1351
|
sessionId = resolveSessionId(storage, next.session?.sessionId);
|
|
@@ -1225,11 +1405,8 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1225
1405
|
getProgressState: () => progress.getState(),
|
|
1226
1406
|
getSession,
|
|
1227
1407
|
updateConfig(next) {
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
configSnapshot.plugins = next.plugins;
|
|
1231
|
-
pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
1232
|
-
}
|
|
1408
|
+
const previousCourseId = courseId;
|
|
1409
|
+
const sessionKeyBefore = JSON.stringify({ sessionId, attemptId, user });
|
|
1233
1410
|
if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
|
|
1234
1411
|
if (next.runtimeVersion !== void 0) {
|
|
1235
1412
|
if (next.runtimeVersion === "v1") warnRuntimeV1Deprecated();
|
|
@@ -1239,6 +1416,19 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1239
1416
|
configSnapshot.session = { ...configSnapshot.session, ...next.session };
|
|
1240
1417
|
}
|
|
1241
1418
|
syncSessionFromConfig(configSnapshot);
|
|
1419
|
+
const sessionKeyAfter = JSON.stringify({ sessionId, attemptId, user });
|
|
1420
|
+
if (next.courseId !== void 0 && next.courseId !== previousCourseId) {
|
|
1421
|
+
progress = createProgressController();
|
|
1422
|
+
}
|
|
1423
|
+
if (next.plugins !== void 0 && next.plugins !== pluginHost) {
|
|
1424
|
+
pluginHost?.disposeAll();
|
|
1425
|
+
configSnapshot.plugins = next.plugins;
|
|
1426
|
+
pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
1427
|
+
pluginHost?.setupAll(getPluginCtx());
|
|
1428
|
+
} else if (next.session !== void 0 && sessionKeyBefore !== sessionKeyAfter && pluginHost && !configSnapshot.deferPluginSetup) {
|
|
1429
|
+
pluginHost.disposeAll();
|
|
1430
|
+
pluginHost.setupAll(getPluginCtx());
|
|
1431
|
+
}
|
|
1242
1432
|
},
|
|
1243
1433
|
setActiveLesson(lessonId, emitFn) {
|
|
1244
1434
|
const wrapped = wrapEmitFn(emitFn);
|
|
@@ -1310,6 +1500,8 @@ export {
|
|
|
1310
1500
|
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
1311
1501
|
PAGE_ALLOWED_CHILD_TYPES,
|
|
1312
1502
|
SESSION_STORAGE_KEY,
|
|
1503
|
+
SLIDE_ALLOWED_CHILD_TYPES,
|
|
1504
|
+
SLIDE_DECK_ALLOWED_CHILD_TYPES,
|
|
1313
1505
|
TELEMETRY_EVENT_CATALOG,
|
|
1314
1506
|
TELEMETRY_EVENT_CATALOG_V2,
|
|
1315
1507
|
TELEMETRY_EVENT_CATALOG_V3,
|
|
@@ -1360,6 +1552,7 @@ export {
|
|
|
1360
1552
|
parseCompoundResumeState,
|
|
1361
1553
|
parseCourseId,
|
|
1362
1554
|
parseLessonId,
|
|
1555
|
+
resetSharedVolatileSessionIdForTests,
|
|
1363
1556
|
resetStoragePortForTests,
|
|
1364
1557
|
resetTelemetryBuilderWarningsForTests,
|
|
1365
1558
|
resolveSessionId,
|
package/package.json
CHANGED
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
"xapiVerb": "http://adlnet.gov/expapi/verbs/experienced",
|
|
10
10
|
"urnPattern": "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
11
11
|
},
|
|
12
|
+
{
|
|
13
|
+
"name": "slide_viewed",
|
|
14
|
+
"description": "Learner viewed a slide in a SlideDeck (Course Presentation)",
|
|
15
|
+
"requiredFields": ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
16
|
+
"dataFields": ["blockId", "slideIndex", "slideTitle"],
|
|
17
|
+
"xapiVerb": "http://adlnet.gov/expapi/verbs/experienced",
|
|
18
|
+
"urnPattern": "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
19
|
+
},
|
|
12
20
|
{
|
|
13
21
|
"name": "compound_page_viewed",
|
|
14
22
|
"description": "Learner activated a page inside a compound container",
|