@lessonkit/core 0.9.3 → 1.0.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/README.md +31 -12
- package/dist/index.cjs +606 -9
- package/dist/index.d.cts +247 -24
- package/dist/index.d.ts +247 -24
- package/dist/index.js +571 -7
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -22,18 +22,51 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ID_MAX_LENGTH: () => ID_MAX_LENGTH,
|
|
24
24
|
ID_PATTERN: () => ID_PATTERN,
|
|
25
|
+
SESSION_STORAGE_KEY: () => SESSION_STORAGE_KEY,
|
|
25
26
|
TELEMETRY_EVENT_CATALOG: () => TELEMETRY_EVENT_CATALOG,
|
|
27
|
+
assertNever: () => assertNever,
|
|
26
28
|
assertValidId: () => assertValidId,
|
|
29
|
+
buildCourseStartedTelemetryEvent: () => buildCourseStartedTelemetryEvent,
|
|
27
30
|
buildLessonkitUrn: () => buildLessonkitUrn,
|
|
28
31
|
buildTelemetryCatalog: () => buildTelemetryCatalog,
|
|
29
|
-
|
|
32
|
+
buildTelemetryEvent: () => buildTelemetryEvent,
|
|
33
|
+
completeCourseWithTelemetry: () => completeCourseWithTelemetry,
|
|
34
|
+
completeLessonWithTelemetry: () => completeLessonWithTelemetry,
|
|
35
|
+
createDefaultClock: () => createDefaultClock,
|
|
36
|
+
createGlobalTimer: () => createGlobalTimer,
|
|
37
|
+
createLessonkitRuntime: () => createLessonkitRuntime,
|
|
38
|
+
createNoopStorage: () => createNoopStorage,
|
|
39
|
+
createPluginRegistry: () => createPluginRegistry,
|
|
40
|
+
createProgressController: () => createProgressController,
|
|
30
41
|
createSessionId: () => createSessionId,
|
|
42
|
+
createSessionStoragePort: () => createSessionStoragePort,
|
|
43
|
+
createTelemetryPipeline: () => createTelemetryPipeline,
|
|
31
44
|
createTrackingClient: () => createTrackingClient,
|
|
32
|
-
|
|
45
|
+
createTrackingPipelineSink: () => createTrackingPipelineSink,
|
|
46
|
+
defineAssessmentPlugin: () => defineAssessmentPlugin,
|
|
47
|
+
defineLifecyclePlugin: () => defineLifecyclePlugin,
|
|
48
|
+
defineTelemetryPlugin: () => defineTelemetryPlugin,
|
|
33
49
|
deriveId: () => deriveId,
|
|
50
|
+
getTabSessionId: () => getTabSessionId,
|
|
51
|
+
hasCourseStarted: () => hasCourseStarted,
|
|
52
|
+
hasCourseStartedEmittedToTracking: () => hasCourseStartedEmittedToTracking,
|
|
53
|
+
hasCourseStartedPipelineDelivered: () => hasCourseStartedPipelineDelivered,
|
|
54
|
+
markCourseStarted: () => markCourseStarted,
|
|
55
|
+
markCourseStartedEmittedToTracking: () => markCourseStartedEmittedToTracking,
|
|
56
|
+
markCourseStartedPipelineDelivered: () => markCourseStartedPipelineDelivered,
|
|
57
|
+
migrateCourseStartedMark: () => migrateCourseStartedMark,
|
|
34
58
|
nowIso: () => nowIso,
|
|
59
|
+
parseBlockId: () => parseBlockId,
|
|
60
|
+
parseCheckId: () => parseCheckId,
|
|
61
|
+
parseCourseId: () => parseCourseId,
|
|
62
|
+
parseLessonId: () => parseLessonId,
|
|
63
|
+
resetStoragePortForTests: () => resetStoragePortForTests,
|
|
64
|
+
resetTelemetryBuilderWarningsForTests: () => resetTelemetryBuilderWarningsForTests,
|
|
65
|
+
resolveSessionId: () => resolveSessionId,
|
|
35
66
|
slugifyId: () => slugifyId,
|
|
36
67
|
telemetryCatalogVersion: () => telemetryCatalogVersion,
|
|
68
|
+
tryBuildTelemetryEvent: () => tryBuildTelemetryEvent,
|
|
69
|
+
tryEmitCourseStarted: () => tryEmitCourseStarted,
|
|
37
70
|
validateId: () => validateId
|
|
38
71
|
});
|
|
39
72
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -42,6 +75,11 @@ module.exports = __toCommonJS(index_exports);
|
|
|
42
75
|
var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
|
|
43
76
|
var ID_MAX_LENGTH = 64;
|
|
44
77
|
|
|
78
|
+
// src/assertNever.ts
|
|
79
|
+
function assertNever(value, message = "Unexpected value") {
|
|
80
|
+
throw new Error(`${message}: ${String(value)}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
45
83
|
// src/validateId.ts
|
|
46
84
|
function validateId(input, path = "id") {
|
|
47
85
|
if (typeof input !== "string") {
|
|
@@ -70,6 +108,22 @@ function validateId(input, path = "id") {
|
|
|
70
108
|
}
|
|
71
109
|
return { ok: true, id };
|
|
72
110
|
}
|
|
111
|
+
function parseCourseId(input) {
|
|
112
|
+
const result = validateId(input, "courseId");
|
|
113
|
+
return result.ok ? result.id : null;
|
|
114
|
+
}
|
|
115
|
+
function parseLessonId(input) {
|
|
116
|
+
const result = validateId(input, "lessonId");
|
|
117
|
+
return result.ok ? result.id : null;
|
|
118
|
+
}
|
|
119
|
+
function parseCheckId(input) {
|
|
120
|
+
const result = validateId(input, "checkId");
|
|
121
|
+
return result.ok ? result.id : null;
|
|
122
|
+
}
|
|
123
|
+
function parseBlockId(input) {
|
|
124
|
+
const result = validateId(input, "blockId");
|
|
125
|
+
return result.ok ? result.id : null;
|
|
126
|
+
}
|
|
73
127
|
function assertValidId(input, path = "id") {
|
|
74
128
|
const result = validateId(input, path);
|
|
75
129
|
if (!result.ok) {
|
|
@@ -194,18 +248,48 @@ function buildTelemetryCatalog() {
|
|
|
194
248
|
}
|
|
195
249
|
|
|
196
250
|
// src/trackingClient.ts
|
|
251
|
+
function isDevEnvironment() {
|
|
252
|
+
const g = globalThis;
|
|
253
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
254
|
+
}
|
|
255
|
+
function invokeTrackingSink(sink, event) {
|
|
256
|
+
let result;
|
|
257
|
+
try {
|
|
258
|
+
result = sink(event);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (isDevEnvironment()) {
|
|
261
|
+
console.warn(
|
|
262
|
+
"[lessonkit] tracking sink failed:",
|
|
263
|
+
err instanceof Error ? err.message : err
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
if (result != null && typeof result.catch === "function") {
|
|
269
|
+
void result.catch((err) => {
|
|
270
|
+
if (isDevEnvironment()) {
|
|
271
|
+
console.warn(
|
|
272
|
+
"[lessonkit] tracking sink failed:",
|
|
273
|
+
err instanceof Error ? err.message : err
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
197
279
|
function createTrackingClient(opts) {
|
|
198
280
|
const sink = opts?.sink;
|
|
199
281
|
const batchSink = opts?.batchSink;
|
|
200
282
|
const batchEnabled = opts?.batch?.enabled ?? Boolean(batchSink);
|
|
201
283
|
const flushIntervalMs = opts?.batch?.flushIntervalMs ?? 5e3;
|
|
202
284
|
const maxBatchSize = opts?.batch?.maxBatchSize ?? 25;
|
|
285
|
+
const maxBufferSize = 1e3;
|
|
286
|
+
let warnedBufferCap = false;
|
|
203
287
|
if (!batchEnabled) {
|
|
204
288
|
let disposed2 = false;
|
|
205
289
|
return {
|
|
206
290
|
track: (event) => {
|
|
207
291
|
if (disposed2) return;
|
|
208
|
-
|
|
292
|
+
if (sink) invokeTrackingSink(sink, event);
|
|
209
293
|
},
|
|
210
294
|
dispose: () => {
|
|
211
295
|
disposed2 = true;
|
|
@@ -264,6 +348,15 @@ function createTrackingClient(opts) {
|
|
|
264
348
|
return {
|
|
265
349
|
track: (event) => {
|
|
266
350
|
if (disposed || disposing) return;
|
|
351
|
+
if (buffer.length >= maxBufferSize) {
|
|
352
|
+
buffer.shift();
|
|
353
|
+
if (!warnedBufferCap && typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
|
|
354
|
+
warnedBufferCap = true;
|
|
355
|
+
console.warn(
|
|
356
|
+
`[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; oldest events are dropped while the sink is unavailable.`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
267
360
|
buffer.push(event);
|
|
268
361
|
if (buffer.length >= maxBatchSize) void flush();
|
|
269
362
|
},
|
|
@@ -295,16 +388,476 @@ function nowIso() {
|
|
|
295
388
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
296
389
|
}
|
|
297
390
|
|
|
298
|
-
// src/
|
|
299
|
-
|
|
300
|
-
|
|
391
|
+
// src/telemetryBuilder.ts
|
|
392
|
+
var warnedMissingQuizLesson = false;
|
|
393
|
+
function isDevEnvironment2() {
|
|
394
|
+
const g = globalThis;
|
|
395
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
396
|
+
}
|
|
397
|
+
function resetTelemetryBuilderWarningsForTests() {
|
|
398
|
+
warnedMissingQuizLesson = false;
|
|
399
|
+
}
|
|
400
|
+
function resolveLessonId(opts, eventName) {
|
|
401
|
+
const lessonId = opts.lessonId ?? opts.data?.lessonId;
|
|
402
|
+
if (!lessonId) throw new Error(`${eventName} requires lessonId`);
|
|
403
|
+
return lessonId;
|
|
404
|
+
}
|
|
405
|
+
function buildTelemetryEvent(opts) {
|
|
406
|
+
const base = {
|
|
407
|
+
timestamp: opts.timestamp ?? nowIso(),
|
|
408
|
+
courseId: opts.courseId,
|
|
409
|
+
sessionId: opts.sessionId,
|
|
410
|
+
attemptId: opts.attemptId,
|
|
411
|
+
user: opts.user
|
|
412
|
+
};
|
|
413
|
+
switch (opts.name) {
|
|
414
|
+
case "course_started":
|
|
415
|
+
return { name: "course_started", ...base };
|
|
416
|
+
case "course_completed":
|
|
417
|
+
return { name: "course_completed", ...base };
|
|
418
|
+
case "lesson_started": {
|
|
419
|
+
const lessonId = resolveLessonId(opts, "lesson_started");
|
|
420
|
+
return {
|
|
421
|
+
name: "lesson_started",
|
|
422
|
+
...base,
|
|
423
|
+
lessonId,
|
|
424
|
+
data: { ...opts.data, lessonId }
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
case "lesson_completed":
|
|
428
|
+
case "lesson_time_on_task": {
|
|
429
|
+
const lessonId = resolveLessonId(opts, opts.name);
|
|
430
|
+
return {
|
|
431
|
+
name: opts.name,
|
|
432
|
+
...base,
|
|
433
|
+
lessonId,
|
|
434
|
+
data: { ...opts.data, lessonId }
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
case "quiz_answered": {
|
|
438
|
+
const lessonId = opts.lessonId;
|
|
439
|
+
if (!lessonId) throw new Error("quiz_answered requires active lessonId");
|
|
440
|
+
return { name: "quiz_answered", ...base, lessonId, data: opts.data };
|
|
441
|
+
}
|
|
442
|
+
case "quiz_completed": {
|
|
443
|
+
const lessonId = opts.lessonId;
|
|
444
|
+
if (!lessonId) throw new Error("quiz_completed requires active lessonId");
|
|
445
|
+
return { name: "quiz_completed", ...base, lessonId, data: opts.data };
|
|
446
|
+
}
|
|
447
|
+
case "interaction":
|
|
448
|
+
return {
|
|
449
|
+
name: "interaction",
|
|
450
|
+
...base,
|
|
451
|
+
lessonId: opts.lessonId,
|
|
452
|
+
data: opts.data
|
|
453
|
+
};
|
|
454
|
+
default:
|
|
455
|
+
return assertNever(opts);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function tryBuildTelemetryEvent(opts) {
|
|
459
|
+
const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
|
|
460
|
+
if (isQuiz && !opts.lessonId) {
|
|
461
|
+
if (isDevEnvironment2() && !warnedMissingQuizLesson) {
|
|
462
|
+
warnedMissingQuizLesson = true;
|
|
463
|
+
console.warn(
|
|
464
|
+
`[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
return buildTelemetryEvent(opts);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/telemetryPipeline.ts
|
|
473
|
+
function isDevEnvironment3() {
|
|
474
|
+
const g = globalThis;
|
|
475
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
476
|
+
}
|
|
477
|
+
function warnSinkFailure(sinkId, err) {
|
|
478
|
+
if (isDevEnvironment3()) {
|
|
479
|
+
console.warn(
|
|
480
|
+
`[lessonkit] telemetry sink "${sinkId}" failed:`,
|
|
481
|
+
err instanceof Error ? err.message : err
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function invokeSink(sink, event, emitCtx) {
|
|
486
|
+
let result;
|
|
487
|
+
try {
|
|
488
|
+
result = sink.emit(event, emitCtx);
|
|
489
|
+
} catch (err) {
|
|
490
|
+
warnSinkFailure(sink.id, err);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (result != null && typeof result.catch === "function") {
|
|
494
|
+
void result.catch((err) => warnSinkFailure(sink.id, err));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function createTelemetryPipeline(sinks) {
|
|
498
|
+
const list = [...sinks];
|
|
499
|
+
return {
|
|
500
|
+
sinks: list,
|
|
501
|
+
emit(event, ctx) {
|
|
502
|
+
const emitCtx = ctx ?? {
|
|
503
|
+
courseId: event.courseId,
|
|
504
|
+
sessionId: event.sessionId,
|
|
505
|
+
attemptId: event.attemptId
|
|
506
|
+
};
|
|
507
|
+
for (const sink of list) {
|
|
508
|
+
invokeSink(sink, event, emitCtx);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function createTrackingPipelineSink(id, track) {
|
|
514
|
+
return {
|
|
515
|
+
id,
|
|
516
|
+
emit(event) {
|
|
517
|
+
track(event);
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/ports.ts
|
|
523
|
+
function createDefaultClock() {
|
|
524
|
+
return {
|
|
525
|
+
nowMs: () => Date.now(),
|
|
526
|
+
nowIso: () => (/* @__PURE__ */ new Date()).toISOString()
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function createNoopStorage() {
|
|
530
|
+
return {
|
|
531
|
+
getItem: () => null,
|
|
532
|
+
setItem: () => {
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function createMemoryBackedSessionStorage(session) {
|
|
537
|
+
const memory = /* @__PURE__ */ new Map();
|
|
538
|
+
let warnedPersistFailure = false;
|
|
539
|
+
const warnPersistFailure = () => {
|
|
540
|
+
if (warnedPersistFailure) return;
|
|
541
|
+
warnedPersistFailure = true;
|
|
542
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
|
|
543
|
+
console.warn(
|
|
544
|
+
"[lessonkit] sessionStorage is unavailable or failed; using in-memory session dedupe for this tab (may reset on full reload)."
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
return {
|
|
549
|
+
getItem: (key) => {
|
|
550
|
+
if (memory.has(key)) return memory.get(key);
|
|
551
|
+
try {
|
|
552
|
+
const value = session.getItem(key);
|
|
553
|
+
if (value !== null) memory.set(key, value);
|
|
554
|
+
return value;
|
|
555
|
+
} catch {
|
|
556
|
+
return memory.get(key) ?? null;
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
setItem: (key, value) => {
|
|
560
|
+
memory.set(key, value);
|
|
561
|
+
try {
|
|
562
|
+
session.setItem(key, value);
|
|
563
|
+
} catch {
|
|
564
|
+
warnPersistFailure();
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
removeItem: (key) => {
|
|
568
|
+
memory.delete(key);
|
|
569
|
+
try {
|
|
570
|
+
session.removeItem(key);
|
|
571
|
+
} catch {
|
|
572
|
+
warnPersistFailure();
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
resetForTests: () => {
|
|
576
|
+
memory.clear();
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function resetStoragePortForTests(storage) {
|
|
581
|
+
storage.resetForTests?.();
|
|
582
|
+
}
|
|
583
|
+
function createSessionStoragePort() {
|
|
584
|
+
if (typeof sessionStorage === "undefined") {
|
|
585
|
+
const memory = /* @__PURE__ */ new Map();
|
|
586
|
+
return {
|
|
587
|
+
getItem: (key) => memory.get(key) ?? null,
|
|
588
|
+
setItem: (key, value) => {
|
|
589
|
+
memory.set(key, value);
|
|
590
|
+
},
|
|
591
|
+
removeItem: (key) => {
|
|
592
|
+
memory.delete(key);
|
|
593
|
+
},
|
|
594
|
+
resetForTests: () => {
|
|
595
|
+
memory.clear();
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
return createMemoryBackedSessionStorage(sessionStorage);
|
|
600
|
+
}
|
|
601
|
+
function createGlobalTimer() {
|
|
602
|
+
return {
|
|
603
|
+
setInterval: (fn, ms) => globalThis.setInterval(fn, ms),
|
|
604
|
+
clearInterval: (id) => globalThis.clearInterval(id)
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// src/progress.ts
|
|
609
|
+
function createProgressController() {
|
|
610
|
+
let activeLessonId;
|
|
611
|
+
let completedLessonIds = /* @__PURE__ */ new Set();
|
|
612
|
+
let courseCompleted = false;
|
|
613
|
+
const lessonStartTimes = /* @__PURE__ */ new Map();
|
|
614
|
+
return {
|
|
615
|
+
getState: () => ({
|
|
616
|
+
activeLessonId,
|
|
617
|
+
completedLessonIds: new Set(completedLessonIds),
|
|
618
|
+
courseCompleted
|
|
619
|
+
}),
|
|
620
|
+
setActiveLesson: (lessonId, startedAtMs) => {
|
|
621
|
+
const previousLessonId = activeLessonId;
|
|
622
|
+
activeLessonId = lessonId;
|
|
623
|
+
lessonStartTimes.set(lessonId, startedAtMs);
|
|
624
|
+
return { previousLessonId };
|
|
625
|
+
},
|
|
626
|
+
completeLesson: (lessonId, completedAtMs) => {
|
|
627
|
+
if (completedLessonIds.has(lessonId)) return { didComplete: false };
|
|
628
|
+
completedLessonIds = new Set(completedLessonIds).add(lessonId);
|
|
629
|
+
if (activeLessonId === lessonId) {
|
|
630
|
+
activeLessonId = void 0;
|
|
631
|
+
}
|
|
632
|
+
const startedAt = lessonStartTimes.get(lessonId);
|
|
633
|
+
lessonStartTimes.delete(lessonId);
|
|
634
|
+
const durationMs = typeof startedAt === "number" ? Math.max(0, completedAtMs - startedAt) : void 0;
|
|
635
|
+
return { durationMs, didComplete: true };
|
|
636
|
+
},
|
|
637
|
+
completeCourse: () => {
|
|
638
|
+
if (courseCompleted) return { didComplete: false };
|
|
639
|
+
courseCompleted = true;
|
|
640
|
+
return { didComplete: true };
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// src/session.ts
|
|
646
|
+
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
647
|
+
function getTabSessionId(storage) {
|
|
648
|
+
return storage.getItem(SESSION_STORAGE_KEY);
|
|
649
|
+
}
|
|
650
|
+
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
651
|
+
var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
|
|
652
|
+
var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
|
|
653
|
+
function resolveSessionId(storage, provided) {
|
|
654
|
+
if (provided) return provided;
|
|
655
|
+
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
656
|
+
if (existing) return existing;
|
|
657
|
+
const id = createSessionId();
|
|
658
|
+
storage.setItem(SESSION_STORAGE_KEY, id);
|
|
659
|
+
return id;
|
|
660
|
+
}
|
|
661
|
+
function courseStartedStorageKey(sessionId, courseId) {
|
|
662
|
+
return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
663
|
+
}
|
|
664
|
+
function courseStartedTrackingStorageKey(sessionId, courseId) {
|
|
665
|
+
return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
666
|
+
}
|
|
667
|
+
function courseStartedPipelineStorageKey(sessionId, courseId) {
|
|
668
|
+
return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
669
|
+
}
|
|
670
|
+
function hasCourseStarted(storage, sessionId, courseId) {
|
|
671
|
+
if (!courseId) return false;
|
|
672
|
+
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
673
|
+
}
|
|
674
|
+
function markCourseStarted(storage, sessionId, courseId) {
|
|
675
|
+
if (!courseId) return;
|
|
676
|
+
storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
677
|
+
}
|
|
678
|
+
function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
679
|
+
if (!courseId) return false;
|
|
680
|
+
return storage.getItem(courseStartedTrackingStorageKey(sessionId, courseId)) === "1";
|
|
681
|
+
}
|
|
682
|
+
function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
683
|
+
if (!courseId) return;
|
|
684
|
+
storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
|
|
685
|
+
}
|
|
686
|
+
function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
687
|
+
if (!courseId) return false;
|
|
688
|
+
return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
|
|
689
|
+
}
|
|
690
|
+
function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
691
|
+
if (!courseId) return;
|
|
692
|
+
storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
|
|
693
|
+
}
|
|
694
|
+
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
695
|
+
if (!courseId || fromSessionId === toSessionId) return;
|
|
696
|
+
if (hasCourseStarted(storage, fromSessionId, courseId)) {
|
|
697
|
+
markCourseStarted(storage, toSessionId, courseId);
|
|
698
|
+
storage.removeItem?.(courseStartedStorageKey(fromSessionId, courseId));
|
|
699
|
+
}
|
|
700
|
+
if (hasCourseStartedEmittedToTracking(storage, fromSessionId, courseId)) {
|
|
701
|
+
markCourseStartedEmittedToTracking(storage, toSessionId, courseId);
|
|
702
|
+
storage.removeItem?.(courseStartedTrackingStorageKey(fromSessionId, courseId));
|
|
703
|
+
}
|
|
704
|
+
if (hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)) {
|
|
705
|
+
markCourseStartedPipelineDelivered(storage, toSessionId, courseId);
|
|
706
|
+
storage.removeItem?.(courseStartedPipelineStorageKey(fromSessionId, courseId));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/runtime/courseLifecycle.ts
|
|
711
|
+
function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
|
|
712
|
+
const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
713
|
+
if (alreadyEmittedToSink) {
|
|
714
|
+
return { emitted: true, marked };
|
|
715
|
+
}
|
|
716
|
+
if (marked) {
|
|
717
|
+
return { emitted: false, marked: true };
|
|
718
|
+
}
|
|
719
|
+
const emitted = deps.emitCourseStartedEvent(ctx);
|
|
720
|
+
if (emitted) {
|
|
721
|
+
markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
722
|
+
}
|
|
723
|
+
return { emitted, marked: emitted };
|
|
724
|
+
}
|
|
725
|
+
function buildCourseStartedTelemetryEvent(ctx) {
|
|
726
|
+
return buildTelemetryEvent({
|
|
727
|
+
name: "course_started",
|
|
728
|
+
courseId: ctx.courseId,
|
|
729
|
+
sessionId: ctx.sessionId,
|
|
730
|
+
attemptId: ctx.attemptId,
|
|
731
|
+
user: ctx.user
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
function completeLessonWithTelemetry(opts) {
|
|
735
|
+
const result = opts.progress.completeLesson(opts.lessonId, opts.nowMs);
|
|
736
|
+
if (!result.didComplete) return false;
|
|
737
|
+
opts.emitLessonCompleted(opts.lessonId, result.durationMs);
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
function completeCourseWithTelemetry(opts) {
|
|
741
|
+
const current = opts.progress.getState();
|
|
742
|
+
if (current.activeLessonId) {
|
|
743
|
+
completeLessonWithTelemetry({
|
|
744
|
+
progress: opts.progress,
|
|
745
|
+
lessonId: current.activeLessonId,
|
|
746
|
+
nowMs: opts.nowMs,
|
|
747
|
+
emitLessonCompleted: opts.emitLessonCompleted
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
const result = opts.progress.completeCourse();
|
|
751
|
+
if (!result.didComplete) return false;
|
|
752
|
+
opts.emitCourseCompleted();
|
|
753
|
+
return true;
|
|
301
754
|
}
|
|
755
|
+
|
|
756
|
+
// src/runtime/createLessonkitRuntime.ts
|
|
757
|
+
function createLessonkitRuntime(config, ports = {}) {
|
|
758
|
+
const storage = ports.storage ?? createSessionStoragePort();
|
|
759
|
+
const clock = ports.clock ?? createDefaultClock();
|
|
760
|
+
const configSnapshot = { ...config };
|
|
761
|
+
let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
|
|
762
|
+
let attemptId = configSnapshot.session?.attemptId;
|
|
763
|
+
let user = configSnapshot.session?.user;
|
|
764
|
+
let courseId = configSnapshot.courseId;
|
|
765
|
+
let progress = createProgressController();
|
|
766
|
+
const getSession = () => ({ sessionId, attemptId, user });
|
|
767
|
+
const syncSessionFromConfig = (next) => {
|
|
768
|
+
sessionId = resolveSessionId(storage, next.session?.sessionId);
|
|
769
|
+
attemptId = next.session?.attemptId;
|
|
770
|
+
user = next.session?.user;
|
|
771
|
+
courseId = next.courseId;
|
|
772
|
+
};
|
|
773
|
+
syncSessionFromConfig(configSnapshot);
|
|
774
|
+
const track = (name, data, emit, lessonId) => {
|
|
775
|
+
const event = tryBuildTelemetryEvent({
|
|
776
|
+
name,
|
|
777
|
+
courseId,
|
|
778
|
+
lessonId: lessonId ?? progress.getState().activeLessonId,
|
|
779
|
+
sessionId,
|
|
780
|
+
attemptId,
|
|
781
|
+
user,
|
|
782
|
+
data
|
|
783
|
+
});
|
|
784
|
+
if (!event) return;
|
|
785
|
+
emit(event);
|
|
786
|
+
};
|
|
787
|
+
const emitLessonCompleted = (lessonId, durationMs, emitFn) => {
|
|
788
|
+
emitFn("lesson_completed", { lessonId, durationMs }, lessonId);
|
|
789
|
+
if (durationMs !== void 0) {
|
|
790
|
+
emitFn("lesson_time_on_task", { lessonId, durationMs }, lessonId);
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
return {
|
|
794
|
+
get config() {
|
|
795
|
+
return configSnapshot;
|
|
796
|
+
},
|
|
797
|
+
get progress() {
|
|
798
|
+
return progress;
|
|
799
|
+
},
|
|
800
|
+
getProgressState: () => progress.getState(),
|
|
801
|
+
getSession,
|
|
802
|
+
updateConfig(next) {
|
|
803
|
+
if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
|
|
804
|
+
if (next.runtimeVersion !== void 0) configSnapshot.runtimeVersion = next.runtimeVersion;
|
|
805
|
+
if (next.plugins !== void 0) configSnapshot.plugins = next.plugins;
|
|
806
|
+
if (next.session !== void 0) {
|
|
807
|
+
configSnapshot.session = { ...configSnapshot.session, ...next.session };
|
|
808
|
+
}
|
|
809
|
+
syncSessionFromConfig(configSnapshot);
|
|
810
|
+
},
|
|
811
|
+
setActiveLesson(lessonId, emitFn) {
|
|
812
|
+
const current = progress.getState();
|
|
813
|
+
if (current.activeLessonId === lessonId) return;
|
|
814
|
+
if (current.completedLessonIds.has(lessonId)) {
|
|
815
|
+
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const previous = current.activeLessonId;
|
|
819
|
+
if (previous && previous !== lessonId) {
|
|
820
|
+
const completed = progress.completeLesson(previous, clock.nowMs());
|
|
821
|
+
if (completed.didComplete) {
|
|
822
|
+
emitLessonCompleted(previous, completed.durationMs, emitFn);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
826
|
+
emitFn("lesson_started", { lessonId }, lessonId);
|
|
827
|
+
},
|
|
828
|
+
completeLesson(lessonId, emitFn) {
|
|
829
|
+
const result = progress.completeLesson(lessonId, clock.nowMs());
|
|
830
|
+
if (!result.didComplete) return;
|
|
831
|
+
emitLessonCompleted(lessonId, result.durationMs, emitFn);
|
|
832
|
+
},
|
|
833
|
+
completeCourse(emitFn) {
|
|
834
|
+
const current = progress.getState();
|
|
835
|
+
if (current.activeLessonId) {
|
|
836
|
+
const lessonResult = progress.completeLesson(current.activeLessonId, clock.nowMs());
|
|
837
|
+
if (lessonResult.didComplete) {
|
|
838
|
+
emitLessonCompleted(current.activeLessonId, lessonResult.durationMs, emitFn);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const result = progress.completeCourse();
|
|
842
|
+
if (!result.didComplete) return;
|
|
843
|
+
emitFn("course_completed");
|
|
844
|
+
},
|
|
845
|
+
track,
|
|
846
|
+
resetForCourseChange(nextCourseId) {
|
|
847
|
+
configSnapshot.courseId = nextCourseId;
|
|
848
|
+
courseId = nextCourseId;
|
|
849
|
+
progress = createProgressController();
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/plugins/registry.ts
|
|
302
855
|
function warnDuplicatePlugin(id) {
|
|
303
856
|
const g = globalThis;
|
|
304
857
|
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
|
|
305
858
|
console.warn(`[lessonkit] plugin id "${id}" was registered more than once; using the latest definition`);
|
|
306
859
|
}
|
|
307
|
-
function
|
|
860
|
+
function createPluginRegistry(plugins = []) {
|
|
308
861
|
const registry = /* @__PURE__ */ new Map();
|
|
309
862
|
for (const plugin of plugins) {
|
|
310
863
|
if (registry.has(plugin.id)) warnDuplicatePlugin(plugin.id);
|
|
@@ -384,21 +937,65 @@ function createPluginHost(plugins = []) {
|
|
|
384
937
|
scoreAssessment
|
|
385
938
|
};
|
|
386
939
|
}
|
|
940
|
+
|
|
941
|
+
// src/plugins/define.ts
|
|
942
|
+
function defineTelemetryPlugin(plugin) {
|
|
943
|
+
return plugin;
|
|
944
|
+
}
|
|
945
|
+
function defineAssessmentPlugin(plugin) {
|
|
946
|
+
return plugin;
|
|
947
|
+
}
|
|
948
|
+
function defineLifecyclePlugin(plugin) {
|
|
949
|
+
return plugin;
|
|
950
|
+
}
|
|
387
951
|
// Annotate the CommonJS export names for ESM import in node:
|
|
388
952
|
0 && (module.exports = {
|
|
389
953
|
ID_MAX_LENGTH,
|
|
390
954
|
ID_PATTERN,
|
|
955
|
+
SESSION_STORAGE_KEY,
|
|
391
956
|
TELEMETRY_EVENT_CATALOG,
|
|
957
|
+
assertNever,
|
|
392
958
|
assertValidId,
|
|
959
|
+
buildCourseStartedTelemetryEvent,
|
|
393
960
|
buildLessonkitUrn,
|
|
394
961
|
buildTelemetryCatalog,
|
|
395
|
-
|
|
962
|
+
buildTelemetryEvent,
|
|
963
|
+
completeCourseWithTelemetry,
|
|
964
|
+
completeLessonWithTelemetry,
|
|
965
|
+
createDefaultClock,
|
|
966
|
+
createGlobalTimer,
|
|
967
|
+
createLessonkitRuntime,
|
|
968
|
+
createNoopStorage,
|
|
969
|
+
createPluginRegistry,
|
|
970
|
+
createProgressController,
|
|
396
971
|
createSessionId,
|
|
972
|
+
createSessionStoragePort,
|
|
973
|
+
createTelemetryPipeline,
|
|
397
974
|
createTrackingClient,
|
|
398
|
-
|
|
975
|
+
createTrackingPipelineSink,
|
|
976
|
+
defineAssessmentPlugin,
|
|
977
|
+
defineLifecyclePlugin,
|
|
978
|
+
defineTelemetryPlugin,
|
|
399
979
|
deriveId,
|
|
980
|
+
getTabSessionId,
|
|
981
|
+
hasCourseStarted,
|
|
982
|
+
hasCourseStartedEmittedToTracking,
|
|
983
|
+
hasCourseStartedPipelineDelivered,
|
|
984
|
+
markCourseStarted,
|
|
985
|
+
markCourseStartedEmittedToTracking,
|
|
986
|
+
markCourseStartedPipelineDelivered,
|
|
987
|
+
migrateCourseStartedMark,
|
|
400
988
|
nowIso,
|
|
989
|
+
parseBlockId,
|
|
990
|
+
parseCheckId,
|
|
991
|
+
parseCourseId,
|
|
992
|
+
parseLessonId,
|
|
993
|
+
resetStoragePortForTests,
|
|
994
|
+
resetTelemetryBuilderWarningsForTests,
|
|
995
|
+
resolveSessionId,
|
|
401
996
|
slugifyId,
|
|
402
997
|
telemetryCatalogVersion,
|
|
998
|
+
tryBuildTelemetryEvent,
|
|
999
|
+
tryEmitCourseStarted,
|
|
403
1000
|
validateId
|
|
404
1001
|
});
|