@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 +64 -4
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +64 -4
- package/package.json +2 -2
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
51
|
+
"@lessonkit/core": "1.6.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"tsup": "^8.5.0",
|