@lessonkit/core 0.9.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -12
- package/dist/index.cjs +556 -9
- package/dist/index.d.cts +184 -19
- package/dist/index.d.ts +184 -19
- package/dist/index.js +528 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lessonkit/core
|
|
2
2
|
|
|
3
|
-
[](https://github.com/eddiethedean/lessonkit/actions/workflows/ci.yml)
|
|
4
|
-
[](https://lessonkit.readthedocs.io/en/latest/)
|
|
5
3
|
[](https://www.npmjs.com/package/@lessonkit/core)
|
|
4
|
+
[](https://lessonkit.readthedocs.io/en/latest/reference/core.html)
|
|
6
5
|
[](https://github.com/eddiethedean/lessonkit/blob/main/LICENSE)
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
**Docs:** [Identity reference](https://lessonkit.readthedocs.io/en/latest/reference/identity.html) · [Telemetry reference](https://lessonkit.readthedocs.io/en/latest/reference/telemetry.html) · [Telemetry & xAPI guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/telemetry-and-xapi.html)
|
|
7
|
+
Headless types, identity helpers, telemetry pipeline, and runtime primitives shared across LessonKit.
|
|
11
8
|
|
|
12
9
|
## Install
|
|
13
10
|
|
|
@@ -15,11 +12,33 @@ Core types and runtime primitives shared across LessonKit packages.
|
|
|
15
12
|
npm install @lessonkit/core
|
|
16
13
|
```
|
|
17
14
|
|
|
18
|
-
##
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import {
|
|
19
|
+
buildTelemetryEvent,
|
|
20
|
+
createLessonkitRuntime,
|
|
21
|
+
createTelemetryPipeline,
|
|
22
|
+
createPluginRegistry,
|
|
23
|
+
buildLessonkitUrn,
|
|
24
|
+
} from "@lessonkit/core";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Exports
|
|
28
|
+
|
|
29
|
+
| Area | Key APIs |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| Identity | `validateId`, `slugifyId`, `buildLessonkitUrn` |
|
|
32
|
+
| Telemetry | `buildTelemetryEvent`, `createTrackingClient`, `createTelemetryPipeline` |
|
|
33
|
+
| Runtime | `createLessonkitRuntime`, progress and session helpers |
|
|
34
|
+
| Plugins | `createPluginRegistry`, `defineTelemetryPlugin`, `defineAssessmentPlugin` |
|
|
35
|
+
|
|
36
|
+
Machine-readable: `@lessonkit/core/telemetry-catalog.v1.json`, `identity-contract.v1.json`
|
|
37
|
+
|
|
38
|
+
## Docs
|
|
39
|
+
|
|
40
|
+
[Core reference](https://lessonkit.readthedocs.io/en/latest/reference/core.html) · [Identity](https://lessonkit.readthedocs.io/en/latest/reference/identity.html) · [Telemetry](https://lessonkit.readthedocs.io/en/latest/reference/telemetry.html) · [Plugins](https://lessonkit.readthedocs.io/en/latest/reference/plugins.html)
|
|
19
41
|
|
|
20
|
-
|
|
21
|
-
- Typed telemetry events (`TelemetryEvent`) and `telemetry-catalog.v1.json`
|
|
22
|
-
- Tracking client (`createTrackingClient`) with optional batching
|
|
23
|
-
- Session id helper (`createSessionId`)
|
|
42
|
+
## License
|
|
24
43
|
|
|
25
|
-
|
|
44
|
+
Apache-2.0
|
package/dist/index.cjs
CHANGED
|
@@ -22,18 +22,44 @@ 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,
|
|
26
27
|
assertValidId: () => assertValidId,
|
|
28
|
+
buildCourseStartedTelemetryEvent: () => buildCourseStartedTelemetryEvent,
|
|
27
29
|
buildLessonkitUrn: () => buildLessonkitUrn,
|
|
28
30
|
buildTelemetryCatalog: () => buildTelemetryCatalog,
|
|
29
|
-
|
|
31
|
+
buildTelemetryEvent: () => buildTelemetryEvent,
|
|
32
|
+
completeCourseWithTelemetry: () => completeCourseWithTelemetry,
|
|
33
|
+
completeLessonWithTelemetry: () => completeLessonWithTelemetry,
|
|
34
|
+
createDefaultClock: () => createDefaultClock,
|
|
35
|
+
createGlobalTimer: () => createGlobalTimer,
|
|
36
|
+
createLessonkitRuntime: () => createLessonkitRuntime,
|
|
37
|
+
createNoopStorage: () => createNoopStorage,
|
|
38
|
+
createPluginRegistry: () => createPluginRegistry,
|
|
39
|
+
createProgressController: () => createProgressController,
|
|
30
40
|
createSessionId: () => createSessionId,
|
|
41
|
+
createSessionStoragePort: () => createSessionStoragePort,
|
|
42
|
+
createTelemetryPipeline: () => createTelemetryPipeline,
|
|
31
43
|
createTrackingClient: () => createTrackingClient,
|
|
32
|
-
|
|
44
|
+
createTrackingPipelineSink: () => createTrackingPipelineSink,
|
|
45
|
+
defineAssessmentPlugin: () => defineAssessmentPlugin,
|
|
46
|
+
defineLifecyclePlugin: () => defineLifecyclePlugin,
|
|
47
|
+
defineTelemetryPlugin: () => defineTelemetryPlugin,
|
|
33
48
|
deriveId: () => deriveId,
|
|
49
|
+
getTabSessionId: () => getTabSessionId,
|
|
50
|
+
hasCourseStarted: () => hasCourseStarted,
|
|
51
|
+
hasCourseStartedEmittedToTracking: () => hasCourseStartedEmittedToTracking,
|
|
52
|
+
markCourseStarted: () => markCourseStarted,
|
|
53
|
+
markCourseStartedEmittedToTracking: () => markCourseStartedEmittedToTracking,
|
|
54
|
+
migrateCourseStartedMark: () => migrateCourseStartedMark,
|
|
34
55
|
nowIso: () => nowIso,
|
|
56
|
+
resetStoragePortForTests: () => resetStoragePortForTests,
|
|
57
|
+
resetTelemetryBuilderWarningsForTests: () => resetTelemetryBuilderWarningsForTests,
|
|
58
|
+
resolveSessionId: () => resolveSessionId,
|
|
35
59
|
slugifyId: () => slugifyId,
|
|
36
60
|
telemetryCatalogVersion: () => telemetryCatalogVersion,
|
|
61
|
+
tryBuildTelemetryEvent: () => tryBuildTelemetryEvent,
|
|
62
|
+
tryEmitCourseStarted: () => tryEmitCourseStarted,
|
|
37
63
|
validateId: () => validateId
|
|
38
64
|
});
|
|
39
65
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -194,18 +220,48 @@ function buildTelemetryCatalog() {
|
|
|
194
220
|
}
|
|
195
221
|
|
|
196
222
|
// src/trackingClient.ts
|
|
223
|
+
function isDevEnvironment() {
|
|
224
|
+
const g = globalThis;
|
|
225
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
226
|
+
}
|
|
227
|
+
function invokeTrackingSink(sink, event) {
|
|
228
|
+
let result;
|
|
229
|
+
try {
|
|
230
|
+
result = sink(event);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
if (isDevEnvironment()) {
|
|
233
|
+
console.warn(
|
|
234
|
+
"[lessonkit] tracking sink failed:",
|
|
235
|
+
err instanceof Error ? err.message : err
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
if (result != null && typeof result.catch === "function") {
|
|
241
|
+
void result.catch((err) => {
|
|
242
|
+
if (isDevEnvironment()) {
|
|
243
|
+
console.warn(
|
|
244
|
+
"[lessonkit] tracking sink failed:",
|
|
245
|
+
err instanceof Error ? err.message : err
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
197
251
|
function createTrackingClient(opts) {
|
|
198
252
|
const sink = opts?.sink;
|
|
199
253
|
const batchSink = opts?.batchSink;
|
|
200
254
|
const batchEnabled = opts?.batch?.enabled ?? Boolean(batchSink);
|
|
201
255
|
const flushIntervalMs = opts?.batch?.flushIntervalMs ?? 5e3;
|
|
202
256
|
const maxBatchSize = opts?.batch?.maxBatchSize ?? 25;
|
|
257
|
+
const maxBufferSize = 1e3;
|
|
258
|
+
let warnedBufferCap = false;
|
|
203
259
|
if (!batchEnabled) {
|
|
204
260
|
let disposed2 = false;
|
|
205
261
|
return {
|
|
206
262
|
track: (event) => {
|
|
207
263
|
if (disposed2) return;
|
|
208
|
-
|
|
264
|
+
if (sink) invokeTrackingSink(sink, event);
|
|
209
265
|
},
|
|
210
266
|
dispose: () => {
|
|
211
267
|
disposed2 = true;
|
|
@@ -264,6 +320,15 @@ function createTrackingClient(opts) {
|
|
|
264
320
|
return {
|
|
265
321
|
track: (event) => {
|
|
266
322
|
if (disposed || disposing) return;
|
|
323
|
+
if (buffer.length >= maxBufferSize) {
|
|
324
|
+
buffer.shift();
|
|
325
|
+
if (!warnedBufferCap && typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
|
|
326
|
+
warnedBufferCap = true;
|
|
327
|
+
console.warn(
|
|
328
|
+
`[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; oldest events are dropped while the sink is unavailable.`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
267
332
|
buffer.push(event);
|
|
268
333
|
if (buffer.length >= maxBatchSize) void flush();
|
|
269
334
|
},
|
|
@@ -295,16 +360,461 @@ function nowIso() {
|
|
|
295
360
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
296
361
|
}
|
|
297
362
|
|
|
298
|
-
// src/
|
|
299
|
-
|
|
300
|
-
|
|
363
|
+
// src/telemetryBuilder.ts
|
|
364
|
+
var warnedMissingQuizLesson = false;
|
|
365
|
+
function isDevEnvironment2() {
|
|
366
|
+
const g = globalThis;
|
|
367
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
368
|
+
}
|
|
369
|
+
function resetTelemetryBuilderWarningsForTests() {
|
|
370
|
+
warnedMissingQuizLesson = false;
|
|
371
|
+
}
|
|
372
|
+
function buildTelemetryEvent(opts) {
|
|
373
|
+
const base = {
|
|
374
|
+
timestamp: opts.timestamp ?? nowIso(),
|
|
375
|
+
courseId: opts.courseId,
|
|
376
|
+
sessionId: opts.sessionId,
|
|
377
|
+
attemptId: opts.attemptId,
|
|
378
|
+
user: opts.user
|
|
379
|
+
};
|
|
380
|
+
switch (opts.name) {
|
|
381
|
+
case "course_started":
|
|
382
|
+
return { name: "course_started", ...base };
|
|
383
|
+
case "course_completed":
|
|
384
|
+
return { name: "course_completed", ...base };
|
|
385
|
+
case "lesson_started": {
|
|
386
|
+
const data = opts.data;
|
|
387
|
+
const lessonId = opts.lessonId ?? data?.lessonId;
|
|
388
|
+
if (!lessonId) throw new Error("lesson_started requires lessonId");
|
|
389
|
+
return {
|
|
390
|
+
name: "lesson_started",
|
|
391
|
+
...base,
|
|
392
|
+
lessonId,
|
|
393
|
+
data: { ...data, lessonId }
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
case "lesson_completed":
|
|
397
|
+
case "lesson_time_on_task": {
|
|
398
|
+
const data = opts.data;
|
|
399
|
+
const lessonId = opts.lessonId ?? data?.lessonId;
|
|
400
|
+
if (!lessonId) throw new Error(`${opts.name} requires lessonId`);
|
|
401
|
+
return {
|
|
402
|
+
name: opts.name,
|
|
403
|
+
...base,
|
|
404
|
+
lessonId,
|
|
405
|
+
data: { ...data, lessonId }
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
case "quiz_answered": {
|
|
409
|
+
const data = opts.data;
|
|
410
|
+
const lessonId = opts.lessonId;
|
|
411
|
+
if (!lessonId) throw new Error("quiz_answered requires active lessonId");
|
|
412
|
+
return { name: "quiz_answered", ...base, lessonId, data };
|
|
413
|
+
}
|
|
414
|
+
case "quiz_completed": {
|
|
415
|
+
const data = opts.data;
|
|
416
|
+
const lessonId = opts.lessonId;
|
|
417
|
+
if (!lessonId) throw new Error("quiz_completed requires active lessonId");
|
|
418
|
+
return { name: "quiz_completed", ...base, lessonId, data };
|
|
419
|
+
}
|
|
420
|
+
case "interaction":
|
|
421
|
+
return {
|
|
422
|
+
name: "interaction",
|
|
423
|
+
...base,
|
|
424
|
+
lessonId: opts.lessonId,
|
|
425
|
+
data: opts.data
|
|
426
|
+
};
|
|
427
|
+
default:
|
|
428
|
+
return { name: opts.name, ...base };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function tryBuildTelemetryEvent(opts) {
|
|
432
|
+
const isQuiz = opts.name === "quiz_answered" || opts.name === "quiz_completed";
|
|
433
|
+
if (isQuiz && !opts.lessonId) {
|
|
434
|
+
if (isDevEnvironment2() && !warnedMissingQuizLesson) {
|
|
435
|
+
warnedMissingQuizLesson = true;
|
|
436
|
+
console.warn(
|
|
437
|
+
`[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
return buildTelemetryEvent(opts);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/telemetryPipeline.ts
|
|
446
|
+
function isDevEnvironment3() {
|
|
447
|
+
const g = globalThis;
|
|
448
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
449
|
+
}
|
|
450
|
+
function warnSinkFailure(sinkId, err) {
|
|
451
|
+
if (isDevEnvironment3()) {
|
|
452
|
+
console.warn(
|
|
453
|
+
`[lessonkit] telemetry sink "${sinkId}" failed:`,
|
|
454
|
+
err instanceof Error ? err.message : err
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function invokeSink(sink, event, emitCtx) {
|
|
459
|
+
let result;
|
|
460
|
+
try {
|
|
461
|
+
result = sink.emit(event, emitCtx);
|
|
462
|
+
} catch (err) {
|
|
463
|
+
warnSinkFailure(sink.id, err);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (result != null && typeof result.catch === "function") {
|
|
467
|
+
void result.catch((err) => warnSinkFailure(sink.id, err));
|
|
468
|
+
}
|
|
301
469
|
}
|
|
470
|
+
function createTelemetryPipeline(sinks) {
|
|
471
|
+
const list = [...sinks];
|
|
472
|
+
return {
|
|
473
|
+
sinks: list,
|
|
474
|
+
emit(event, ctx) {
|
|
475
|
+
const emitCtx = ctx ?? {
|
|
476
|
+
courseId: event.courseId,
|
|
477
|
+
sessionId: event.sessionId,
|
|
478
|
+
attemptId: event.attemptId
|
|
479
|
+
};
|
|
480
|
+
for (const sink of list) {
|
|
481
|
+
invokeSink(sink, event, emitCtx);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function createTrackingPipelineSink(id, track) {
|
|
487
|
+
return {
|
|
488
|
+
id,
|
|
489
|
+
emit(event) {
|
|
490
|
+
track(event);
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/ports.ts
|
|
496
|
+
function createDefaultClock() {
|
|
497
|
+
return {
|
|
498
|
+
nowMs: () => Date.now(),
|
|
499
|
+
nowIso: () => (/* @__PURE__ */ new Date()).toISOString()
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function createNoopStorage() {
|
|
503
|
+
return {
|
|
504
|
+
getItem: () => null,
|
|
505
|
+
setItem: () => {
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function createMemoryBackedSessionStorage(session) {
|
|
510
|
+
const memory = /* @__PURE__ */ new Map();
|
|
511
|
+
let warnedPersistFailure = false;
|
|
512
|
+
const warnPersistFailure = () => {
|
|
513
|
+
if (warnedPersistFailure) return;
|
|
514
|
+
warnedPersistFailure = true;
|
|
515
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
|
|
516
|
+
console.warn(
|
|
517
|
+
"[lessonkit] sessionStorage is unavailable or failed; using in-memory session dedupe for this tab (may reset on full reload)."
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
return {
|
|
522
|
+
getItem: (key) => {
|
|
523
|
+
if (memory.has(key)) return memory.get(key);
|
|
524
|
+
try {
|
|
525
|
+
const value = session.getItem(key);
|
|
526
|
+
if (value !== null) memory.set(key, value);
|
|
527
|
+
return value;
|
|
528
|
+
} catch {
|
|
529
|
+
return memory.get(key) ?? null;
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
setItem: (key, value) => {
|
|
533
|
+
memory.set(key, value);
|
|
534
|
+
try {
|
|
535
|
+
session.setItem(key, value);
|
|
536
|
+
} catch {
|
|
537
|
+
warnPersistFailure();
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
removeItem: (key) => {
|
|
541
|
+
memory.delete(key);
|
|
542
|
+
try {
|
|
543
|
+
session.removeItem(key);
|
|
544
|
+
} catch {
|
|
545
|
+
warnPersistFailure();
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
resetForTests: () => {
|
|
549
|
+
memory.clear();
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function resetStoragePortForTests(storage) {
|
|
554
|
+
storage.resetForTests?.();
|
|
555
|
+
}
|
|
556
|
+
function createSessionStoragePort() {
|
|
557
|
+
if (typeof sessionStorage === "undefined") {
|
|
558
|
+
const memory = /* @__PURE__ */ new Map();
|
|
559
|
+
return {
|
|
560
|
+
getItem: (key) => memory.get(key) ?? null,
|
|
561
|
+
setItem: (key, value) => {
|
|
562
|
+
memory.set(key, value);
|
|
563
|
+
},
|
|
564
|
+
removeItem: (key) => {
|
|
565
|
+
memory.delete(key);
|
|
566
|
+
},
|
|
567
|
+
resetForTests: () => {
|
|
568
|
+
memory.clear();
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
return createMemoryBackedSessionStorage(sessionStorage);
|
|
573
|
+
}
|
|
574
|
+
function createGlobalTimer() {
|
|
575
|
+
return {
|
|
576
|
+
setInterval: (fn, ms) => globalThis.setInterval(fn, ms),
|
|
577
|
+
clearInterval: (id) => globalThis.clearInterval(id)
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/progress.ts
|
|
582
|
+
function createProgressController() {
|
|
583
|
+
let activeLessonId;
|
|
584
|
+
let completedLessonIds = /* @__PURE__ */ new Set();
|
|
585
|
+
let courseCompleted = false;
|
|
586
|
+
const lessonStartTimes = /* @__PURE__ */ new Map();
|
|
587
|
+
return {
|
|
588
|
+
getState: () => ({
|
|
589
|
+
activeLessonId,
|
|
590
|
+
completedLessonIds: new Set(completedLessonIds),
|
|
591
|
+
courseCompleted
|
|
592
|
+
}),
|
|
593
|
+
setActiveLesson: (lessonId, startedAtMs) => {
|
|
594
|
+
const previousLessonId = activeLessonId;
|
|
595
|
+
activeLessonId = lessonId;
|
|
596
|
+
lessonStartTimes.set(lessonId, startedAtMs);
|
|
597
|
+
return { previousLessonId };
|
|
598
|
+
},
|
|
599
|
+
completeLesson: (lessonId, completedAtMs) => {
|
|
600
|
+
if (completedLessonIds.has(lessonId)) return { didComplete: false };
|
|
601
|
+
completedLessonIds = new Set(completedLessonIds).add(lessonId);
|
|
602
|
+
if (activeLessonId === lessonId) {
|
|
603
|
+
activeLessonId = void 0;
|
|
604
|
+
}
|
|
605
|
+
const startedAt = lessonStartTimes.get(lessonId);
|
|
606
|
+
lessonStartTimes.delete(lessonId);
|
|
607
|
+
const durationMs = typeof startedAt === "number" ? Math.max(0, completedAtMs - startedAt) : void 0;
|
|
608
|
+
return { durationMs, didComplete: true };
|
|
609
|
+
},
|
|
610
|
+
completeCourse: () => {
|
|
611
|
+
if (courseCompleted) return { didComplete: false };
|
|
612
|
+
courseCompleted = true;
|
|
613
|
+
return { didComplete: true };
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/session.ts
|
|
619
|
+
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
620
|
+
function getTabSessionId(storage) {
|
|
621
|
+
return storage.getItem(SESSION_STORAGE_KEY);
|
|
622
|
+
}
|
|
623
|
+
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
624
|
+
var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
|
|
625
|
+
function resolveSessionId(storage, provided) {
|
|
626
|
+
if (provided) return provided;
|
|
627
|
+
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
628
|
+
if (existing) return existing;
|
|
629
|
+
const id = createSessionId();
|
|
630
|
+
storage.setItem(SESSION_STORAGE_KEY, id);
|
|
631
|
+
return id;
|
|
632
|
+
}
|
|
633
|
+
function courseStartedStorageKey(sessionId, courseId) {
|
|
634
|
+
return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
635
|
+
}
|
|
636
|
+
function courseStartedTrackingStorageKey(sessionId, courseId) {
|
|
637
|
+
return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
638
|
+
}
|
|
639
|
+
function hasCourseStarted(storage, sessionId, courseId) {
|
|
640
|
+
if (!courseId) return false;
|
|
641
|
+
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
642
|
+
}
|
|
643
|
+
function markCourseStarted(storage, sessionId, courseId) {
|
|
644
|
+
if (!courseId) return;
|
|
645
|
+
storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
646
|
+
}
|
|
647
|
+
function hasCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
648
|
+
if (!courseId) return false;
|
|
649
|
+
return storage.getItem(courseStartedTrackingStorageKey(sessionId, courseId)) === "1";
|
|
650
|
+
}
|
|
651
|
+
function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
|
|
652
|
+
if (!courseId) return;
|
|
653
|
+
storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
|
|
654
|
+
}
|
|
655
|
+
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
656
|
+
if (!courseId || fromSessionId === toSessionId) return;
|
|
657
|
+
if (hasCourseStarted(storage, fromSessionId, courseId)) {
|
|
658
|
+
markCourseStarted(storage, toSessionId, courseId);
|
|
659
|
+
storage.removeItem?.(courseStartedStorageKey(fromSessionId, courseId));
|
|
660
|
+
}
|
|
661
|
+
if (hasCourseStartedEmittedToTracking(storage, fromSessionId, courseId)) {
|
|
662
|
+
markCourseStartedEmittedToTracking(storage, toSessionId, courseId);
|
|
663
|
+
storage.removeItem?.(courseStartedTrackingStorageKey(fromSessionId, courseId));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// src/runtime/courseLifecycle.ts
|
|
668
|
+
function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
|
|
669
|
+
const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
670
|
+
if (alreadyEmittedToSink) {
|
|
671
|
+
return { emitted: true, marked };
|
|
672
|
+
}
|
|
673
|
+
if (marked) {
|
|
674
|
+
return { emitted: false, marked: true };
|
|
675
|
+
}
|
|
676
|
+
const emitted = deps.emitCourseStartedEvent(ctx);
|
|
677
|
+
if (emitted) {
|
|
678
|
+
markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
679
|
+
}
|
|
680
|
+
return { emitted, marked: emitted };
|
|
681
|
+
}
|
|
682
|
+
function buildCourseStartedTelemetryEvent(ctx) {
|
|
683
|
+
return buildTelemetryEvent({
|
|
684
|
+
name: "course_started",
|
|
685
|
+
courseId: ctx.courseId,
|
|
686
|
+
sessionId: ctx.sessionId,
|
|
687
|
+
attemptId: ctx.attemptId,
|
|
688
|
+
user: ctx.user
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
function completeLessonWithTelemetry(opts) {
|
|
692
|
+
const result = opts.progress.completeLesson(opts.lessonId, opts.nowMs);
|
|
693
|
+
if (!result.didComplete) return false;
|
|
694
|
+
opts.emitLessonCompleted(opts.lessonId, result.durationMs);
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
function completeCourseWithTelemetry(opts) {
|
|
698
|
+
const current = opts.progress.getState();
|
|
699
|
+
if (current.activeLessonId) {
|
|
700
|
+
completeLessonWithTelemetry({
|
|
701
|
+
progress: opts.progress,
|
|
702
|
+
lessonId: current.activeLessonId,
|
|
703
|
+
nowMs: opts.nowMs,
|
|
704
|
+
emitLessonCompleted: opts.emitLessonCompleted
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
const result = opts.progress.completeCourse();
|
|
708
|
+
if (!result.didComplete) return false;
|
|
709
|
+
opts.emitCourseCompleted();
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/runtime/createLessonkitRuntime.ts
|
|
714
|
+
function createLessonkitRuntime(config, ports = {}) {
|
|
715
|
+
const storage = ports.storage ?? createSessionStoragePort();
|
|
716
|
+
const clock = ports.clock ?? createDefaultClock();
|
|
717
|
+
const configSnapshot = { ...config };
|
|
718
|
+
let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
|
|
719
|
+
let attemptId = configSnapshot.session?.attemptId;
|
|
720
|
+
let user = configSnapshot.session?.user;
|
|
721
|
+
let courseId = configSnapshot.courseId;
|
|
722
|
+
let progress = createProgressController();
|
|
723
|
+
const getSession = () => ({ sessionId, attemptId, user });
|
|
724
|
+
const syncSessionFromConfig = (next) => {
|
|
725
|
+
sessionId = resolveSessionId(storage, next.session?.sessionId);
|
|
726
|
+
attemptId = next.session?.attemptId;
|
|
727
|
+
user = next.session?.user;
|
|
728
|
+
courseId = next.courseId;
|
|
729
|
+
};
|
|
730
|
+
syncSessionFromConfig(configSnapshot);
|
|
731
|
+
const track = (name, data, emit, lessonId) => {
|
|
732
|
+
const event = tryBuildTelemetryEvent({
|
|
733
|
+
name,
|
|
734
|
+
courseId,
|
|
735
|
+
lessonId: lessonId ?? progress.getState().activeLessonId,
|
|
736
|
+
sessionId,
|
|
737
|
+
attemptId,
|
|
738
|
+
user,
|
|
739
|
+
data
|
|
740
|
+
});
|
|
741
|
+
if (!event) return;
|
|
742
|
+
emit(event);
|
|
743
|
+
};
|
|
744
|
+
const emitLessonCompleted = (lessonId, durationMs, emitFn) => {
|
|
745
|
+
emitFn("lesson_completed", { lessonId, durationMs }, lessonId);
|
|
746
|
+
if (durationMs !== void 0) {
|
|
747
|
+
emitFn("lesson_time_on_task", { lessonId, durationMs }, lessonId);
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
return {
|
|
751
|
+
get config() {
|
|
752
|
+
return configSnapshot;
|
|
753
|
+
},
|
|
754
|
+
get progress() {
|
|
755
|
+
return progress;
|
|
756
|
+
},
|
|
757
|
+
getProgressState: () => progress.getState(),
|
|
758
|
+
getSession,
|
|
759
|
+
updateConfig(next) {
|
|
760
|
+
if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
|
|
761
|
+
if (next.runtimeVersion !== void 0) configSnapshot.runtimeVersion = next.runtimeVersion;
|
|
762
|
+
if (next.plugins !== void 0) configSnapshot.plugins = next.plugins;
|
|
763
|
+
if (next.session !== void 0) {
|
|
764
|
+
configSnapshot.session = { ...configSnapshot.session, ...next.session };
|
|
765
|
+
}
|
|
766
|
+
syncSessionFromConfig(configSnapshot);
|
|
767
|
+
},
|
|
768
|
+
setActiveLesson(lessonId, emitFn) {
|
|
769
|
+
const current = progress.getState();
|
|
770
|
+
if (current.activeLessonId === lessonId) return;
|
|
771
|
+
if (current.completedLessonIds.has(lessonId)) {
|
|
772
|
+
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const previous = current.activeLessonId;
|
|
776
|
+
if (previous && previous !== lessonId) {
|
|
777
|
+
const completed = progress.completeLesson(previous, clock.nowMs());
|
|
778
|
+
if (completed.didComplete) {
|
|
779
|
+
emitLessonCompleted(previous, completed.durationMs, emitFn);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
783
|
+
emitFn("lesson_started", { lessonId }, lessonId);
|
|
784
|
+
},
|
|
785
|
+
completeLesson(lessonId, emitFn) {
|
|
786
|
+
const result = progress.completeLesson(lessonId, clock.nowMs());
|
|
787
|
+
if (!result.didComplete) return;
|
|
788
|
+
emitLessonCompleted(lessonId, result.durationMs, emitFn);
|
|
789
|
+
},
|
|
790
|
+
completeCourse(emitFn) {
|
|
791
|
+
const current = progress.getState();
|
|
792
|
+
if (current.activeLessonId) {
|
|
793
|
+
const lessonResult = progress.completeLesson(current.activeLessonId, clock.nowMs());
|
|
794
|
+
if (lessonResult.didComplete) {
|
|
795
|
+
emitLessonCompleted(current.activeLessonId, lessonResult.durationMs, emitFn);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const result = progress.completeCourse();
|
|
799
|
+
if (!result.didComplete) return;
|
|
800
|
+
emitFn("course_completed");
|
|
801
|
+
},
|
|
802
|
+
track,
|
|
803
|
+
resetForCourseChange(nextCourseId) {
|
|
804
|
+
configSnapshot.courseId = nextCourseId;
|
|
805
|
+
courseId = nextCourseId;
|
|
806
|
+
progress = createProgressController();
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/plugins/registry.ts
|
|
302
812
|
function warnDuplicatePlugin(id) {
|
|
303
813
|
const g = globalThis;
|
|
304
814
|
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
|
|
305
815
|
console.warn(`[lessonkit] plugin id "${id}" was registered more than once; using the latest definition`);
|
|
306
816
|
}
|
|
307
|
-
function
|
|
817
|
+
function createPluginRegistry(plugins = []) {
|
|
308
818
|
const registry = /* @__PURE__ */ new Map();
|
|
309
819
|
for (const plugin of plugins) {
|
|
310
820
|
if (registry.has(plugin.id)) warnDuplicatePlugin(plugin.id);
|
|
@@ -384,21 +894,58 @@ function createPluginHost(plugins = []) {
|
|
|
384
894
|
scoreAssessment
|
|
385
895
|
};
|
|
386
896
|
}
|
|
897
|
+
|
|
898
|
+
// src/plugins/define.ts
|
|
899
|
+
function defineTelemetryPlugin(plugin) {
|
|
900
|
+
return plugin;
|
|
901
|
+
}
|
|
902
|
+
function defineAssessmentPlugin(plugin) {
|
|
903
|
+
return plugin;
|
|
904
|
+
}
|
|
905
|
+
function defineLifecyclePlugin(plugin) {
|
|
906
|
+
return plugin;
|
|
907
|
+
}
|
|
387
908
|
// Annotate the CommonJS export names for ESM import in node:
|
|
388
909
|
0 && (module.exports = {
|
|
389
910
|
ID_MAX_LENGTH,
|
|
390
911
|
ID_PATTERN,
|
|
912
|
+
SESSION_STORAGE_KEY,
|
|
391
913
|
TELEMETRY_EVENT_CATALOG,
|
|
392
914
|
assertValidId,
|
|
915
|
+
buildCourseStartedTelemetryEvent,
|
|
393
916
|
buildLessonkitUrn,
|
|
394
917
|
buildTelemetryCatalog,
|
|
395
|
-
|
|
918
|
+
buildTelemetryEvent,
|
|
919
|
+
completeCourseWithTelemetry,
|
|
920
|
+
completeLessonWithTelemetry,
|
|
921
|
+
createDefaultClock,
|
|
922
|
+
createGlobalTimer,
|
|
923
|
+
createLessonkitRuntime,
|
|
924
|
+
createNoopStorage,
|
|
925
|
+
createPluginRegistry,
|
|
926
|
+
createProgressController,
|
|
396
927
|
createSessionId,
|
|
928
|
+
createSessionStoragePort,
|
|
929
|
+
createTelemetryPipeline,
|
|
397
930
|
createTrackingClient,
|
|
398
|
-
|
|
931
|
+
createTrackingPipelineSink,
|
|
932
|
+
defineAssessmentPlugin,
|
|
933
|
+
defineLifecyclePlugin,
|
|
934
|
+
defineTelemetryPlugin,
|
|
399
935
|
deriveId,
|
|
936
|
+
getTabSessionId,
|
|
937
|
+
hasCourseStarted,
|
|
938
|
+
hasCourseStartedEmittedToTracking,
|
|
939
|
+
markCourseStarted,
|
|
940
|
+
markCourseStartedEmittedToTracking,
|
|
941
|
+
migrateCourseStartedMark,
|
|
400
942
|
nowIso,
|
|
943
|
+
resetStoragePortForTests,
|
|
944
|
+
resetTelemetryBuilderWarningsForTests,
|
|
945
|
+
resolveSessionId,
|
|
401
946
|
slugifyId,
|
|
402
947
|
telemetryCatalogVersion,
|
|
948
|
+
tryBuildTelemetryEvent,
|
|
949
|
+
tryEmitCourseStarted,
|
|
403
950
|
validateId
|
|
404
951
|
});
|