@lessonkit/core 1.4.0 → 1.5.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-KFXFQ6B2.js} +230 -45
- package/dist/index.cjs +481 -110
- package/dist/index.d.cts +56 -12
- package/dist/index.d.ts +56 -12
- package/dist/index.js +322 -122
- package/dist/{testing-BhVGckZ5.d.cts → testing-BFr8oEfw.d.cts} +46 -7
- package/dist/{testing-BhVGckZ5.d.ts → testing-BFr8oEfw.d.ts} +46 -7
- 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 +37 -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,75 @@
|
|
|
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
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
64
|
+
}
|
|
65
|
+
throw new Error(
|
|
66
|
+
"[lessonkit] createSessionId requires crypto.randomUUID or crypto.getRandomValues"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
2
69
|
function createSessionId() {
|
|
3
70
|
const g = globalThis;
|
|
4
71
|
if (g.crypto?.randomUUID) return g.crypto.randomUUID();
|
|
5
|
-
return
|
|
72
|
+
return randomSessionIdFallback();
|
|
6
73
|
}
|
|
7
74
|
|
|
8
75
|
// src/time.ts
|
|
@@ -291,6 +358,34 @@ var TELEMETRY_EVENT_REGISTRY = {
|
|
|
291
358
|
data: opts.data
|
|
292
359
|
};
|
|
293
360
|
}
|
|
361
|
+
},
|
|
362
|
+
branch_node_viewed: {
|
|
363
|
+
requiresLessonId: true,
|
|
364
|
+
build: (opts, base) => {
|
|
365
|
+
if (opts.name !== "branch_node_viewed") throw new Error("unexpected event");
|
|
366
|
+
const lessonId = opts.lessonId;
|
|
367
|
+
if (!lessonId) throw new Error("branch_node_viewed requires active lessonId");
|
|
368
|
+
return {
|
|
369
|
+
name: "branch_node_viewed",
|
|
370
|
+
...base,
|
|
371
|
+
lessonId,
|
|
372
|
+
data: opts.data
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
branch_selected: {
|
|
377
|
+
requiresLessonId: true,
|
|
378
|
+
build: (opts, base) => {
|
|
379
|
+
if (opts.name !== "branch_selected") throw new Error("unexpected event");
|
|
380
|
+
const lessonId = opts.lessonId;
|
|
381
|
+
if (!lessonId) throw new Error("branch_selected requires active lessonId");
|
|
382
|
+
return {
|
|
383
|
+
name: "branch_selected",
|
|
384
|
+
...base,
|
|
385
|
+
lessonId,
|
|
386
|
+
data: opts.data
|
|
387
|
+
};
|
|
388
|
+
}
|
|
294
389
|
}
|
|
295
390
|
};
|
|
296
391
|
function buildTelemetryEventFromRegistry(opts) {
|
|
@@ -323,8 +418,8 @@ function buildTelemetryEvent(opts) {
|
|
|
323
418
|
}
|
|
324
419
|
function tryBuildTelemetryEvent(opts) {
|
|
325
420
|
const entry = getTelemetryEventRegistryEntry(opts.name);
|
|
326
|
-
if (entry.requiresLessonId && !opts.lessonId
|
|
327
|
-
if (isDevEnvironment()) {
|
|
421
|
+
if (entry.requiresLessonId && !opts.lessonId) {
|
|
422
|
+
if (isDevEnvironment() && entry.tryBuildMissingLessonWarning) {
|
|
328
423
|
if (entry.tryBuildMissingLessonWarning === "quiz" && !warnedMissingQuizLesson) {
|
|
329
424
|
warnedMissingQuizLesson = true;
|
|
330
425
|
console.warn(
|
|
@@ -351,14 +446,44 @@ function createDefaultClock() {
|
|
|
351
446
|
};
|
|
352
447
|
}
|
|
353
448
|
function createNoopStorage() {
|
|
449
|
+
const memory = /* @__PURE__ */ new Map();
|
|
354
450
|
return {
|
|
355
|
-
getItem: () => null,
|
|
356
|
-
setItem: () =>
|
|
451
|
+
getItem: (key) => memory.get(key) ?? null,
|
|
452
|
+
setItem: (key, value) => {
|
|
453
|
+
memory.set(key, value);
|
|
454
|
+
return true;
|
|
455
|
+
},
|
|
456
|
+
removeItem: (key) => {
|
|
457
|
+
memory.delete(key);
|
|
458
|
+
},
|
|
459
|
+
resetForTests: () => {
|
|
460
|
+
memory.clear();
|
|
461
|
+
}
|
|
357
462
|
};
|
|
358
463
|
}
|
|
359
464
|
function createMemoryBackedSessionStorage(session) {
|
|
360
465
|
const memory = /* @__PURE__ */ new Map();
|
|
466
|
+
const tombstones = /* @__PURE__ */ new Set();
|
|
361
467
|
let warnedPersistFailure = false;
|
|
468
|
+
const syncFromStorageEvent = (key, newValue) => {
|
|
469
|
+
if (key === null) {
|
|
470
|
+
memory.clear();
|
|
471
|
+
tombstones.clear();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
tombstones.delete(key);
|
|
475
|
+
if (newValue === null) {
|
|
476
|
+
memory.delete(key);
|
|
477
|
+
} else {
|
|
478
|
+
memory.set(key, newValue);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
if (typeof window !== "undefined") {
|
|
482
|
+
window.addEventListener("storage", (event) => {
|
|
483
|
+
if (event.storageArea !== sessionStorage) return;
|
|
484
|
+
syncFromStorageEvent(event.key, event.newValue);
|
|
485
|
+
});
|
|
486
|
+
}
|
|
362
487
|
const warnPersistFailure = () => {
|
|
363
488
|
if (warnedPersistFailure) return;
|
|
364
489
|
warnedPersistFailure = true;
|
|
@@ -371,6 +496,7 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
371
496
|
};
|
|
372
497
|
return {
|
|
373
498
|
getItem: (key) => {
|
|
499
|
+
if (tombstones.has(key)) return null;
|
|
374
500
|
if (memory.has(key)) return memory.get(key);
|
|
375
501
|
try {
|
|
376
502
|
const value = session.getItem(key);
|
|
@@ -381,6 +507,7 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
381
507
|
}
|
|
382
508
|
},
|
|
383
509
|
setItem: (key, value) => {
|
|
510
|
+
tombstones.delete(key);
|
|
384
511
|
memory.set(key, value);
|
|
385
512
|
try {
|
|
386
513
|
session.setItem(key, value);
|
|
@@ -394,12 +521,15 @@ function createMemoryBackedSessionStorage(session) {
|
|
|
394
521
|
memory.delete(key);
|
|
395
522
|
try {
|
|
396
523
|
session.removeItem(key);
|
|
524
|
+
tombstones.delete(key);
|
|
397
525
|
} catch {
|
|
398
526
|
warnPersistFailure();
|
|
527
|
+
tombstones.add(key);
|
|
399
528
|
}
|
|
400
529
|
},
|
|
401
530
|
resetForTests: () => {
|
|
402
531
|
memory.clear();
|
|
532
|
+
tombstones.clear();
|
|
403
533
|
}
|
|
404
534
|
};
|
|
405
535
|
}
|
|
@@ -449,7 +579,6 @@ function createGlobalTimer() {
|
|
|
449
579
|
// src/session.ts
|
|
450
580
|
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
451
581
|
var volatileSessionIds = /* @__PURE__ */ new WeakMap();
|
|
452
|
-
var sharedVolatileSessionId = null;
|
|
453
582
|
function isDevEnvironment2() {
|
|
454
583
|
const g = globalThis;
|
|
455
584
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
@@ -460,10 +589,23 @@ function getTabSessionId(storage) {
|
|
|
460
589
|
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
461
590
|
var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
|
|
462
591
|
var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
|
|
592
|
+
var COURSE_STARTED_XAPI_PREFIX = "lessonkit:course_started_xapi:";
|
|
593
|
+
function sessionKeySegment(sessionId) {
|
|
594
|
+
const validated = validateId(sessionId);
|
|
595
|
+
return validated.ok ? validated.id : encodeURIComponent(sessionId);
|
|
596
|
+
}
|
|
463
597
|
function resolveSessionId(storage, provided) {
|
|
464
598
|
if (provided !== void 0) {
|
|
465
599
|
const trimmed = provided.trim();
|
|
466
|
-
if (trimmed.length > 0)
|
|
600
|
+
if (trimmed.length > 0) {
|
|
601
|
+
const validated = validateId(trimmed);
|
|
602
|
+
if (validated.ok) return validated.id;
|
|
603
|
+
if (isDevEnvironment2()) {
|
|
604
|
+
console.warn(
|
|
605
|
+
`[lessonkit] Invalid sessionId "${trimmed}"; falling back to tab or generated id.`
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
467
609
|
}
|
|
468
610
|
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
469
611
|
if (existing) return existing;
|
|
@@ -472,27 +614,27 @@ function resolveSessionId(storage, provided) {
|
|
|
472
614
|
const id = createSessionId();
|
|
473
615
|
const persisted = storage.setItem(SESSION_STORAGE_KEY, id);
|
|
474
616
|
if (!persisted) {
|
|
475
|
-
|
|
476
|
-
sharedVolatileSessionId = id;
|
|
477
|
-
}
|
|
478
|
-
volatileSessionIds.set(storage, sharedVolatileSessionId);
|
|
617
|
+
volatileSessionIds.set(storage, id);
|
|
479
618
|
if (isDevEnvironment2()) {
|
|
480
619
|
console.warn(
|
|
481
|
-
"[lessonkit] session id could not be persisted;
|
|
620
|
+
"[lessonkit] session id could not be persisted; using in-memory id for this storage."
|
|
482
621
|
);
|
|
483
622
|
}
|
|
484
|
-
return
|
|
623
|
+
return id;
|
|
485
624
|
}
|
|
486
625
|
return id;
|
|
487
626
|
}
|
|
488
627
|
function courseStartedStorageKey(sessionId, courseId) {
|
|
489
|
-
return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
628
|
+
return `${COURSE_STARTED_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
490
629
|
}
|
|
491
630
|
function courseStartedTrackingStorageKey(sessionId, courseId) {
|
|
492
|
-
return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
631
|
+
return `${COURSE_STARTED_TRACKING_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
493
632
|
}
|
|
494
633
|
function courseStartedPipelineStorageKey(sessionId, courseId) {
|
|
495
|
-
return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
634
|
+
return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
635
|
+
}
|
|
636
|
+
function courseStartedXapiStorageKey(sessionId, courseId) {
|
|
637
|
+
return `${COURSE_STARTED_XAPI_PREFIX}${sessionKeySegment(sessionId)}:${courseId ?? ""}`;
|
|
496
638
|
}
|
|
497
639
|
function hasCourseStarted(storage, sessionId, courseId) {
|
|
498
640
|
if (!courseId) return false;
|
|
@@ -518,27 +660,52 @@ function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
|
|
|
518
660
|
if (!courseId) return false;
|
|
519
661
|
return storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
|
|
520
662
|
}
|
|
663
|
+
function hasCourseStartedXapiSent(storage, sessionId, courseId) {
|
|
664
|
+
if (!courseId) return false;
|
|
665
|
+
return storage.getItem(courseStartedXapiStorageKey(sessionId, courseId)) === "1";
|
|
666
|
+
}
|
|
667
|
+
function markCourseStartedXapiSent(storage, sessionId, courseId) {
|
|
668
|
+
if (!courseId) return false;
|
|
669
|
+
return storage.setItem(courseStartedXapiStorageKey(sessionId, courseId), "1");
|
|
670
|
+
}
|
|
521
671
|
function resetSharedVolatileSessionIdForTests() {
|
|
522
|
-
|
|
672
|
+
}
|
|
673
|
+
function migrateStorageMark(storage, fromKey, toKey, hasMark) {
|
|
674
|
+
if (!hasMark) return;
|
|
675
|
+
if (storage.setItem(toKey, "1")) {
|
|
676
|
+
storage.removeItem?.(fromKey);
|
|
677
|
+
}
|
|
523
678
|
}
|
|
524
679
|
function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
|
|
525
680
|
if (!courseId || fromSessionId === toSessionId) return;
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
storage
|
|
537
|
-
|
|
681
|
+
migrateStorageMark(
|
|
682
|
+
storage,
|
|
683
|
+
courseStartedStorageKey(fromSessionId, courseId),
|
|
684
|
+
courseStartedStorageKey(toSessionId, courseId),
|
|
685
|
+
hasCourseStarted(storage, fromSessionId, courseId)
|
|
686
|
+
);
|
|
687
|
+
migrateStorageMark(
|
|
688
|
+
storage,
|
|
689
|
+
courseStartedTrackingStorageKey(fromSessionId, courseId),
|
|
690
|
+
courseStartedTrackingStorageKey(toSessionId, courseId),
|
|
691
|
+
hasCourseStartedEmittedToTracking(storage, fromSessionId, courseId)
|
|
692
|
+
);
|
|
693
|
+
migrateStorageMark(
|
|
694
|
+
storage,
|
|
695
|
+
courseStartedPipelineStorageKey(fromSessionId, courseId),
|
|
696
|
+
courseStartedPipelineStorageKey(toSessionId, courseId),
|
|
697
|
+
hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)
|
|
698
|
+
);
|
|
699
|
+
migrateStorageMark(
|
|
700
|
+
storage,
|
|
701
|
+
courseStartedXapiStorageKey(fromSessionId, courseId),
|
|
702
|
+
courseStartedXapiStorageKey(toSessionId, courseId),
|
|
703
|
+
hasCourseStartedXapiSent(storage, fromSessionId, courseId)
|
|
704
|
+
);
|
|
538
705
|
}
|
|
539
706
|
|
|
540
707
|
// src/runtime/courseLifecycle.ts
|
|
541
|
-
var courseStartedEmitFlights = /* @__PURE__ */ new
|
|
708
|
+
var courseStartedEmitFlights = /* @__PURE__ */ new Map();
|
|
542
709
|
function resetCourseStartedEmitFlightForTests() {
|
|
543
710
|
courseStartedEmitFlights.clear();
|
|
544
711
|
}
|
|
@@ -546,24 +713,32 @@ function tryEmitCourseStarted(ctx, deps, alreadyEmittedToSink) {
|
|
|
546
713
|
const flightKey = `${ctx.sessionId}:${ctx.courseId}`;
|
|
547
714
|
const marked = hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
548
715
|
if (alreadyEmittedToSink) {
|
|
549
|
-
return { emitted: true, marked };
|
|
716
|
+
return Promise.resolve({ emitted: true, marked });
|
|
550
717
|
}
|
|
551
|
-
|
|
552
|
-
|
|
718
|
+
const existing = courseStartedEmitFlights.get(flightKey);
|
|
719
|
+
if (existing) {
|
|
720
|
+
return existing;
|
|
553
721
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
722
|
+
const flight = Promise.resolve().then(() => {
|
|
723
|
+
try {
|
|
724
|
+
const emitted = deps.emitCourseStartedEvent(ctx);
|
|
725
|
+
if (emitted && !marked) {
|
|
726
|
+
markCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId);
|
|
727
|
+
}
|
|
728
|
+
return {
|
|
729
|
+
emitted,
|
|
730
|
+
marked: hasCourseStarted(ctx.storage, ctx.sessionId, ctx.courseId)
|
|
731
|
+
};
|
|
732
|
+
} catch {
|
|
733
|
+
return { emitted: false, marked };
|
|
734
|
+
} finally {
|
|
735
|
+
if (courseStartedEmitFlights.get(flightKey) === flight) {
|
|
736
|
+
courseStartedEmitFlights.delete(flightKey);
|
|
737
|
+
}
|
|
559
738
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
};
|
|
564
|
-
} finally {
|
|
565
|
-
courseStartedEmitFlights.delete(flightKey);
|
|
566
|
-
}
|
|
739
|
+
});
|
|
740
|
+
courseStartedEmitFlights.set(flightKey, flight);
|
|
741
|
+
return flight;
|
|
567
742
|
}
|
|
568
743
|
function buildCourseStartedTelemetryEvent(ctx) {
|
|
569
744
|
return buildTelemetryEvent({
|
|
@@ -597,6 +772,14 @@ function completeCourseWithTelemetry(opts) {
|
|
|
597
772
|
}
|
|
598
773
|
|
|
599
774
|
export {
|
|
775
|
+
ID_PATTERN,
|
|
776
|
+
ID_MAX_LENGTH,
|
|
777
|
+
validateId,
|
|
778
|
+
parseCourseId,
|
|
779
|
+
parseLessonId,
|
|
780
|
+
parseCheckId,
|
|
781
|
+
parseBlockId,
|
|
782
|
+
assertValidId,
|
|
600
783
|
isDevEnvironment,
|
|
601
784
|
warnDev,
|
|
602
785
|
createSessionId,
|
|
@@ -618,6 +801,8 @@ export {
|
|
|
618
801
|
markCourseStartedEmittedToTracking,
|
|
619
802
|
hasCourseStartedPipelineDelivered,
|
|
620
803
|
markCourseStartedPipelineDelivered,
|
|
804
|
+
hasCourseStartedXapiSent,
|
|
805
|
+
markCourseStartedXapiSent,
|
|
621
806
|
resetSharedVolatileSessionIdForTests,
|
|
622
807
|
migrateCourseStartedMark,
|
|
623
808
|
resetCourseStartedEmitFlightForTests,
|