@lessonkit/core 1.0.0 → 1.0.2

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 CHANGED
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  ID_PATTERN: () => ID_PATTERN,
25
25
  SESSION_STORAGE_KEY: () => SESSION_STORAGE_KEY,
26
26
  TELEMETRY_EVENT_CATALOG: () => TELEMETRY_EVENT_CATALOG,
27
+ assertNever: () => assertNever,
27
28
  assertValidId: () => assertValidId,
28
29
  buildCourseStartedTelemetryEvent: () => buildCourseStartedTelemetryEvent,
29
30
  buildLessonkitUrn: () => buildLessonkitUrn,
@@ -49,10 +50,16 @@ __export(index_exports, {
49
50
  getTabSessionId: () => getTabSessionId,
50
51
  hasCourseStarted: () => hasCourseStarted,
51
52
  hasCourseStartedEmittedToTracking: () => hasCourseStartedEmittedToTracking,
53
+ hasCourseStartedPipelineDelivered: () => hasCourseStartedPipelineDelivered,
52
54
  markCourseStarted: () => markCourseStarted,
53
55
  markCourseStartedEmittedToTracking: () => markCourseStartedEmittedToTracking,
56
+ markCourseStartedPipelineDelivered: () => markCourseStartedPipelineDelivered,
54
57
  migrateCourseStartedMark: () => migrateCourseStartedMark,
55
58
  nowIso: () => nowIso,
59
+ parseBlockId: () => parseBlockId,
60
+ parseCheckId: () => parseCheckId,
61
+ parseCourseId: () => parseCourseId,
62
+ parseLessonId: () => parseLessonId,
56
63
  resetStoragePortForTests: () => resetStoragePortForTests,
57
64
  resetTelemetryBuilderWarningsForTests: () => resetTelemetryBuilderWarningsForTests,
58
65
  resolveSessionId: () => resolveSessionId,
@@ -68,6 +75,11 @@ module.exports = __toCommonJS(index_exports);
68
75
  var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
69
76
  var ID_MAX_LENGTH = 64;
70
77
 
78
+ // src/assertNever.ts
79
+ function assertNever(value, message = "Unexpected value") {
80
+ throw new Error(`${message}: ${String(value)}`);
81
+ }
82
+
71
83
  // src/validateId.ts
72
84
  function validateId(input, path = "id") {
73
85
  if (typeof input !== "string") {
@@ -96,6 +108,22 @@ function validateId(input, path = "id") {
96
108
  }
97
109
  return { ok: true, id };
98
110
  }
111
+ function parseCourseId(input) {
112
+ const result = validateId(input, "courseId");
113
+ return result.ok ? result.id : null;
114
+ }
115
+ function parseLessonId(input) {
116
+ const result = validateId(input, "lessonId");
117
+ return result.ok ? result.id : null;
118
+ }
119
+ function parseCheckId(input) {
120
+ const result = validateId(input, "checkId");
121
+ return result.ok ? result.id : null;
122
+ }
123
+ function parseBlockId(input) {
124
+ const result = validateId(input, "blockId");
125
+ return result.ok ? result.id : null;
126
+ }
99
127
  function assertValidId(input, path = "id") {
100
128
  const result = validateId(input, path);
101
129
  if (!result.ok) {
@@ -251,6 +279,11 @@ function invokeTrackingSink(sink, event) {
251
279
  function createTrackingClient(opts) {
252
280
  const sink = opts?.sink;
253
281
  const batchSink = opts?.batchSink;
282
+ if (batchSink != null && opts?.batch?.enabled === false) {
283
+ throw new Error(
284
+ "[lessonkit] tracking.batchSink cannot be used with batch.enabled: false; omit batch.enabled or set it to true"
285
+ );
286
+ }
254
287
  const batchEnabled = opts?.batch?.enabled ?? Boolean(batchSink);
255
288
  const flushIntervalMs = opts?.batch?.flushIntervalMs ?? 5e3;
256
289
  const maxBatchSize = opts?.batch?.maxBatchSize ?? 25;
@@ -322,7 +355,7 @@ function createTrackingClient(opts) {
322
355
  if (disposed || disposing) return;
323
356
  if (buffer.length >= maxBufferSize) {
324
357
  buffer.shift();
325
- if (!warnedBufferCap && typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
358
+ if (!warnedBufferCap && isDevEnvironment()) {
326
359
  warnedBufferCap = true;
327
360
  console.warn(
328
361
  `[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; oldest events are dropped while the sink is unavailable.`
@@ -369,6 +402,11 @@ function isDevEnvironment2() {
369
402
  function resetTelemetryBuilderWarningsForTests() {
370
403
  warnedMissingQuizLesson = false;
371
404
  }
405
+ function resolveLessonId(opts, eventName) {
406
+ const lessonId = opts.lessonId ?? opts.data?.lessonId;
407
+ if (!lessonId) throw new Error(`${eventName} requires lessonId`);
408
+ return lessonId;
409
+ }
372
410
  function buildTelemetryEvent(opts) {
373
411
  const base = {
374
412
  timestamp: opts.timestamp ?? nowIso(),
@@ -383,39 +421,33 @@ function buildTelemetryEvent(opts) {
383
421
  case "course_completed":
384
422
  return { name: "course_completed", ...base };
385
423
  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");
424
+ const lessonId = resolveLessonId(opts, "lesson_started");
389
425
  return {
390
426
  name: "lesson_started",
391
427
  ...base,
392
428
  lessonId,
393
- data: { ...data, lessonId }
429
+ data: { ...opts.data, lessonId }
394
430
  };
395
431
  }
396
432
  case "lesson_completed":
397
433
  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`);
434
+ const lessonId = resolveLessonId(opts, opts.name);
401
435
  return {
402
436
  name: opts.name,
403
437
  ...base,
404
438
  lessonId,
405
- data: { ...data, lessonId }
439
+ data: { ...opts.data, lessonId }
406
440
  };
407
441
  }
408
442
  case "quiz_answered": {
409
- const data = opts.data;
410
443
  const lessonId = opts.lessonId;
411
444
  if (!lessonId) throw new Error("quiz_answered requires active lessonId");
412
- return { name: "quiz_answered", ...base, lessonId, data };
445
+ return { name: "quiz_answered", ...base, lessonId, data: opts.data };
413
446
  }
414
447
  case "quiz_completed": {
415
- const data = opts.data;
416
448
  const lessonId = opts.lessonId;
417
449
  if (!lessonId) throw new Error("quiz_completed requires active lessonId");
418
- return { name: "quiz_completed", ...base, lessonId, data };
450
+ return { name: "quiz_completed", ...base, lessonId, data: opts.data };
419
451
  }
420
452
  case "interaction":
421
453
  return {
@@ -425,7 +457,7 @@ function buildTelemetryEvent(opts) {
425
457
  data: opts.data
426
458
  };
427
459
  default:
428
- return { name: opts.name, ...base };
460
+ return assertNever(opts);
429
461
  }
430
462
  }
431
463
  function tryBuildTelemetryEvent(opts) {
@@ -512,7 +544,8 @@ function createMemoryBackedSessionStorage(session) {
512
544
  const warnPersistFailure = () => {
513
545
  if (warnedPersistFailure) return;
514
546
  warnedPersistFailure = true;
515
- if (typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
547
+ const g = globalThis;
548
+ if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "development") {
516
549
  console.warn(
517
550
  "[lessonkit] sessionStorage is unavailable or failed; using in-memory session dedupe for this tab (may reset on full reload)."
518
551
  );
@@ -553,23 +586,37 @@ function createMemoryBackedSessionStorage(session) {
553
586
  function resetStoragePortForTests(storage) {
554
587
  storage.resetForTests?.();
555
588
  }
589
+ function createInMemorySessionStoragePort() {
590
+ const memory = /* @__PURE__ */ new Map();
591
+ return {
592
+ getItem: (key) => memory.get(key) ?? null,
593
+ setItem: (key, value) => {
594
+ memory.set(key, value);
595
+ },
596
+ removeItem: (key) => {
597
+ memory.delete(key);
598
+ },
599
+ resetForTests: () => {
600
+ memory.clear();
601
+ }
602
+ };
603
+ }
604
+ function resolveBrowserSessionStorage() {
605
+ try {
606
+ if (typeof sessionStorage === "undefined" || sessionStorage == null) {
607
+ return null;
608
+ }
609
+ return sessionStorage;
610
+ } catch {
611
+ return null;
612
+ }
613
+ }
556
614
  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
- };
615
+ const session = resolveBrowserSessionStorage();
616
+ if (!session) {
617
+ return createInMemorySessionStoragePort();
571
618
  }
572
- return createMemoryBackedSessionStorage(sessionStorage);
619
+ return createMemoryBackedSessionStorage(session);
573
620
  }
574
621
  function createGlobalTimer() {
575
622
  return {
@@ -622,6 +669,7 @@ function getTabSessionId(storage) {
622
669
  }
623
670
  var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
624
671
  var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
672
+ var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
625
673
  function resolveSessionId(storage, provided) {
626
674
  if (provided) return provided;
627
675
  const existing = storage.getItem(SESSION_STORAGE_KEY);
@@ -636,6 +684,9 @@ function courseStartedStorageKey(sessionId, courseId) {
636
684
  function courseStartedTrackingStorageKey(sessionId, courseId) {
637
685
  return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
638
686
  }
687
+ function courseStartedPipelineStorageKey(sessionId, courseId) {
688
+ return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
689
+ }
639
690
  function hasCourseStarted(storage, sessionId, courseId) {
640
691
  if (!courseId) return false;
641
692
  return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
@@ -652,6 +703,14 @@ function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
652
703
  if (!courseId) return;
653
704
  storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
654
705
  }
706
+ function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
707
+ if (!courseId) return false;
708
+ return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
709
+ }
710
+ function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
711
+ if (!courseId) return;
712
+ storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
713
+ }
655
714
  function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
656
715
  if (!courseId || fromSessionId === toSessionId) return;
657
716
  if (hasCourseStarted(storage, fromSessionId, courseId)) {
@@ -662,6 +721,10 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
662
721
  markCourseStartedEmittedToTracking(storage, toSessionId, courseId);
663
722
  storage.removeItem?.(courseStartedTrackingStorageKey(fromSessionId, courseId));
664
723
  }
724
+ if (hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)) {
725
+ markCourseStartedPipelineDelivered(storage, toSessionId, courseId);
726
+ storage.removeItem?.(courseStartedPipelineStorageKey(fromSessionId, courseId));
727
+ }
665
728
  }
666
729
 
667
730
  // src/runtime/courseLifecycle.ts
@@ -911,6 +974,7 @@ function defineLifecyclePlugin(plugin) {
911
974
  ID_PATTERN,
912
975
  SESSION_STORAGE_KEY,
913
976
  TELEMETRY_EVENT_CATALOG,
977
+ assertNever,
914
978
  assertValidId,
915
979
  buildCourseStartedTelemetryEvent,
916
980
  buildLessonkitUrn,
@@ -936,10 +1000,16 @@ function defineLifecyclePlugin(plugin) {
936
1000
  getTabSessionId,
937
1001
  hasCourseStarted,
938
1002
  hasCourseStartedEmittedToTracking,
1003
+ hasCourseStartedPipelineDelivered,
939
1004
  markCourseStarted,
940
1005
  markCourseStartedEmittedToTracking,
1006
+ markCourseStartedPipelineDelivered,
941
1007
  migrateCourseStartedMark,
942
1008
  nowIso,
1009
+ parseBlockId,
1010
+ parseCheckId,
1011
+ parseCourseId,
1012
+ parseLessonId,
943
1013
  resetStoragePortForTests,
944
1014
  resetTelemetryBuilderWarningsForTests,
945
1015
  resolveSessionId,
package/dist/index.d.cts CHANGED
@@ -2,6 +2,8 @@ type CourseId = string;
2
2
  type LessonId = string;
3
3
  type CheckId = string;
4
4
  type BlockId = string;
5
+ /** Stable URN string returned by {@link buildLessonkitUrn}. */
6
+ type LessonkitUrn = string;
5
7
  type IdentityValidationIssue = {
6
8
  path: string;
7
9
  message: string;
@@ -13,12 +15,27 @@ type IdentityValidationResult = {
13
15
  ok: false;
14
16
  issues: IdentityValidationIssue[];
15
17
  };
18
+ type IdentityIdPath = "courseId" | "lessonId" | "checkId" | "blockId" | "id";
16
19
  /** LessonKit id format: letter first, then alphanumeric, `_`, `-`; length 1–64. */
17
20
  declare const ID_PATTERN: RegExp;
18
21
  declare const ID_MAX_LENGTH = 64;
19
22
 
20
- declare function validateId(input: unknown, path?: string): IdentityValidationResult;
21
- declare function assertValidId(input: unknown, path?: string): string;
23
+ /**
24
+ * Exhaustiveness helper for switch/default branches.
25
+ * @throws when called at runtime with an unexpected value.
26
+ */
27
+ declare function assertNever(value: never, message?: string): never;
28
+
29
+ declare function validateId(input: unknown, path?: IdentityIdPath | string): IdentityValidationResult;
30
+ declare function parseCourseId(input: unknown): CourseId | null;
31
+ declare function parseLessonId(input: unknown): LessonId | null;
32
+ declare function parseCheckId(input: unknown): CheckId | null;
33
+ declare function parseBlockId(input: unknown): BlockId | null;
34
+ declare function assertValidId(input: unknown, path: "courseId"): CourseId;
35
+ declare function assertValidId(input: unknown, path: "lessonId"): LessonId;
36
+ declare function assertValidId(input: unknown, path: "checkId"): CheckId;
37
+ declare function assertValidId(input: unknown, path: "blockId"): BlockId;
38
+ declare function assertValidId(input: unknown, path?: IdentityIdPath | string): string;
22
39
 
23
40
  /** Convert human-readable text to a candidate LessonKit id (may still need collision handling via deriveId). */
24
41
  declare function slugifyId(input: string): string;
@@ -35,7 +52,7 @@ type LessonkitUrnParts = {
35
52
  * Build a stable LessonKit URN for courses, lessons, checks, and blocks.
36
53
  * Segments are validated and encoded in path order.
37
54
  */
38
- declare function buildLessonkitUrn(parts: LessonkitUrnParts): string;
55
+ declare function buildLessonkitUrn(parts: LessonkitUrnParts): LessonkitUrn;
39
56
 
40
57
  type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "interaction";
41
58
  type TelemetryUser = {
@@ -109,6 +126,12 @@ type TelemetryEvent = (TelemetryEventBase & {
109
126
  lessonId?: LessonId;
110
127
  data?: InteractionData;
111
128
  });
129
+ /** Payload shape for a telemetry event name. */
130
+ type TelemetryDataFor<N extends TelemetryEventName> = Extract<TelemetryEvent, {
131
+ name: N;
132
+ }> extends {
133
+ data?: infer D;
134
+ } ? D : never;
112
135
  type TelemetrySink = (event: TelemetryEvent) => void | Promise<void>;
113
136
  type TelemetryBatchSink = (events: TelemetryEvent[]) => void | Promise<void>;
114
137
  type TrackingClient = {
@@ -143,16 +166,46 @@ declare function createSessionId(): string;
143
166
 
144
167
  declare function nowIso(): string;
145
168
 
146
- type BuildTelemetryEventInput = {
147
- name: TelemetryEventName;
169
+ type BuildTelemetryEventContext = {
148
170
  courseId: CourseId;
149
- lessonId?: LessonId;
150
171
  sessionId?: string;
151
172
  attemptId?: string;
152
173
  user?: TelemetryUser;
153
- data?: unknown;
154
174
  timestamp?: string;
155
175
  };
176
+ type BuildTelemetryEventInput = (BuildTelemetryEventContext & {
177
+ name: "course_started";
178
+ lessonId?: LessonId;
179
+ data?: undefined;
180
+ }) | (BuildTelemetryEventContext & {
181
+ name: "course_completed";
182
+ lessonId?: LessonId;
183
+ data?: undefined;
184
+ }) | (BuildTelemetryEventContext & {
185
+ name: "lesson_started";
186
+ lessonId?: LessonId;
187
+ data?: LessonLifecycleData;
188
+ }) | (BuildTelemetryEventContext & {
189
+ name: "lesson_completed";
190
+ lessonId?: LessonId;
191
+ data?: LessonLifecycleData;
192
+ }) | (BuildTelemetryEventContext & {
193
+ name: "lesson_time_on_task";
194
+ lessonId?: LessonId;
195
+ data?: LessonLifecycleData;
196
+ }) | (BuildTelemetryEventContext & {
197
+ name: "quiz_answered";
198
+ lessonId?: LessonId;
199
+ data: QuizAnsweredData;
200
+ }) | (BuildTelemetryEventContext & {
201
+ name: "quiz_completed";
202
+ lessonId?: LessonId;
203
+ data: QuizCompletedData;
204
+ }) | (BuildTelemetryEventContext & {
205
+ name: "interaction";
206
+ lessonId?: LessonId;
207
+ data?: InteractionData;
208
+ });
156
209
  /** Reset dev-warning state (tests only). */
157
210
  declare function resetTelemetryBuilderWarningsForTests(): void;
158
211
  /**
@@ -166,7 +219,7 @@ declare function buildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryE
166
219
  declare function tryBuildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryEvent | null;
167
220
 
168
221
  type EmitContext = {
169
- courseId: string;
222
+ courseId: CourseId;
170
223
  sessionId?: string;
171
224
  attemptId?: string;
172
225
  };
@@ -230,6 +283,8 @@ declare function hasCourseStarted(storage: StoragePort, sessionId: string, cours
230
283
  declare function markCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
231
284
  declare function hasCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
232
285
  declare function markCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
286
+ declare function hasCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
287
+ declare function markCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
233
288
  declare function migrateCourseStartedMark(storage: StoragePort, fromSessionId: string, toSessionId: string, courseId?: CourseId): void;
234
289
 
235
290
  /** Plugin category — aligns with roadmap extension areas. */
@@ -241,8 +296,8 @@ type LessonkitPluginContext = {
241
296
  user?: TelemetryUser;
242
297
  };
243
298
  type AssessmentScoreInput = {
244
- checkId: string;
245
- lessonId?: string;
299
+ checkId: CheckId;
300
+ lessonId?: LessonId;
246
301
  response: unknown;
247
302
  };
248
303
  type AssessmentScoreResult = {
@@ -347,6 +402,9 @@ type HeadlessRuntimePorts = {
347
402
  storage?: StoragePort;
348
403
  clock?: ClockPort;
349
404
  };
405
+ type TelemetryEmitFn = {
406
+ <N extends TelemetryEventName>(name: N, data?: TelemetryDataFor<N>, lessonId?: LessonId): void;
407
+ };
350
408
  type HeadlessLessonkitRuntime = {
351
409
  readonly config: HeadlessLessonkitConfig;
352
410
  readonly progress: ProgressController;
@@ -357,10 +415,10 @@ type HeadlessLessonkitRuntime = {
357
415
  user?: TelemetryUser;
358
416
  };
359
417
  updateConfig: (next: Partial<HeadlessLessonkitConfig>) => void;
360
- setActiveLesson: (lessonId: LessonId, emit: (name: TelemetryEventName, data?: unknown, lessonId?: LessonId) => void) => void;
361
- completeLesson: (lessonId: LessonId, emit: (name: TelemetryEventName, data?: unknown, lessonId?: LessonId) => void) => void;
362
- completeCourse: (emit: (name: TelemetryEventName, data?: unknown, lessonId?: LessonId) => void) => void;
363
- track: (name: TelemetryEventName, data: unknown | undefined, emit: (event: ReturnType<typeof tryBuildTelemetryEvent>) => void, lessonId?: LessonId) => void;
418
+ setActiveLesson: (lessonId: LessonId, emit: TelemetryEmitFn) => void;
419
+ completeLesson: (lessonId: LessonId, emit: TelemetryEmitFn) => void;
420
+ completeCourse: (emit: TelemetryEmitFn) => void;
421
+ track: <N extends TelemetryEventName>(name: N, data: TelemetryDataFor<N> | undefined, emit: (event: TelemetryEvent) => void, lessonId?: LessonId) => void;
364
422
  resetForCourseChange: (courseId: CourseId) => void;
365
423
  };
366
424
  declare function createLessonkitRuntime(config: HeadlessLessonkitConfig, ports?: HeadlessRuntimePorts): HeadlessLessonkitRuntime;
@@ -371,4 +429,4 @@ declare function defineTelemetryPlugin(plugin: TelemetryPlugin): LessonkitPlugin
371
429
  declare function defineAssessmentPlugin(plugin: AssessmentPlugin): LessonkitPlugin;
372
430
  declare function defineLifecyclePlugin(plugin: LifecyclePlugin): LessonkitPlugin;
373
431
 
374
- export { type AssessmentPlugin, type AssessmentScoreInput, type AssessmentScoreResult, type BlockId, type BuildTelemetryEventInput, type CheckId, type ClockPort, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, ID_MAX_LENGTH, ID_PATTERN, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrnParts, type LifecyclePlugin, type PluginHost, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildTelemetryCatalog, buildTelemetryEvent, completeCourseWithTelemetry, completeLessonWithTelemetry, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, markCourseStarted, markCourseStartedEmittedToTracking, migrateCourseStartedMark, nowIso, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, slugifyId, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
432
+ export { type AssessmentPlugin, type AssessmentScoreInput, type AssessmentScoreResult, type BlockId, type BuildTelemetryEventInput, type CheckId, type ClockPort, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, ID_MAX_LENGTH, ID_PATTERN, type IdentityIdPath, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrn, type LessonkitUrnParts, type LifecyclePlugin, type PluginHost, type PluginIdentity, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryDataFor, type TelemetryEmitFn, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertNever, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildTelemetryCatalog, buildTelemetryEvent, completeCourseWithTelemetry, completeLessonWithTelemetry, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, hasCourseStartedPipelineDelivered, markCourseStarted, markCourseStartedEmittedToTracking, markCourseStartedPipelineDelivered, migrateCourseStartedMark, nowIso, parseBlockId, parseCheckId, parseCourseId, parseLessonId, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, slugifyId, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ type CourseId = string;
2
2
  type LessonId = string;
3
3
  type CheckId = string;
4
4
  type BlockId = string;
5
+ /** Stable URN string returned by {@link buildLessonkitUrn}. */
6
+ type LessonkitUrn = string;
5
7
  type IdentityValidationIssue = {
6
8
  path: string;
7
9
  message: string;
@@ -13,12 +15,27 @@ type IdentityValidationResult = {
13
15
  ok: false;
14
16
  issues: IdentityValidationIssue[];
15
17
  };
18
+ type IdentityIdPath = "courseId" | "lessonId" | "checkId" | "blockId" | "id";
16
19
  /** LessonKit id format: letter first, then alphanumeric, `_`, `-`; length 1–64. */
17
20
  declare const ID_PATTERN: RegExp;
18
21
  declare const ID_MAX_LENGTH = 64;
19
22
 
20
- declare function validateId(input: unknown, path?: string): IdentityValidationResult;
21
- declare function assertValidId(input: unknown, path?: string): string;
23
+ /**
24
+ * Exhaustiveness helper for switch/default branches.
25
+ * @throws when called at runtime with an unexpected value.
26
+ */
27
+ declare function assertNever(value: never, message?: string): never;
28
+
29
+ declare function validateId(input: unknown, path?: IdentityIdPath | string): IdentityValidationResult;
30
+ declare function parseCourseId(input: unknown): CourseId | null;
31
+ declare function parseLessonId(input: unknown): LessonId | null;
32
+ declare function parseCheckId(input: unknown): CheckId | null;
33
+ declare function parseBlockId(input: unknown): BlockId | null;
34
+ declare function assertValidId(input: unknown, path: "courseId"): CourseId;
35
+ declare function assertValidId(input: unknown, path: "lessonId"): LessonId;
36
+ declare function assertValidId(input: unknown, path: "checkId"): CheckId;
37
+ declare function assertValidId(input: unknown, path: "blockId"): BlockId;
38
+ declare function assertValidId(input: unknown, path?: IdentityIdPath | string): string;
22
39
 
23
40
  /** Convert human-readable text to a candidate LessonKit id (may still need collision handling via deriveId). */
24
41
  declare function slugifyId(input: string): string;
@@ -35,7 +52,7 @@ type LessonkitUrnParts = {
35
52
  * Build a stable LessonKit URN for courses, lessons, checks, and blocks.
36
53
  * Segments are validated and encoded in path order.
37
54
  */
38
- declare function buildLessonkitUrn(parts: LessonkitUrnParts): string;
55
+ declare function buildLessonkitUrn(parts: LessonkitUrnParts): LessonkitUrn;
39
56
 
40
57
  type TelemetryEventName = "course_started" | "course_completed" | "lesson_started" | "lesson_completed" | "lesson_time_on_task" | "quiz_answered" | "quiz_completed" | "interaction";
41
58
  type TelemetryUser = {
@@ -109,6 +126,12 @@ type TelemetryEvent = (TelemetryEventBase & {
109
126
  lessonId?: LessonId;
110
127
  data?: InteractionData;
111
128
  });
129
+ /** Payload shape for a telemetry event name. */
130
+ type TelemetryDataFor<N extends TelemetryEventName> = Extract<TelemetryEvent, {
131
+ name: N;
132
+ }> extends {
133
+ data?: infer D;
134
+ } ? D : never;
112
135
  type TelemetrySink = (event: TelemetryEvent) => void | Promise<void>;
113
136
  type TelemetryBatchSink = (events: TelemetryEvent[]) => void | Promise<void>;
114
137
  type TrackingClient = {
@@ -143,16 +166,46 @@ declare function createSessionId(): string;
143
166
 
144
167
  declare function nowIso(): string;
145
168
 
146
- type BuildTelemetryEventInput = {
147
- name: TelemetryEventName;
169
+ type BuildTelemetryEventContext = {
148
170
  courseId: CourseId;
149
- lessonId?: LessonId;
150
171
  sessionId?: string;
151
172
  attemptId?: string;
152
173
  user?: TelemetryUser;
153
- data?: unknown;
154
174
  timestamp?: string;
155
175
  };
176
+ type BuildTelemetryEventInput = (BuildTelemetryEventContext & {
177
+ name: "course_started";
178
+ lessonId?: LessonId;
179
+ data?: undefined;
180
+ }) | (BuildTelemetryEventContext & {
181
+ name: "course_completed";
182
+ lessonId?: LessonId;
183
+ data?: undefined;
184
+ }) | (BuildTelemetryEventContext & {
185
+ name: "lesson_started";
186
+ lessonId?: LessonId;
187
+ data?: LessonLifecycleData;
188
+ }) | (BuildTelemetryEventContext & {
189
+ name: "lesson_completed";
190
+ lessonId?: LessonId;
191
+ data?: LessonLifecycleData;
192
+ }) | (BuildTelemetryEventContext & {
193
+ name: "lesson_time_on_task";
194
+ lessonId?: LessonId;
195
+ data?: LessonLifecycleData;
196
+ }) | (BuildTelemetryEventContext & {
197
+ name: "quiz_answered";
198
+ lessonId?: LessonId;
199
+ data: QuizAnsweredData;
200
+ }) | (BuildTelemetryEventContext & {
201
+ name: "quiz_completed";
202
+ lessonId?: LessonId;
203
+ data: QuizCompletedData;
204
+ }) | (BuildTelemetryEventContext & {
205
+ name: "interaction";
206
+ lessonId?: LessonId;
207
+ data?: InteractionData;
208
+ });
156
209
  /** Reset dev-warning state (tests only). */
157
210
  declare function resetTelemetryBuilderWarningsForTests(): void;
158
211
  /**
@@ -166,7 +219,7 @@ declare function buildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryE
166
219
  declare function tryBuildTelemetryEvent(opts: BuildTelemetryEventInput): TelemetryEvent | null;
167
220
 
168
221
  type EmitContext = {
169
- courseId: string;
222
+ courseId: CourseId;
170
223
  sessionId?: string;
171
224
  attemptId?: string;
172
225
  };
@@ -230,6 +283,8 @@ declare function hasCourseStarted(storage: StoragePort, sessionId: string, cours
230
283
  declare function markCourseStarted(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
231
284
  declare function hasCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
232
285
  declare function markCourseStartedEmittedToTracking(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
286
+ declare function hasCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): boolean;
287
+ declare function markCourseStartedPipelineDelivered(storage: StoragePort, sessionId: string, courseId?: CourseId): void;
233
288
  declare function migrateCourseStartedMark(storage: StoragePort, fromSessionId: string, toSessionId: string, courseId?: CourseId): void;
234
289
 
235
290
  /** Plugin category — aligns with roadmap extension areas. */
@@ -241,8 +296,8 @@ type LessonkitPluginContext = {
241
296
  user?: TelemetryUser;
242
297
  };
243
298
  type AssessmentScoreInput = {
244
- checkId: string;
245
- lessonId?: string;
299
+ checkId: CheckId;
300
+ lessonId?: LessonId;
246
301
  response: unknown;
247
302
  };
248
303
  type AssessmentScoreResult = {
@@ -347,6 +402,9 @@ type HeadlessRuntimePorts = {
347
402
  storage?: StoragePort;
348
403
  clock?: ClockPort;
349
404
  };
405
+ type TelemetryEmitFn = {
406
+ <N extends TelemetryEventName>(name: N, data?: TelemetryDataFor<N>, lessonId?: LessonId): void;
407
+ };
350
408
  type HeadlessLessonkitRuntime = {
351
409
  readonly config: HeadlessLessonkitConfig;
352
410
  readonly progress: ProgressController;
@@ -357,10 +415,10 @@ type HeadlessLessonkitRuntime = {
357
415
  user?: TelemetryUser;
358
416
  };
359
417
  updateConfig: (next: Partial<HeadlessLessonkitConfig>) => void;
360
- setActiveLesson: (lessonId: LessonId, emit: (name: TelemetryEventName, data?: unknown, lessonId?: LessonId) => void) => void;
361
- completeLesson: (lessonId: LessonId, emit: (name: TelemetryEventName, data?: unknown, lessonId?: LessonId) => void) => void;
362
- completeCourse: (emit: (name: TelemetryEventName, data?: unknown, lessonId?: LessonId) => void) => void;
363
- track: (name: TelemetryEventName, data: unknown | undefined, emit: (event: ReturnType<typeof tryBuildTelemetryEvent>) => void, lessonId?: LessonId) => void;
418
+ setActiveLesson: (lessonId: LessonId, emit: TelemetryEmitFn) => void;
419
+ completeLesson: (lessonId: LessonId, emit: TelemetryEmitFn) => void;
420
+ completeCourse: (emit: TelemetryEmitFn) => void;
421
+ track: <N extends TelemetryEventName>(name: N, data: TelemetryDataFor<N> | undefined, emit: (event: TelemetryEvent) => void, lessonId?: LessonId) => void;
364
422
  resetForCourseChange: (courseId: CourseId) => void;
365
423
  };
366
424
  declare function createLessonkitRuntime(config: HeadlessLessonkitConfig, ports?: HeadlessRuntimePorts): HeadlessLessonkitRuntime;
@@ -371,4 +429,4 @@ declare function defineTelemetryPlugin(plugin: TelemetryPlugin): LessonkitPlugin
371
429
  declare function defineAssessmentPlugin(plugin: AssessmentPlugin): LessonkitPlugin;
372
430
  declare function defineLifecyclePlugin(plugin: LifecyclePlugin): LessonkitPlugin;
373
431
 
374
- export { type AssessmentPlugin, type AssessmentScoreInput, type AssessmentScoreResult, type BlockId, type BuildTelemetryEventInput, type CheckId, type ClockPort, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, ID_MAX_LENGTH, ID_PATTERN, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrnParts, type LifecyclePlugin, type PluginHost, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildTelemetryCatalog, buildTelemetryEvent, completeCourseWithTelemetry, completeLessonWithTelemetry, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, markCourseStarted, markCourseStartedEmittedToTracking, migrateCourseStartedMark, nowIso, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, slugifyId, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
432
+ export { type AssessmentPlugin, type AssessmentScoreInput, type AssessmentScoreResult, type BlockId, type BuildTelemetryEventInput, type CheckId, type ClockPort, type CourseId, type CourseLifecycleContext, type CourseLifecycleDeps, type EmitContext, type HeadlessLessonkitConfig, type HeadlessLessonkitRuntime, type HeadlessRuntimePorts, ID_MAX_LENGTH, ID_PATTERN, type IdentityIdPath, type IdentityValidationIssue, type IdentityValidationResult, type InteractionBlockRegistration, type InteractionData, type InteractionPlugin, type LessonCompletionEmitter, type LessonId, type LessonLifecycleData, type LessonkitPlugin, type LessonkitPluginContext, type LessonkitPluginKind, type LessonkitRuntimeVersion, type LessonkitUrn, type LessonkitUrnParts, type LifecyclePlugin, type PluginHost, type PluginIdentity, type PluginRegistry, type ProgressController, type ProgressState, type QuizAnsweredData, type QuizCompletedData, SESSION_STORAGE_KEY, type StoragePort, TELEMETRY_EVENT_CATALOG, type TelemetryBatchSink, type TelemetryCatalogEntry, type TelemetryDataFor, type TelemetryEmitFn, type TelemetryEvent, type TelemetryEventBase, type TelemetryEventName, type TelemetryPipeline, type TelemetryPipelineSink, type TelemetryPlugin, type TelemetrySink, type TelemetryUser, type TimerPort, type TrackingClient, assertNever, assertValidId, buildCourseStartedTelemetryEvent, buildLessonkitUrn, buildTelemetryCatalog, buildTelemetryEvent, completeCourseWithTelemetry, completeLessonWithTelemetry, createDefaultClock, createGlobalTimer, createLessonkitRuntime, createNoopStorage, createPluginRegistry, createProgressController, createSessionId, createSessionStoragePort, createTelemetryPipeline, createTrackingClient, createTrackingPipelineSink, defineAssessmentPlugin, defineLifecyclePlugin, defineTelemetryPlugin, deriveId, getTabSessionId, hasCourseStarted, hasCourseStartedEmittedToTracking, hasCourseStartedPipelineDelivered, markCourseStarted, markCourseStartedEmittedToTracking, markCourseStartedPipelineDelivered, migrateCourseStartedMark, nowIso, parseBlockId, parseCheckId, parseCourseId, parseLessonId, resetStoragePortForTests, resetTelemetryBuilderWarningsForTests, resolveSessionId, slugifyId, telemetryCatalogVersion, tryBuildTelemetryEvent, tryEmitCourseStarted, validateId };
package/dist/index.js CHANGED
@@ -2,6 +2,11 @@
2
2
  var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
3
3
  var ID_MAX_LENGTH = 64;
4
4
 
5
+ // src/assertNever.ts
6
+ function assertNever(value, message = "Unexpected value") {
7
+ throw new Error(`${message}: ${String(value)}`);
8
+ }
9
+
5
10
  // src/validateId.ts
6
11
  function validateId(input, path = "id") {
7
12
  if (typeof input !== "string") {
@@ -30,6 +35,22 @@ function validateId(input, path = "id") {
30
35
  }
31
36
  return { ok: true, id };
32
37
  }
38
+ function parseCourseId(input) {
39
+ const result = validateId(input, "courseId");
40
+ return result.ok ? result.id : null;
41
+ }
42
+ function parseLessonId(input) {
43
+ const result = validateId(input, "lessonId");
44
+ return result.ok ? result.id : null;
45
+ }
46
+ function parseCheckId(input) {
47
+ const result = validateId(input, "checkId");
48
+ return result.ok ? result.id : null;
49
+ }
50
+ function parseBlockId(input) {
51
+ const result = validateId(input, "blockId");
52
+ return result.ok ? result.id : null;
53
+ }
33
54
  function assertValidId(input, path = "id") {
34
55
  const result = validateId(input, path);
35
56
  if (!result.ok) {
@@ -185,6 +206,11 @@ function invokeTrackingSink(sink, event) {
185
206
  function createTrackingClient(opts) {
186
207
  const sink = opts?.sink;
187
208
  const batchSink = opts?.batchSink;
209
+ if (batchSink != null && opts?.batch?.enabled === false) {
210
+ throw new Error(
211
+ "[lessonkit] tracking.batchSink cannot be used with batch.enabled: false; omit batch.enabled or set it to true"
212
+ );
213
+ }
188
214
  const batchEnabled = opts?.batch?.enabled ?? Boolean(batchSink);
189
215
  const flushIntervalMs = opts?.batch?.flushIntervalMs ?? 5e3;
190
216
  const maxBatchSize = opts?.batch?.maxBatchSize ?? 25;
@@ -256,7 +282,7 @@ function createTrackingClient(opts) {
256
282
  if (disposed || disposing) return;
257
283
  if (buffer.length >= maxBufferSize) {
258
284
  buffer.shift();
259
- if (!warnedBufferCap && typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
285
+ if (!warnedBufferCap && isDevEnvironment()) {
260
286
  warnedBufferCap = true;
261
287
  console.warn(
262
288
  `[lessonkit] telemetry batch buffer capped at ${maxBufferSize} events; oldest events are dropped while the sink is unavailable.`
@@ -303,6 +329,11 @@ function isDevEnvironment2() {
303
329
  function resetTelemetryBuilderWarningsForTests() {
304
330
  warnedMissingQuizLesson = false;
305
331
  }
332
+ function resolveLessonId(opts, eventName) {
333
+ const lessonId = opts.lessonId ?? opts.data?.lessonId;
334
+ if (!lessonId) throw new Error(`${eventName} requires lessonId`);
335
+ return lessonId;
336
+ }
306
337
  function buildTelemetryEvent(opts) {
307
338
  const base = {
308
339
  timestamp: opts.timestamp ?? nowIso(),
@@ -317,39 +348,33 @@ function buildTelemetryEvent(opts) {
317
348
  case "course_completed":
318
349
  return { name: "course_completed", ...base };
319
350
  case "lesson_started": {
320
- const data = opts.data;
321
- const lessonId = opts.lessonId ?? data?.lessonId;
322
- if (!lessonId) throw new Error("lesson_started requires lessonId");
351
+ const lessonId = resolveLessonId(opts, "lesson_started");
323
352
  return {
324
353
  name: "lesson_started",
325
354
  ...base,
326
355
  lessonId,
327
- data: { ...data, lessonId }
356
+ data: { ...opts.data, lessonId }
328
357
  };
329
358
  }
330
359
  case "lesson_completed":
331
360
  case "lesson_time_on_task": {
332
- const data = opts.data;
333
- const lessonId = opts.lessonId ?? data?.lessonId;
334
- if (!lessonId) throw new Error(`${opts.name} requires lessonId`);
361
+ const lessonId = resolveLessonId(opts, opts.name);
335
362
  return {
336
363
  name: opts.name,
337
364
  ...base,
338
365
  lessonId,
339
- data: { ...data, lessonId }
366
+ data: { ...opts.data, lessonId }
340
367
  };
341
368
  }
342
369
  case "quiz_answered": {
343
- const data = opts.data;
344
370
  const lessonId = opts.lessonId;
345
371
  if (!lessonId) throw new Error("quiz_answered requires active lessonId");
346
- return { name: "quiz_answered", ...base, lessonId, data };
372
+ return { name: "quiz_answered", ...base, lessonId, data: opts.data };
347
373
  }
348
374
  case "quiz_completed": {
349
- const data = opts.data;
350
375
  const lessonId = opts.lessonId;
351
376
  if (!lessonId) throw new Error("quiz_completed requires active lessonId");
352
- return { name: "quiz_completed", ...base, lessonId, data };
377
+ return { name: "quiz_completed", ...base, lessonId, data: opts.data };
353
378
  }
354
379
  case "interaction":
355
380
  return {
@@ -359,7 +384,7 @@ function buildTelemetryEvent(opts) {
359
384
  data: opts.data
360
385
  };
361
386
  default:
362
- return { name: opts.name, ...base };
387
+ return assertNever(opts);
363
388
  }
364
389
  }
365
390
  function tryBuildTelemetryEvent(opts) {
@@ -446,7 +471,8 @@ function createMemoryBackedSessionStorage(session) {
446
471
  const warnPersistFailure = () => {
447
472
  if (warnedPersistFailure) return;
448
473
  warnedPersistFailure = true;
449
- if (typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
474
+ const g = globalThis;
475
+ if (typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "development") {
450
476
  console.warn(
451
477
  "[lessonkit] sessionStorage is unavailable or failed; using in-memory session dedupe for this tab (may reset on full reload)."
452
478
  );
@@ -487,23 +513,37 @@ function createMemoryBackedSessionStorage(session) {
487
513
  function resetStoragePortForTests(storage) {
488
514
  storage.resetForTests?.();
489
515
  }
516
+ function createInMemorySessionStoragePort() {
517
+ const memory = /* @__PURE__ */ new Map();
518
+ return {
519
+ getItem: (key) => memory.get(key) ?? null,
520
+ setItem: (key, value) => {
521
+ memory.set(key, value);
522
+ },
523
+ removeItem: (key) => {
524
+ memory.delete(key);
525
+ },
526
+ resetForTests: () => {
527
+ memory.clear();
528
+ }
529
+ };
530
+ }
531
+ function resolveBrowserSessionStorage() {
532
+ try {
533
+ if (typeof sessionStorage === "undefined" || sessionStorage == null) {
534
+ return null;
535
+ }
536
+ return sessionStorage;
537
+ } catch {
538
+ return null;
539
+ }
540
+ }
490
541
  function createSessionStoragePort() {
491
- if (typeof sessionStorage === "undefined") {
492
- const memory = /* @__PURE__ */ new Map();
493
- return {
494
- getItem: (key) => memory.get(key) ?? null,
495
- setItem: (key, value) => {
496
- memory.set(key, value);
497
- },
498
- removeItem: (key) => {
499
- memory.delete(key);
500
- },
501
- resetForTests: () => {
502
- memory.clear();
503
- }
504
- };
542
+ const session = resolveBrowserSessionStorage();
543
+ if (!session) {
544
+ return createInMemorySessionStoragePort();
505
545
  }
506
- return createMemoryBackedSessionStorage(sessionStorage);
546
+ return createMemoryBackedSessionStorage(session);
507
547
  }
508
548
  function createGlobalTimer() {
509
549
  return {
@@ -556,6 +596,7 @@ function getTabSessionId(storage) {
556
596
  }
557
597
  var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
558
598
  var COURSE_STARTED_TRACKING_PREFIX = "lessonkit:course_started_tracking:";
599
+ var COURSE_STARTED_PIPELINE_PREFIX = "lessonkit:course_started_pipeline:";
559
600
  function resolveSessionId(storage, provided) {
560
601
  if (provided) return provided;
561
602
  const existing = storage.getItem(SESSION_STORAGE_KEY);
@@ -570,6 +611,9 @@ function courseStartedStorageKey(sessionId, courseId) {
570
611
  function courseStartedTrackingStorageKey(sessionId, courseId) {
571
612
  return `${COURSE_STARTED_TRACKING_PREFIX}${sessionId}:${courseId ?? ""}`;
572
613
  }
614
+ function courseStartedPipelineStorageKey(sessionId, courseId) {
615
+ return `${COURSE_STARTED_PIPELINE_PREFIX}${sessionId}:${courseId ?? ""}`;
616
+ }
573
617
  function hasCourseStarted(storage, sessionId, courseId) {
574
618
  if (!courseId) return false;
575
619
  return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
@@ -586,6 +630,14 @@ function markCourseStartedEmittedToTracking(storage, sessionId, courseId) {
586
630
  if (!courseId) return;
587
631
  storage.setItem(courseStartedTrackingStorageKey(sessionId, courseId), "1");
588
632
  }
633
+ function hasCourseStartedPipelineDelivered(storage, sessionId, courseId) {
634
+ if (!courseId) return false;
635
+ return storage.getItem(courseStartedPipelineStorageKey(sessionId, courseId)) === "1";
636
+ }
637
+ function markCourseStartedPipelineDelivered(storage, sessionId, courseId) {
638
+ if (!courseId) return;
639
+ storage.setItem(courseStartedPipelineStorageKey(sessionId, courseId), "1");
640
+ }
589
641
  function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId) {
590
642
  if (!courseId || fromSessionId === toSessionId) return;
591
643
  if (hasCourseStarted(storage, fromSessionId, courseId)) {
@@ -596,6 +648,10 @@ function migrateCourseStartedMark(storage, fromSessionId, toSessionId, courseId)
596
648
  markCourseStartedEmittedToTracking(storage, toSessionId, courseId);
597
649
  storage.removeItem?.(courseStartedTrackingStorageKey(fromSessionId, courseId));
598
650
  }
651
+ if (hasCourseStartedPipelineDelivered(storage, fromSessionId, courseId)) {
652
+ markCourseStartedPipelineDelivered(storage, toSessionId, courseId);
653
+ storage.removeItem?.(courseStartedPipelineStorageKey(fromSessionId, courseId));
654
+ }
599
655
  }
600
656
 
601
657
  // src/runtime/courseLifecycle.ts
@@ -844,6 +900,7 @@ export {
844
900
  ID_PATTERN,
845
901
  SESSION_STORAGE_KEY,
846
902
  TELEMETRY_EVENT_CATALOG,
903
+ assertNever,
847
904
  assertValidId,
848
905
  buildCourseStartedTelemetryEvent,
849
906
  buildLessonkitUrn,
@@ -869,10 +926,16 @@ export {
869
926
  getTabSessionId,
870
927
  hasCourseStarted,
871
928
  hasCourseStartedEmittedToTracking,
929
+ hasCourseStartedPipelineDelivered,
872
930
  markCourseStarted,
873
931
  markCourseStartedEmittedToTracking,
932
+ markCourseStartedPipelineDelivered,
874
933
  migrateCourseStartedMark,
875
934
  nowIso,
935
+ parseBlockId,
936
+ parseCheckId,
937
+ parseCourseId,
938
+ parseLessonId,
876
939
  resetStoragePortForTests,
877
940
  resetTelemetryBuilderWarningsForTests,
878
941
  resolveSessionId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/core",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "private": false,
5
5
  "description": "Shared types and telemetry primitives for LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -48,8 +48,9 @@
48
48
  "lint": "echo \"(no lint configured yet)\""
49
49
  },
50
50
  "devDependencies": {
51
+ "@types/node": "^22.13.10",
51
52
  "tsup": "^8.5.0",
52
53
  "typescript": "^5.8.3",
53
- "vitest": "^3.2.4"
54
+ "vitest": "^4.1.8"
54
55
  }
55
56
  }