@lessonkit/core 1.2.0 → 1.3.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/dist/index.cjs +219 -53
- package/dist/index.d.cts +32 -8
- package/dist/index.d.ts +32 -8
- package/dist/index.js +216 -53
- package/package.json +1 -1
- package/telemetry-catalog.v3.json +8 -0
package/dist/index.cjs
CHANGED
|
@@ -29,6 +29,8 @@ __export(index_exports, {
|
|
|
29
29
|
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES: () => INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
30
30
|
PAGE_ALLOWED_CHILD_TYPES: () => PAGE_ALLOWED_CHILD_TYPES,
|
|
31
31
|
SESSION_STORAGE_KEY: () => SESSION_STORAGE_KEY,
|
|
32
|
+
SLIDE_ALLOWED_CHILD_TYPES: () => SLIDE_ALLOWED_CHILD_TYPES,
|
|
33
|
+
SLIDE_DECK_ALLOWED_CHILD_TYPES: () => SLIDE_DECK_ALLOWED_CHILD_TYPES,
|
|
32
34
|
TELEMETRY_EVENT_CATALOG: () => TELEMETRY_EVENT_CATALOG,
|
|
33
35
|
TELEMETRY_EVENT_CATALOG_V2: () => TELEMETRY_EVENT_CATALOG_V2,
|
|
34
36
|
TELEMETRY_EVENT_CATALOG_V3: () => TELEMETRY_EVENT_CATALOG_V3,
|
|
@@ -79,6 +81,7 @@ __export(index_exports, {
|
|
|
79
81
|
parseCompoundResumeState: () => parseCompoundResumeState,
|
|
80
82
|
parseCourseId: () => parseCourseId,
|
|
81
83
|
parseLessonId: () => parseLessonId,
|
|
84
|
+
resetSharedVolatileSessionIdForTests: () => resetSharedVolatileSessionIdForTests,
|
|
82
85
|
resetStoragePortForTests: () => resetStoragePortForTests,
|
|
83
86
|
resetTelemetryBuilderWarningsForTests: () => resetTelemetryBuilderWarningsForTests,
|
|
84
87
|
resolveSessionId: () => resolveSessionId,
|
|
@@ -155,21 +158,40 @@ function assertValidId(input, path = "id") {
|
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
// src/slugify.ts
|
|
161
|
+
function shortHash(input) {
|
|
162
|
+
let h = 0;
|
|
163
|
+
for (let i = 0; i < input.length; i++) {
|
|
164
|
+
h = Math.imul(31, h) + input.charCodeAt(i) >>> 0;
|
|
165
|
+
}
|
|
166
|
+
return h.toString(36);
|
|
167
|
+
}
|
|
168
|
+
function uniqueFallbackId(input, usedIds) {
|
|
169
|
+
const hash = shortHash(input);
|
|
170
|
+
for (let n = 0; n < 100; n++) {
|
|
171
|
+
const candidate = (n === 0 ? `id-${hash}` : `id-${hash}-${n}`).slice(0, 64);
|
|
172
|
+
const validated2 = validateId(candidate);
|
|
173
|
+
if (validated2.ok && !usedIds.has(validated2.id)) return validated2.id;
|
|
174
|
+
}
|
|
175
|
+
const timed = `id-${hash}-${Date.now().toString(36)}`.slice(0, 64);
|
|
176
|
+
const validated = validateId(timed);
|
|
177
|
+
return validated.ok ? validated.id : `id-${hash}`;
|
|
178
|
+
}
|
|
158
179
|
function slugifyId(input) {
|
|
159
180
|
const slug = input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-").slice(0, 64);
|
|
160
|
-
if (!slug.length) return
|
|
161
|
-
const candidate = /^[a-z]/.test(slug) ? slug : `id-${slug}
|
|
181
|
+
if (!slug.length) return uniqueFallbackId(input, /* @__PURE__ */ new Set());
|
|
182
|
+
const candidate = /^[a-z]/.test(slug) ? slug : `id-${slug}`.slice(0, 64);
|
|
162
183
|
const validated = validateId(candidate);
|
|
163
|
-
return validated.ok ? validated.id :
|
|
184
|
+
return validated.ok ? validated.id : uniqueFallbackId(input, /* @__PURE__ */ new Set());
|
|
164
185
|
}
|
|
165
186
|
function deriveId(title, usedIds = /* @__PURE__ */ new Set()) {
|
|
166
187
|
const base = slugifyId(title);
|
|
167
|
-
if (!usedIds.has(base)) return base;
|
|
188
|
+
if (!usedIds.has(base) && validateId(base).ok) return base;
|
|
168
189
|
for (let n = 2; n < 1e3; n++) {
|
|
169
|
-
const candidate = `${base}-${n}
|
|
170
|
-
|
|
190
|
+
const candidate = `${base}-${n}`.slice(0, 64);
|
|
191
|
+
const validated = validateId(candidate);
|
|
192
|
+
if (validated.ok && !usedIds.has(validated.id)) return validated.id;
|
|
171
193
|
}
|
|
172
|
-
return `${
|
|
194
|
+
return uniqueFallbackId(`${title}-${Date.now()}`, usedIds);
|
|
173
195
|
}
|
|
174
196
|
|
|
175
197
|
// src/urn.ts
|
|
@@ -217,6 +239,27 @@ function clampCompoundPageIndex(index, pageCount) {
|
|
|
217
239
|
if (pageCount < 1) return 0;
|
|
218
240
|
return Math.min(Math.max(0, Math.floor(index)), pageCount - 1);
|
|
219
241
|
}
|
|
242
|
+
function isJsonPrimitive(value) {
|
|
243
|
+
return value === null || typeof value === "boolean" || typeof value === "string" || typeof value === "number" && Number.isFinite(value);
|
|
244
|
+
}
|
|
245
|
+
function isPlainStringKeyMap(value) {
|
|
246
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
247
|
+
return Object.entries(value).every(
|
|
248
|
+
([key, entry]) => typeof key === "string" && isJsonPrimitive(entry)
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
function isValidChildResumeValue(value) {
|
|
252
|
+
if (isJsonPrimitive(value)) return true;
|
|
253
|
+
if (Array.isArray(value)) return value.every((item) => isJsonPrimitive(item));
|
|
254
|
+
if (isPlainStringKeyMap(value)) return true;
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
function isPlainSerializableChildState(value) {
|
|
258
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
259
|
+
return Object.values(value).every(
|
|
260
|
+
(entry) => isValidChildResumeValue(entry)
|
|
261
|
+
);
|
|
262
|
+
}
|
|
220
263
|
function parseCompoundResumeState(raw) {
|
|
221
264
|
if (!raw || typeof raw !== "object") return null;
|
|
222
265
|
const obj = raw;
|
|
@@ -225,7 +268,7 @@ function parseCompoundResumeState(raw) {
|
|
|
225
268
|
const childStates = {};
|
|
226
269
|
if (obj.childStates && typeof obj.childStates === "object" && !Array.isArray(obj.childStates)) {
|
|
227
270
|
for (const [key, value] of Object.entries(obj.childStates)) {
|
|
228
|
-
if (
|
|
271
|
+
if (isPlainSerializableChildState(value)) {
|
|
229
272
|
childStates[key] = value;
|
|
230
273
|
}
|
|
231
274
|
}
|
|
@@ -239,22 +282,40 @@ function parseCompoundResumeState(raw) {
|
|
|
239
282
|
};
|
|
240
283
|
}
|
|
241
284
|
|
|
285
|
+
// src/internal/env.ts
|
|
286
|
+
function isDevEnvironment() {
|
|
287
|
+
const g = globalThis;
|
|
288
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
289
|
+
}
|
|
290
|
+
function warnDev(message, err) {
|
|
291
|
+
if (!isDevEnvironment()) return;
|
|
292
|
+
console.warn(message, err instanceof Error ? err.message : err);
|
|
293
|
+
}
|
|
294
|
+
|
|
242
295
|
// src/compoundState.ts
|
|
243
296
|
var COMPOUND_STATE_PREFIX = "lessonkit:compound:";
|
|
244
297
|
function compoundStateStorageKey(courseId, compoundId) {
|
|
245
298
|
return `${COMPOUND_STATE_PREFIX}${courseId}:${compoundId}`;
|
|
246
299
|
}
|
|
247
300
|
function loadCompoundState(storage, courseId, compoundId) {
|
|
248
|
-
const
|
|
301
|
+
const key = compoundStateStorageKey(courseId, compoundId);
|
|
302
|
+
const raw = storage.getItem(key);
|
|
249
303
|
if (!raw) return null;
|
|
250
304
|
try {
|
|
251
|
-
|
|
305
|
+
const parsed = parseCompoundResumeState(JSON.parse(raw));
|
|
306
|
+
if (parsed === null && isDevEnvironment()) {
|
|
307
|
+
console.warn(`[lessonkit] Ignoring corrupt compound resume state at ${key}`);
|
|
308
|
+
}
|
|
309
|
+
return parsed;
|
|
252
310
|
} catch {
|
|
311
|
+
if (isDevEnvironment()) {
|
|
312
|
+
console.warn(`[lessonkit] Ignoring corrupt compound resume state at ${key}`);
|
|
313
|
+
}
|
|
253
314
|
return null;
|
|
254
315
|
}
|
|
255
316
|
}
|
|
256
317
|
function saveCompoundState(storage, courseId, compoundId, state) {
|
|
257
|
-
storage.setItem(compoundStateStorageKey(courseId, compoundId), JSON.stringify(state));
|
|
318
|
+
return storage.setItem(compoundStateStorageKey(courseId, compoundId), JSON.stringify(state));
|
|
258
319
|
}
|
|
259
320
|
function clearCompoundState(storage, courseId, compoundId) {
|
|
260
321
|
storage.removeItem?.(compoundStateStorageKey(courseId, compoundId));
|
|
@@ -284,6 +345,28 @@ var PAGE_ALLOWED_CHILD_TYPES = [
|
|
|
284
345
|
"ProgressTracker"
|
|
285
346
|
];
|
|
286
347
|
var INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES = ["Page"];
|
|
348
|
+
var SLIDE_ALLOWED_CHILD_TYPES = [
|
|
349
|
+
"Text",
|
|
350
|
+
"Heading",
|
|
351
|
+
"Image",
|
|
352
|
+
"Scenario",
|
|
353
|
+
"Reflection",
|
|
354
|
+
"Quiz",
|
|
355
|
+
"KnowledgeCheck",
|
|
356
|
+
"TrueFalse",
|
|
357
|
+
"FillInTheBlanks",
|
|
358
|
+
"DragAndDrop",
|
|
359
|
+
"DragTheWords",
|
|
360
|
+
"MarkTheWords",
|
|
361
|
+
"Accordion",
|
|
362
|
+
"DialogCards",
|
|
363
|
+
"Flashcards",
|
|
364
|
+
"ImageHotspots",
|
|
365
|
+
"FindHotspot",
|
|
366
|
+
"FindMultipleHotspots",
|
|
367
|
+
"ImageSlider"
|
|
368
|
+
];
|
|
369
|
+
var SLIDE_DECK_ALLOWED_CHILD_TYPES = ["Slide"];
|
|
287
370
|
var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
|
|
288
371
|
"TrueFalse",
|
|
289
372
|
"FillInTheBlanks",
|
|
@@ -298,11 +381,15 @@ var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
|
|
|
298
381
|
var ALLOWLISTS = {
|
|
299
382
|
Page: PAGE_ALLOWED_CHILD_TYPES,
|
|
300
383
|
InteractiveBook: INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
384
|
+
Slide: SLIDE_ALLOWED_CHILD_TYPES,
|
|
385
|
+
SlideDeck: SLIDE_DECK_ALLOWED_CHILD_TYPES,
|
|
301
386
|
AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES
|
|
302
387
|
};
|
|
303
388
|
var COMPOUND_MAX_NESTING_DEPTH = {
|
|
304
389
|
Page: 1,
|
|
305
390
|
InteractiveBook: 2,
|
|
391
|
+
Slide: 1,
|
|
392
|
+
SlideDeck: 2,
|
|
306
393
|
AssessmentSequence: 1
|
|
307
394
|
};
|
|
308
395
|
function getAllowedChildTypes(parent) {
|
|
@@ -420,6 +507,14 @@ var TELEMETRY_EVENT_CATALOG_V3 = [
|
|
|
420
507
|
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
421
508
|
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
422
509
|
},
|
|
510
|
+
{
|
|
511
|
+
name: "slide_viewed",
|
|
512
|
+
description: "Learner viewed a slide in a SlideDeck (Course Presentation)",
|
|
513
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
514
|
+
dataFields: ["blockId", "slideIndex", "slideTitle"],
|
|
515
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
516
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
517
|
+
},
|
|
423
518
|
{
|
|
424
519
|
name: "compound_page_viewed",
|
|
425
520
|
description: "Learner activated a page inside a compound container",
|
|
@@ -465,16 +560,6 @@ function buildTelemetryCatalogV3() {
|
|
|
465
560
|
return TELEMETRY_EVENT_CATALOG_V3.map((entry) => ({ ...entry }));
|
|
466
561
|
}
|
|
467
562
|
|
|
468
|
-
// src/internal/env.ts
|
|
469
|
-
function isDevEnvironment() {
|
|
470
|
-
const g = globalThis;
|
|
471
|
-
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
472
|
-
}
|
|
473
|
-
function warnDev(message, err) {
|
|
474
|
-
if (!isDevEnvironment()) return;
|
|
475
|
-
console.warn(message, err instanceof Error ? err.message : err);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
563
|
// src/internal/sinkInvoke.ts
|
|
479
564
|
function invokeTrackingSink(sink, event) {
|
|
480
565
|
let result;
|
|
@@ -539,41 +624,58 @@ function createTrackingClient(opts) {
|
|
|
539
624
|
let disposing = false;
|
|
540
625
|
let intervalId;
|
|
541
626
|
const runFlush = () => {
|
|
542
|
-
if (!buffer.length) return Promise.resolve();
|
|
627
|
+
if (!buffer.length) return Promise.resolve(true);
|
|
543
628
|
const events = buffer.splice(0, buffer.length);
|
|
544
|
-
let sent = 0;
|
|
545
629
|
let succeeded = false;
|
|
546
630
|
return Promise.resolve().then(async () => {
|
|
547
631
|
if (batchSink) {
|
|
548
632
|
await batchSink(events);
|
|
549
633
|
} else {
|
|
550
|
-
for (
|
|
551
|
-
|
|
552
|
-
|
|
634
|
+
for (let i = 0; i < events.length; i++) {
|
|
635
|
+
try {
|
|
636
|
+
await sink?.(events[i]);
|
|
637
|
+
} catch {
|
|
638
|
+
buffer.unshift(...events.slice(i));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
553
641
|
}
|
|
554
642
|
}
|
|
555
643
|
succeeded = true;
|
|
556
644
|
}).catch(() => {
|
|
557
|
-
|
|
558
|
-
|
|
645
|
+
if (batchSink) {
|
|
646
|
+
buffer.unshift(...events);
|
|
647
|
+
}
|
|
648
|
+
}).then(async () => {
|
|
559
649
|
if (succeeded && buffer.length > 0 && !disposed) {
|
|
560
650
|
return runFlush();
|
|
561
651
|
}
|
|
652
|
+
return succeeded;
|
|
562
653
|
});
|
|
563
654
|
};
|
|
564
655
|
const flush = () => {
|
|
565
|
-
if (disposed) return Promise.resolve();
|
|
656
|
+
if (disposed) return Promise.resolve(true);
|
|
566
657
|
if (flushInFlight) return flushInFlight;
|
|
567
|
-
if (!buffer.length) return Promise.resolve();
|
|
658
|
+
if (!buffer.length) return Promise.resolve(true);
|
|
568
659
|
flushInFlight = runFlush().finally(() => {
|
|
569
660
|
flushInFlight = null;
|
|
570
661
|
});
|
|
571
662
|
return flushInFlight;
|
|
572
663
|
};
|
|
664
|
+
const MAX_DISPOSE_FLUSH_ATTEMPTS = 10;
|
|
573
665
|
const drainAll = async () => {
|
|
574
|
-
|
|
575
|
-
while (buffer.length > 0) {
|
|
576
|
-
await flush();
|
|
666
|
+
let attempts = 0;
|
|
667
|
+
while (buffer.length > 0 && attempts < MAX_DISPOSE_FLUSH_ATTEMPTS) {
|
|
668
|
+
const delivered = await flush();
|
|
669
|
+
attempts += 1;
|
|
670
|
+
if (!delivered) break;
|
|
671
|
+
}
|
|
672
|
+
if (buffer.length > 0) {
|
|
673
|
+
if (isDevEnvironment()) {
|
|
674
|
+
console.warn(
|
|
675
|
+
`[lessonkit] dropped ${buffer.length} buffered telemetry event(s) after dispose flush cap`
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
buffer.length = 0;
|
|
577
679
|
}
|
|
578
680
|
};
|
|
579
681
|
intervalId = flushIntervalMs > 0 ? globalThis.setInterval(() => void flush(), flushIntervalMs) : void 0;
|
|
@@ -582,13 +684,14 @@ function createTrackingClient(opts) {
|
|
|
582
684
|
track: (event) => {
|
|
583
685
|
if (disposed || disposing) return;
|
|
584
686
|
if (buffer.length >= maxBufferSize) {
|
|
585
|
-
|
|
687
|
+
opts?.onBufferDrop?.();
|
|
586
688
|
if (!warnedBufferCap && isDevEnvironment()) {
|
|
587
689
|
warnedBufferCap = true;
|
|
588
690
|
console.warn(
|
|
589
|
-
`[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events;
|
|
691
|
+
`[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; new events are dropped until the buffer drains.`
|
|
590
692
|
);
|
|
591
693
|
}
|
|
694
|
+
return;
|
|
592
695
|
}
|
|
593
696
|
buffer.push(event);
|
|
594
697
|
if (buffer.length >= maxBatchSize) void flush();
|
|
@@ -746,6 +849,20 @@ var TELEMETRY_EVENT_REGISTRY = {
|
|
|
746
849
|
};
|
|
747
850
|
}
|
|
748
851
|
},
|
|
852
|
+
slide_viewed: {
|
|
853
|
+
requiresLessonId: true,
|
|
854
|
+
build: (opts, base) => {
|
|
855
|
+
if (opts.name !== "slide_viewed") throw new Error("unexpected event");
|
|
856
|
+
const lessonId = opts.lessonId;
|
|
857
|
+
if (!lessonId) throw new Error("slide_viewed requires active lessonId");
|
|
858
|
+
return {
|
|
859
|
+
name: "slide_viewed",
|
|
860
|
+
...base,
|
|
861
|
+
lessonId,
|
|
862
|
+
data: opts.data
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
},
|
|
749
866
|
compound_page_viewed: {
|
|
750
867
|
requiresLessonId: true,
|
|
751
868
|
build: (opts, base) => {
|
|
@@ -894,8 +1011,7 @@ function createDefaultClock() {
|
|
|
894
1011
|
function createNoopStorage() {
|
|
895
1012
|
return {
|
|
896
1013
|
getItem: () => null,
|
|
897
|
-
setItem: () =>
|
|
898
|
-
}
|
|
1014
|
+
setItem: () => true
|
|
899
1015
|
};
|
|
900
1016
|
}
|
|
901
1017
|
function createMemoryBackedSessionStorage(session) {
|
|
@@ -926,8 +1042,10 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
926
1042
|
memory.set(key, value);
|
|
927
1043
|
try {
|
|
928
1044
|
session.setItem(key, value);
|
|
1045
|
+
return true;
|
|
929
1046
|
} catch {
|
|
930
1047
|
warnPersistFailure();
|
|
1048
|
+
return false;
|
|
931
1049
|
}
|
|
932
1050
|
},
|
|
933
1051
|
removeItem: (key) => {
|
|
@@ -952,6 +1070,7 @@ function createInMemorySessionStoragePort() {
|
|
|
952
1070
|
getItem: (key) => memory.get(key) ?? null,
|
|
953
1071
|
setItem: (key, value) => {
|
|
954
1072
|
memory.set(key, value);
|
|
1073
|
+
return true;
|
|
955
1074
|
},
|
|
956
1075
|
removeItem: (key) => {
|
|
957
1076
|
memory.delete(key);
|
|
@@ -1004,7 +1123,12 @@ function createProgressController() {
|
|
|
1004
1123
|
return { previousLessonId };
|
|
1005
1124
|
},
|
|
1006
1125
|
completeLesson: (lessonId, completedAtMs) => {
|
|
1007
|
-
if (completedLessonIds.has(lessonId))
|
|
1126
|
+
if (completedLessonIds.has(lessonId)) {
|
|
1127
|
+
if (activeLessonId === lessonId) {
|
|
1128
|
+
activeLessonId = void 0;
|
|
1129
|
+
}
|
|
1130
|
+
return { didComplete: false };
|
|
1131
|
+
}
|
|
1008
1132
|
completedLessonIds = new Set(completedLessonIds).add(lessonId);
|
|
1009
1133
|
if (activeLessonId === lessonId) {
|
|
1010
1134
|
activeLessonId = void 0;
|
|
@@ -1024,6 +1148,12 @@ function createProgressController() {
|
|
|
1024
1148
|
|
|
1025
1149
|
// src/session.ts
|
|
1026
1150
|
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
1151
|
+
var volatileSessionIds = /* @__PURE__ */ new WeakMap();
|
|
1152
|
+
var sharedVolatileSessionId = null;
|
|
1153
|
+
function isDevEnvironment2() {
|
|
1154
|
+
const g = globalThis;
|
|
1155
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
1156
|
+
}
|
|
1027
1157
|
function getTabSessionId(storage) {
|
|
1028
1158
|
return storage.getItem(SESSION_STORAGE_KEY);
|
|
1029
1159
|
}
|
|
@@ -1031,11 +1161,28 @@ var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
|
1031
1161
|
var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
|
|
1032
1162
|
var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
|
|
1033
1163
|
function resolveSessionId(storage, provided) {
|
|
1034
|
-
if (provided)
|
|
1164
|
+
if (provided !== void 0) {
|
|
1165
|
+
const trimmed = provided.trim();
|
|
1166
|
+
if (trimmed.length > 0) return trimmed;
|
|
1167
|
+
}
|
|
1035
1168
|
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
1036
1169
|
if (existing) return existing;
|
|
1170
|
+
const volatile = volatileSessionIds.get(storage);
|
|
1171
|
+
if (volatile) return volatile;
|
|
1037
1172
|
const id = createSessionId();
|
|
1038
|
-
storage.setItem(SESSION_STORAGE_KEY, id);
|
|
1173
|
+
const persisted = storage.setItem(SESSION_STORAGE_KEY, id);
|
|
1174
|
+
if (!persisted) {
|
|
1175
|
+
if (!sharedVolatileSessionId) {
|
|
1176
|
+
sharedVolatileSessionId = id;
|
|
1177
|
+
}
|
|
1178
|
+
volatileSessionIds.set(storage, sharedVolatileSessionId);
|
|
1179
|
+
if (isDevEnvironment2()) {
|
|
1180
|
+
console.warn(
|
|
1181
|
+
"[lessonkit] session id could not be persisted; reusing in-memory id for this tab."
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
return sharedVolatileSessionId;
|
|
1185
|
+
}
|
|
1039
1186
|
return id;
|
|
1040
1187
|
}
|
|
1041
1188
|
function courseStartedStorageKey(sessionId, courseId) {
|
|
@@ -1052,8 +1199,8 @@ function hasCourseStarted(storage, sessionId, courseId) {
|
|
|
1052
1199
|
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
1053
1200
|
}
|
|
1054
1201
|
function markCourseStarted(storage, sessionId, courseId) {
|
|
1055
|
-
if (!courseId) return;
|
|
1056
|
-
storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
1202
|
+
if (!courseId) return false;
|
|
1203
|
+
return storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
1057
1204
|
}
|
|
1058
1205
|
function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
1059
1206
|
if (!courseId) return false;
|
|
@@ -1071,6 +1218,9 @@ function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
|
1071
1218
|
if (!courseId) return;
|
|
1072
1219
|
storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
|
|
1073
1220
|
}
|
|
1221
|
+
function resetSharedVolatileSessionIdForTests() {
|
|
1222
|
+
sharedVolatileSessionId = null;
|
|
1223
|
+
}
|
|
1074
1224
|
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
1075
1225
|
if (!courseId || fromSessionId === toSessionId) return;
|
|
1076
1226
|
if (hasCourseStarted(storage, fromSessionId, courseId)) {
|
|
@@ -1093,14 +1243,14 @@ function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
|
|
|
1093
1243
|
if (alreadyEmittedToSink) {
|
|
1094
1244
|
return { emitted: true, marked };
|
|
1095
1245
|
}
|
|
1096
|
-
if (marked) {
|
|
1097
|
-
return { emitted: false, marked: true };
|
|
1098
|
-
}
|
|
1099
1246
|
const emitted = deps.emitCourseStartedEvent(ctx);
|
|
1100
|
-
if (emitted) {
|
|
1247
|
+
if (emitted && !marked) {
|
|
1101
1248
|
markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
1102
1249
|
}
|
|
1103
|
-
return {
|
|
1250
|
+
return {
|
|
1251
|
+
emitted,
|
|
1252
|
+
marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
|
|
1253
|
+
};
|
|
1104
1254
|
}
|
|
1105
1255
|
function buildCourseStartedTelemetryEvent(ctx) {
|
|
1106
1256
|
return buildTelemetryEvent({
|
|
@@ -1190,7 +1340,7 @@ function createPluginRegistry(plugins = []) {
|
|
|
1190
1340
|
const composeTrackingSink = (sink, ctxSource) => {
|
|
1191
1341
|
if (!sink) return void 0;
|
|
1192
1342
|
const resolveCtx = () => typeof ctxSource === "function" ? ctxSource() : ctxSource;
|
|
1193
|
-
const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}`;
|
|
1343
|
+
const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}\0${ctx.user?.id ?? ""}`;
|
|
1194
1344
|
const layers = [];
|
|
1195
1345
|
let composed = sink;
|
|
1196
1346
|
for (const plugin of list) {
|
|
@@ -1261,6 +1411,9 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1261
1411
|
attemptId,
|
|
1262
1412
|
user
|
|
1263
1413
|
});
|
|
1414
|
+
if (!configSnapshot.deferPluginSetup) {
|
|
1415
|
+
pluginHost?.setupAll(getPluginCtx());
|
|
1416
|
+
}
|
|
1264
1417
|
const getSession = () => ({ sessionId, attemptId, user });
|
|
1265
1418
|
const syncSessionFromConfig = (next) => {
|
|
1266
1419
|
sessionId = resolveSessionId(storage, next.session?.sessionId);
|
|
@@ -1320,11 +1473,8 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1320
1473
|
getProgressState: () => progress.getState(),
|
|
1321
1474
|
getSession,
|
|
1322
1475
|
updateConfig(next) {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
configSnapshot.plugins = next.plugins;
|
|
1326
|
-
pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
1327
|
-
}
|
|
1476
|
+
const previousCourseId = courseId;
|
|
1477
|
+
const sessionKeyBefore = JSON.stringify({ sessionId, attemptId, user });
|
|
1328
1478
|
if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
|
|
1329
1479
|
if (next.runtimeVersion !== void 0) {
|
|
1330
1480
|
if (next.runtimeVersion === "v1") warnRuntimeV1Deprecated();
|
|
@@ -1334,6 +1484,19 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1334
1484
|
configSnapshot.session = { ...configSnapshot.session, ...next.session };
|
|
1335
1485
|
}
|
|
1336
1486
|
syncSessionFromConfig(configSnapshot);
|
|
1487
|
+
const sessionKeyAfter = JSON.stringify({ sessionId, attemptId, user });
|
|
1488
|
+
if (next.courseId !== void 0 && next.courseId !== previousCourseId) {
|
|
1489
|
+
progress = createProgressController();
|
|
1490
|
+
}
|
|
1491
|
+
if (next.plugins !== void 0 && next.plugins !== pluginHost) {
|
|
1492
|
+
pluginHost?.disposeAll();
|
|
1493
|
+
configSnapshot.plugins = next.plugins;
|
|
1494
|
+
pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
1495
|
+
pluginHost?.setupAll(getPluginCtx());
|
|
1496
|
+
} else if (next.session !== void 0 && sessionKeyBefore !== sessionKeyAfter && pluginHost && !configSnapshot.deferPluginSetup) {
|
|
1497
|
+
pluginHost.disposeAll();
|
|
1498
|
+
pluginHost.setupAll(getPluginCtx());
|
|
1499
|
+
}
|
|
1337
1500
|
},
|
|
1338
1501
|
setActiveLesson(lessonId, emitFn) {
|
|
1339
1502
|
const wrapped = wrapEmitFn(emitFn);
|
|
@@ -1406,6 +1569,8 @@ function defineLifecyclePlugin(plugin) {
|
|
|
1406
1569
|
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
1407
1570
|
PAGE_ALLOWED_CHILD_TYPES,
|
|
1408
1571
|
SESSION_STORAGE_KEY,
|
|
1572
|
+
SLIDE_ALLOWED_CHILD_TYPES,
|
|
1573
|
+
SLIDE_DECK_ALLOWED_CHILD_TYPES,
|
|
1409
1574
|
TELEMETRY_EVENT_CATALOG,
|
|
1410
1575
|
TELEMETRY_EVENT_CATALOG_V2,
|
|
1411
1576
|
TELEMETRY_EVENT_CATALOG_V3,
|
|
@@ -1456,6 +1621,7 @@ function defineLifecyclePlugin(plugin) {
|
|
|
1456
1621
|
parseCompoundResumeState,
|
|
1457
1622
|
parseCourseId,
|
|
1458
1623
|
parseLessonId,
|
|
1624
|
+
resetSharedVolatileSessionIdForTests,
|
|
1459
1625
|
resetStoragePortForTests,
|
|
1460
1626
|
resetTelemetryBuilderWarningsForTests,
|
|
1461
1627
|
resolveSessionId,
|
package/dist/index.d.cts
CHANGED
|
@@ -92,7 +92,7 @@ type AssessmentBaseProps = AssessmentBehaviour & {
|
|
|
92
92
|
passingScore?: number;
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
-
type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "assessment_answered" | "assessment_completed" | "interaction" | "book_page_viewed" | "compound_page_viewed" | "hotspot_opened" | "accordion_section_toggled" | "flashcard_flipped" | "image_slider_changed";
|
|
95
|
+
type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "assessment_answered" | "assessment_completed" | "interaction" | "book_page_viewed" | "slide_viewed" | "compound_page_viewed" | "hotspot_opened" | "accordion_section_toggled" | "flashcard_flipped" | "image_slider_changed";
|
|
96
96
|
type TelemetryUser = {
|
|
97
97
|
id?: string;
|
|
98
98
|
email?: string;
|
|
@@ -150,6 +150,11 @@ type BookPageViewedData = {
|
|
|
150
150
|
pageIndex: number;
|
|
151
151
|
pageTitle?: string;
|
|
152
152
|
};
|
|
153
|
+
type SlideViewedData = {
|
|
154
|
+
blockId: BlockId;
|
|
155
|
+
slideIndex: number;
|
|
156
|
+
slideTitle?: string;
|
|
157
|
+
};
|
|
153
158
|
type CompoundPageViewedData = {
|
|
154
159
|
blockId: BlockId;
|
|
155
160
|
pageIndex: number;
|
|
@@ -217,6 +222,10 @@ type TelemetryEvent = (TelemetryEventBase & {
|
|
|
217
222
|
name: "book_page_viewed";
|
|
218
223
|
lessonId: LessonId;
|
|
219
224
|
data: BookPageViewedData;
|
|
225
|
+
}) | (TelemetryEventBase & {
|
|
226
|
+
name: "slide_viewed";
|
|
227
|
+
lessonId: LessonId;
|
|
228
|
+
data: SlideViewedData;
|
|
220
229
|
}) | (TelemetryEventBase & {
|
|
221
230
|
name: "compound_page_viewed";
|
|
222
231
|
lessonId: LessonId;
|
|
@@ -248,7 +257,8 @@ type TelemetrySink = (event: TelemetryEvent) => void | Promise<void>;
|
|
|
248
257
|
type TelemetryBatchSink = (events: TelemetryEvent[]) => void | Promise<void>;
|
|
249
258
|
type TrackingClient = {
|
|
250
259
|
track: (event: TelemetryEvent) => void;
|
|
251
|
-
|
|
260
|
+
/** Resolves to true when all buffered events were delivered; false when a sink failure re-queued events. */
|
|
261
|
+
flush?: () => void | Promise<boolean>;
|
|
252
262
|
dispose?: () => void | Promise<void>;
|
|
253
263
|
};
|
|
254
264
|
|
|
@@ -289,7 +299,8 @@ type CompoundBaseProps = {
|
|
|
289
299
|
|
|
290
300
|
type StoragePort = {
|
|
291
301
|
getItem: (key: string) => string | null;
|
|
292
|
-
|
|
302
|
+
/** Returns false when the value could not be durably persisted (e.g. sessionStorage quota). */
|
|
303
|
+
setItem: (key: string, value: string) => boolean;
|
|
293
304
|
removeItem?: (key: string) => void;
|
|
294
305
|
/** @internal Test helper to clear in-memory fallback state. */
|
|
295
306
|
resetForTests?: () => void;
|
|
@@ -310,14 +321,17 @@ declare function createGlobalTimer(): TimerPort;
|
|
|
310
321
|
|
|
311
322
|
declare function compoundStateStorageKey(courseId: CourseId, compoundId: BlockId): string;
|
|
312
323
|
declare function loadCompoundState(storage: StoragePort, courseId: CourseId, compoundId: BlockId): CompoundResumeState | null;
|
|
313
|
-
declare function saveCompoundState(storage: StoragePort, courseId: CourseId, compoundId: BlockId, state: CompoundResumeState):
|
|
324
|
+
declare function saveCompoundState(storage: StoragePort, courseId: CourseId, compoundId: BlockId, state: CompoundResumeState): boolean;
|
|
314
325
|
declare function clearCompoundState(storage: StoragePort, courseId: CourseId, compoundId: BlockId): void;
|
|
315
326
|
|
|
316
327
|
/** Canonical compound child allowlists (H5P sub-content curation). */
|
|
317
328
|
declare const PAGE_ALLOWED_CHILD_TYPES: readonly ["Text", "Heading", "Image", "Scenario", "Reflection", "Quiz", "KnowledgeCheck", "TrueFalse", "FillInTheBlanks", "DragAndDrop", "DragTheWords", "MarkTheWords", "Accordion", "DialogCards", "Flashcards", "ImageHotspots", "FindHotspot", "FindMultipleHotspots", "ImageSlider", "ProgressTracker"];
|
|
318
329
|
declare const INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES: readonly ["Page"];
|
|
330
|
+
/** Per-slide content (H5P Course Presentation slide row). Excludes ProgressTracker. */
|
|
331
|
+
declare const SLIDE_ALLOWED_CHILD_TYPES: readonly ["Text", "Heading", "Image", "Scenario", "Reflection", "Quiz", "KnowledgeCheck", "TrueFalse", "FillInTheBlanks", "DragAndDrop", "DragTheWords", "MarkTheWords", "Accordion", "DialogCards", "Flashcards", "ImageHotspots", "FindHotspot", "FindMultipleHotspots", "ImageSlider"];
|
|
332
|
+
declare const SLIDE_DECK_ALLOWED_CHILD_TYPES: readonly ["Slide"];
|
|
319
333
|
declare const ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES: readonly ["TrueFalse", "FillInTheBlanks", "DragAndDrop", "DragTheWords", "MarkTheWords", "Quiz", "KnowledgeCheck", "FindHotspot", "FindMultipleHotspots"];
|
|
320
|
-
type CompoundParentType = "Page" | "InteractiveBook" | "AssessmentSequence";
|
|
334
|
+
type CompoundParentType = "Page" | "InteractiveBook" | "Slide" | "SlideDeck" | "AssessmentSequence";
|
|
321
335
|
declare const COMPOUND_MAX_NESTING_DEPTH: Record<CompoundParentType, number>;
|
|
322
336
|
declare function getAllowedChildTypes(parent: CompoundParentType): readonly string[];
|
|
323
337
|
declare function isChildTypeAllowed(parent: CompoundParentType, childType: string): boolean;
|
|
@@ -349,7 +363,7 @@ declare const TELEMETRY_EVENT_CATALOG_V2: TelemetryCatalogV2Entry[];
|
|
|
349
363
|
declare function buildTelemetryCatalogV2(): TelemetryCatalogV2Entry[];
|
|
350
364
|
|
|
351
365
|
declare const telemetryCatalogV3Version: 3;
|
|
352
|
-
type TelemetryCatalogV3EventName = Extract<TelemetryEventName, "book_page_viewed" | "compound_page_viewed" | "hotspot_opened" | "accordion_section_toggled" | "flashcard_flipped" | "image_slider_changed">;
|
|
366
|
+
type TelemetryCatalogV3EventName = Extract<TelemetryEventName, "book_page_viewed" | "slide_viewed" | "compound_page_viewed" | "hotspot_opened" | "accordion_section_toggled" | "flashcard_flipped" | "image_slider_changed">;
|
|
353
367
|
type TelemetryCatalogV3Entry = {
|
|
354
368
|
name: TelemetryCatalogV3EventName;
|
|
355
369
|
description: string;
|
|
@@ -369,6 +383,8 @@ declare function createTrackingClient(opts?: {
|
|
|
369
383
|
maxBatchSize?: number;
|
|
370
384
|
};
|
|
371
385
|
batchSink?: TelemetryBatchSink;
|
|
386
|
+
/** Called when an event is dropped because the batch buffer is at cap (including in production). */
|
|
387
|
+
onBufferDrop?: () => void;
|
|
372
388
|
}): TrackingClient;
|
|
373
389
|
|
|
374
390
|
declare function createSessionId(): string;
|
|
@@ -426,6 +442,10 @@ type BuildTelemetryEventInput = (BuildTelemetryEventContext & {
|
|
|
426
442
|
name: "book_page_viewed";
|
|
427
443
|
lessonId?: LessonId;
|
|
428
444
|
data: BookPageViewedData;
|
|
445
|
+
}) | (BuildTelemetryEventContext & {
|
|
446
|
+
name: "slide_viewed";
|
|
447
|
+
lessonId?: LessonId;
|
|
448
|
+
data: SlideViewedData;
|
|
429
449
|
}) | (BuildTelemetryEventContext & {
|
|
430
450
|
name: "compound_page_viewed";
|
|
431
451
|
lessonId?: LessonId;
|
|
@@ -501,11 +521,13 @@ declare const SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
|
501
521
|
declare function getTabSessionId(storage: StoragePort): string | null;
|
|
502
522
|
declare function resolveSessionId(storage: StoragePort, provided?: string): string;
|
|
503
523
|
declare function hasCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
504
|
-
declare function markCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId):
|
|
524
|
+
declare function markCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
505
525
|
declare function hasCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
506
526
|
declare function markCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
|
|
507
527
|
declare function hasCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
508
528
|
declare function markCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
|
|
529
|
+
/** @internal Reset shared volatile session id between tests. */
|
|
530
|
+
declare function resetSharedVolatileSessionIdForTests(): void;
|
|
509
531
|
declare function migrateCourseStartedMark(storage: StoragePort, fromSessionId: string, toSessionId: string, courseId?: CourseId): void;
|
|
510
532
|
|
|
511
533
|
/** Plugin category — aligns with roadmap extension areas. */
|
|
@@ -623,6 +645,8 @@ type HeadlessLessonkitConfig = {
|
|
|
623
645
|
};
|
|
624
646
|
/** Plugin list or registry; hooks run on {@link HeadlessLessonkitRuntime.track} and lifecycle emits. */
|
|
625
647
|
plugins?: HeadlessLessonkitPlugins;
|
|
648
|
+
/** When true, skip initial {@link PluginHost.setupAll}; host caller runs setup (React v2 provider). */
|
|
649
|
+
deferPluginSetup?: boolean;
|
|
626
650
|
};
|
|
627
651
|
type HeadlessRuntimePorts = {
|
|
628
652
|
storage?: StoragePort;
|
|
@@ -668,4 +692,4 @@ declare function buildPluginContext(opts: {
|
|
|
668
692
|
user?: TelemetryUser;
|
|
669
693
|
}): LessonkitPluginContext;
|
|
670
694
|
|
|
671
|
-
export { ACCORDION_FORBIDDEN_CHILD_TYPES, ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES, type AccordionSectionToggledData, type AssessmentAnsweredData, type AssessmentBaseProps, type AssessmentBehaviour, type AssessmentCompletedData, type AssessmentHandle, type AssessmentInteractionType, type AssessmentPlugin, type AssessmentResumeState, type AssessmentScoreInput, type AssessmentScoreResult, type AssessmentXAPIData, type BlockId, type BookPageViewedData, type BuildTelemetryEventInput, COMPOUND_MAX_NESTING_DEPTH, COMPOUND_RESUME_SCHEMA_VERSION, type CheckId, type ClockPort, type CompoundBaseProps, type CompoundHandle, type CompoundPageViewedData, type CompoundParentType, type CompoundResumeInput, type CompoundResumeState, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type FlashcardFlippedData, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, type HotspotOpenedData, ID_MAX_LENGTH, ID_PATTERN, INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES, type IdentityIdPath, type IdentityValidationIssue, type IdentityValidationResult, type ImageSliderChangedData, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrn, type LessonkitUrnParts, type LifecyclePlugin, PAGE_ALLOWED_CHILD_TYPES, type PluginHost, type PluginIdentity, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, TELEMETRY_EVENT_CATALOG_V2, TELEMETRY_EVENT_CATALOG_V3, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryCatalogV2Entry, type TelemetryCatalogV3Entry, type TelemetryDataFor, type TelemetryEmitFn, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertNever, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildPluginContext, buildTelemetryCatalog, buildTelemetryCatalogV2, buildTelemetryCatalogV3, buildTelemetryEvent, clampCompoundPageIndex, clearCompoundState, completeCourseWithTelemetry, completeLessonWithTelemetry, compoundStateStorageKey, createCompoundResumeState, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getAllowedChildTypes, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, hasCourseStartedPipelineDelivered, isChildTypeAllowed, loadCompoundState, markCourseStarted, markCourseStartedEmittedToTracking, markCourseStartedPipelineDelivered, migrateCourseStartedMark, nowIso, parseBlockId, parseCheckId, parseCompoundResumeState, parseCourseId, parseLessonId, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, saveCompoundState, slugifyId, telemetryCatalogV2Version, telemetryCatalogV3Version, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
|
|
695
|
+
export { ACCORDION_FORBIDDEN_CHILD_TYPES, ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES, type AccordionSectionToggledData, type AssessmentAnsweredData, type AssessmentBaseProps, type AssessmentBehaviour, type AssessmentCompletedData, type AssessmentHandle, type AssessmentInteractionType, type AssessmentPlugin, type AssessmentResumeState, type AssessmentScoreInput, type AssessmentScoreResult, type AssessmentXAPIData, type BlockId, type BookPageViewedData, type BuildTelemetryEventInput, COMPOUND_MAX_NESTING_DEPTH, COMPOUND_RESUME_SCHEMA_VERSION, type CheckId, type ClockPort, type CompoundBaseProps, type CompoundHandle, type CompoundPageViewedData, type CompoundParentType, type CompoundResumeInput, type CompoundResumeState, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type FlashcardFlippedData, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, type HotspotOpenedData, ID_MAX_LENGTH, ID_PATTERN, INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES, type IdentityIdPath, type IdentityValidationIssue, type IdentityValidationResult, type ImageSliderChangedData, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrn, type LessonkitUrnParts, type LifecyclePlugin, PAGE_ALLOWED_CHILD_TYPES, type PluginHost, type PluginIdentity, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, SLIDE_ALLOWED_CHILD_TYPES, SLIDE_DECK_ALLOWED_CHILD_TYPES, type SlideViewedData, type StoragePort, TELEMETRY_EVENT_CATALOG, TELEMETRY_EVENT_CATALOG_V2, TELEMETRY_EVENT_CATALOG_V3, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryCatalogV2Entry, type TelemetryCatalogV3Entry, type TelemetryDataFor, type TelemetryEmitFn, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertNever, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildPluginContext, buildTelemetryCatalog, buildTelemetryCatalogV2, buildTelemetryCatalogV3, buildTelemetryEvent, clampCompoundPageIndex, clearCompoundState, completeCourseWithTelemetry, completeLessonWithTelemetry, compoundStateStorageKey, createCompoundResumeState, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getAllowedChildTypes, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, hasCourseStartedPipelineDelivered, isChildTypeAllowed, loadCompoundState, markCourseStarted, markCourseStartedEmittedToTracking, markCourseStartedPipelineDelivered, migrateCourseStartedMark, nowIso, parseBlockId, parseCheckId, parseCompoundResumeState, parseCourseId, parseLessonId, resetSharedVolatileSessionIdForTests, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, saveCompoundState, slugifyId, telemetryCatalogV2Version, telemetryCatalogV3Version, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
|
package/dist/index.d.ts
CHANGED
|
@@ -92,7 +92,7 @@ type AssessmentBaseProps = AssessmentBehaviour & {
|
|
|
92
92
|
passingScore?: number;
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
-
type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "assessment_answered" | "assessment_completed" | "interaction" | "book_page_viewed" | "compound_page_viewed" | "hotspot_opened" | "accordion_section_toggled" | "flashcard_flipped" | "image_slider_changed";
|
|
95
|
+
type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "assessment_answered" | "assessment_completed" | "interaction" | "book_page_viewed" | "slide_viewed" | "compound_page_viewed" | "hotspot_opened" | "accordion_section_toggled" | "flashcard_flipped" | "image_slider_changed";
|
|
96
96
|
type TelemetryUser = {
|
|
97
97
|
id?: string;
|
|
98
98
|
email?: string;
|
|
@@ -150,6 +150,11 @@ type BookPageViewedData = {
|
|
|
150
150
|
pageIndex: number;
|
|
151
151
|
pageTitle?: string;
|
|
152
152
|
};
|
|
153
|
+
type SlideViewedData = {
|
|
154
|
+
blockId: BlockId;
|
|
155
|
+
slideIndex: number;
|
|
156
|
+
slideTitle?: string;
|
|
157
|
+
};
|
|
153
158
|
type CompoundPageViewedData = {
|
|
154
159
|
blockId: BlockId;
|
|
155
160
|
pageIndex: number;
|
|
@@ -217,6 +222,10 @@ type TelemetryEvent = (TelemetryEventBase & {
|
|
|
217
222
|
name: "book_page_viewed";
|
|
218
223
|
lessonId: LessonId;
|
|
219
224
|
data: BookPageViewedData;
|
|
225
|
+
}) | (TelemetryEventBase & {
|
|
226
|
+
name: "slide_viewed";
|
|
227
|
+
lessonId: LessonId;
|
|
228
|
+
data: SlideViewedData;
|
|
220
229
|
}) | (TelemetryEventBase & {
|
|
221
230
|
name: "compound_page_viewed";
|
|
222
231
|
lessonId: LessonId;
|
|
@@ -248,7 +257,8 @@ type TelemetrySink = (event: TelemetryEvent) => void | Promise<void>;
|
|
|
248
257
|
type TelemetryBatchSink = (events: TelemetryEvent[]) => void | Promise<void>;
|
|
249
258
|
type TrackingClient = {
|
|
250
259
|
track: (event: TelemetryEvent) => void;
|
|
251
|
-
|
|
260
|
+
/** Resolves to true when all buffered events were delivered; false when a sink failure re-queued events. */
|
|
261
|
+
flush?: () => void | Promise<boolean>;
|
|
252
262
|
dispose?: () => void | Promise<void>;
|
|
253
263
|
};
|
|
254
264
|
|
|
@@ -289,7 +299,8 @@ type CompoundBaseProps = {
|
|
|
289
299
|
|
|
290
300
|
type StoragePort = {
|
|
291
301
|
getItem: (key: string) => string | null;
|
|
292
|
-
|
|
302
|
+
/** Returns false when the value could not be durably persisted (e.g. sessionStorage quota). */
|
|
303
|
+
setItem: (key: string, value: string) => boolean;
|
|
293
304
|
removeItem?: (key: string) => void;
|
|
294
305
|
/** @internal Test helper to clear in-memory fallback state. */
|
|
295
306
|
resetForTests?: () => void;
|
|
@@ -310,14 +321,17 @@ declare function createGlobalTimer(): TimerPort;
|
|
|
310
321
|
|
|
311
322
|
declare function compoundStateStorageKey(courseId: CourseId, compoundId: BlockId): string;
|
|
312
323
|
declare function loadCompoundState(storage: StoragePort, courseId: CourseId, compoundId: BlockId): CompoundResumeState | null;
|
|
313
|
-
declare function saveCompoundState(storage: StoragePort, courseId: CourseId, compoundId: BlockId, state: CompoundResumeState):
|
|
324
|
+
declare function saveCompoundState(storage: StoragePort, courseId: CourseId, compoundId: BlockId, state: CompoundResumeState): boolean;
|
|
314
325
|
declare function clearCompoundState(storage: StoragePort, courseId: CourseId, compoundId: BlockId): void;
|
|
315
326
|
|
|
316
327
|
/** Canonical compound child allowlists (H5P sub-content curation). */
|
|
317
328
|
declare const PAGE_ALLOWED_CHILD_TYPES: readonly ["Text", "Heading", "Image", "Scenario", "Reflection", "Quiz", "KnowledgeCheck", "TrueFalse", "FillInTheBlanks", "DragAndDrop", "DragTheWords", "MarkTheWords", "Accordion", "DialogCards", "Flashcards", "ImageHotspots", "FindHotspot", "FindMultipleHotspots", "ImageSlider", "ProgressTracker"];
|
|
318
329
|
declare const INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES: readonly ["Page"];
|
|
330
|
+
/** Per-slide content (H5P Course Presentation slide row). Excludes ProgressTracker. */
|
|
331
|
+
declare const SLIDE_ALLOWED_CHILD_TYPES: readonly ["Text", "Heading", "Image", "Scenario", "Reflection", "Quiz", "KnowledgeCheck", "TrueFalse", "FillInTheBlanks", "DragAndDrop", "DragTheWords", "MarkTheWords", "Accordion", "DialogCards", "Flashcards", "ImageHotspots", "FindHotspot", "FindMultipleHotspots", "ImageSlider"];
|
|
332
|
+
declare const SLIDE_DECK_ALLOWED_CHILD_TYPES: readonly ["Slide"];
|
|
319
333
|
declare const ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES: readonly ["TrueFalse", "FillInTheBlanks", "DragAndDrop", "DragTheWords", "MarkTheWords", "Quiz", "KnowledgeCheck", "FindHotspot", "FindMultipleHotspots"];
|
|
320
|
-
type CompoundParentType = "Page" | "InteractiveBook" | "AssessmentSequence";
|
|
334
|
+
type CompoundParentType = "Page" | "InteractiveBook" | "Slide" | "SlideDeck" | "AssessmentSequence";
|
|
321
335
|
declare const COMPOUND_MAX_NESTING_DEPTH: Record<CompoundParentType, number>;
|
|
322
336
|
declare function getAllowedChildTypes(parent: CompoundParentType): readonly string[];
|
|
323
337
|
declare function isChildTypeAllowed(parent: CompoundParentType, childType: string): boolean;
|
|
@@ -349,7 +363,7 @@ declare const TELEMETRY_EVENT_CATALOG_V2: TelemetryCatalogV2Entry[];
|
|
|
349
363
|
declare function buildTelemetryCatalogV2(): TelemetryCatalogV2Entry[];
|
|
350
364
|
|
|
351
365
|
declare const telemetryCatalogV3Version: 3;
|
|
352
|
-
type TelemetryCatalogV3EventName = Extract<TelemetryEventName, "book_page_viewed" | "compound_page_viewed" | "hotspot_opened" | "accordion_section_toggled" | "flashcard_flipped" | "image_slider_changed">;
|
|
366
|
+
type TelemetryCatalogV3EventName = Extract<TelemetryEventName, "book_page_viewed" | "slide_viewed" | "compound_page_viewed" | "hotspot_opened" | "accordion_section_toggled" | "flashcard_flipped" | "image_slider_changed">;
|
|
353
367
|
type TelemetryCatalogV3Entry = {
|
|
354
368
|
name: TelemetryCatalogV3EventName;
|
|
355
369
|
description: string;
|
|
@@ -369,6 +383,8 @@ declare function createTrackingClient(opts?: {
|
|
|
369
383
|
maxBatchSize?: number;
|
|
370
384
|
};
|
|
371
385
|
batchSink?: TelemetryBatchSink;
|
|
386
|
+
/** Called when an event is dropped because the batch buffer is at cap (including in production). */
|
|
387
|
+
onBufferDrop?: () => void;
|
|
372
388
|
}): TrackingClient;
|
|
373
389
|
|
|
374
390
|
declare function createSessionId(): string;
|
|
@@ -426,6 +442,10 @@ type BuildTelemetryEventInput = (BuildTelemetryEventContext & {
|
|
|
426
442
|
name: "book_page_viewed";
|
|
427
443
|
lessonId?: LessonId;
|
|
428
444
|
data: BookPageViewedData;
|
|
445
|
+
}) | (BuildTelemetryEventContext & {
|
|
446
|
+
name: "slide_viewed";
|
|
447
|
+
lessonId?: LessonId;
|
|
448
|
+
data: SlideViewedData;
|
|
429
449
|
}) | (BuildTelemetryEventContext & {
|
|
430
450
|
name: "compound_page_viewed";
|
|
431
451
|
lessonId?: LessonId;
|
|
@@ -501,11 +521,13 @@ declare const SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
|
501
521
|
declare function getTabSessionId(storage: StoragePort): string | null;
|
|
502
522
|
declare function resolveSessionId(storage: StoragePort, provided?: string): string;
|
|
503
523
|
declare function hasCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
504
|
-
declare function markCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId):
|
|
524
|
+
declare function markCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
505
525
|
declare function hasCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
506
526
|
declare function markCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
|
|
507
527
|
declare function hasCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
|
|
508
528
|
declare function markCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
|
|
529
|
+
/** @internal Reset shared volatile session id between tests. */
|
|
530
|
+
declare function resetSharedVolatileSessionIdForTests(): void;
|
|
509
531
|
declare function migrateCourseStartedMark(storage: StoragePort, fromSessionId: string, toSessionId: string, courseId?: CourseId): void;
|
|
510
532
|
|
|
511
533
|
/** Plugin category — aligns with roadmap extension areas. */
|
|
@@ -623,6 +645,8 @@ type HeadlessLessonkitConfig = {
|
|
|
623
645
|
};
|
|
624
646
|
/** Plugin list or registry; hooks run on {@link HeadlessLessonkitRuntime.track} and lifecycle emits. */
|
|
625
647
|
plugins?: HeadlessLessonkitPlugins;
|
|
648
|
+
/** When true, skip initial {@link PluginHost.setupAll}; host caller runs setup (React v2 provider). */
|
|
649
|
+
deferPluginSetup?: boolean;
|
|
626
650
|
};
|
|
627
651
|
type HeadlessRuntimePorts = {
|
|
628
652
|
storage?: StoragePort;
|
|
@@ -668,4 +692,4 @@ declare function buildPluginContext(opts: {
|
|
|
668
692
|
user?: TelemetryUser;
|
|
669
693
|
}): LessonkitPluginContext;
|
|
670
694
|
|
|
671
|
-
export { ACCORDION_FORBIDDEN_CHILD_TYPES, ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES, type AccordionSectionToggledData, type AssessmentAnsweredData, type AssessmentBaseProps, type AssessmentBehaviour, type AssessmentCompletedData, type AssessmentHandle, type AssessmentInteractionType, type AssessmentPlugin, type AssessmentResumeState, type AssessmentScoreInput, type AssessmentScoreResult, type AssessmentXAPIData, type BlockId, type BookPageViewedData, type BuildTelemetryEventInput, COMPOUND_MAX_NESTING_DEPTH, COMPOUND_RESUME_SCHEMA_VERSION, type CheckId, type ClockPort, type CompoundBaseProps, type CompoundHandle, type CompoundPageViewedData, type CompoundParentType, type CompoundResumeInput, type CompoundResumeState, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type FlashcardFlippedData, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, type HotspotOpenedData, ID_MAX_LENGTH, ID_PATTERN, INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES, type IdentityIdPath, type IdentityValidationIssue, type IdentityValidationResult, type ImageSliderChangedData, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrn, type LessonkitUrnParts, type LifecyclePlugin, PAGE_ALLOWED_CHILD_TYPES, type PluginHost, type PluginIdentity, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, TELEMETRY_EVENT_CATALOG_V2, TELEMETRY_EVENT_CATALOG_V3, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryCatalogV2Entry, type TelemetryCatalogV3Entry, type TelemetryDataFor, type TelemetryEmitFn, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertNever, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildPluginContext, buildTelemetryCatalog, buildTelemetryCatalogV2, buildTelemetryCatalogV3, buildTelemetryEvent, clampCompoundPageIndex, clearCompoundState, completeCourseWithTelemetry, completeLessonWithTelemetry, compoundStateStorageKey, createCompoundResumeState, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getAllowedChildTypes, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, hasCourseStartedPipelineDelivered, isChildTypeAllowed, loadCompoundState, markCourseStarted, markCourseStartedEmittedToTracking, markCourseStartedPipelineDelivered, migrateCourseStartedMark, nowIso, parseBlockId, parseCheckId, parseCompoundResumeState, parseCourseId, parseLessonId, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, saveCompoundState, slugifyId, telemetryCatalogV2Version, telemetryCatalogV3Version, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
|
|
695
|
+
export { ACCORDION_FORBIDDEN_CHILD_TYPES, ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES, type AccordionSectionToggledData, type AssessmentAnsweredData, type AssessmentBaseProps, type AssessmentBehaviour, type AssessmentCompletedData, type AssessmentHandle, type AssessmentInteractionType, type AssessmentPlugin, type AssessmentResumeState, type AssessmentScoreInput, type AssessmentScoreResult, type AssessmentXAPIData, type BlockId, type BookPageViewedData, type BuildTelemetryEventInput, COMPOUND_MAX_NESTING_DEPTH, COMPOUND_RESUME_SCHEMA_VERSION, type CheckId, type ClockPort, type CompoundBaseProps, type CompoundHandle, type CompoundPageViewedData, type CompoundParentType, type CompoundResumeInput, type CompoundResumeState, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type FlashcardFlippedData, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, type HotspotOpenedData, ID_MAX_LENGTH, ID_PATTERN, INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES, type IdentityIdPath, type IdentityValidationIssue, type IdentityValidationResult, type ImageSliderChangedData, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrn, type LessonkitUrnParts, type LifecyclePlugin, PAGE_ALLOWED_CHILD_TYPES, type PluginHost, type PluginIdentity, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, SLIDE_ALLOWED_CHILD_TYPES, SLIDE_DECK_ALLOWED_CHILD_TYPES, type SlideViewedData, type StoragePort, TELEMETRY_EVENT_CATALOG, TELEMETRY_EVENT_CATALOG_V2, TELEMETRY_EVENT_CATALOG_V3, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryCatalogV2Entry, type TelemetryCatalogV3Entry, type TelemetryDataFor, type TelemetryEmitFn, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertNever, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildPluginContext, buildTelemetryCatalog, buildTelemetryCatalogV2, buildTelemetryCatalogV3, buildTelemetryEvent, clampCompoundPageIndex, clearCompoundState, completeCourseWithTelemetry, completeLessonWithTelemetry, compoundStateStorageKey, createCompoundResumeState, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getAllowedChildTypes, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, hasCourseStartedPipelineDelivered, isChildTypeAllowed, loadCompoundState, markCourseStarted, markCourseStartedEmittedToTracking, markCourseStartedPipelineDelivered, migrateCourseStartedMark, nowIso, parseBlockId, parseCheckId, parseCompoundResumeState, parseCourseId, parseLessonId, resetSharedVolatileSessionIdForTests, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, saveCompoundState, slugifyId, telemetryCatalogV2Version, telemetryCatalogV3Version, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
|
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;
|
|
@@ -444,41 +526,58 @@ function createTrackingClient(opts) {
|
|
|
444
526
|
let disposing = false;
|
|
445
527
|
let intervalId;
|
|
446
528
|
const runFlush = () => {
|
|
447
|
-
if (!buffer.length) return Promise.resolve();
|
|
529
|
+
if (!buffer.length) return Promise.resolve(true);
|
|
448
530
|
const events = buffer.splice(0, buffer.length);
|
|
449
|
-
let sent = 0;
|
|
450
531
|
let succeeded = false;
|
|
451
532
|
return Promise.resolve().then(async () => {
|
|
452
533
|
if (batchSink) {
|
|
453
534
|
await batchSink(events);
|
|
454
535
|
} else {
|
|
455
|
-
for (
|
|
456
|
-
|
|
457
|
-
|
|
536
|
+
for (let i = 0; i < events.length; i++) {
|
|
537
|
+
try {
|
|
538
|
+
await sink?.(events[i]);
|
|
539
|
+
} catch {
|
|
540
|
+
buffer.unshift(...events.slice(i));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
458
543
|
}
|
|
459
544
|
}
|
|
460
545
|
succeeded = true;
|
|
461
546
|
}).catch(() => {
|
|
462
|
-
|
|
463
|
-
|
|
547
|
+
if (batchSink) {
|
|
548
|
+
buffer.unshift(...events);
|
|
549
|
+
}
|
|
550
|
+
}).then(async () => {
|
|
464
551
|
if (succeeded && buffer.length > 0 && !disposed) {
|
|
465
552
|
return runFlush();
|
|
466
553
|
}
|
|
554
|
+
return succeeded;
|
|
467
555
|
});
|
|
468
556
|
};
|
|
469
557
|
const flush = () => {
|
|
470
|
-
if (disposed) return Promise.resolve();
|
|
558
|
+
if (disposed) return Promise.resolve(true);
|
|
471
559
|
if (flushInFlight) return flushInFlight;
|
|
472
|
-
if (!buffer.length) return Promise.resolve();
|
|
560
|
+
if (!buffer.length) return Promise.resolve(true);
|
|
473
561
|
flushInFlight = runFlush().finally(() => {
|
|
474
562
|
flushInFlight = null;
|
|
475
563
|
});
|
|
476
564
|
return flushInFlight;
|
|
477
565
|
};
|
|
566
|
+
const MAX_DISPOSE_FLUSH_ATTEMPTS = 10;
|
|
478
567
|
const drainAll = async () => {
|
|
479
|
-
|
|
480
|
-
while (buffer.length > 0) {
|
|
481
|
-
await flush();
|
|
568
|
+
let attempts = 0;
|
|
569
|
+
while (buffer.length > 0 && attempts < MAX_DISPOSE_FLUSH_ATTEMPTS) {
|
|
570
|
+
const delivered = await flush();
|
|
571
|
+
attempts += 1;
|
|
572
|
+
if (!delivered) break;
|
|
573
|
+
}
|
|
574
|
+
if (buffer.length > 0) {
|
|
575
|
+
if (isDevEnvironment()) {
|
|
576
|
+
console.warn(
|
|
577
|
+
`[lessonkit] dropped ${buffer.length} buffered telemetry event(s) after dispose flush cap`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
buffer.length = 0;
|
|
482
581
|
}
|
|
483
582
|
};
|
|
484
583
|
intervalId = flushIntervalMs > 0 ? globalThis.setInterval(() => void flush(), flushIntervalMs) : void 0;
|
|
@@ -487,13 +586,14 @@ function createTrackingClient(opts) {
|
|
|
487
586
|
track: (event) => {
|
|
488
587
|
if (disposed || disposing) return;
|
|
489
588
|
if (buffer.length >= maxBufferSize) {
|
|
490
|
-
|
|
589
|
+
opts?.onBufferDrop?.();
|
|
491
590
|
if (!warnedBufferCap && isDevEnvironment()) {
|
|
492
591
|
warnedBufferCap = true;
|
|
493
592
|
console.warn(
|
|
494
|
-
`[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events;
|
|
593
|
+
`[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; new events are dropped until the buffer drains.`
|
|
495
594
|
);
|
|
496
595
|
}
|
|
596
|
+
return;
|
|
497
597
|
}
|
|
498
598
|
buffer.push(event);
|
|
499
599
|
if (buffer.length >= maxBatchSize) void flush();
|
|
@@ -651,6 +751,20 @@ var TELEMETRY_EVENT_REGISTRY = {
|
|
|
651
751
|
};
|
|
652
752
|
}
|
|
653
753
|
},
|
|
754
|
+
slide_viewed: {
|
|
755
|
+
requiresLessonId: true,
|
|
756
|
+
build: (opts, base) => {
|
|
757
|
+
if (opts.name !== "slide_viewed") throw new Error("unexpected event");
|
|
758
|
+
const lessonId = opts.lessonId;
|
|
759
|
+
if (!lessonId) throw new Error("slide_viewed requires active lessonId");
|
|
760
|
+
return {
|
|
761
|
+
name: "slide_viewed",
|
|
762
|
+
...base,
|
|
763
|
+
lessonId,
|
|
764
|
+
data: opts.data
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
},
|
|
654
768
|
compound_page_viewed: {
|
|
655
769
|
requiresLessonId: true,
|
|
656
770
|
build: (opts, base) => {
|
|
@@ -799,8 +913,7 @@ function createDefaultClock() {
|
|
|
799
913
|
function createNoopStorage() {
|
|
800
914
|
return {
|
|
801
915
|
getItem: () => null,
|
|
802
|
-
setItem: () =>
|
|
803
|
-
}
|
|
916
|
+
setItem: () => true
|
|
804
917
|
};
|
|
805
918
|
}
|
|
806
919
|
function createMemoryBackedSessionStorage(session) {
|
|
@@ -831,8 +944,10 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
831
944
|
memory.set(key, value);
|
|
832
945
|
try {
|
|
833
946
|
session.setItem(key, value);
|
|
947
|
+
return true;
|
|
834
948
|
} catch {
|
|
835
949
|
warnPersistFailure();
|
|
950
|
+
return false;
|
|
836
951
|
}
|
|
837
952
|
},
|
|
838
953
|
removeItem: (key) => {
|
|
@@ -857,6 +972,7 @@ function createInMemorySessionStoragePort() {
|
|
|
857
972
|
getItem: (key) => memory.get(key) ?? null,
|
|
858
973
|
setItem: (key, value) => {
|
|
859
974
|
memory.set(key, value);
|
|
975
|
+
return true;
|
|
860
976
|
},
|
|
861
977
|
removeItem: (key) => {
|
|
862
978
|
memory.delete(key);
|
|
@@ -909,7 +1025,12 @@ function createProgressController() {
|
|
|
909
1025
|
return { previousLessonId };
|
|
910
1026
|
},
|
|
911
1027
|
completeLesson: (lessonId, completedAtMs) => {
|
|
912
|
-
if (completedLessonIds.has(lessonId))
|
|
1028
|
+
if (completedLessonIds.has(lessonId)) {
|
|
1029
|
+
if (activeLessonId === lessonId) {
|
|
1030
|
+
activeLessonId = void 0;
|
|
1031
|
+
}
|
|
1032
|
+
return { didComplete: false };
|
|
1033
|
+
}
|
|
913
1034
|
completedLessonIds = new Set(completedLessonIds).add(lessonId);
|
|
914
1035
|
if (activeLessonId === lessonId) {
|
|
915
1036
|
activeLessonId = void 0;
|
|
@@ -929,6 +1050,12 @@ function createProgressController() {
|
|
|
929
1050
|
|
|
930
1051
|
// src/session.ts
|
|
931
1052
|
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
1053
|
+
var volatileSessionIds = /* @__PURE__ */ new WeakMap();
|
|
1054
|
+
var sharedVolatileSessionId = null;
|
|
1055
|
+
function isDevEnvironment2() {
|
|
1056
|
+
const g = globalThis;
|
|
1057
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
1058
|
+
}
|
|
932
1059
|
function getTabSessionId(storage) {
|
|
933
1060
|
return storage.getItem(SESSION_STORAGE_KEY);
|
|
934
1061
|
}
|
|
@@ -936,11 +1063,28 @@ var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
|
936
1063
|
var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
|
|
937
1064
|
var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
|
|
938
1065
|
function resolveSessionId(storage, provided) {
|
|
939
|
-
if (provided)
|
|
1066
|
+
if (provided !== void 0) {
|
|
1067
|
+
const trimmed = provided.trim();
|
|
1068
|
+
if (trimmed.length > 0) return trimmed;
|
|
1069
|
+
}
|
|
940
1070
|
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
941
1071
|
if (existing) return existing;
|
|
1072
|
+
const volatile = volatileSessionIds.get(storage);
|
|
1073
|
+
if (volatile) return volatile;
|
|
942
1074
|
const id = createSessionId();
|
|
943
|
-
storage.setItem(SESSION_STORAGE_KEY, id);
|
|
1075
|
+
const persisted = storage.setItem(SESSION_STORAGE_KEY, id);
|
|
1076
|
+
if (!persisted) {
|
|
1077
|
+
if (!sharedVolatileSessionId) {
|
|
1078
|
+
sharedVolatileSessionId = id;
|
|
1079
|
+
}
|
|
1080
|
+
volatileSessionIds.set(storage, sharedVolatileSessionId);
|
|
1081
|
+
if (isDevEnvironment2()) {
|
|
1082
|
+
console.warn(
|
|
1083
|
+
"[lessonkit] session id could not be persisted; reusing in-memory id for this tab."
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
return sharedVolatileSessionId;
|
|
1087
|
+
}
|
|
944
1088
|
return id;
|
|
945
1089
|
}
|
|
946
1090
|
function courseStartedStorageKey(sessionId, courseId) {
|
|
@@ -957,8 +1101,8 @@ function hasCourseStarted(storage, sessionId, courseId) {
|
|
|
957
1101
|
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
958
1102
|
}
|
|
959
1103
|
function markCourseStarted(storage, sessionId, courseId) {
|
|
960
|
-
if (!courseId) return;
|
|
961
|
-
storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
1104
|
+
if (!courseId) return false;
|
|
1105
|
+
return storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
962
1106
|
}
|
|
963
1107
|
function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
964
1108
|
if (!courseId) return false;
|
|
@@ -976,6 +1120,9 @@ function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
|
976
1120
|
if (!courseId) return;
|
|
977
1121
|
storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
|
|
978
1122
|
}
|
|
1123
|
+
function resetSharedVolatileSessionIdForTests() {
|
|
1124
|
+
sharedVolatileSessionId = null;
|
|
1125
|
+
}
|
|
979
1126
|
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
980
1127
|
if (!courseId || fromSessionId === toSessionId) return;
|
|
981
1128
|
if (hasCourseStarted(storage, fromSessionId, courseId)) {
|
|
@@ -998,14 +1145,14 @@ function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
|
|
|
998
1145
|
if (alreadyEmittedToSink) {
|
|
999
1146
|
return { emitted: true, marked };
|
|
1000
1147
|
}
|
|
1001
|
-
if (marked) {
|
|
1002
|
-
return { emitted: false, marked: true };
|
|
1003
|
-
}
|
|
1004
1148
|
const emitted = deps.emitCourseStartedEvent(ctx);
|
|
1005
|
-
if (emitted) {
|
|
1149
|
+
if (emitted && !marked) {
|
|
1006
1150
|
markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
1007
1151
|
}
|
|
1008
|
-
return {
|
|
1152
|
+
return {
|
|
1153
|
+
emitted,
|
|
1154
|
+
marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
|
|
1155
|
+
};
|
|
1009
1156
|
}
|
|
1010
1157
|
function buildCourseStartedTelemetryEvent(ctx) {
|
|
1011
1158
|
return buildTelemetryEvent({
|
|
@@ -1095,7 +1242,7 @@ function createPluginRegistry(plugins = []) {
|
|
|
1095
1242
|
const composeTrackingSink = (sink, ctxSource) => {
|
|
1096
1243
|
if (!sink) return void 0;
|
|
1097
1244
|
const resolveCtx = () => typeof ctxSource === "function" ? ctxSource() : ctxSource;
|
|
1098
|
-
const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}`;
|
|
1245
|
+
const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}\0${ctx.user?.id ?? ""}`;
|
|
1099
1246
|
const layers = [];
|
|
1100
1247
|
let composed = sink;
|
|
1101
1248
|
for (const plugin of list) {
|
|
@@ -1166,6 +1313,9 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1166
1313
|
attemptId,
|
|
1167
1314
|
user
|
|
1168
1315
|
});
|
|
1316
|
+
if (!configSnapshot.deferPluginSetup) {
|
|
1317
|
+
pluginHost?.setupAll(getPluginCtx());
|
|
1318
|
+
}
|
|
1169
1319
|
const getSession = () => ({ sessionId, attemptId, user });
|
|
1170
1320
|
const syncSessionFromConfig = (next) => {
|
|
1171
1321
|
sessionId = resolveSessionId(storage, next.session?.sessionId);
|
|
@@ -1225,11 +1375,8 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1225
1375
|
getProgressState: () => progress.getState(),
|
|
1226
1376
|
getSession,
|
|
1227
1377
|
updateConfig(next) {
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
configSnapshot.plugins = next.plugins;
|
|
1231
|
-
pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
1232
|
-
}
|
|
1378
|
+
const previousCourseId = courseId;
|
|
1379
|
+
const sessionKeyBefore = JSON.stringify({ sessionId, attemptId, user });
|
|
1233
1380
|
if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
|
|
1234
1381
|
if (next.runtimeVersion !== void 0) {
|
|
1235
1382
|
if (next.runtimeVersion === "v1") warnRuntimeV1Deprecated();
|
|
@@ -1239,6 +1386,19 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1239
1386
|
configSnapshot.session = { ...configSnapshot.session, ...next.session };
|
|
1240
1387
|
}
|
|
1241
1388
|
syncSessionFromConfig(configSnapshot);
|
|
1389
|
+
const sessionKeyAfter = JSON.stringify({ sessionId, attemptId, user });
|
|
1390
|
+
if (next.courseId !== void 0 && next.courseId !== previousCourseId) {
|
|
1391
|
+
progress = createProgressController();
|
|
1392
|
+
}
|
|
1393
|
+
if (next.plugins !== void 0 && next.plugins !== pluginHost) {
|
|
1394
|
+
pluginHost?.disposeAll();
|
|
1395
|
+
configSnapshot.plugins = next.plugins;
|
|
1396
|
+
pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
1397
|
+
pluginHost?.setupAll(getPluginCtx());
|
|
1398
|
+
} else if (next.session !== void 0 && sessionKeyBefore !== sessionKeyAfter && pluginHost && !configSnapshot.deferPluginSetup) {
|
|
1399
|
+
pluginHost.disposeAll();
|
|
1400
|
+
pluginHost.setupAll(getPluginCtx());
|
|
1401
|
+
}
|
|
1242
1402
|
},
|
|
1243
1403
|
setActiveLesson(lessonId, emitFn) {
|
|
1244
1404
|
const wrapped = wrapEmitFn(emitFn);
|
|
@@ -1310,6 +1470,8 @@ export {
|
|
|
1310
1470
|
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
1311
1471
|
PAGE_ALLOWED_CHILD_TYPES,
|
|
1312
1472
|
SESSION_STORAGE_KEY,
|
|
1473
|
+
SLIDE_ALLOWED_CHILD_TYPES,
|
|
1474
|
+
SLIDE_DECK_ALLOWED_CHILD_TYPES,
|
|
1313
1475
|
TELEMETRY_EVENT_CATALOG,
|
|
1314
1476
|
TELEMETRY_EVENT_CATALOG_V2,
|
|
1315
1477
|
TELEMETRY_EVENT_CATALOG_V3,
|
|
@@ -1360,6 +1522,7 @@ export {
|
|
|
1360
1522
|
parseCompoundResumeState,
|
|
1361
1523
|
parseCourseId,
|
|
1362
1524
|
parseLessonId,
|
|
1525
|
+
resetSharedVolatileSessionIdForTests,
|
|
1363
1526
|
resetStoragePortForTests,
|
|
1364
1527
|
resetTelemetryBuilderWarningsForTests,
|
|
1365
1528
|
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",
|