@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.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;
|
|
@@ -535,45 +620,66 @@ function createTrackingClient(opts) {
|
|
|
535
620
|
}
|
|
536
621
|
const buffer = [];
|
|
537
622
|
let flushInFlight = null;
|
|
623
|
+
let inflightExitBatch = null;
|
|
538
624
|
let disposed = false;
|
|
539
625
|
let disposing = false;
|
|
540
626
|
let intervalId;
|
|
541
627
|
const runFlush = () => {
|
|
542
|
-
if (!buffer.length) return Promise.resolve();
|
|
628
|
+
if (!buffer.length) return Promise.resolve(true);
|
|
543
629
|
const events = buffer.splice(0, buffer.length);
|
|
544
|
-
|
|
630
|
+
inflightExitBatch = events;
|
|
545
631
|
let succeeded = false;
|
|
546
632
|
return Promise.resolve().then(async () => {
|
|
547
633
|
if (batchSink) {
|
|
548
634
|
await batchSink(events);
|
|
549
635
|
} else {
|
|
550
|
-
for (
|
|
551
|
-
|
|
552
|
-
|
|
636
|
+
for (let i = 0; i < events.length; i++) {
|
|
637
|
+
try {
|
|
638
|
+
await sink?.(events[i]);
|
|
639
|
+
} catch {
|
|
640
|
+
buffer.unshift(...events.slice(i));
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
553
643
|
}
|
|
554
644
|
}
|
|
555
645
|
succeeded = true;
|
|
556
646
|
}).catch(() => {
|
|
557
|
-
|
|
558
|
-
|
|
647
|
+
if (batchSink) {
|
|
648
|
+
buffer.unshift(...events);
|
|
649
|
+
}
|
|
650
|
+
}).then(async () => {
|
|
559
651
|
if (succeeded && buffer.length > 0 && !disposed) {
|
|
560
652
|
return runFlush();
|
|
561
653
|
}
|
|
654
|
+
return succeeded;
|
|
655
|
+
}).finally(() => {
|
|
656
|
+
inflightExitBatch = null;
|
|
562
657
|
});
|
|
563
658
|
};
|
|
564
659
|
const flush = () => {
|
|
565
|
-
if (disposed) return Promise.resolve();
|
|
660
|
+
if (disposed) return Promise.resolve(true);
|
|
566
661
|
if (flushInFlight) return flushInFlight;
|
|
567
|
-
if (!buffer.length) return Promise.resolve();
|
|
662
|
+
if (!buffer.length) return Promise.resolve(true);
|
|
568
663
|
flushInFlight = runFlush().finally(() => {
|
|
569
664
|
flushInFlight = null;
|
|
570
665
|
});
|
|
571
666
|
return flushInFlight;
|
|
572
667
|
};
|
|
668
|
+
const MAX_DISPOSE_FLUSH_ATTEMPTS = 10;
|
|
573
669
|
const drainAll = async () => {
|
|
574
|
-
|
|
575
|
-
while (buffer.length > 0) {
|
|
576
|
-
await flush();
|
|
670
|
+
let attempts = 0;
|
|
671
|
+
while (buffer.length > 0 && attempts < MAX_DISPOSE_FLUSH_ATTEMPTS) {
|
|
672
|
+
const delivered = await flush();
|
|
673
|
+
attempts += 1;
|
|
674
|
+
if (!delivered) break;
|
|
675
|
+
}
|
|
676
|
+
if (buffer.length > 0) {
|
|
677
|
+
if (isDevEnvironment()) {
|
|
678
|
+
console.warn(
|
|
679
|
+
`[lessonkit] dropped ${buffer.length} buffered telemetry event(s) after dispose flush cap`
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
buffer.length = 0;
|
|
577
683
|
}
|
|
578
684
|
};
|
|
579
685
|
intervalId = flushIntervalMs > 0 ? globalThis.setInterval(() => void flush(), flushIntervalMs) : void 0;
|
|
@@ -582,18 +688,35 @@ function createTrackingClient(opts) {
|
|
|
582
688
|
track: (event) => {
|
|
583
689
|
if (disposed || disposing) return;
|
|
584
690
|
if (buffer.length >= maxBufferSize) {
|
|
585
|
-
|
|
691
|
+
opts?.onBufferDrop?.();
|
|
586
692
|
if (!warnedBufferCap && isDevEnvironment()) {
|
|
587
693
|
warnedBufferCap = true;
|
|
588
694
|
console.warn(
|
|
589
|
-
`[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events;
|
|
695
|
+
`[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; new events are dropped until the buffer drains.`
|
|
590
696
|
);
|
|
591
697
|
}
|
|
698
|
+
return;
|
|
592
699
|
}
|
|
593
700
|
buffer.push(event);
|
|
594
701
|
if (buffer.length >= maxBatchSize) void flush();
|
|
595
702
|
},
|
|
596
703
|
flush,
|
|
704
|
+
flushOnExit: opts?.exitBatchSink ? () => {
|
|
705
|
+
const fromBuffer = buffer.splice(0, buffer.length);
|
|
706
|
+
const fromInflight = inflightExitBatch ? [...inflightExitBatch] : [];
|
|
707
|
+
const events = [...fromInflight, ...fromBuffer];
|
|
708
|
+
if (!events.length) return;
|
|
709
|
+
try {
|
|
710
|
+
const result = opts.exitBatchSink(events);
|
|
711
|
+
if (result != null && typeof result.catch === "function") {
|
|
712
|
+
void result.catch(() => {
|
|
713
|
+
buffer.unshift(...events);
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
} catch {
|
|
717
|
+
buffer.unshift(...events);
|
|
718
|
+
}
|
|
719
|
+
} : void 0,
|
|
597
720
|
dispose: () => {
|
|
598
721
|
if (disposed || disposing) return Promise.resolve();
|
|
599
722
|
disposing = true;
|
|
@@ -746,6 +869,20 @@ var TELEMETRY_EVENT_REGISTRY = {
|
|
|
746
869
|
};
|
|
747
870
|
}
|
|
748
871
|
},
|
|
872
|
+
slide_viewed: {
|
|
873
|
+
requiresLessonId: true,
|
|
874
|
+
build: (opts, base) => {
|
|
875
|
+
if (opts.name !== "slide_viewed") throw new Error("unexpected event");
|
|
876
|
+
const lessonId = opts.lessonId;
|
|
877
|
+
if (!lessonId) throw new Error("slide_viewed requires active lessonId");
|
|
878
|
+
return {
|
|
879
|
+
name: "slide_viewed",
|
|
880
|
+
...base,
|
|
881
|
+
lessonId,
|
|
882
|
+
data: opts.data
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
},
|
|
749
886
|
compound_page_viewed: {
|
|
750
887
|
requiresLessonId: true,
|
|
751
888
|
build: (opts, base) => {
|
|
@@ -894,8 +1031,7 @@ function createDefaultClock() {
|
|
|
894
1031
|
function createNoopStorage() {
|
|
895
1032
|
return {
|
|
896
1033
|
getItem: () => null,
|
|
897
|
-
setItem: () =>
|
|
898
|
-
}
|
|
1034
|
+
setItem: () => true
|
|
899
1035
|
};
|
|
900
1036
|
}
|
|
901
1037
|
function createMemoryBackedSessionStorage(session) {
|
|
@@ -926,8 +1062,10 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
926
1062
|
memory.set(key, value);
|
|
927
1063
|
try {
|
|
928
1064
|
session.setItem(key, value);
|
|
1065
|
+
return true;
|
|
929
1066
|
} catch {
|
|
930
1067
|
warnPersistFailure();
|
|
1068
|
+
return false;
|
|
931
1069
|
}
|
|
932
1070
|
},
|
|
933
1071
|
removeItem: (key) => {
|
|
@@ -952,6 +1090,7 @@ function createInMemorySessionStoragePort() {
|
|
|
952
1090
|
getItem: (key) => memory.get(key) ?? null,
|
|
953
1091
|
setItem: (key, value) => {
|
|
954
1092
|
memory.set(key, value);
|
|
1093
|
+
return true;
|
|
955
1094
|
},
|
|
956
1095
|
removeItem: (key) => {
|
|
957
1096
|
memory.delete(key);
|
|
@@ -1004,7 +1143,12 @@ function createProgressController() {
|
|
|
1004
1143
|
return { previousLessonId };
|
|
1005
1144
|
},
|
|
1006
1145
|
completeLesson: (lessonId, completedAtMs) => {
|
|
1007
|
-
if (completedLessonIds.has(lessonId))
|
|
1146
|
+
if (completedLessonIds.has(lessonId)) {
|
|
1147
|
+
if (activeLessonId === lessonId) {
|
|
1148
|
+
activeLessonId = void 0;
|
|
1149
|
+
}
|
|
1150
|
+
return { didComplete: false };
|
|
1151
|
+
}
|
|
1008
1152
|
completedLessonIds = new Set(completedLessonIds).add(lessonId);
|
|
1009
1153
|
if (activeLessonId === lessonId) {
|
|
1010
1154
|
activeLessonId = void 0;
|
|
@@ -1024,6 +1168,12 @@ function createProgressController() {
|
|
|
1024
1168
|
|
|
1025
1169
|
// src/session.ts
|
|
1026
1170
|
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
1171
|
+
var volatileSessionIds = /* @__PURE__ */ new WeakMap();
|
|
1172
|
+
var sharedVolatileSessionId = null;
|
|
1173
|
+
function isDevEnvironment2() {
|
|
1174
|
+
const g = globalThis;
|
|
1175
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
1176
|
+
}
|
|
1027
1177
|
function getTabSessionId(storage) {
|
|
1028
1178
|
return storage.getItem(SESSION_STORAGE_KEY);
|
|
1029
1179
|
}
|
|
@@ -1031,11 +1181,28 @@ var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
|
1031
1181
|
var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
|
|
1032
1182
|
var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
|
|
1033
1183
|
function resolveSessionId(storage, provided) {
|
|
1034
|
-
if (provided)
|
|
1184
|
+
if (provided !== void 0) {
|
|
1185
|
+
const trimmed = provided.trim();
|
|
1186
|
+
if (trimmed.length > 0) return trimmed;
|
|
1187
|
+
}
|
|
1035
1188
|
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
1036
1189
|
if (existing) return existing;
|
|
1190
|
+
const volatile = volatileSessionIds.get(storage);
|
|
1191
|
+
if (volatile) return volatile;
|
|
1037
1192
|
const id = createSessionId();
|
|
1038
|
-
storage.setItem(SESSION_STORAGE_KEY, id);
|
|
1193
|
+
const persisted = storage.setItem(SESSION_STORAGE_KEY, id);
|
|
1194
|
+
if (!persisted) {
|
|
1195
|
+
if (!sharedVolatileSessionId) {
|
|
1196
|
+
sharedVolatileSessionId = id;
|
|
1197
|
+
}
|
|
1198
|
+
volatileSessionIds.set(storage, sharedVolatileSessionId);
|
|
1199
|
+
if (isDevEnvironment2()) {
|
|
1200
|
+
console.warn(
|
|
1201
|
+
"[lessonkit] session id could not be persisted; reusing in-memory id for this tab."
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
return sharedVolatileSessionId;
|
|
1205
|
+
}
|
|
1039
1206
|
return id;
|
|
1040
1207
|
}
|
|
1041
1208
|
function courseStartedStorageKey(sessionId, courseId) {
|
|
@@ -1052,24 +1219,27 @@ function hasCourseStarted(storage, sessionId, courseId) {
|
|
|
1052
1219
|
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
1053
1220
|
}
|
|
1054
1221
|
function markCourseStarted(storage, sessionId, courseId) {
|
|
1055
|
-
if (!courseId) return;
|
|
1056
|
-
storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
1222
|
+
if (!courseId) return false;
|
|
1223
|
+
return storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
1057
1224
|
}
|
|
1058
1225
|
function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
1059
1226
|
if (!courseId) return false;
|
|
1060
1227
|
return storage.getItem(courseStartedTrackingStorageKey(sessionId, courseId)) === "1";
|
|
1061
1228
|
}
|
|
1062
1229
|
function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
1063
|
-
if (!courseId) return;
|
|
1064
|
-
storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
|
|
1230
|
+
if (!courseId) return false;
|
|
1231
|
+
return storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
|
|
1065
1232
|
}
|
|
1066
1233
|
function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
1067
1234
|
if (!courseId) return false;
|
|
1068
1235
|
return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
|
|
1069
1236
|
}
|
|
1070
1237
|
function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
1071
|
-
if (!courseId) return;
|
|
1072
|
-
storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
|
|
1238
|
+
if (!courseId) return false;
|
|
1239
|
+
return storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
|
|
1240
|
+
}
|
|
1241
|
+
function resetSharedVolatileSessionIdForTests() {
|
|
1242
|
+
sharedVolatileSessionId = null;
|
|
1073
1243
|
}
|
|
1074
1244
|
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
1075
1245
|
if (!courseId || fromSessionId === toSessionId) return;
|
|
@@ -1088,19 +1258,29 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
|
|
|
1088
1258
|
}
|
|
1089
1259
|
|
|
1090
1260
|
// src/runtime/courseLifecycle.ts
|
|
1261
|
+
var courseStartedEmitFlights = /* @__PURE__ */ new Set();
|
|
1091
1262
|
function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
|
|
1263
|
+
const flightKey = `${ctx.sessionId}:${ctx.courseId}`;
|
|
1092
1264
|
const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
1093
1265
|
if (alreadyEmittedToSink) {
|
|
1094
1266
|
return { emitted: true, marked };
|
|
1095
1267
|
}
|
|
1096
|
-
if (
|
|
1097
|
-
return { emitted: false, marked
|
|
1268
|
+
if (courseStartedEmitFlights.has(flightKey)) {
|
|
1269
|
+
return { emitted: false, marked };
|
|
1098
1270
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1271
|
+
courseStartedEmitFlights.add(flightKey);
|
|
1272
|
+
try {
|
|
1273
|
+
const emitted = deps.emitCourseStartedEvent(ctx);
|
|
1274
|
+
if (emitted && !marked) {
|
|
1275
|
+
markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
1276
|
+
}
|
|
1277
|
+
return {
|
|
1278
|
+
emitted,
|
|
1279
|
+
marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
|
|
1280
|
+
};
|
|
1281
|
+
} finally {
|
|
1282
|
+
courseStartedEmitFlights.delete(flightKey);
|
|
1102
1283
|
}
|
|
1103
|
-
return { emitted, marked: emitted };
|
|
1104
1284
|
}
|
|
1105
1285
|
function buildCourseStartedTelemetryEvent(ctx) {
|
|
1106
1286
|
return buildTelemetryEvent({
|
|
@@ -1190,7 +1370,7 @@ function createPluginRegistry(plugins = []) {
|
|
|
1190
1370
|
const composeTrackingSink = (sink, ctxSource) => {
|
|
1191
1371
|
if (!sink) return void 0;
|
|
1192
1372
|
const resolveCtx = () => typeof ctxSource === "function" ? ctxSource() : ctxSource;
|
|
1193
|
-
const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}`;
|
|
1373
|
+
const ctxKey = (ctx) => `${ctx.courseId}\0${ctx.sessionId ?? ""}\0${ctx.attemptId ?? ""}\0${ctx.user?.id ?? ""}`;
|
|
1194
1374
|
const layers = [];
|
|
1195
1375
|
let composed = sink;
|
|
1196
1376
|
for (const plugin of list) {
|
|
@@ -1261,6 +1441,9 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1261
1441
|
attemptId,
|
|
1262
1442
|
user
|
|
1263
1443
|
});
|
|
1444
|
+
if (!configSnapshot.deferPluginSetup) {
|
|
1445
|
+
pluginHost?.setupAll(getPluginCtx());
|
|
1446
|
+
}
|
|
1264
1447
|
const getSession = () => ({ sessionId, attemptId, user });
|
|
1265
1448
|
const syncSessionFromConfig = (next) => {
|
|
1266
1449
|
sessionId = resolveSessionId(storage, next.session?.sessionId);
|
|
@@ -1320,11 +1503,8 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1320
1503
|
getProgressState: () => progress.getState(),
|
|
1321
1504
|
getSession,
|
|
1322
1505
|
updateConfig(next) {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
configSnapshot.plugins = next.plugins;
|
|
1326
|
-
pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
1327
|
-
}
|
|
1506
|
+
const previousCourseId = courseId;
|
|
1507
|
+
const sessionKeyBefore = JSON.stringify({ sessionId, attemptId, user });
|
|
1328
1508
|
if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
|
|
1329
1509
|
if (next.runtimeVersion !== void 0) {
|
|
1330
1510
|
if (next.runtimeVersion === "v1") warnRuntimeV1Deprecated();
|
|
@@ -1334,6 +1514,19 @@ function createLessonkitRuntime(config, ports = {}) {
|
|
|
1334
1514
|
configSnapshot.session = { ...configSnapshot.session, ...next.session };
|
|
1335
1515
|
}
|
|
1336
1516
|
syncSessionFromConfig(configSnapshot);
|
|
1517
|
+
const sessionKeyAfter = JSON.stringify({ sessionId, attemptId, user });
|
|
1518
|
+
if (next.courseId !== void 0 && next.courseId !== previousCourseId) {
|
|
1519
|
+
progress = createProgressController();
|
|
1520
|
+
}
|
|
1521
|
+
if (next.plugins !== void 0 && next.plugins !== pluginHost) {
|
|
1522
|
+
pluginHost?.disposeAll();
|
|
1523
|
+
configSnapshot.plugins = next.plugins;
|
|
1524
|
+
pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
1525
|
+
pluginHost?.setupAll(getPluginCtx());
|
|
1526
|
+
} else if (next.session !== void 0 && sessionKeyBefore !== sessionKeyAfter && pluginHost && !configSnapshot.deferPluginSetup) {
|
|
1527
|
+
pluginHost.disposeAll();
|
|
1528
|
+
pluginHost.setupAll(getPluginCtx());
|
|
1529
|
+
}
|
|
1337
1530
|
},
|
|
1338
1531
|
setActiveLesson(lessonId, emitFn) {
|
|
1339
1532
|
const wrapped = wrapEmitFn(emitFn);
|
|
@@ -1406,6 +1599,8 @@ function defineLifecyclePlugin(plugin) {
|
|
|
1406
1599
|
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
1407
1600
|
PAGE_ALLOWED_CHILD_TYPES,
|
|
1408
1601
|
SESSION_STORAGE_KEY,
|
|
1602
|
+
SLIDE_ALLOWED_CHILD_TYPES,
|
|
1603
|
+
SLIDE_DECK_ALLOWED_CHILD_TYPES,
|
|
1409
1604
|
TELEMETRY_EVENT_CATALOG,
|
|
1410
1605
|
TELEMETRY_EVENT_CATALOG_V2,
|
|
1411
1606
|
TELEMETRY_EVENT_CATALOG_V3,
|
|
@@ -1456,6 +1651,7 @@ function defineLifecyclePlugin(plugin) {
|
|
|
1456
1651
|
parseCompoundResumeState,
|
|
1457
1652
|
parseCourseId,
|
|
1458
1653
|
parseLessonId,
|
|
1654
|
+
resetSharedVolatileSessionIdForTests,
|
|
1459
1655
|
resetStoragePortForTests,
|
|
1460
1656
|
resetTelemetryBuilderWarningsForTests,
|
|
1461
1657
|
resolveSessionId,
|