@lessonkit/core 1.4.0 → 1.6.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 +28 -2
- package/dist/{chunk-PEWFPVQ6.js → chunk-NGCHHJSM.js} +357 -50
- package/dist/index.cjs +822 -131
- package/dist/index.d.cts +142 -15
- package/dist/index.d.ts +142 -15
- package/dist/index.js +534 -138
- package/dist/{testing-BhVGckZ5.d.cts → testing-CzgxF1Ru.d.cts} +193 -8
- package/dist/{testing-BhVGckZ5.d.ts → testing-CzgxF1Ru.d.ts} +193 -8
- package/dist/testing.cjs +1 -3
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/package.json +3 -3
- package/telemetry-catalog.v3.json +177 -0
package/README.md
CHANGED
|
@@ -6,12 +6,22 @@
|
|
|
6
6
|
|
|
7
7
|
Headless types, identity helpers, telemetry pipeline, and runtime primitives shared across LessonKit.
|
|
8
8
|
|
|
9
|
+
## When to install
|
|
10
|
+
|
|
11
|
+
- Custom headless runtime (no React UI)
|
|
12
|
+
- Telemetry plugins, batch pipelines, or custom tracking clients
|
|
13
|
+
- Validating IDs, URNs, and manifest fields in your own tooling
|
|
14
|
+
|
|
15
|
+
Most course authors only need `@lessonkit/react`, which re-exports common APIs.
|
|
16
|
+
|
|
9
17
|
## Install
|
|
10
18
|
|
|
11
19
|
```bash
|
|
12
20
|
npm install @lessonkit/core
|
|
13
21
|
```
|
|
14
22
|
|
|
23
|
+
Requires Node.js **18+** minimum.
|
|
24
|
+
|
|
15
25
|
## Usage
|
|
16
26
|
|
|
17
27
|
```typescript
|
|
@@ -21,7 +31,16 @@ import {
|
|
|
21
31
|
createTelemetryPipeline,
|
|
22
32
|
createPluginRegistry,
|
|
23
33
|
buildLessonkitUrn,
|
|
34
|
+
validateId,
|
|
24
35
|
} from "@lessonkit/core";
|
|
36
|
+
|
|
37
|
+
const event = buildTelemetryEvent({
|
|
38
|
+
name: "quiz_answered",
|
|
39
|
+
courseId: "my-course",
|
|
40
|
+
lessonId: "lesson-1",
|
|
41
|
+
checkId: "check-1",
|
|
42
|
+
data: { correct: true, score: 1 },
|
|
43
|
+
});
|
|
25
44
|
```
|
|
26
45
|
|
|
27
46
|
## Exports
|
|
@@ -33,11 +52,18 @@ import {
|
|
|
33
52
|
| Runtime | `createLessonkitRuntime`, progress and session helpers |
|
|
34
53
|
| Plugins | `createPluginRegistry`, `defineTelemetryPlugin`, `defineAssessmentPlugin` |
|
|
35
54
|
|
|
36
|
-
Machine-readable: `@lessonkit/core/telemetry-catalog.
|
|
55
|
+
Machine-readable: `@lessonkit/core/telemetry-catalog.v3.json` (current; v1–v3 retained), `identity-contract.v1.json`
|
|
56
|
+
|
|
57
|
+
## Common issues
|
|
58
|
+
|
|
59
|
+
| Symptom | Fix |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `buildTelemetryEvent` validation error | Ensure `courseId`, `lessonId`, and event-specific IDs match [identity rules](https://lessonkit.readthedocs.io/en/latest/reference/identity.html) |
|
|
62
|
+
| Plugin not firing | Register with `createPluginRegistry` and pass plugins in `LessonkitProvider` config |
|
|
37
63
|
|
|
38
64
|
## Docs
|
|
39
65
|
|
|
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)
|
|
66
|
+
[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) · [TypeDoc API index](https://lessonkit.readthedocs.io/en/latest/reference/api.html)
|
|
41
67
|
|
|
42
68
|
## License
|
|
43
69
|
|
|
@@ -1,8 +1,78 @@
|
|
|
1
|
+
// src/identityTypes.ts
|
|
2
|
+
var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
|
|
3
|
+
var ID_MAX_LENGTH = 64;
|
|
4
|
+
|
|
5
|
+
// src/validateId.ts
|
|
6
|
+
function validateId(input, path = "id") {
|
|
7
|
+
if (typeof input !== "string") {
|
|
8
|
+
return { ok: false, issues: [{ path, message: "id must be a string" }] };
|
|
9
|
+
}
|
|
10
|
+
const id = input.trim();
|
|
11
|
+
if (!id.length) {
|
|
12
|
+
return { ok: false, issues: [{ path, message: "id must not be empty" }] };
|
|
13
|
+
}
|
|
14
|
+
if (id.length > ID_MAX_LENGTH) {
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
issues: [{ path, message: `id must be at most ${ID_MAX_LENGTH} characters` }]
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (!ID_PATTERN.test(id)) {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
issues: [
|
|
24
|
+
{
|
|
25
|
+
path,
|
|
26
|
+
message: "id must start with a letter and contain only letters, digits, underscores, and hyphens"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return { ok: true, id };
|
|
32
|
+
}
|
|
33
|
+
function parseCourseId(input) {
|
|
34
|
+
const result = validateId(input, "courseId");
|
|
35
|
+
return result.ok ? result.id : null;
|
|
36
|
+
}
|
|
37
|
+
function parseLessonId(input) {
|
|
38
|
+
const result = validateId(input, "lessonId");
|
|
39
|
+
return result.ok ? result.id : null;
|
|
40
|
+
}
|
|
41
|
+
function parseCheckId(input) {
|
|
42
|
+
const result = validateId(input, "checkId");
|
|
43
|
+
return result.ok ? result.id : null;
|
|
44
|
+
}
|
|
45
|
+
function parseBlockId(input) {
|
|
46
|
+
const result = validateId(input, "blockId");
|
|
47
|
+
return result.ok ? result.id : null;
|
|
48
|
+
}
|
|
49
|
+
function assertValidId(input, path = "id") {
|
|
50
|
+
const result = validateId(input, path);
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
throw new Error(result.issues.map((i) => `${i.path}: ${i.message}`).join("; "));
|
|
53
|
+
}
|
|
54
|
+
return result.id;
|
|
55
|
+
}
|
|
56
|
+
|
|
1
57
|
// src/ids.ts
|
|
58
|
+
function randomSessionIdFallback() {
|
|
59
|
+
const g = globalThis;
|
|
60
|
+
if (g.crypto?.getRandomValues) {
|
|
61
|
+
const bytes = new Uint8Array(16);
|
|
62
|
+
g.crypto.getRandomValues(bytes);
|
|
63
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
64
|
+
return `s-${hex}`;
|
|
65
|
+
}
|
|
66
|
+
throw new Error(
|
|
67
|
+
"[lessonkit] createSessionId requires crypto.randomUUID or crypto.getRandomValues"
|
|
68
|
+
);
|
|
69
|
+
}
|
|
2
70
|
function createSessionId() {
|
|
3
71
|
const g = globalThis;
|
|
4
|
-
if (g.crypto?.randomUUID)
|
|
5
|
-
|
|
72
|
+
if (g.crypto?.randomUUID) {
|
|
73
|
+
return `s-${g.crypto.randomUUID().replace(/-/g, "")}`;
|
|
74
|
+
}
|
|
75
|
+
return randomSessionIdFallback();
|
|
6
76
|
}
|
|
7
77
|
|
|
8
78
|
// src/time.ts
|
|
@@ -291,6 +361,118 @@ var TELEMETRY_EVENT_REGISTRY = {
|
|
|
291
361
|
data: opts.data
|
|
292
362
|
};
|
|
293
363
|
}
|
|
364
|
+
},
|
|
365
|
+
branch_node_viewed: {
|
|
366
|
+
requiresLessonId: true,
|
|
367
|
+
build: (opts, base) => {
|
|
368
|
+
if (opts.name !== "branch_node_viewed") throw new Error("unexpected event");
|
|
369
|
+
const lessonId = opts.lessonId;
|
|
370
|
+
if (!lessonId) throw new Error("branch_node_viewed requires active lessonId");
|
|
371
|
+
return {
|
|
372
|
+
name: "branch_node_viewed",
|
|
373
|
+
...base,
|
|
374
|
+
lessonId,
|
|
375
|
+
data: opts.data
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
branch_selected: {
|
|
380
|
+
requiresLessonId: true,
|
|
381
|
+
build: (opts, base) => {
|
|
382
|
+
if (opts.name !== "branch_selected") throw new Error("unexpected event");
|
|
383
|
+
const lessonId = opts.lessonId;
|
|
384
|
+
if (!lessonId) throw new Error("branch_selected requires active lessonId");
|
|
385
|
+
return {
|
|
386
|
+
name: "branch_selected",
|
|
387
|
+
...base,
|
|
388
|
+
lessonId,
|
|
389
|
+
data: opts.data
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
image_juxtaposition_changed: {
|
|
394
|
+
build: (opts, base) => ({
|
|
395
|
+
name: "image_juxtaposition_changed",
|
|
396
|
+
...base,
|
|
397
|
+
lessonId: opts.lessonId,
|
|
398
|
+
data: opts.data
|
|
399
|
+
})
|
|
400
|
+
},
|
|
401
|
+
timeline_event_viewed: {
|
|
402
|
+
build: (opts, base) => ({
|
|
403
|
+
name: "timeline_event_viewed",
|
|
404
|
+
...base,
|
|
405
|
+
lessonId: opts.lessonId,
|
|
406
|
+
data: opts.data
|
|
407
|
+
})
|
|
408
|
+
},
|
|
409
|
+
image_sequence_changed: {
|
|
410
|
+
build: (opts, base) => ({
|
|
411
|
+
name: "image_sequence_changed",
|
|
412
|
+
...base,
|
|
413
|
+
lessonId: opts.lessonId,
|
|
414
|
+
data: opts.data
|
|
415
|
+
})
|
|
416
|
+
},
|
|
417
|
+
audio_recording_started: {
|
|
418
|
+
build: (opts, base) => ({
|
|
419
|
+
name: "audio_recording_started",
|
|
420
|
+
...base,
|
|
421
|
+
lessonId: opts.lessonId,
|
|
422
|
+
data: opts.data
|
|
423
|
+
})
|
|
424
|
+
},
|
|
425
|
+
audio_recording_completed: {
|
|
426
|
+
build: (opts, base) => ({
|
|
427
|
+
name: "audio_recording_completed",
|
|
428
|
+
...base,
|
|
429
|
+
lessonId: opts.lessonId,
|
|
430
|
+
data: opts.data
|
|
431
|
+
})
|
|
432
|
+
},
|
|
433
|
+
qr_content_revealed: {
|
|
434
|
+
build: (opts, base) => ({
|
|
435
|
+
name: "qr_content_revealed",
|
|
436
|
+
...base,
|
|
437
|
+
lessonId: opts.lessonId,
|
|
438
|
+
data: opts.data
|
|
439
|
+
})
|
|
440
|
+
},
|
|
441
|
+
advent_door_opened: {
|
|
442
|
+
build: (opts, base) => ({
|
|
443
|
+
name: "advent_door_opened",
|
|
444
|
+
...base,
|
|
445
|
+
lessonId: opts.lessonId,
|
|
446
|
+
data: opts.data
|
|
447
|
+
})
|
|
448
|
+
},
|
|
449
|
+
map_stage_viewed: {
|
|
450
|
+
requiresLessonId: true,
|
|
451
|
+
build: (opts, base) => {
|
|
452
|
+
if (opts.name !== "map_stage_viewed") throw new Error("unexpected event");
|
|
453
|
+
const lessonId = opts.lessonId;
|
|
454
|
+
if (!lessonId) throw new Error("map_stage_viewed requires active lessonId");
|
|
455
|
+
return {
|
|
456
|
+
name: "map_stage_viewed",
|
|
457
|
+
...base,
|
|
458
|
+
lessonId,
|
|
459
|
+
data: opts.data
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
map_exit_selected: {
|
|
464
|
+
requiresLessonId: true,
|
|
465
|
+
build: (opts, base) => {
|
|
466
|
+
if (opts.name !== "map_exit_selected") throw new Error("unexpected event");
|
|
467
|
+
const lessonId = opts.lessonId;
|
|
468
|
+
if (!lessonId) throw new Error("map_exit_selected requires active lessonId");
|
|
469
|
+
return {
|
|
470
|
+
name: "map_exit_selected",
|
|
471
|
+
...base,
|
|
472
|
+
lessonId,
|
|
473
|
+
data: opts.data
|
|
474
|
+
};
|
|
475
|
+
}
|
|
294
476
|
}
|
|
295
477
|
};
|
|
296
478
|
function buildTelemetryEventFromRegistry(opts) {
|
|
@@ -323,8 +505,8 @@ function buildTelemetryEvent(opts) {
|
|
|
323
505
|
}
|
|
324
506
|
function tryBuildTelemetryEvent(opts) {
|
|
325
507
|
const entry = getTelemetryEventRegistryEntry(opts.name);
|
|
326
|
-
if (entry.requiresLessonId && !opts.lessonId
|
|
327
|
-
if (isDevEnvironment()) {
|
|
508
|
+
if (entry.requiresLessonId && !opts.lessonId) {
|
|
509
|
+
if (isDevEnvironment() && entry.tryBuildMissingLessonWarning) {
|
|
328
510
|
if (entry.tryBuildMissingLessonWarning === "quiz" && !warnedMissingQuizLesson) {
|
|
329
511
|
warnedMissingQuizLesson = true;
|
|
330
512
|
console.warn(
|
|
@@ -351,14 +533,44 @@ function createDefaultClock() {
|
|
|
351
533
|
};
|
|
352
534
|
}
|
|
353
535
|
function createNoopStorage() {
|
|
536
|
+
const memory = /* @__PURE__ */ new Map();
|
|
354
537
|
return {
|
|
355
|
-
getItem: () => null,
|
|
356
|
-
setItem: () =>
|
|
538
|
+
getItem: (key) => memory.get(key) ?? null,
|
|
539
|
+
setItem: (key, value) => {
|
|
540
|
+
memory.set(key, value);
|
|
541
|
+
return true;
|
|
542
|
+
},
|
|
543
|
+
removeItem: (key) => {
|
|
544
|
+
memory.delete(key);
|
|
545
|
+
},
|
|
546
|
+
resetForTests: () => {
|
|
547
|
+
memory.clear();
|
|
548
|
+
}
|
|
357
549
|
};
|
|
358
550
|
}
|
|
359
551
|
function createMemoryBackedSessionStorage(session) {
|
|
360
552
|
const memory = /* @__PURE__ */ new Map();
|
|
553
|
+
const tombstones = /* @__PURE__ */ new Set();
|
|
361
554
|
let warnedPersistFailure = false;
|
|
555
|
+
const syncFromStorageEvent = (key, newValue) => {
|
|
556
|
+
if (key === null) {
|
|
557
|
+
memory.clear();
|
|
558
|
+
tombstones.clear();
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
tombstones.delete(key);
|
|
562
|
+
if (newValue === null) {
|
|
563
|
+
memory.delete(key);
|
|
564
|
+
} else {
|
|
565
|
+
memory.set(key, newValue);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
if (typeof window !== "undefined") {
|
|
569
|
+
window.addEventListener("storage", (event) => {
|
|
570
|
+
if (event.storageArea !== sessionStorage) return;
|
|
571
|
+
syncFromStorageEvent(event.key, event.newValue);
|
|
572
|
+
});
|
|
573
|
+
}
|
|
362
574
|
const warnPersistFailure = () => {
|
|
363
575
|
if (warnedPersistFailure) return;
|
|
364
576
|
warnedPersistFailure = true;
|
|
@@ -369,24 +581,31 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
369
581
|
);
|
|
370
582
|
}
|
|
371
583
|
};
|
|
584
|
+
const bypassCacheForKey = (key) => key === "lessonkit:sessionId" || key.startsWith("lessonkit:course_started");
|
|
372
585
|
return {
|
|
373
586
|
getItem: (key) => {
|
|
374
|
-
if (
|
|
587
|
+
if (tombstones.has(key)) return null;
|
|
588
|
+
if (!bypassCacheForKey(key) && memory.has(key)) return memory.get(key);
|
|
375
589
|
try {
|
|
376
590
|
const value = session.getItem(key);
|
|
377
591
|
if (value !== null) memory.set(key, value);
|
|
592
|
+
else if (bypassCacheForKey(key)) memory.delete(key);
|
|
378
593
|
return value;
|
|
379
594
|
} catch {
|
|
380
595
|
return memory.get(key) ?? null;
|
|
381
596
|
}
|
|
382
597
|
},
|
|
383
598
|
setItem: (key, value) => {
|
|
384
|
-
|
|
599
|
+
tombstones.delete(key);
|
|
385
600
|
try {
|
|
386
601
|
session.setItem(key, value);
|
|
602
|
+
memory.set(key, value);
|
|
387
603
|
return true;
|
|
388
604
|
} catch {
|
|
389
605
|
warnPersistFailure();
|
|
606
|
+
if (!bypassCacheForKey(key)) {
|
|
607
|
+
memory.set(key, value);
|
|
608
|
+
}
|
|
390
609
|
return false;
|
|
391
610
|
}
|
|
392
611
|
},
|
|
@@ -394,12 +613,15 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
394
613
|
memory.delete(key);
|
|
395
614
|
try {
|
|
396
615
|
session.removeItem(key);
|
|
616
|
+
tombstones.delete(key);
|
|
397
617
|
} catch {
|
|
398
618
|
warnPersistFailure();
|
|
619
|
+
tombstones.add(key);
|
|
399
620
|
}
|
|
400
621
|
},
|
|
401
622
|
resetForTests: () => {
|
|
402
623
|
memory.clear();
|
|
624
|
+
tombstones.clear();
|
|
403
625
|
}
|
|
404
626
|
};
|
|
405
627
|
}
|
|
@@ -449,7 +671,6 @@ function createGlobalTimer() {
|
|
|
449
671
|
// src/session.ts
|
|
450
672
|
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
451
673
|
var volatileSessionIds = /* @__PURE__ */ new WeakMap();
|
|
452
|
-
var sharedVolatileSessionId = null;
|
|
453
674
|
function isDevEnvironment2() {
|
|
454
675
|
const g = globalThis;
|
|
455
676
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
@@ -460,39 +681,62 @@ function getTabSessionId(storage) {
|
|
|
460
681
|
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
461
682
|
var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
|
|
462
683
|
var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
|
|
684
|
+
var COURSE_STARTED_XAPI_PREFIX = "lessonkit:course_started_xapi:";
|
|
685
|
+
function sessionKeySegment(sessionId) {
|
|
686
|
+
const validated = validateId(sessionId);
|
|
687
|
+
return validated.ok ? validated.id : encodeURIComponent(sessionId);
|
|
688
|
+
}
|
|
463
689
|
function resolveSessionId(storage, provided) {
|
|
464
690
|
if (provided !== void 0) {
|
|
465
691
|
const trimmed = provided.trim();
|
|
466
|
-
if (trimmed.length > 0)
|
|
692
|
+
if (trimmed.length > 0) {
|
|
693
|
+
const validated = validateId(trimmed);
|
|
694
|
+
if (validated.ok) return validated.id;
|
|
695
|
+
if (isDevEnvironment2()) {
|
|
696
|
+
console.warn(
|
|
697
|
+
`[lessonkit] Invalid sessionId "${trimmed}"; falling back to tab or generated id.`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
467
701
|
}
|
|
468
702
|
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
469
|
-
if (existing)
|
|
703
|
+
if (existing) {
|
|
704
|
+
const trimmedExisting = existing.trim();
|
|
705
|
+
const validatedExisting = validateId(trimmedExisting);
|
|
706
|
+
if (validatedExisting.ok) return validatedExisting.id;
|
|
707
|
+
storage.removeItem?.(SESSION_STORAGE_KEY);
|
|
708
|
+
if (isDevEnvironment2()) {
|
|
709
|
+
console.warn(
|
|
710
|
+
`[lessonkit] Invalid stored sessionId "${existing}"; generating a new id.`
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
470
714
|
const volatile = volatileSessionIds.get(storage);
|
|
471
715
|
if (volatile) return volatile;
|
|
472
716
|
const id = createSessionId();
|
|
473
717
|
const persisted = storage.setItem(SESSION_STORAGE_KEY, id);
|
|
474
718
|
if (!persisted) {
|
|
475
|
-
|
|
476
|
-
sharedVolatileSessionId = id;
|
|
477
|
-
}
|
|
478
|
-
volatileSessionIds.set(storage, sharedVolatileSessionId);
|
|
719
|
+
volatileSessionIds.set(storage, id);
|
|
479
720
|
if (isDevEnvironment2()) {
|
|
480
721
|
console.warn(
|
|
481
|
-
"[lessonkit] session id could not be persisted;
|
|
722
|
+
"[lessonkit] session id could not be persisted; using in-memory id for this storage."
|
|
482
723
|
);
|
|
483
724
|
}
|
|
484
|
-
return
|
|
725
|
+
return id;
|
|
485
726
|
}
|
|
486
727
|
return id;
|
|
487
728
|
}
|
|
488
729
|
function courseStartedStorageKey(sessionId, courseId) {
|
|
489
|
-
return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
730
|
+
return `${COURSE_STARTED_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
490
731
|
}
|
|
491
732
|
function courseStartedTrackingStorageKey(sessionId, courseId) {
|
|
492
|
-
return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
733
|
+
return `${COURSE_STARTED_TRACKING_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
493
734
|
}
|
|
494
735
|
function courseStartedPipelineStorageKey(sessionId, courseId) {
|
|
495
|
-
return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
736
|
+
return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
737
|
+
}
|
|
738
|
+
function courseStartedXapiStorageKey(sessionId, courseId) {
|
|
739
|
+
return `${COURSE_STARTED_XAPI_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
496
740
|
}
|
|
497
741
|
function hasCourseStarted(storage, sessionId, courseId) {
|
|
498
742
|
if (!courseId) return false;
|
|
@@ -518,27 +762,52 @@ function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
|
518
762
|
if (!courseId) return false;
|
|
519
763
|
return storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
|
|
520
764
|
}
|
|
765
|
+
function hasCourseStartedXapiSent(storage, sessionId, courseId) {
|
|
766
|
+
if (!courseId) return false;
|
|
767
|
+
return storage.getItem(courseStartedXapiStorageKey(sessionId, courseId)) === "1";
|
|
768
|
+
}
|
|
769
|
+
function markCourseStartedXapiSent(storage, sessionId, courseId) {
|
|
770
|
+
if (!courseId) return false;
|
|
771
|
+
return storage.setItem(courseStartedXapiStorageKey(sessionId, courseId), "1");
|
|
772
|
+
}
|
|
521
773
|
function resetSharedVolatileSessionIdForTests() {
|
|
522
|
-
|
|
774
|
+
}
|
|
775
|
+
function migrateStorageMark(storage, fromKey, toKey, hasMark) {
|
|
776
|
+
if (!hasMark) return;
|
|
777
|
+
if (storage.setItem(toKey, "1")) {
|
|
778
|
+
storage.removeItem?.(fromKey);
|
|
779
|
+
}
|
|
523
780
|
}
|
|
524
781
|
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
525
782
|
if (!courseId || fromSessionId === toSessionId) return;
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
storage
|
|
537
|
-
|
|
783
|
+
migrateStorageMark(
|
|
784
|
+
storage,
|
|
785
|
+
courseStartedStorageKey(fromSessionId, courseId),
|
|
786
|
+
courseStartedStorageKey(toSessionId, courseId),
|
|
787
|
+
hasCourseStarted(storage, fromSessionId, courseId)
|
|
788
|
+
);
|
|
789
|
+
migrateStorageMark(
|
|
790
|
+
storage,
|
|
791
|
+
courseStartedTrackingStorageKey(fromSessionId, courseId),
|
|
792
|
+
courseStartedTrackingStorageKey(toSessionId, courseId),
|
|
793
|
+
hasCourseStartedEmittedToTracking(storage, fromSessionId, courseId)
|
|
794
|
+
);
|
|
795
|
+
migrateStorageMark(
|
|
796
|
+
storage,
|
|
797
|
+
courseStartedPipelineStorageKey(fromSessionId, courseId),
|
|
798
|
+
courseStartedPipelineStorageKey(toSessionId, courseId),
|
|
799
|
+
hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)
|
|
800
|
+
);
|
|
801
|
+
migrateStorageMark(
|
|
802
|
+
storage,
|
|
803
|
+
courseStartedXapiStorageKey(fromSessionId, courseId),
|
|
804
|
+
courseStartedXapiStorageKey(toSessionId, courseId),
|
|
805
|
+
hasCourseStartedXapiSent(storage, fromSessionId, courseId)
|
|
806
|
+
);
|
|
538
807
|
}
|
|
539
808
|
|
|
540
809
|
// src/runtime/courseLifecycle.ts
|
|
541
|
-
var courseStartedEmitFlights = /* @__PURE__ */ new
|
|
810
|
+
var courseStartedEmitFlights = /* @__PURE__ */ new Map();
|
|
542
811
|
function resetCourseStartedEmitFlightForTests() {
|
|
543
812
|
courseStartedEmitFlights.clear();
|
|
544
813
|
}
|
|
@@ -546,24 +815,43 @@ function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
|
|
|
546
815
|
const flightKey = `${ctx.sessionId}:${ctx.courseId}`;
|
|
547
816
|
const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
548
817
|
if (alreadyEmittedToSink) {
|
|
549
|
-
|
|
818
|
+
const markPersisted = marked ? true : markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
819
|
+
return Promise.resolve({
|
|
820
|
+
emitted: true,
|
|
821
|
+
marked: markPersisted
|
|
822
|
+
});
|
|
550
823
|
}
|
|
551
|
-
if (
|
|
552
|
-
return {
|
|
824
|
+
if (marked && hasCourseStartedEmittedToTracking(ctx.storage, ctx.sessionId, ctx.courseId)) {
|
|
825
|
+
return Promise.resolve({
|
|
826
|
+
emitted: true,
|
|
827
|
+
marked: true
|
|
828
|
+
});
|
|
553
829
|
}
|
|
554
|
-
courseStartedEmitFlights.
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if (emitted && !marked) {
|
|
558
|
-
markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
559
|
-
}
|
|
560
|
-
return {
|
|
561
|
-
emitted,
|
|
562
|
-
marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
|
|
563
|
-
};
|
|
564
|
-
} finally {
|
|
565
|
-
courseStartedEmitFlights.delete(flightKey);
|
|
830
|
+
const existing = courseStartedEmitFlights.get(flightKey);
|
|
831
|
+
if (existing) {
|
|
832
|
+
return existing;
|
|
566
833
|
}
|
|
834
|
+
const flight = Promise.resolve().then(() => {
|
|
835
|
+
try {
|
|
836
|
+
const emitted = deps.emitCourseStartedEvent(ctx);
|
|
837
|
+
const markPersisted = emitted && !marked ? markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId) : marked;
|
|
838
|
+
return {
|
|
839
|
+
emitted,
|
|
840
|
+
marked: markPersisted
|
|
841
|
+
};
|
|
842
|
+
} catch {
|
|
843
|
+
return {
|
|
844
|
+
emitted: false,
|
|
845
|
+
marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
|
|
846
|
+
};
|
|
847
|
+
} finally {
|
|
848
|
+
if (courseStartedEmitFlights.get(flightKey) === flight) {
|
|
849
|
+
courseStartedEmitFlights.delete(flightKey);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
courseStartedEmitFlights.set(flightKey, flight);
|
|
854
|
+
return flight;
|
|
567
855
|
}
|
|
568
856
|
function buildCourseStartedTelemetryEvent(ctx) {
|
|
569
857
|
return buildTelemetryEvent({
|
|
@@ -591,12 +879,29 @@ function completeCourseWithTelemetry(opts) {
|
|
|
591
879
|
});
|
|
592
880
|
}
|
|
593
881
|
const result = opts.progress.completeCourse();
|
|
594
|
-
if (!result.didComplete)
|
|
882
|
+
if (!result.didComplete) {
|
|
883
|
+
const after = opts.progress.getState();
|
|
884
|
+
if (after.activeLessonId) {
|
|
885
|
+
const lessonResult = opts.progress.completeLesson(after.activeLessonId, opts.nowMs);
|
|
886
|
+
if (lessonResult.didComplete) {
|
|
887
|
+
opts.emitLessonCompleted(after.activeLessonId, lessonResult.durationMs);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
595
892
|
opts.emitCourseCompleted();
|
|
596
893
|
return true;
|
|
597
894
|
}
|
|
598
895
|
|
|
599
896
|
export {
|
|
897
|
+
ID_PATTERN,
|
|
898
|
+
ID_MAX_LENGTH,
|
|
899
|
+
validateId,
|
|
900
|
+
parseCourseId,
|
|
901
|
+
parseLessonId,
|
|
902
|
+
parseCheckId,
|
|
903
|
+
parseBlockId,
|
|
904
|
+
assertValidId,
|
|
600
905
|
isDevEnvironment,
|
|
601
906
|
warnDev,
|
|
602
907
|
createSessionId,
|
|
@@ -618,6 +923,8 @@ export {
|
|
|
618
923
|
markCourseStartedEmittedToTracking,
|
|
619
924
|
hasCourseStartedPipelineDelivered,
|
|
620
925
|
markCourseStartedPipelineDelivered,
|
|
926
|
+
hasCourseStartedXapiSent,
|
|
927
|
+
markCourseStartedXapiSent,
|
|
621
928
|
resetSharedVolatileSessionIdForTests,
|
|
622
929
|
migrateCourseStartedMark,
|
|
623
930
|
resetCourseStartedEmitFlightForTests,
|