@lessonkit/core 1.0.2 → 1.2.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/dist/index.cjs +626 -177
- package/dist/index.d.cts +264 -25
- package/dist/index.d.ts +264 -25
- package/dist/index.js +604 -177
- package/package.json +6 -2
- package/telemetry-catalog.v2.json +21 -0
- package/telemetry-catalog.v3.json +53 -0
package/dist/index.cjs
CHANGED
|
@@ -20,18 +20,33 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
ACCORDION_FORBIDDEN_CHILD_TYPES: () => ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
24
|
+
ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES: () => ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
|
|
25
|
+
COMPOUND_MAX_NESTING_DEPTH: () => COMPOUND_MAX_NESTING_DEPTH,
|
|
26
|
+
COMPOUND_RESUME_SCHEMA_VERSION: () => COMPOUND_RESUME_SCHEMA_VERSION,
|
|
23
27
|
ID_MAX_LENGTH: () => ID_MAX_LENGTH,
|
|
24
28
|
ID_PATTERN: () => ID_PATTERN,
|
|
29
|
+
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES: () => INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
30
|
+
PAGE_ALLOWED_CHILD_TYPES: () => PAGE_ALLOWED_CHILD_TYPES,
|
|
25
31
|
SESSION_STORAGE_KEY: () => SESSION_STORAGE_KEY,
|
|
26
32
|
TELEMETRY_EVENT_CATALOG: () => TELEMETRY_EVENT_CATALOG,
|
|
33
|
+
TELEMETRY_EVENT_CATALOG_V2: () => TELEMETRY_EVENT_CATALOG_V2,
|
|
34
|
+
TELEMETRY_EVENT_CATALOG_V3: () => TELEMETRY_EVENT_CATALOG_V3,
|
|
27
35
|
assertNever: () => assertNever,
|
|
28
36
|
assertValidId: () => assertValidId,
|
|
29
37
|
buildCourseStartedTelemetryEvent: () => buildCourseStartedTelemetryEvent,
|
|
30
38
|
buildLessonkitUrn: () => buildLessonkitUrn,
|
|
39
|
+
buildPluginContext: () => buildPluginContext,
|
|
31
40
|
buildTelemetryCatalog: () => buildTelemetryCatalog,
|
|
41
|
+
buildTelemetryCatalogV2: () => buildTelemetryCatalogV2,
|
|
42
|
+
buildTelemetryCatalogV3: () => buildTelemetryCatalogV3,
|
|
32
43
|
buildTelemetryEvent: () => buildTelemetryEvent,
|
|
44
|
+
clampCompoundPageIndex: () => clampCompoundPageIndex,
|
|
45
|
+
clearCompoundState: () => clearCompoundState,
|
|
33
46
|
completeCourseWithTelemetry: () => completeCourseWithTelemetry,
|
|
34
47
|
completeLessonWithTelemetry: () => completeLessonWithTelemetry,
|
|
48
|
+
compoundStateStorageKey: () => compoundStateStorageKey,
|
|
49
|
+
createCompoundResumeState: () => createCompoundResumeState,
|
|
35
50
|
createDefaultClock: () => createDefaultClock,
|
|
36
51
|
createGlobalTimer: () => createGlobalTimer,
|
|
37
52
|
createLessonkitRuntime: () => createLessonkitRuntime,
|
|
@@ -47,10 +62,13 @@ __export(index_exports, {
|
|
|
47
62
|
defineLifecyclePlugin: () => defineLifecyclePlugin,
|
|
48
63
|
defineTelemetryPlugin: () => defineTelemetryPlugin,
|
|
49
64
|
deriveId: () => deriveId,
|
|
65
|
+
getAllowedChildTypes: () => getAllowedChildTypes,
|
|
50
66
|
getTabSessionId: () => getTabSessionId,
|
|
51
67
|
hasCourseStarted: () => hasCourseStarted,
|
|
52
68
|
hasCourseStartedEmittedToTracking: () => hasCourseStartedEmittedToTracking,
|
|
53
69
|
hasCourseStartedPipelineDelivered: () => hasCourseStartedPipelineDelivered,
|
|
70
|
+
isChildTypeAllowed: () => isChildTypeAllowed,
|
|
71
|
+
loadCompoundState: () => loadCompoundState,
|
|
54
72
|
markCourseStarted: () => markCourseStarted,
|
|
55
73
|
markCourseStartedEmittedToTracking: () => markCourseStartedEmittedToTracking,
|
|
56
74
|
markCourseStartedPipelineDelivered: () => markCourseStartedPipelineDelivered,
|
|
@@ -58,12 +76,16 @@ __export(index_exports, {
|
|
|
58
76
|
nowIso: () => nowIso,
|
|
59
77
|
parseBlockId: () => parseBlockId,
|
|
60
78
|
parseCheckId: () => parseCheckId,
|
|
79
|
+
parseCompoundResumeState: () => parseCompoundResumeState,
|
|
61
80
|
parseCourseId: () => parseCourseId,
|
|
62
81
|
parseLessonId: () => parseLessonId,
|
|
63
82
|
resetStoragePortForTests: () => resetStoragePortForTests,
|
|
64
83
|
resetTelemetryBuilderWarningsForTests: () => resetTelemetryBuilderWarningsForTests,
|
|
65
84
|
resolveSessionId: () => resolveSessionId,
|
|
85
|
+
saveCompoundState: () => saveCompoundState,
|
|
66
86
|
slugifyId: () => slugifyId,
|
|
87
|
+
telemetryCatalogV2Version: () => telemetryCatalogV2Version,
|
|
88
|
+
telemetryCatalogV3Version: () => telemetryCatalogV3Version,
|
|
67
89
|
telemetryCatalogVersion: () => telemetryCatalogVersion,
|
|
68
90
|
tryBuildTelemetryEvent: () => tryBuildTelemetryEvent,
|
|
69
91
|
tryEmitCourseStarted: () => tryEmitCourseStarted,
|
|
@@ -175,6 +197,122 @@ function buildLessonkitUrn(parts) {
|
|
|
175
197
|
return urn;
|
|
176
198
|
}
|
|
177
199
|
|
|
200
|
+
// src/compound.ts
|
|
201
|
+
var COMPOUND_RESUME_SCHEMA_VERSION = 1;
|
|
202
|
+
function createCompoundResumeState(input = {}) {
|
|
203
|
+
const childStates = {};
|
|
204
|
+
if (input.childStates) {
|
|
205
|
+
for (const [key, value] of Object.entries(input.childStates)) {
|
|
206
|
+
childStates[key] = value;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
schemaVersion: COMPOUND_RESUME_SCHEMA_VERSION,
|
|
211
|
+
activePageIndex: input.activePageIndex ?? 0,
|
|
212
|
+
...input.activeChapterIndex !== void 0 ? { activeChapterIndex: input.activeChapterIndex } : {},
|
|
213
|
+
childStates
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function clampCompoundPageIndex(index, pageCount) {
|
|
217
|
+
if (pageCount < 1) return 0;
|
|
218
|
+
return Math.min(Math.max(0, Math.floor(index)), pageCount - 1);
|
|
219
|
+
}
|
|
220
|
+
function parseCompoundResumeState(raw) {
|
|
221
|
+
if (!raw || typeof raw !== "object") return null;
|
|
222
|
+
const obj = raw;
|
|
223
|
+
if (obj.schemaVersion !== COMPOUND_RESUME_SCHEMA_VERSION) return null;
|
|
224
|
+
if (typeof obj.activePageIndex !== "number" || !Number.isFinite(obj.activePageIndex)) return null;
|
|
225
|
+
const childStates = {};
|
|
226
|
+
if (obj.childStates && typeof obj.childStates === "object" && !Array.isArray(obj.childStates)) {
|
|
227
|
+
for (const [key, value] of Object.entries(obj.childStates)) {
|
|
228
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
229
|
+
childStates[key] = value;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const activeChapterIndex = typeof obj.activeChapterIndex === "number" && Number.isFinite(obj.activeChapterIndex) ? obj.activeChapterIndex : void 0;
|
|
234
|
+
return {
|
|
235
|
+
schemaVersion: COMPOUND_RESUME_SCHEMA_VERSION,
|
|
236
|
+
activePageIndex: Math.max(0, Math.floor(obj.activePageIndex)),
|
|
237
|
+
...activeChapterIndex !== void 0 ? { activeChapterIndex: Math.max(0, Math.floor(activeChapterIndex)) } : {},
|
|
238
|
+
childStates
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/compoundState.ts
|
|
243
|
+
var COMPOUND_STATE_PREFIX = "lessonkit:compound:";
|
|
244
|
+
function compoundStateStorageKey(courseId, compoundId) {
|
|
245
|
+
return `${COMPOUND_STATE_PREFIX}${courseId}:${compoundId}`;
|
|
246
|
+
}
|
|
247
|
+
function loadCompoundState(storage, courseId, compoundId) {
|
|
248
|
+
const raw = storage.getItem(compoundStateStorageKey(courseId, compoundId));
|
|
249
|
+
if (!raw) return null;
|
|
250
|
+
try {
|
|
251
|
+
return parseCompoundResumeState(JSON.parse(raw));
|
|
252
|
+
} catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function saveCompoundState(storage, courseId, compoundId, state) {
|
|
257
|
+
storage.setItem(compoundStateStorageKey(courseId, compoundId), JSON.stringify(state));
|
|
258
|
+
}
|
|
259
|
+
function clearCompoundState(storage, courseId, compoundId) {
|
|
260
|
+
storage.removeItem?.(compoundStateStorageKey(courseId, compoundId));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/compoundAllowlists.ts
|
|
264
|
+
var PAGE_ALLOWED_CHILD_TYPES = [
|
|
265
|
+
"Text",
|
|
266
|
+
"Heading",
|
|
267
|
+
"Image",
|
|
268
|
+
"Scenario",
|
|
269
|
+
"Reflection",
|
|
270
|
+
"Quiz",
|
|
271
|
+
"KnowledgeCheck",
|
|
272
|
+
"TrueFalse",
|
|
273
|
+
"FillInTheBlanks",
|
|
274
|
+
"DragAndDrop",
|
|
275
|
+
"DragTheWords",
|
|
276
|
+
"MarkTheWords",
|
|
277
|
+
"Accordion",
|
|
278
|
+
"DialogCards",
|
|
279
|
+
"Flashcards",
|
|
280
|
+
"ImageHotspots",
|
|
281
|
+
"FindHotspot",
|
|
282
|
+
"FindMultipleHotspots",
|
|
283
|
+
"ImageSlider",
|
|
284
|
+
"ProgressTracker"
|
|
285
|
+
];
|
|
286
|
+
var INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES = ["Page"];
|
|
287
|
+
var ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES = [
|
|
288
|
+
"TrueFalse",
|
|
289
|
+
"FillInTheBlanks",
|
|
290
|
+
"DragAndDrop",
|
|
291
|
+
"DragTheWords",
|
|
292
|
+
"MarkTheWords",
|
|
293
|
+
"Quiz",
|
|
294
|
+
"KnowledgeCheck",
|
|
295
|
+
"FindHotspot",
|
|
296
|
+
"FindMultipleHotspots"
|
|
297
|
+
];
|
|
298
|
+
var ALLOWLISTS = {
|
|
299
|
+
Page: PAGE_ALLOWED_CHILD_TYPES,
|
|
300
|
+
InteractiveBook: INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
301
|
+
AssessmentSequence: ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES
|
|
302
|
+
};
|
|
303
|
+
var COMPOUND_MAX_NESTING_DEPTH = {
|
|
304
|
+
Page: 1,
|
|
305
|
+
InteractiveBook: 2,
|
|
306
|
+
AssessmentSequence: 1
|
|
307
|
+
};
|
|
308
|
+
function getAllowedChildTypes(parent) {
|
|
309
|
+
return ALLOWLISTS[parent];
|
|
310
|
+
}
|
|
311
|
+
function isChildTypeAllowed(parent, childType) {
|
|
312
|
+
return ALLOWLISTS[parent].includes(childType);
|
|
313
|
+
}
|
|
314
|
+
var ACCORDION_FORBIDDEN_CHILD_TYPES = ["Accordion"];
|
|
315
|
+
|
|
178
316
|
// src/telemetryCatalog.ts
|
|
179
317
|
var telemetryCatalogVersion = 1;
|
|
180
318
|
var TELEMETRY_EVENT_CATALOG = [
|
|
@@ -247,35 +385,125 @@ function buildTelemetryCatalog() {
|
|
|
247
385
|
return TELEMETRY_EVENT_CATALOG.map((entry) => ({ ...entry }));
|
|
248
386
|
}
|
|
249
387
|
|
|
250
|
-
// src/
|
|
388
|
+
// src/telemetryCatalogV2.ts
|
|
389
|
+
var telemetryCatalogV2Version = 2;
|
|
390
|
+
var TELEMETRY_EVENT_CATALOG_V2 = [
|
|
391
|
+
{
|
|
392
|
+
name: "assessment_answered",
|
|
393
|
+
description: "Learner submitted an assessment interaction answer",
|
|
394
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
395
|
+
dataFields: ["checkId", "interactionType", "question", "response", "correct"],
|
|
396
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/answered",
|
|
397
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}"
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: "assessment_completed",
|
|
401
|
+
description: "Assessment interaction completed (passing criteria met)",
|
|
402
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
403
|
+
dataFields: ["checkId", "interactionType", "score", "maxScore", "passingScore"],
|
|
404
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/completed",
|
|
405
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:check:{checkId}"
|
|
406
|
+
}
|
|
407
|
+
];
|
|
408
|
+
function buildTelemetryCatalogV2() {
|
|
409
|
+
return TELEMETRY_EVENT_CATALOG_V2.map((entry) => ({ ...entry }));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/telemetryCatalogV3.ts
|
|
413
|
+
var telemetryCatalogV3Version = 3;
|
|
414
|
+
var TELEMETRY_EVENT_CATALOG_V3 = [
|
|
415
|
+
{
|
|
416
|
+
name: "book_page_viewed",
|
|
417
|
+
description: "Learner viewed a page/chapter in an Interactive Book",
|
|
418
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
419
|
+
dataFields: ["blockId", "pageIndex", "pageTitle"],
|
|
420
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
421
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: "compound_page_viewed",
|
|
425
|
+
description: "Learner activated a page inside a compound container",
|
|
426
|
+
requiredFields: ["courseId", "lessonId", "sessionId", "timestamp"],
|
|
427
|
+
dataFields: ["blockId", "pageIndex", "parentType"],
|
|
428
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
429
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: "hotspot_opened",
|
|
433
|
+
description: "Learner opened an image hotspot popover",
|
|
434
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
435
|
+
dataFields: ["blockId", "hotspotId"],
|
|
436
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
437
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
name: "accordion_section_toggled",
|
|
441
|
+
description: "Learner expanded or collapsed an accordion section",
|
|
442
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
443
|
+
dataFields: ["blockId", "sectionId", "expanded"],
|
|
444
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
445
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "flashcard_flipped",
|
|
449
|
+
description: "Learner flipped a flashcard",
|
|
450
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
451
|
+
dataFields: ["blockId", "cardIndex", "face"],
|
|
452
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
453
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
name: "image_slider_changed",
|
|
457
|
+
description: "Learner changed the active slide in an image slider",
|
|
458
|
+
requiredFields: ["courseId", "sessionId", "timestamp"],
|
|
459
|
+
dataFields: ["blockId", "slideIndex"],
|
|
460
|
+
xapiVerb: "http://adlnet.gov/expapi/verbs/experienced",
|
|
461
|
+
urnPattern: "urn:lessonkit:course:{courseId}:lesson:{lessonId}:block:{blockId}"
|
|
462
|
+
}
|
|
463
|
+
];
|
|
464
|
+
function buildTelemetryCatalogV3() {
|
|
465
|
+
return TELEMETRY_EVENT_CATALOG_V3.map((entry) => ({ ...entry }));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/internal/env.ts
|
|
251
469
|
function isDevEnvironment() {
|
|
252
470
|
const g = globalThis;
|
|
253
471
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
254
472
|
}
|
|
473
|
+
function warnDev(message, err) {
|
|
474
|
+
if (!isDevEnvironment()) return;
|
|
475
|
+
console.warn(message, err instanceof Error ? err.message : err);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/internal/sinkInvoke.ts
|
|
255
479
|
function invokeTrackingSink(sink, event) {
|
|
256
480
|
let result;
|
|
257
481
|
try {
|
|
258
482
|
result = sink(event);
|
|
259
483
|
} catch (err) {
|
|
260
|
-
|
|
261
|
-
console.warn(
|
|
262
|
-
"[lessonkit] tracking sink failed:",
|
|
263
|
-
err instanceof Error ? err.message : err
|
|
264
|
-
);
|
|
265
|
-
}
|
|
484
|
+
warnDev("[lessonkit] tracking sink failed:", err);
|
|
266
485
|
throw err;
|
|
267
486
|
}
|
|
268
487
|
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
|
-
});
|
|
488
|
+
void result.catch((err) => warnDev("[lessonkit] tracking sink failed:", err));
|
|
277
489
|
}
|
|
278
490
|
}
|
|
491
|
+
function invokePipelineSink(sinkId, emit) {
|
|
492
|
+
let result;
|
|
493
|
+
try {
|
|
494
|
+
result = emit();
|
|
495
|
+
} catch (err) {
|
|
496
|
+
warnDev(`[lessonkit] telemetry sink "${sinkId}" failed:`, err);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (result != null && typeof result.catch === "function") {
|
|
500
|
+
void result.catch(
|
|
501
|
+
(err) => warnDev(`[lessonkit] telemetry sink "${sinkId}" failed:`, err)
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/trackingClient.ts
|
|
279
507
|
function createTrackingClient(opts) {
|
|
280
508
|
const sink = opts?.sink;
|
|
281
509
|
const batchSink = opts?.batchSink;
|
|
@@ -393,81 +621,234 @@ function nowIso() {
|
|
|
393
621
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
394
622
|
}
|
|
395
623
|
|
|
396
|
-
// src/
|
|
397
|
-
var warnedMissingQuizLesson = false;
|
|
398
|
-
function isDevEnvironment2() {
|
|
399
|
-
const g = globalThis;
|
|
400
|
-
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
401
|
-
}
|
|
402
|
-
function resetTelemetryBuilderWarningsForTests() {
|
|
403
|
-
warnedMissingQuizLesson = false;
|
|
404
|
-
}
|
|
624
|
+
// src/telemetry/eventRegistry.ts
|
|
405
625
|
function resolveLessonId(opts, eventName) {
|
|
406
626
|
const lessonId = opts.lessonId ?? opts.data?.lessonId;
|
|
407
627
|
if (!lessonId) throw new Error(`${eventName} requires lessonId`);
|
|
408
628
|
return lessonId;
|
|
409
629
|
}
|
|
410
|
-
function
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
630
|
+
function withLessonScopedData(name, base, lessonId, data) {
|
|
631
|
+
return { name, ...base, lessonId, data: { ...data, lessonId } };
|
|
632
|
+
}
|
|
633
|
+
var TELEMETRY_EVENT_REGISTRY = {
|
|
634
|
+
course_started: {
|
|
635
|
+
build: (_opts, base) => ({ name: "course_started", ...base })
|
|
636
|
+
},
|
|
637
|
+
course_completed: {
|
|
638
|
+
build: (_opts, base) => ({ name: "course_completed", ...base })
|
|
639
|
+
},
|
|
640
|
+
lesson_started: {
|
|
641
|
+
requiresLessonId: true,
|
|
642
|
+
build: (opts, base) => {
|
|
643
|
+
if (opts.name !== "lesson_started") throw new Error("unexpected event");
|
|
424
644
|
const lessonId = resolveLessonId(opts, "lesson_started");
|
|
645
|
+
return withLessonScopedData("lesson_started", base, lessonId, opts.data);
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
lesson_completed: {
|
|
649
|
+
requiresLessonId: true,
|
|
650
|
+
build: (opts, base) => {
|
|
651
|
+
if (opts.name !== "lesson_completed") throw new Error("unexpected event");
|
|
652
|
+
const lessonId = resolveLessonId(opts, opts.name);
|
|
653
|
+
return withLessonScopedData(opts.name, base, lessonId, opts.data);
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
lesson_time_on_task: {
|
|
657
|
+
requiresLessonId: true,
|
|
658
|
+
build: (opts, base) => {
|
|
659
|
+
if (opts.name !== "lesson_time_on_task") throw new Error("unexpected event");
|
|
660
|
+
const lessonId = resolveLessonId(opts, opts.name);
|
|
661
|
+
return withLessonScopedData(opts.name, base, lessonId, opts.data);
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
quiz_answered: {
|
|
665
|
+
requiresLessonId: true,
|
|
666
|
+
tryBuildMissingLessonWarning: "quiz",
|
|
667
|
+
build: (opts, base) => {
|
|
668
|
+
if (opts.name !== "quiz_answered") throw new Error("unexpected event");
|
|
669
|
+
const lessonId = opts.lessonId;
|
|
670
|
+
if (!lessonId) throw new Error("quiz_answered requires active lessonId");
|
|
425
671
|
return {
|
|
426
|
-
name: "
|
|
672
|
+
name: "quiz_answered",
|
|
427
673
|
...base,
|
|
428
674
|
lessonId,
|
|
429
|
-
data:
|
|
675
|
+
data: opts.data
|
|
430
676
|
};
|
|
431
677
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
678
|
+
},
|
|
679
|
+
quiz_completed: {
|
|
680
|
+
requiresLessonId: true,
|
|
681
|
+
tryBuildMissingLessonWarning: "quiz",
|
|
682
|
+
build: (opts, base) => {
|
|
683
|
+
if (opts.name !== "quiz_completed") throw new Error("unexpected event");
|
|
684
|
+
const lessonId = opts.lessonId;
|
|
685
|
+
if (!lessonId) throw new Error("quiz_completed requires active lessonId");
|
|
435
686
|
return {
|
|
436
|
-
name:
|
|
687
|
+
name: "quiz_completed",
|
|
437
688
|
...base,
|
|
438
689
|
lessonId,
|
|
439
|
-
data:
|
|
690
|
+
data: opts.data
|
|
440
691
|
};
|
|
441
692
|
}
|
|
442
|
-
|
|
693
|
+
},
|
|
694
|
+
assessment_answered: {
|
|
695
|
+
requiresLessonId: true,
|
|
696
|
+
tryBuildMissingLessonWarning: "assessment",
|
|
697
|
+
build: (opts, base) => {
|
|
698
|
+
if (opts.name !== "assessment_answered") throw new Error("unexpected event");
|
|
443
699
|
const lessonId = opts.lessonId;
|
|
444
|
-
if (!lessonId) throw new Error("
|
|
445
|
-
return {
|
|
700
|
+
if (!lessonId) throw new Error("assessment_answered requires active lessonId");
|
|
701
|
+
return {
|
|
702
|
+
name: "assessment_answered",
|
|
703
|
+
...base,
|
|
704
|
+
lessonId,
|
|
705
|
+
data: opts.data
|
|
706
|
+
};
|
|
446
707
|
}
|
|
447
|
-
|
|
708
|
+
},
|
|
709
|
+
assessment_completed: {
|
|
710
|
+
requiresLessonId: true,
|
|
711
|
+
tryBuildMissingLessonWarning: "assessment",
|
|
712
|
+
build: (opts, base) => {
|
|
713
|
+
if (opts.name !== "assessment_completed") throw new Error("unexpected event");
|
|
448
714
|
const lessonId = opts.lessonId;
|
|
449
|
-
if (!lessonId) throw new Error("
|
|
450
|
-
return {
|
|
715
|
+
if (!lessonId) throw new Error("assessment_completed requires active lessonId");
|
|
716
|
+
return {
|
|
717
|
+
name: "assessment_completed",
|
|
718
|
+
...base,
|
|
719
|
+
lessonId,
|
|
720
|
+
data: opts.data
|
|
721
|
+
};
|
|
451
722
|
}
|
|
452
|
-
|
|
723
|
+
},
|
|
724
|
+
interaction: {
|
|
725
|
+
build: (opts, base) => {
|
|
726
|
+
if (opts.name !== "interaction") throw new Error("unexpected event");
|
|
453
727
|
return {
|
|
454
728
|
name: "interaction",
|
|
455
729
|
...base,
|
|
456
730
|
lessonId: opts.lessonId,
|
|
457
731
|
data: opts.data
|
|
458
732
|
};
|
|
459
|
-
|
|
460
|
-
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
book_page_viewed: {
|
|
736
|
+
requiresLessonId: true,
|
|
737
|
+
build: (opts, base) => {
|
|
738
|
+
if (opts.name !== "book_page_viewed") throw new Error("unexpected event");
|
|
739
|
+
const lessonId = opts.lessonId;
|
|
740
|
+
if (!lessonId) throw new Error("book_page_viewed requires active lessonId");
|
|
741
|
+
return {
|
|
742
|
+
name: "book_page_viewed",
|
|
743
|
+
...base,
|
|
744
|
+
lessonId,
|
|
745
|
+
data: opts.data
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
compound_page_viewed: {
|
|
750
|
+
requiresLessonId: true,
|
|
751
|
+
build: (opts, base) => {
|
|
752
|
+
if (opts.name !== "compound_page_viewed") throw new Error("unexpected event");
|
|
753
|
+
const lessonId = opts.lessonId;
|
|
754
|
+
if (!lessonId) throw new Error("compound_page_viewed requires active lessonId");
|
|
755
|
+
return {
|
|
756
|
+
name: "compound_page_viewed",
|
|
757
|
+
...base,
|
|
758
|
+
lessonId,
|
|
759
|
+
data: opts.data
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
hotspot_opened: {
|
|
764
|
+
build: (opts, base) => {
|
|
765
|
+
if (opts.name !== "hotspot_opened") throw new Error("unexpected event");
|
|
766
|
+
return {
|
|
767
|
+
name: "hotspot_opened",
|
|
768
|
+
...base,
|
|
769
|
+
lessonId: opts.lessonId,
|
|
770
|
+
data: opts.data
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
accordion_section_toggled: {
|
|
775
|
+
build: (opts, base) => {
|
|
776
|
+
if (opts.name !== "accordion_section_toggled") throw new Error("unexpected event");
|
|
777
|
+
return {
|
|
778
|
+
name: "accordion_section_toggled",
|
|
779
|
+
...base,
|
|
780
|
+
lessonId: opts.lessonId,
|
|
781
|
+
data: opts.data
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
flashcard_flipped: {
|
|
786
|
+
build: (opts, base) => {
|
|
787
|
+
if (opts.name !== "flashcard_flipped") throw new Error("unexpected event");
|
|
788
|
+
return {
|
|
789
|
+
name: "flashcard_flipped",
|
|
790
|
+
...base,
|
|
791
|
+
lessonId: opts.lessonId,
|
|
792
|
+
data: opts.data
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
image_slider_changed: {
|
|
797
|
+
build: (opts, base) => {
|
|
798
|
+
if (opts.name !== "image_slider_changed") throw new Error("unexpected event");
|
|
799
|
+
return {
|
|
800
|
+
name: "image_slider_changed",
|
|
801
|
+
...base,
|
|
802
|
+
lessonId: opts.lessonId,
|
|
803
|
+
data: opts.data
|
|
804
|
+
};
|
|
805
|
+
}
|
|
461
806
|
}
|
|
807
|
+
};
|
|
808
|
+
function buildTelemetryEventFromRegistry(opts) {
|
|
809
|
+
const entry = TELEMETRY_EVENT_REGISTRY[opts.name];
|
|
810
|
+
if (!entry) {
|
|
811
|
+
throw new Error("Unexpected value");
|
|
812
|
+
}
|
|
813
|
+
const base = {
|
|
814
|
+
timestamp: opts.timestamp ?? nowIso(),
|
|
815
|
+
courseId: opts.courseId,
|
|
816
|
+
sessionId: opts.sessionId,
|
|
817
|
+
attemptId: opts.attemptId,
|
|
818
|
+
user: opts.user
|
|
819
|
+
};
|
|
820
|
+
return entry.build(opts, base);
|
|
821
|
+
}
|
|
822
|
+
function getTelemetryEventRegistryEntry(name) {
|
|
823
|
+
return TELEMETRY_EVENT_REGISTRY[name];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/telemetryBuilder.ts
|
|
827
|
+
var warnedMissingQuizLesson = false;
|
|
828
|
+
var warnedMissingAssessmentLesson = false;
|
|
829
|
+
function resetTelemetryBuilderWarningsForTests() {
|
|
830
|
+
warnedMissingQuizLesson = false;
|
|
831
|
+
warnedMissingAssessmentLesson = false;
|
|
832
|
+
}
|
|
833
|
+
function buildTelemetryEvent(opts) {
|
|
834
|
+
return buildTelemetryEventFromRegistry(opts);
|
|
462
835
|
}
|
|
463
836
|
function tryBuildTelemetryEvent(opts) {
|
|
464
|
-
const
|
|
465
|
-
if (
|
|
466
|
-
if (
|
|
467
|
-
warnedMissingQuizLesson
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
837
|
+
const entry = getTelemetryEventRegistryEntry(opts.name);
|
|
838
|
+
if (entry.requiresLessonId && !opts.lessonId && entry.tryBuildMissingLessonWarning) {
|
|
839
|
+
if (isDevEnvironment()) {
|
|
840
|
+
if (entry.tryBuildMissingLessonWarning === "quiz" && !warnedMissingQuizLesson) {
|
|
841
|
+
warnedMissingQuizLesson = true;
|
|
842
|
+
console.warn(
|
|
843
|
+
`[lessonkit] ${opts.name} skipped: wrap <Quiz> in <Lesson> so an active lessonId is available`
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
if (entry.tryBuildMissingLessonWarning === "assessment" && !warnedMissingAssessmentLesson) {
|
|
847
|
+
warnedMissingAssessmentLesson = true;
|
|
848
|
+
console.warn(
|
|
849
|
+
`[lessonkit] ${opts.name} skipped: wrap assessment blocks in <Lesson> so an active lessonId is available`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
471
852
|
}
|
|
472
853
|
return null;
|
|
473
854
|
}
|
|
@@ -475,29 +856,8 @@ function tryBuildTelemetryEvent(opts) {
|
|
|
475
856
|
}
|
|
476
857
|
|
|
477
858
|
// src/telemetryPipeline.ts
|
|
478
|
-
function isDevEnvironment3() {
|
|
479
|
-
const g = globalThis;
|
|
480
|
-
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
481
|
-
}
|
|
482
|
-
function warnSinkFailure(sinkId, err) {
|
|
483
|
-
if (isDevEnvironment3()) {
|
|
484
|
-
console.warn(
|
|
485
|
-
`[lessonkit] telemetry sink "${sinkId}" failed:`,
|
|
486
|
-
err instanceof Error ? err.message : err
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
859
|
function invokeSink(sink, event, emitCtx) {
|
|
491
|
-
|
|
492
|
-
try {
|
|
493
|
-
result = sink.emit(event, emitCtx);
|
|
494
|
-
} catch (err) {
|
|
495
|
-
warnSinkFailure(sink.id, err);
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
if (result != null && typeof result.catch === "function") {
|
|
499
|
-
void result.catch((err) => warnSinkFailure(sink.id, err));
|
|
500
|
-
}
|
|
860
|
+
invokePipelineSink(sink.id, () => sink.emit(event, emitCtx));
|
|
501
861
|
}
|
|
502
862
|
function createTelemetryPipeline(sinks) {
|
|
503
863
|
const list = [...sinks];
|
|
@@ -773,101 +1133,13 @@ function completeCourseWithTelemetry(opts) {
|
|
|
773
1133
|
return true;
|
|
774
1134
|
}
|
|
775
1135
|
|
|
776
|
-
// src/
|
|
777
|
-
function
|
|
778
|
-
const storage = ports.storage ?? createSessionStoragePort();
|
|
779
|
-
const clock = ports.clock ?? createDefaultClock();
|
|
780
|
-
const configSnapshot = { ...config };
|
|
781
|
-
let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
|
|
782
|
-
let attemptId = configSnapshot.session?.attemptId;
|
|
783
|
-
let user = configSnapshot.session?.user;
|
|
784
|
-
let courseId = configSnapshot.courseId;
|
|
785
|
-
let progress = createProgressController();
|
|
786
|
-
const getSession = () => ({ sessionId, attemptId, user });
|
|
787
|
-
const syncSessionFromConfig = (next) => {
|
|
788
|
-
sessionId = resolveSessionId(storage, next.session?.sessionId);
|
|
789
|
-
attemptId = next.session?.attemptId;
|
|
790
|
-
user = next.session?.user;
|
|
791
|
-
courseId = next.courseId;
|
|
792
|
-
};
|
|
793
|
-
syncSessionFromConfig(configSnapshot);
|
|
794
|
-
const track = (name, data, emit, lessonId) => {
|
|
795
|
-
const event = tryBuildTelemetryEvent({
|
|
796
|
-
name,
|
|
797
|
-
courseId,
|
|
798
|
-
lessonId: lessonId ?? progress.getState().activeLessonId,
|
|
799
|
-
sessionId,
|
|
800
|
-
attemptId,
|
|
801
|
-
user,
|
|
802
|
-
data
|
|
803
|
-
});
|
|
804
|
-
if (!event) return;
|
|
805
|
-
emit(event);
|
|
806
|
-
};
|
|
807
|
-
const emitLessonCompleted = (lessonId, durationMs, emitFn) => {
|
|
808
|
-
emitFn("lesson_completed", { lessonId, durationMs }, lessonId);
|
|
809
|
-
if (durationMs !== void 0) {
|
|
810
|
-
emitFn("lesson_time_on_task", { lessonId, durationMs }, lessonId);
|
|
811
|
-
}
|
|
812
|
-
};
|
|
1136
|
+
// src/plugins/context.ts
|
|
1137
|
+
function buildPluginContext(opts) {
|
|
813
1138
|
return {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
return progress;
|
|
819
|
-
},
|
|
820
|
-
getProgressState: () => progress.getState(),
|
|
821
|
-
getSession,
|
|
822
|
-
updateConfig(next) {
|
|
823
|
-
if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
|
|
824
|
-
if (next.runtimeVersion !== void 0) configSnapshot.runtimeVersion = next.runtimeVersion;
|
|
825
|
-
if (next.plugins !== void 0) configSnapshot.plugins = next.plugins;
|
|
826
|
-
if (next.session !== void 0) {
|
|
827
|
-
configSnapshot.session = { ...configSnapshot.session, ...next.session };
|
|
828
|
-
}
|
|
829
|
-
syncSessionFromConfig(configSnapshot);
|
|
830
|
-
},
|
|
831
|
-
setActiveLesson(lessonId, emitFn) {
|
|
832
|
-
const current = progress.getState();
|
|
833
|
-
if (current.activeLessonId === lessonId) return;
|
|
834
|
-
if (current.completedLessonIds.has(lessonId)) {
|
|
835
|
-
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
const previous = current.activeLessonId;
|
|
839
|
-
if (previous && previous !== lessonId) {
|
|
840
|
-
const completed = progress.completeLesson(previous, clock.nowMs());
|
|
841
|
-
if (completed.didComplete) {
|
|
842
|
-
emitLessonCompleted(previous, completed.durationMs, emitFn);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
846
|
-
emitFn("lesson_started", { lessonId }, lessonId);
|
|
847
|
-
},
|
|
848
|
-
completeLesson(lessonId, emitFn) {
|
|
849
|
-
const result = progress.completeLesson(lessonId, clock.nowMs());
|
|
850
|
-
if (!result.didComplete) return;
|
|
851
|
-
emitLessonCompleted(lessonId, result.durationMs, emitFn);
|
|
852
|
-
},
|
|
853
|
-
completeCourse(emitFn) {
|
|
854
|
-
const current = progress.getState();
|
|
855
|
-
if (current.activeLessonId) {
|
|
856
|
-
const lessonResult = progress.completeLesson(current.activeLessonId, clock.nowMs());
|
|
857
|
-
if (lessonResult.didComplete) {
|
|
858
|
-
emitLessonCompleted(current.activeLessonId, lessonResult.durationMs, emitFn);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
const result = progress.completeCourse();
|
|
862
|
-
if (!result.didComplete) return;
|
|
863
|
-
emitFn("course_completed");
|
|
864
|
-
},
|
|
865
|
-
track,
|
|
866
|
-
resetForCourseChange(nextCourseId) {
|
|
867
|
-
configSnapshot.courseId = nextCourseId;
|
|
868
|
-
courseId = nextCourseId;
|
|
869
|
-
progress = createProgressController();
|
|
870
|
-
}
|
|
1139
|
+
courseId: opts.courseId,
|
|
1140
|
+
sessionId: opts.sessionId,
|
|
1141
|
+
attemptId: opts.attemptId,
|
|
1142
|
+
user: opts.user
|
|
871
1143
|
};
|
|
872
1144
|
}
|
|
873
1145
|
|
|
@@ -958,6 +1230,161 @@ function createPluginRegistry(plugins = []) {
|
|
|
958
1230
|
};
|
|
959
1231
|
}
|
|
960
1232
|
|
|
1233
|
+
// src/runtime/createLessonkitRuntime.ts
|
|
1234
|
+
function resolvePluginHost(plugins) {
|
|
1235
|
+
if (!plugins) return null;
|
|
1236
|
+
if (typeof plugins === "object" && "runTelemetry" in plugins) return plugins;
|
|
1237
|
+
if (Array.isArray(plugins) && plugins.length > 0) return createPluginRegistry(plugins);
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
function warnRuntimeV1Deprecated() {
|
|
1241
|
+
const g = globalThis;
|
|
1242
|
+
if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production") return;
|
|
1243
|
+
console.warn(
|
|
1244
|
+
'[lessonkit] runtimeVersion "v1" is deprecated; use "v2" (default). v1 will be removed in LessonKit 2.0.'
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
function createLessonkitRuntime(config, ports = {}) {
|
|
1248
|
+
if (config.runtimeVersion === "v1") warnRuntimeV1Deprecated();
|
|
1249
|
+
const storage = ports.storage ?? createSessionStoragePort();
|
|
1250
|
+
const clock = ports.clock ?? createDefaultClock();
|
|
1251
|
+
const configSnapshot = { ...config };
|
|
1252
|
+
let sessionId = resolveSessionId(storage, configSnapshot.session?.sessionId);
|
|
1253
|
+
let attemptId = configSnapshot.session?.attemptId;
|
|
1254
|
+
let user = configSnapshot.session?.user;
|
|
1255
|
+
let courseId = configSnapshot.courseId;
|
|
1256
|
+
let progress = createProgressController();
|
|
1257
|
+
let pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
1258
|
+
const getPluginCtx = () => buildPluginContext({
|
|
1259
|
+
courseId,
|
|
1260
|
+
sessionId,
|
|
1261
|
+
attemptId,
|
|
1262
|
+
user
|
|
1263
|
+
});
|
|
1264
|
+
const getSession = () => ({ sessionId, attemptId, user });
|
|
1265
|
+
const syncSessionFromConfig = (next) => {
|
|
1266
|
+
sessionId = resolveSessionId(storage, next.session?.sessionId);
|
|
1267
|
+
attemptId = next.session?.attemptId;
|
|
1268
|
+
user = next.session?.user;
|
|
1269
|
+
courseId = next.courseId;
|
|
1270
|
+
};
|
|
1271
|
+
const applyPluginsToEvent = (event) => {
|
|
1272
|
+
if (!pluginHost) return event;
|
|
1273
|
+
return pluginHost.runTelemetry(event, getPluginCtx());
|
|
1274
|
+
};
|
|
1275
|
+
const buildAndApply = (name, data, lessonId) => {
|
|
1276
|
+
const event = tryBuildTelemetryEvent({
|
|
1277
|
+
name,
|
|
1278
|
+
courseId,
|
|
1279
|
+
lessonId: lessonId ?? progress.getState().activeLessonId,
|
|
1280
|
+
sessionId,
|
|
1281
|
+
attemptId,
|
|
1282
|
+
user,
|
|
1283
|
+
data
|
|
1284
|
+
});
|
|
1285
|
+
if (!event) return null;
|
|
1286
|
+
return applyPluginsToEvent(event);
|
|
1287
|
+
};
|
|
1288
|
+
const wrapEmitFn = (emitFn) => {
|
|
1289
|
+
return (name, data, lessonId) => {
|
|
1290
|
+
const event = buildAndApply(name, data, lessonId);
|
|
1291
|
+
if (event === null) return;
|
|
1292
|
+
const eventLessonId = "lessonId" in event ? event.lessonId : lessonId;
|
|
1293
|
+
const eventData = "data" in event ? event.data : data;
|
|
1294
|
+
emitFn(event.name, eventData, eventLessonId);
|
|
1295
|
+
};
|
|
1296
|
+
};
|
|
1297
|
+
syncSessionFromConfig(configSnapshot);
|
|
1298
|
+
const track = (name, data, emit, lessonId) => {
|
|
1299
|
+
const event = buildAndApply(name, data, lessonId);
|
|
1300
|
+
if (!event) return;
|
|
1301
|
+
emit(event);
|
|
1302
|
+
};
|
|
1303
|
+
const emitLessonCompletedEvents = (lessonId, durationMs, emitFn) => {
|
|
1304
|
+
const wrapped = wrapEmitFn(emitFn);
|
|
1305
|
+
wrapped("lesson_completed", { lessonId, durationMs }, lessonId);
|
|
1306
|
+
if (durationMs !== void 0) {
|
|
1307
|
+
wrapped("lesson_time_on_task", { lessonId, durationMs }, lessonId);
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
return {
|
|
1311
|
+
get config() {
|
|
1312
|
+
return configSnapshot;
|
|
1313
|
+
},
|
|
1314
|
+
get progress() {
|
|
1315
|
+
return progress;
|
|
1316
|
+
},
|
|
1317
|
+
get pluginHost() {
|
|
1318
|
+
return pluginHost;
|
|
1319
|
+
},
|
|
1320
|
+
getProgressState: () => progress.getState(),
|
|
1321
|
+
getSession,
|
|
1322
|
+
updateConfig(next) {
|
|
1323
|
+
if (next.plugins !== void 0 && next.plugins !== pluginHost) {
|
|
1324
|
+
pluginHost?.disposeAll();
|
|
1325
|
+
configSnapshot.plugins = next.plugins;
|
|
1326
|
+
pluginHost = resolvePluginHost(configSnapshot.plugins);
|
|
1327
|
+
}
|
|
1328
|
+
if (next.courseId !== void 0) configSnapshot.courseId = next.courseId;
|
|
1329
|
+
if (next.runtimeVersion !== void 0) {
|
|
1330
|
+
if (next.runtimeVersion === "v1") warnRuntimeV1Deprecated();
|
|
1331
|
+
configSnapshot.runtimeVersion = next.runtimeVersion;
|
|
1332
|
+
}
|
|
1333
|
+
if (next.session !== void 0) {
|
|
1334
|
+
configSnapshot.session = { ...configSnapshot.session, ...next.session };
|
|
1335
|
+
}
|
|
1336
|
+
syncSessionFromConfig(configSnapshot);
|
|
1337
|
+
},
|
|
1338
|
+
setActiveLesson(lessonId, emitFn) {
|
|
1339
|
+
const wrapped = wrapEmitFn(emitFn);
|
|
1340
|
+
const current = progress.getState();
|
|
1341
|
+
if (current.activeLessonId === lessonId) return;
|
|
1342
|
+
if (current.completedLessonIds.has(lessonId)) {
|
|
1343
|
+
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
const previous = current.activeLessonId;
|
|
1347
|
+
if (previous && previous !== lessonId) {
|
|
1348
|
+
const completed = progress.completeLesson(previous, clock.nowMs());
|
|
1349
|
+
if (completed.didComplete) {
|
|
1350
|
+
emitLessonCompletedEvents(previous, completed.durationMs, wrapped);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
progress.setActiveLesson(lessonId, clock.nowMs());
|
|
1354
|
+
wrapped("lesson_started", { lessonId }, lessonId);
|
|
1355
|
+
},
|
|
1356
|
+
completeLesson(lessonId, emitFn) {
|
|
1357
|
+
completeLessonWithTelemetry({
|
|
1358
|
+
progress,
|
|
1359
|
+
lessonId,
|
|
1360
|
+
nowMs: clock.nowMs(),
|
|
1361
|
+
emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, wrapEmitFn(emitFn))
|
|
1362
|
+
});
|
|
1363
|
+
},
|
|
1364
|
+
completeCourse(emitFn) {
|
|
1365
|
+
completeCourseWithTelemetry({
|
|
1366
|
+
progress,
|
|
1367
|
+
nowMs: clock.nowMs(),
|
|
1368
|
+
emitLessonCompleted: (id, durationMs) => emitLessonCompletedEvents(id, durationMs, wrapEmitFn(emitFn)),
|
|
1369
|
+
emitCourseCompleted: () => wrapEmitFn(emitFn)("course_completed")
|
|
1370
|
+
});
|
|
1371
|
+
},
|
|
1372
|
+
track,
|
|
1373
|
+
scoreAssessment(input, _lessonId) {
|
|
1374
|
+
if (!pluginHost) return null;
|
|
1375
|
+
return pluginHost.scoreAssessment(input, getPluginCtx());
|
|
1376
|
+
},
|
|
1377
|
+
resetForCourseChange(nextCourseId) {
|
|
1378
|
+
configSnapshot.courseId = nextCourseId;
|
|
1379
|
+
courseId = nextCourseId;
|
|
1380
|
+
progress = createProgressController();
|
|
1381
|
+
},
|
|
1382
|
+
dispose() {
|
|
1383
|
+
pluginHost?.disposeAll();
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
|
|
961
1388
|
// src/plugins/define.ts
|
|
962
1389
|
function defineTelemetryPlugin(plugin) {
|
|
963
1390
|
return plugin;
|
|
@@ -970,18 +1397,33 @@ function defineLifecyclePlugin(plugin) {
|
|
|
970
1397
|
}
|
|
971
1398
|
// Annotate the CommonJS export names for ESM import in node:
|
|
972
1399
|
0 && (module.exports = {
|
|
1400
|
+
ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
1401
|
+
ASSESSMENT_SEQUENCE_ALLOWED_CHILD_TYPES,
|
|
1402
|
+
COMPOUND_MAX_NESTING_DEPTH,
|
|
1403
|
+
COMPOUND_RESUME_SCHEMA_VERSION,
|
|
973
1404
|
ID_MAX_LENGTH,
|
|
974
1405
|
ID_PATTERN,
|
|
1406
|
+
INTERACTIVE_BOOK_ALLOWED_CHILD_TYPES,
|
|
1407
|
+
PAGE_ALLOWED_CHILD_TYPES,
|
|
975
1408
|
SESSION_STORAGE_KEY,
|
|
976
1409
|
TELEMETRY_EVENT_CATALOG,
|
|
1410
|
+
TELEMETRY_EVENT_CATALOG_V2,
|
|
1411
|
+
TELEMETRY_EVENT_CATALOG_V3,
|
|
977
1412
|
assertNever,
|
|
978
1413
|
assertValidId,
|
|
979
1414
|
buildCourseStartedTelemetryEvent,
|
|
980
1415
|
buildLessonkitUrn,
|
|
1416
|
+
buildPluginContext,
|
|
981
1417
|
buildTelemetryCatalog,
|
|
1418
|
+
buildTelemetryCatalogV2,
|
|
1419
|
+
buildTelemetryCatalogV3,
|
|
982
1420
|
buildTelemetryEvent,
|
|
1421
|
+
clampCompoundPageIndex,
|
|
1422
|
+
clearCompoundState,
|
|
983
1423
|
completeCourseWithTelemetry,
|
|
984
1424
|
completeLessonWithTelemetry,
|
|
1425
|
+
compoundStateStorageKey,
|
|
1426
|
+
createCompoundResumeState,
|
|
985
1427
|
createDefaultClock,
|
|
986
1428
|
createGlobalTimer,
|
|
987
1429
|
createLessonkitRuntime,
|
|
@@ -997,10 +1439,13 @@ function defineLifecyclePlugin(plugin) {
|
|
|
997
1439
|
defineLifecyclePlugin,
|
|
998
1440
|
defineTelemetryPlugin,
|
|
999
1441
|
deriveId,
|
|
1442
|
+
getAllowedChildTypes,
|
|
1000
1443
|
getTabSessionId,
|
|
1001
1444
|
hasCourseStarted,
|
|
1002
1445
|
hasCourseStartedEmittedToTracking,
|
|
1003
1446
|
hasCourseStartedPipelineDelivered,
|
|
1447
|
+
isChildTypeAllowed,
|
|
1448
|
+
loadCompoundState,
|
|
1004
1449
|
markCourseStarted,
|
|
1005
1450
|
markCourseStartedEmittedToTracking,
|
|
1006
1451
|
markCourseStartedPipelineDelivered,
|
|
@@ -1008,12 +1453,16 @@ function defineLifecyclePlugin(plugin) {
|
|
|
1008
1453
|
nowIso,
|
|
1009
1454
|
parseBlockId,
|
|
1010
1455
|
parseCheckId,
|
|
1456
|
+
parseCompoundResumeState,
|
|
1011
1457
|
parseCourseId,
|
|
1012
1458
|
parseLessonId,
|
|
1013
1459
|
resetStoragePortForTests,
|
|
1014
1460
|
resetTelemetryBuilderWarningsForTests,
|
|
1015
1461
|
resolveSessionId,
|
|
1462
|
+
saveCompoundState,
|
|
1016
1463
|
slugifyId,
|
|
1464
|
+
telemetryCatalogV2Version,
|
|
1465
|
+
telemetryCatalogV3Version,
|
|
1017
1466
|
telemetryCatalogVersion,
|
|
1018
1467
|
tryBuildTelemetryEvent,
|
|
1019
1468
|
tryEmitCourseStarted,
|