@lessonkit/xapi 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -218,16 +218,20 @@ function createInMemoryXAPIQueue(opts) {
218
218
  if (buffer.length >= maxSize) {
219
219
  if (headInFlight) {
220
220
  if (buffer.length > 1) {
221
+ const evicted = buffer[1];
221
222
  buffer.splice(1, 1);
222
223
  opts?.onCap?.();
224
+ opts?.onOverflow?.(evicted);
223
225
  } else {
224
226
  opts?.onCap?.();
225
227
  opts?.onOverflow?.(normalized);
226
228
  return;
227
229
  }
228
230
  } else {
231
+ const evicted = buffer[0];
229
232
  buffer.shift();
230
233
  opts?.onCap?.();
234
+ opts?.onOverflow?.(evicted);
231
235
  }
232
236
  }
233
237
  buffer.push(normalized);
@@ -244,7 +248,9 @@ function createInMemoryXAPIQueue(opts) {
244
248
  return flushInFlight;
245
249
  },
246
250
  flushOnExit: (exitTransport) => {
251
+ const skipId = headInFlightId;
247
252
  for (const statement of buffer) {
253
+ if (statement.id === skipId) continue;
248
254
  try {
249
255
  exitTransport(statement);
250
256
  } catch {
@@ -286,13 +292,17 @@ function loadDeadLetterStatements() {
286
292
  return [];
287
293
  }
288
294
  }
289
- function persistDeadLetterStatement(statement) {
295
+ function persistDeadLetterStatement(statement, opts) {
290
296
  const storage = readStorage();
291
297
  if (!storage) return;
292
298
  try {
293
299
  const existing = loadDeadLetterStatements();
294
300
  if (existing.some((s) => s.id === statement.id)) return;
295
- const next = [...existing, statement].slice(-MAX_DEAD_LETTER);
301
+ const combined = [...existing, statement];
302
+ if (combined.length > MAX_DEAD_LETTER) {
303
+ opts?.onTruncated?.(combined.length - MAX_DEAD_LETTER);
304
+ }
305
+ const next = combined.slice(-MAX_DEAD_LETTER);
296
306
  storage.setItem(STORAGE_KEY, JSON.stringify(next));
297
307
  } catch {
298
308
  }
@@ -426,6 +436,7 @@ var TELEMETRY_XAPI_MAPPERS = {
426
436
  lesson_completed: (event, ctx) => {
427
437
  if (event.name !== "lesson_completed") return null;
428
438
  const lessonId = event.lessonId;
439
+ if (!lessonId) return null;
429
440
  const data = event.data;
430
441
  const result = {};
431
442
  if (typeof data?.durationMs === "number") {
@@ -553,6 +564,50 @@ var TELEMETRY_XAPI_MAPPERS = {
553
564
  XAPIVerbs.experienced,
554
565
  ctx.timestamp
555
566
  );
567
+ },
568
+ image_juxtaposition_changed: experiencedBlockMapper,
569
+ timeline_event_viewed: experiencedBlockMapper,
570
+ image_sequence_changed: experiencedBlockMapper,
571
+ audio_recording_started: experiencedBlockMapper,
572
+ audio_recording_completed: (event, ctx) => {
573
+ if (event.name !== "audio_recording_completed") return null;
574
+ const lessonId = event.lessonId;
575
+ const blockId = event.data.blockId;
576
+ if (!blockId) return null;
577
+ return statementFor(
578
+ event,
579
+ (0, import_core.buildLessonkitUrn)({ courseId: ctx.courseId, lessonId, blockId }),
580
+ XAPIVerbs.completed,
581
+ ctx.timestamp
582
+ );
583
+ },
584
+ qr_content_revealed: experiencedBlockMapper,
585
+ advent_door_opened: experiencedBlockMapper,
586
+ map_stage_viewed: (event, ctx) => {
587
+ if (event.name !== "map_stage_viewed") return null;
588
+ const lessonId = event.lessonId;
589
+ const blockId = event.data.blockId;
590
+ const stageId = event.data.stageId;
591
+ if (!lessonId || !blockId || !stageId) return null;
592
+ return statementFor(
593
+ event,
594
+ (0, import_core.buildLessonkitUrn)({ courseId: ctx.courseId, lessonId, blockId, nodeId: stageId }),
595
+ XAPIVerbs.experienced,
596
+ ctx.timestamp
597
+ );
598
+ },
599
+ map_exit_selected: (event, ctx) => {
600
+ if (event.name !== "map_exit_selected") return null;
601
+ const lessonId = event.lessonId;
602
+ const blockId = event.data.blockId;
603
+ const toStageId = event.data.toStageId;
604
+ if (!lessonId || !blockId || !toStageId) return null;
605
+ return statementFor(
606
+ event,
607
+ (0, import_core.buildLessonkitUrn)({ courseId: ctx.courseId, lessonId, blockId, nodeId: toStageId }),
608
+ XAPIVerbs.experienced,
609
+ ctx.timestamp
610
+ );
556
611
  }
557
612
  };
558
613
  function telemetryEventToXAPIStatement(event) {
@@ -604,10 +659,14 @@ function createXAPIClient(opts) {
604
659
  onDepth: opts?.onQueueDepth,
605
660
  onCap: opts?.onQueueCap ?? defaultQueueCapHandler,
606
661
  onOverflow: (statement) => {
607
- persistDeadLetterStatement(statement);
662
+ persistDeadLetterStatement(statement, {
663
+ onTruncated: opts?.onDeadLetterTruncated
664
+ });
608
665
  },
609
666
  onHeadSkipped: (statement, err) => {
610
- persistDeadLetterStatement(statement);
667
+ persistDeadLetterStatement(statement, {
668
+ onTruncated: opts?.onDeadLetterTruncated
669
+ });
611
670
  (opts?.onHeadSkipped ?? defaultHeadSkippedHandler)(statement, err);
612
671
  }
613
672
  });
@@ -700,6 +759,7 @@ function createXAPIClient(opts) {
700
759
  }
701
760
  return;
702
761
  }
762
+ queue.removeById(normalized.id);
703
763
  inflightStatements.set(normalized.id, normalized);
704
764
  inflightPayload.set(normalized.id, normalized);
705
765
  const flight = Promise.resolve().then(async () => {
package/dist/index.d.cts CHANGED
@@ -94,6 +94,8 @@ declare function createXAPIClient(opts?: {
94
94
  maxHeadFailures?: number;
95
95
  onQueueDepth?: (size: number) => void;
96
96
  onQueueCap?: () => void;
97
+ /** Called when dead-letter storage drops older entries beyond the cap (200). */
98
+ onDeadLetterTruncated?: (droppedCount: number) => void;
97
99
  onHeadSkipped?: (statement: XAPIStatement, err: unknown) => void;
98
100
  /** Called when transport fails after retries (statement is re-queued). */
99
101
  onTransportError?: (err: unknown) => void;
@@ -160,7 +162,9 @@ type FetchBatchSinkBundle = {
160
162
  declare function createFetchBatchSink(opts: CreateFetchBatchSinkOptions): FetchBatchSinkBundle;
161
163
 
162
164
  declare function loadDeadLetterStatements(): XAPIStatement[];
163
- declare function persistDeadLetterStatement(statement: XAPIStatement): void;
165
+ declare function persistDeadLetterStatement(statement: XAPIStatement, opts?: {
166
+ onTruncated?: (droppedCount: number) => void;
167
+ }): void;
164
168
 
165
169
  /**
166
170
  * Map a LessonKit telemetry event to an xAPI statement, or null if the event should not emit xAPI.
package/dist/index.d.ts CHANGED
@@ -94,6 +94,8 @@ declare function createXAPIClient(opts?: {
94
94
  maxHeadFailures?: number;
95
95
  onQueueDepth?: (size: number) => void;
96
96
  onQueueCap?: () => void;
97
+ /** Called when dead-letter storage drops older entries beyond the cap (200). */
98
+ onDeadLetterTruncated?: (droppedCount: number) => void;
97
99
  onHeadSkipped?: (statement: XAPIStatement, err: unknown) => void;
98
100
  /** Called when transport fails after retries (statement is re-queued). */
99
101
  onTransportError?: (err: unknown) => void;
@@ -160,7 +162,9 @@ type FetchBatchSinkBundle = {
160
162
  declare function createFetchBatchSink(opts: CreateFetchBatchSinkOptions): FetchBatchSinkBundle;
161
163
 
162
164
  declare function loadDeadLetterStatements(): XAPIStatement[];
163
- declare function persistDeadLetterStatement(statement: XAPIStatement): void;
165
+ declare function persistDeadLetterStatement(statement: XAPIStatement, opts?: {
166
+ onTruncated?: (droppedCount: number) => void;
167
+ }): void;
164
168
 
165
169
  /**
166
170
  * Map a LessonKit telemetry event to an xAPI statement, or null if the event should not emit xAPI.
package/dist/index.js CHANGED
@@ -181,16 +181,20 @@ function createInMemoryXAPIQueue(opts) {
181
181
  if (buffer.length >= maxSize) {
182
182
  if (headInFlight) {
183
183
  if (buffer.length > 1) {
184
+ const evicted = buffer[1];
184
185
  buffer.splice(1, 1);
185
186
  opts?.onCap?.();
187
+ opts?.onOverflow?.(evicted);
186
188
  } else {
187
189
  opts?.onCap?.();
188
190
  opts?.onOverflow?.(normalized);
189
191
  return;
190
192
  }
191
193
  } else {
194
+ const evicted = buffer[0];
192
195
  buffer.shift();
193
196
  opts?.onCap?.();
197
+ opts?.onOverflow?.(evicted);
194
198
  }
195
199
  }
196
200
  buffer.push(normalized);
@@ -207,7 +211,9 @@ function createInMemoryXAPIQueue(opts) {
207
211
  return flushInFlight;
208
212
  },
209
213
  flushOnExit: (exitTransport) => {
214
+ const skipId = headInFlightId;
210
215
  for (const statement of buffer) {
216
+ if (statement.id === skipId) continue;
211
217
  try {
212
218
  exitTransport(statement);
213
219
  } catch {
@@ -249,13 +255,17 @@ function loadDeadLetterStatements() {
249
255
  return [];
250
256
  }
251
257
  }
252
- function persistDeadLetterStatement(statement) {
258
+ function persistDeadLetterStatement(statement, opts) {
253
259
  const storage = readStorage();
254
260
  if (!storage) return;
255
261
  try {
256
262
  const existing = loadDeadLetterStatements();
257
263
  if (existing.some((s) => s.id === statement.id)) return;
258
- const next = [...existing, statement].slice(-MAX_DEAD_LETTER);
264
+ const combined = [...existing, statement];
265
+ if (combined.length > MAX_DEAD_LETTER) {
266
+ opts?.onTruncated?.(combined.length - MAX_DEAD_LETTER);
267
+ }
268
+ const next = combined.slice(-MAX_DEAD_LETTER);
259
269
  storage.setItem(STORAGE_KEY, JSON.stringify(next));
260
270
  } catch {
261
271
  }
@@ -389,6 +399,7 @@ var TELEMETRY_XAPI_MAPPERS = {
389
399
  lesson_completed: (event, ctx) => {
390
400
  if (event.name !== "lesson_completed") return null;
391
401
  const lessonId = event.lessonId;
402
+ if (!lessonId) return null;
392
403
  const data = event.data;
393
404
  const result = {};
394
405
  if (typeof data?.durationMs === "number") {
@@ -516,6 +527,50 @@ var TELEMETRY_XAPI_MAPPERS = {
516
527
  XAPIVerbs.experienced,
517
528
  ctx.timestamp
518
529
  );
530
+ },
531
+ image_juxtaposition_changed: experiencedBlockMapper,
532
+ timeline_event_viewed: experiencedBlockMapper,
533
+ image_sequence_changed: experiencedBlockMapper,
534
+ audio_recording_started: experiencedBlockMapper,
535
+ audio_recording_completed: (event, ctx) => {
536
+ if (event.name !== "audio_recording_completed") return null;
537
+ const lessonId = event.lessonId;
538
+ const blockId = event.data.blockId;
539
+ if (!blockId) return null;
540
+ return statementFor(
541
+ event,
542
+ buildLessonkitUrn({ courseId: ctx.courseId, lessonId, blockId }),
543
+ XAPIVerbs.completed,
544
+ ctx.timestamp
545
+ );
546
+ },
547
+ qr_content_revealed: experiencedBlockMapper,
548
+ advent_door_opened: experiencedBlockMapper,
549
+ map_stage_viewed: (event, ctx) => {
550
+ if (event.name !== "map_stage_viewed") return null;
551
+ const lessonId = event.lessonId;
552
+ const blockId = event.data.blockId;
553
+ const stageId = event.data.stageId;
554
+ if (!lessonId || !blockId || !stageId) return null;
555
+ return statementFor(
556
+ event,
557
+ buildLessonkitUrn({ courseId: ctx.courseId, lessonId, blockId, nodeId: stageId }),
558
+ XAPIVerbs.experienced,
559
+ ctx.timestamp
560
+ );
561
+ },
562
+ map_exit_selected: (event, ctx) => {
563
+ if (event.name !== "map_exit_selected") return null;
564
+ const lessonId = event.lessonId;
565
+ const blockId = event.data.blockId;
566
+ const toStageId = event.data.toStageId;
567
+ if (!lessonId || !blockId || !toStageId) return null;
568
+ return statementFor(
569
+ event,
570
+ buildLessonkitUrn({ courseId: ctx.courseId, lessonId, blockId, nodeId: toStageId }),
571
+ XAPIVerbs.experienced,
572
+ ctx.timestamp
573
+ );
519
574
  }
520
575
  };
521
576
  function telemetryEventToXAPIStatement(event) {
@@ -567,10 +622,14 @@ function createXAPIClient(opts) {
567
622
  onDepth: opts?.onQueueDepth,
568
623
  onCap: opts?.onQueueCap ?? defaultQueueCapHandler,
569
624
  onOverflow: (statement) => {
570
- persistDeadLetterStatement(statement);
625
+ persistDeadLetterStatement(statement, {
626
+ onTruncated: opts?.onDeadLetterTruncated
627
+ });
571
628
  },
572
629
  onHeadSkipped: (statement, err) => {
573
- persistDeadLetterStatement(statement);
630
+ persistDeadLetterStatement(statement, {
631
+ onTruncated: opts?.onDeadLetterTruncated
632
+ });
574
633
  (opts?.onHeadSkipped ?? defaultHeadSkippedHandler)(statement, err);
575
634
  }
576
635
  });
@@ -663,6 +722,7 @@ function createXAPIClient(opts) {
663
722
  }
664
723
  return;
665
724
  }
725
+ queue.removeById(normalized.id);
666
726
  inflightStatements.set(normalized.id, normalized);
667
727
  inflightPayload.set(normalized.id, normalized);
668
728
  const flight = Promise.resolve().then(async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/xapi",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "private": false,
5
5
  "description": "xAPI statement generation primitives for LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -48,7 +48,7 @@
48
48
  "lint": "eslint --max-warnings 0 \"src/**/*.{ts,tsx}\" \"test/**/*.{ts,tsx}\""
49
49
  },
50
50
  "dependencies": {
51
- "@lessonkit/core": "1.5.0"
51
+ "@lessonkit/core": "1.6.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "tsup": "^8.5.0",