@lessonkit/xapi 1.1.0 → 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/README.md CHANGED
@@ -36,6 +36,7 @@ Map from telemetry events: `telemetryEventToXAPIStatement(event)` — uses canon
36
36
 
37
37
  - No transport → statements queue in memory (dev warns once).
38
38
  - Transport failure → re-queue; call `flush()` to retry.
39
+ - Queue capped at **1000** statements by default; oldest dropped when full (`onCap` / `createInMemoryXAPIQueue({ onCap })`).
39
40
  - Concurrent `flush()` calls are coalesced.
40
41
 
41
42
  ## Docs
package/dist/index.cjs CHANGED
@@ -27,15 +27,21 @@ __export(index_exports, {
27
27
  module.exports = __toCommonJS(index_exports);
28
28
 
29
29
  // src/queue.ts
30
- function createInMemoryXAPIQueue() {
30
+ var DEFAULT_MAX_QUEUE_SIZE = 1e3;
31
+ function createInMemoryXAPIQueue(opts) {
32
+ const maxSize = opts?.maxSize ?? DEFAULT_MAX_QUEUE_SIZE;
31
33
  const buffer = [];
32
34
  let flushInFlight = null;
35
+ const notifyDepth = () => {
36
+ opts?.onDepth?.(buffer.length);
37
+ };
33
38
  const runFlush = async (transport) => {
34
39
  while (buffer.length) {
35
40
  const statement = buffer[0];
36
41
  try {
37
42
  await transport(statement);
38
43
  buffer.shift();
44
+ notifyDepth();
39
45
  } catch {
40
46
  return;
41
47
  }
@@ -44,7 +50,12 @@ function createInMemoryXAPIQueue() {
44
50
  return {
45
51
  enqueue: (statement) => {
46
52
  if (statement.id && buffer.some((s) => s.id === statement.id)) return;
53
+ if (buffer.length >= maxSize) {
54
+ buffer.shift();
55
+ opts?.onCap?.();
56
+ }
47
57
  buffer.push(statement);
58
+ notifyDepth();
48
59
  },
49
60
  size: () => buffer.length,
50
61
  flush: async (transport) => {
@@ -59,11 +70,10 @@ function createInMemoryXAPIQueue() {
59
70
  }
60
71
 
61
72
  // src/client.ts
62
- var import_core3 = require("@lessonkit/core");
73
+ var import_core2 = require("@lessonkit/core");
63
74
 
64
75
  // src/telemetryMap.ts
65
76
  var import_core = require("@lessonkit/core");
66
- var import_core2 = require("@lessonkit/core");
67
77
 
68
78
  // src/id.ts
69
79
  function cryptoRandomId() {
@@ -87,130 +97,16 @@ var XAPIVerbs = {
87
97
  answered: "http://adlnet.gov/expapi/verbs/answered",
88
98
  experienced: "http://adlnet.gov/expapi/verbs/experienced"
89
99
  };
90
- function telemetryEventToXAPIStatement(event) {
91
- const { courseId } = event;
92
- switch (event.name) {
93
- case "course_started":
94
- return statementFor((0, import_core2.buildLessonkitUrn)({ courseId }), XAPIVerbs.initialized, event.timestamp);
95
- case "course_completed":
96
- return statementFor((0, import_core2.buildLessonkitUrn)({ courseId }), XAPIVerbs.completed, event.timestamp);
97
- case "lesson_started": {
98
- const lessonId = event.lessonId;
99
- return statementFor(
100
- (0, import_core2.buildLessonkitUrn)({ courseId, lessonId }),
101
- XAPIVerbs.initialized,
102
- event.timestamp
103
- );
104
- }
105
- case "lesson_completed": {
106
- const lessonId = event.lessonId;
107
- const data = event.data;
108
- const result = {};
109
- if (typeof data?.durationMs === "number") {
110
- result.duration = formatDurationMs(data.durationMs);
111
- }
112
- if (typeof data?.success === "boolean") result.success = data.success;
113
- if (typeof data?.score === "number" || typeof data?.maxScore === "number") {
114
- const max = typeof data.maxScore === "number" ? data.maxScore : void 0;
115
- const raw = typeof data.score === "number" ? data.score : void 0;
116
- result.score = {
117
- raw,
118
- max,
119
- min: 0,
120
- scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
121
- };
122
- }
123
- return statementFor((0, import_core2.buildLessonkitUrn)({ courseId, lessonId }), XAPIVerbs.completed, event.timestamp, {
124
- result: Object.keys(result).length ? result : void 0
125
- });
126
- }
127
- case "lesson_time_on_task":
128
- return null;
129
- case "quiz_answered": {
130
- const lessonId = event.lessonId;
131
- const checkId = event.data.checkId;
132
- const result = {};
133
- if (typeof event.data.correct === "boolean") {
134
- result.success = event.data.correct;
135
- }
136
- return statementFor(
137
- (0, import_core2.buildLessonkitUrn)({ courseId, lessonId, checkId }),
138
- XAPIVerbs.answered,
139
- event.timestamp,
140
- { result: Object.keys(result).length ? result : void 0 }
141
- );
142
- }
143
- case "quiz_completed": {
144
- const lessonId = event.lessonId;
145
- const checkId = event.data.checkId;
146
- const { score, maxScore } = event.data;
147
- const result = {};
148
- if (typeof score === "number" || typeof maxScore === "number") {
149
- const max = typeof maxScore === "number" ? maxScore : void 0;
150
- const raw = typeof score === "number" ? score : void 0;
151
- result.score = {
152
- raw,
153
- max,
154
- min: 0,
155
- scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
156
- };
157
- }
158
- return statementFor(
159
- (0, import_core2.buildLessonkitUrn)({ courseId, lessonId, checkId }),
160
- XAPIVerbs.completed,
161
- event.timestamp,
162
- { result: Object.keys(result).length ? result : void 0 }
163
- );
164
- }
165
- case "assessment_answered": {
166
- const lessonId = event.lessonId;
167
- const checkId = event.data.checkId;
168
- const result = {};
169
- if (typeof event.data.correct === "boolean") {
170
- result.success = event.data.correct;
171
- }
172
- return statementFor(
173
- (0, import_core2.buildLessonkitUrn)({ courseId, lessonId, checkId }),
174
- XAPIVerbs.answered,
175
- event.timestamp,
176
- { result: Object.keys(result).length ? result : void 0 }
177
- );
178
- }
179
- case "assessment_completed": {
180
- const lessonId = event.lessonId;
181
- const checkId = event.data.checkId;
182
- const { score, maxScore } = event.data;
183
- const result = {};
184
- if (typeof score === "number" || typeof maxScore === "number") {
185
- const max = typeof maxScore === "number" ? maxScore : void 0;
186
- const raw = typeof score === "number" ? score : void 0;
187
- result.score = {
188
- raw,
189
- max,
190
- min: 0,
191
- scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
192
- };
193
- }
194
- return statementFor(
195
- (0, import_core2.buildLessonkitUrn)({ courseId, lessonId, checkId }),
196
- XAPIVerbs.completed,
197
- event.timestamp,
198
- { result: Object.keys(result).length ? result : void 0 }
199
- );
200
- }
201
- case "interaction": {
202
- const lessonId = event.lessonId;
203
- const blockId = event.data?.blockId;
204
- if (!lessonId || !blockId) return null;
205
- return statementFor(
206
- (0, import_core2.buildLessonkitUrn)({ courseId, lessonId, blockId }),
207
- XAPIVerbs.experienced,
208
- event.timestamp
209
- );
210
- }
211
- default:
212
- return (0, import_core.assertNever)(event, "Unhandled telemetry event");
213
- }
100
+ function buildXapiScoreResult(opts) {
101
+ const max = typeof opts.maxScore === "number" ? opts.maxScore : void 0;
102
+ const raw = typeof opts.score === "number" ? opts.score : void 0;
103
+ if (typeof raw !== "number" && typeof max !== "number") return void 0;
104
+ return {
105
+ raw,
106
+ max,
107
+ min: 0,
108
+ scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
109
+ };
214
110
  }
215
111
  function statementFor(objectId, verb, timestamp, extra) {
216
112
  return {
@@ -222,6 +118,113 @@ function statementFor(objectId, verb, timestamp, extra) {
222
118
  context: extra?.context
223
119
  };
224
120
  }
121
+ function experiencedBlockStatement(courseId, lessonId, blockId, timestamp) {
122
+ return statementFor(
123
+ (0, import_core.buildLessonkitUrn)({ courseId, lessonId, blockId }),
124
+ XAPIVerbs.experienced,
125
+ timestamp
126
+ );
127
+ }
128
+ var experiencedBlockMapper = (event, ctx) => {
129
+ if (event.name === "interaction") {
130
+ const lessonId2 = event.lessonId;
131
+ const blockId2 = event.data?.blockId;
132
+ if (!lessonId2 || !blockId2) return null;
133
+ return experiencedBlockStatement(ctx.courseId, lessonId2, blockId2, ctx.timestamp);
134
+ }
135
+ const lessonId = event.lessonId;
136
+ const blockId = "data" in event && event.data && "blockId" in event.data ? event.data.blockId : void 0;
137
+ if (!lessonId || !blockId || typeof blockId !== "string") return null;
138
+ return experiencedBlockStatement(ctx.courseId, lessonId, blockId, ctx.timestamp);
139
+ };
140
+ var TELEMETRY_XAPI_MAPPERS = {
141
+ course_started: (_event, ctx) => statementFor((0, import_core.buildLessonkitUrn)({ courseId: ctx.courseId }), XAPIVerbs.initialized, ctx.timestamp),
142
+ course_completed: (_event, ctx) => statementFor((0, import_core.buildLessonkitUrn)({ courseId: ctx.courseId }), XAPIVerbs.completed, ctx.timestamp),
143
+ lesson_started: (event, ctx) => {
144
+ const lessonId = event.name === "lesson_started" ? event.lessonId : void 0;
145
+ if (!lessonId) return null;
146
+ return statementFor(
147
+ (0, import_core.buildLessonkitUrn)({ courseId: ctx.courseId, lessonId }),
148
+ XAPIVerbs.initialized,
149
+ ctx.timestamp
150
+ );
151
+ },
152
+ lesson_completed: (event, ctx) => {
153
+ if (event.name !== "lesson_completed") return null;
154
+ const lessonId = event.lessonId;
155
+ const data = event.data;
156
+ const result = {};
157
+ if (typeof data?.durationMs === "number") {
158
+ result.duration = formatDurationMs(data.durationMs);
159
+ }
160
+ if (typeof data?.success === "boolean") result.success = data.success;
161
+ const score = buildXapiScoreResult({ score: data?.score, maxScore: data?.maxScore });
162
+ if (score) result.score = score;
163
+ return statementFor((0, import_core.buildLessonkitUrn)({ courseId: ctx.courseId, lessonId }), XAPIVerbs.completed, ctx.timestamp, {
164
+ result: Object.keys(result).length ? result : void 0
165
+ });
166
+ },
167
+ lesson_time_on_task: () => null,
168
+ quiz_answered: (event, ctx) => {
169
+ if (event.name !== "quiz_answered") return null;
170
+ const result = {};
171
+ if (typeof event.data.correct === "boolean") result.success = event.data.correct;
172
+ return statementFor(
173
+ (0, import_core.buildLessonkitUrn)({ courseId: ctx.courseId, lessonId: event.lessonId, checkId: event.data.checkId }),
174
+ XAPIVerbs.answered,
175
+ ctx.timestamp,
176
+ { result: Object.keys(result).length ? result : void 0 }
177
+ );
178
+ },
179
+ quiz_completed: (event, ctx) => {
180
+ if (event.name !== "quiz_completed") return null;
181
+ const score = buildXapiScoreResult({ score: event.data.score, maxScore: event.data.maxScore });
182
+ return statementFor(
183
+ (0, import_core.buildLessonkitUrn)({ courseId: ctx.courseId, lessonId: event.lessonId, checkId: event.data.checkId }),
184
+ XAPIVerbs.completed,
185
+ ctx.timestamp,
186
+ { result: score ? { score } : void 0 }
187
+ );
188
+ },
189
+ assessment_answered: (event, ctx) => {
190
+ if (event.name !== "assessment_answered") return null;
191
+ const result = {};
192
+ if (typeof event.data.correct === "boolean") result.success = event.data.correct;
193
+ return statementFor(
194
+ (0, import_core.buildLessonkitUrn)({ courseId: ctx.courseId, lessonId: event.lessonId, checkId: event.data.checkId }),
195
+ XAPIVerbs.answered,
196
+ ctx.timestamp,
197
+ { result: Object.keys(result).length ? result : void 0 }
198
+ );
199
+ },
200
+ assessment_completed: (event, ctx) => {
201
+ if (event.name !== "assessment_completed") return null;
202
+ const score = buildXapiScoreResult({ score: event.data.score, maxScore: event.data.maxScore });
203
+ return statementFor(
204
+ (0, import_core.buildLessonkitUrn)({ courseId: ctx.courseId, lessonId: event.lessonId, checkId: event.data.checkId }),
205
+ XAPIVerbs.completed,
206
+ ctx.timestamp,
207
+ { result: score ? { score } : void 0 }
208
+ );
209
+ },
210
+ interaction: experiencedBlockMapper,
211
+ book_page_viewed: experiencedBlockMapper,
212
+ compound_page_viewed: experiencedBlockMapper,
213
+ hotspot_opened: experiencedBlockMapper,
214
+ accordion_section_toggled: experiencedBlockMapper,
215
+ flashcard_flipped: experiencedBlockMapper,
216
+ image_slider_changed: experiencedBlockMapper
217
+ };
218
+ function telemetryEventToXAPIStatement(event) {
219
+ const mapper = TELEMETRY_XAPI_MAPPERS[event.name];
220
+ if (!mapper) {
221
+ throw new Error(`Unhandled telemetry event: ${event.name}`);
222
+ }
223
+ return mapper(event, {
224
+ courseId: event.courseId,
225
+ timestamp: event.timestamp
226
+ });
227
+ }
225
228
 
226
229
  // src/client.ts
227
230
  function isDevEnvironment() {
@@ -231,7 +234,11 @@ function isDevEnvironment() {
231
234
  function createXAPIClient(opts) {
232
235
  const transport = opts?.transport;
233
236
  const courseId = opts?.courseId;
234
- const queue = opts?.queue ?? createInMemoryXAPIQueue();
237
+ const queue = opts?.queue ?? createInMemoryXAPIQueue({
238
+ maxSize: opts?.maxQueueSize,
239
+ onDepth: opts?.onQueueDepth,
240
+ onCap: opts?.onQueueCap
241
+ });
235
242
  let warnedNoTransport = false;
236
243
  let warnedTransportFailure = false;
237
244
  const inflightById = /* @__PURE__ */ new Map();
@@ -293,7 +300,7 @@ function createXAPIClient(opts) {
293
300
  if (!courseId) return;
294
301
  emit({
295
302
  name: "lesson_started",
296
- timestamp: (0, import_core3.nowIso)(),
303
+ timestamp: (0, import_core2.nowIso)(),
297
304
  courseId,
298
305
  lessonId,
299
306
  data: { lessonId }
@@ -309,7 +316,7 @@ function createXAPIClient(opts) {
309
316
  if (!courseId) return;
310
317
  emit({
311
318
  name: "lesson_completed",
312
- timestamp: (0, import_core3.nowIso)(),
319
+ timestamp: (0, import_core2.nowIso)(),
313
320
  courseId,
314
321
  lessonId,
315
322
  data: { lessonId, durationMs, score, maxScore, success }
@@ -319,7 +326,7 @@ function createXAPIClient(opts) {
319
326
  if (!courseId) return;
320
327
  emit({
321
328
  name: "course_completed",
322
- timestamp: (0, import_core3.nowIso)(),
329
+ timestamp: (0, import_core2.nowIso)(),
323
330
  courseId
324
331
  });
325
332
  }
package/dist/index.d.cts CHANGED
@@ -52,12 +52,24 @@ type XAPIClient = {
52
52
  completeCourse: () => void;
53
53
  };
54
54
 
55
- declare function createInMemoryXAPIQueue(): XAPIQueue;
55
+ type InMemoryXAPIQueueOptions = {
56
+ /** Maximum queued statements (default 1000). Oldest entries are dropped when full. */
57
+ maxSize?: number;
58
+ /** Called after enqueue with the current queue size. */
59
+ onDepth?: (size: number) => void;
60
+ /** Called when an oldest statement is dropped because the queue is at maxSize. */
61
+ onCap?: () => void;
62
+ };
63
+ declare function createInMemoryXAPIQueue(opts?: InMemoryXAPIQueueOptions): XAPIQueue;
56
64
 
57
65
  declare function createXAPIClient(opts?: {
58
66
  transport?: XAPITransport;
59
67
  courseId?: CourseId;
60
68
  queue?: XAPIQueue;
69
+ /** When creating the default in-memory queue (max size 1000 unless overridden). */
70
+ maxQueueSize?: number;
71
+ onQueueDepth?: (size: number) => void;
72
+ onQueueCap?: () => void;
61
73
  }): XAPIClient;
62
74
 
63
75
  /**
@@ -66,4 +78,4 @@ declare function createXAPIClient(opts?: {
66
78
  */
67
79
  declare function telemetryEventToXAPIStatement(event: TelemetryEvent): XAPIStatement | null;
68
80
 
69
- export { type XAPIClient, type XAPIObjectDefinition, type XAPIQueue, type XAPIResult, type XAPIScore, type XAPIStatement, type XAPITransport, type XAPIVerbIri, createInMemoryXAPIQueue, createXAPIClient, telemetryEventToXAPIStatement };
81
+ export { type InMemoryXAPIQueueOptions, type XAPIClient, type XAPIObjectDefinition, type XAPIQueue, type XAPIResult, type XAPIScore, type XAPIStatement, type XAPITransport, type XAPIVerbIri, createInMemoryXAPIQueue, createXAPIClient, telemetryEventToXAPIStatement };
package/dist/index.d.ts CHANGED
@@ -52,12 +52,24 @@ type XAPIClient = {
52
52
  completeCourse: () => void;
53
53
  };
54
54
 
55
- declare function createInMemoryXAPIQueue(): XAPIQueue;
55
+ type InMemoryXAPIQueueOptions = {
56
+ /** Maximum queued statements (default 1000). Oldest entries are dropped when full. */
57
+ maxSize?: number;
58
+ /** Called after enqueue with the current queue size. */
59
+ onDepth?: (size: number) => void;
60
+ /** Called when an oldest statement is dropped because the queue is at maxSize. */
61
+ onCap?: () => void;
62
+ };
63
+ declare function createInMemoryXAPIQueue(opts?: InMemoryXAPIQueueOptions): XAPIQueue;
56
64
 
57
65
  declare function createXAPIClient(opts?: {
58
66
  transport?: XAPITransport;
59
67
  courseId?: CourseId;
60
68
  queue?: XAPIQueue;
69
+ /** When creating the default in-memory queue (max size 1000 unless overridden). */
70
+ maxQueueSize?: number;
71
+ onQueueDepth?: (size: number) => void;
72
+ onQueueCap?: () => void;
61
73
  }): XAPIClient;
62
74
 
63
75
  /**
@@ -66,4 +78,4 @@ declare function createXAPIClient(opts?: {
66
78
  */
67
79
  declare function telemetryEventToXAPIStatement(event: TelemetryEvent): XAPIStatement | null;
68
80
 
69
- export { type XAPIClient, type XAPIObjectDefinition, type XAPIQueue, type XAPIResult, type XAPIScore, type XAPIStatement, type XAPITransport, type XAPIVerbIri, createInMemoryXAPIQueue, createXAPIClient, telemetryEventToXAPIStatement };
81
+ export { type InMemoryXAPIQueueOptions, type XAPIClient, type XAPIObjectDefinition, type XAPIQueue, type XAPIResult, type XAPIScore, type XAPIStatement, type XAPITransport, type XAPIVerbIri, createInMemoryXAPIQueue, createXAPIClient, telemetryEventToXAPIStatement };
package/dist/index.js CHANGED
@@ -1,13 +1,19 @@
1
1
  // src/queue.ts
2
- function createInMemoryXAPIQueue() {
2
+ var DEFAULT_MAX_QUEUE_SIZE = 1e3;
3
+ function createInMemoryXAPIQueue(opts) {
4
+ const maxSize = opts?.maxSize ?? DEFAULT_MAX_QUEUE_SIZE;
3
5
  const buffer = [];
4
6
  let flushInFlight = null;
7
+ const notifyDepth = () => {
8
+ opts?.onDepth?.(buffer.length);
9
+ };
5
10
  const runFlush = async (transport) => {
6
11
  while (buffer.length) {
7
12
  const statement = buffer[0];
8
13
  try {
9
14
  await transport(statement);
10
15
  buffer.shift();
16
+ notifyDepth();
11
17
  } catch {
12
18
  return;
13
19
  }
@@ -16,7 +22,12 @@ function createInMemoryXAPIQueue() {
16
22
  return {
17
23
  enqueue: (statement) => {
18
24
  if (statement.id && buffer.some((s) => s.id === statement.id)) return;
25
+ if (buffer.length >= maxSize) {
26
+ buffer.shift();
27
+ opts?.onCap?.();
28
+ }
19
29
  buffer.push(statement);
30
+ notifyDepth();
20
31
  },
21
32
  size: () => buffer.length,
22
33
  flush: async (transport) => {
@@ -34,7 +45,6 @@ function createInMemoryXAPIQueue() {
34
45
  import { nowIso } from "@lessonkit/core";
35
46
 
36
47
  // src/telemetryMap.ts
37
- import { assertNever } from "@lessonkit/core";
38
48
  import { buildLessonkitUrn } from "@lessonkit/core";
39
49
 
40
50
  // src/id.ts
@@ -59,130 +69,16 @@ var XAPIVerbs = {
59
69
  answered: "http://adlnet.gov/expapi/verbs/answered",
60
70
  experienced: "http://adlnet.gov/expapi/verbs/experienced"
61
71
  };
62
- function telemetryEventToXAPIStatement(event) {
63
- const { courseId } = event;
64
- switch (event.name) {
65
- case "course_started":
66
- return statementFor(buildLessonkitUrn({ courseId }), XAPIVerbs.initialized, event.timestamp);
67
- case "course_completed":
68
- return statementFor(buildLessonkitUrn({ courseId }), XAPIVerbs.completed, event.timestamp);
69
- case "lesson_started": {
70
- const lessonId = event.lessonId;
71
- return statementFor(
72
- buildLessonkitUrn({ courseId, lessonId }),
73
- XAPIVerbs.initialized,
74
- event.timestamp
75
- );
76
- }
77
- case "lesson_completed": {
78
- const lessonId = event.lessonId;
79
- const data = event.data;
80
- const result = {};
81
- if (typeof data?.durationMs === "number") {
82
- result.duration = formatDurationMs(data.durationMs);
83
- }
84
- if (typeof data?.success === "boolean") result.success = data.success;
85
- if (typeof data?.score === "number" || typeof data?.maxScore === "number") {
86
- const max = typeof data.maxScore === "number" ? data.maxScore : void 0;
87
- const raw = typeof data.score === "number" ? data.score : void 0;
88
- result.score = {
89
- raw,
90
- max,
91
- min: 0,
92
- scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
93
- };
94
- }
95
- return statementFor(buildLessonkitUrn({ courseId, lessonId }), XAPIVerbs.completed, event.timestamp, {
96
- result: Object.keys(result).length ? result : void 0
97
- });
98
- }
99
- case "lesson_time_on_task":
100
- return null;
101
- case "quiz_answered": {
102
- const lessonId = event.lessonId;
103
- const checkId = event.data.checkId;
104
- const result = {};
105
- if (typeof event.data.correct === "boolean") {
106
- result.success = event.data.correct;
107
- }
108
- return statementFor(
109
- buildLessonkitUrn({ courseId, lessonId, checkId }),
110
- XAPIVerbs.answered,
111
- event.timestamp,
112
- { result: Object.keys(result).length ? result : void 0 }
113
- );
114
- }
115
- case "quiz_completed": {
116
- const lessonId = event.lessonId;
117
- const checkId = event.data.checkId;
118
- const { score, maxScore } = event.data;
119
- const result = {};
120
- if (typeof score === "number" || typeof maxScore === "number") {
121
- const max = typeof maxScore === "number" ? maxScore : void 0;
122
- const raw = typeof score === "number" ? score : void 0;
123
- result.score = {
124
- raw,
125
- max,
126
- min: 0,
127
- scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
128
- };
129
- }
130
- return statementFor(
131
- buildLessonkitUrn({ courseId, lessonId, checkId }),
132
- XAPIVerbs.completed,
133
- event.timestamp,
134
- { result: Object.keys(result).length ? result : void 0 }
135
- );
136
- }
137
- case "assessment_answered": {
138
- const lessonId = event.lessonId;
139
- const checkId = event.data.checkId;
140
- const result = {};
141
- if (typeof event.data.correct === "boolean") {
142
- result.success = event.data.correct;
143
- }
144
- return statementFor(
145
- buildLessonkitUrn({ courseId, lessonId, checkId }),
146
- XAPIVerbs.answered,
147
- event.timestamp,
148
- { result: Object.keys(result).length ? result : void 0 }
149
- );
150
- }
151
- case "assessment_completed": {
152
- const lessonId = event.lessonId;
153
- const checkId = event.data.checkId;
154
- const { score, maxScore } = event.data;
155
- const result = {};
156
- if (typeof score === "number" || typeof maxScore === "number") {
157
- const max = typeof maxScore === "number" ? maxScore : void 0;
158
- const raw = typeof score === "number" ? score : void 0;
159
- result.score = {
160
- raw,
161
- max,
162
- min: 0,
163
- scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
164
- };
165
- }
166
- return statementFor(
167
- buildLessonkitUrn({ courseId, lessonId, checkId }),
168
- XAPIVerbs.completed,
169
- event.timestamp,
170
- { result: Object.keys(result).length ? result : void 0 }
171
- );
172
- }
173
- case "interaction": {
174
- const lessonId = event.lessonId;
175
- const blockId = event.data?.blockId;
176
- if (!lessonId || !blockId) return null;
177
- return statementFor(
178
- buildLessonkitUrn({ courseId, lessonId, blockId }),
179
- XAPIVerbs.experienced,
180
- event.timestamp
181
- );
182
- }
183
- default:
184
- return assertNever(event, "Unhandled telemetry event");
185
- }
72
+ function buildXapiScoreResult(opts) {
73
+ const max = typeof opts.maxScore === "number" ? opts.maxScore : void 0;
74
+ const raw = typeof opts.score === "number" ? opts.score : void 0;
75
+ if (typeof raw !== "number" && typeof max !== "number") return void 0;
76
+ return {
77
+ raw,
78
+ max,
79
+ min: 0,
80
+ scaled: typeof raw === "number" && typeof max === "number" && max > 0 ? raw / max : void 0
81
+ };
186
82
  }
187
83
  function statementFor(objectId, verb, timestamp, extra) {
188
84
  return {
@@ -194,6 +90,113 @@ function statementFor(objectId, verb, timestamp, extra) {
194
90
  context: extra?.context
195
91
  };
196
92
  }
93
+ function experiencedBlockStatement(courseId, lessonId, blockId, timestamp) {
94
+ return statementFor(
95
+ buildLessonkitUrn({ courseId, lessonId, blockId }),
96
+ XAPIVerbs.experienced,
97
+ timestamp
98
+ );
99
+ }
100
+ var experiencedBlockMapper = (event, ctx) => {
101
+ if (event.name === "interaction") {
102
+ const lessonId2 = event.lessonId;
103
+ const blockId2 = event.data?.blockId;
104
+ if (!lessonId2 || !blockId2) return null;
105
+ return experiencedBlockStatement(ctx.courseId, lessonId2, blockId2, ctx.timestamp);
106
+ }
107
+ const lessonId = event.lessonId;
108
+ const blockId = "data" in event && event.data && "blockId" in event.data ? event.data.blockId : void 0;
109
+ if (!lessonId || !blockId || typeof blockId !== "string") return null;
110
+ return experiencedBlockStatement(ctx.courseId, lessonId, blockId, ctx.timestamp);
111
+ };
112
+ var TELEMETRY_XAPI_MAPPERS = {
113
+ course_started: (_event, ctx) => statementFor(buildLessonkitUrn({ courseId: ctx.courseId }), XAPIVerbs.initialized, ctx.timestamp),
114
+ course_completed: (_event, ctx) => statementFor(buildLessonkitUrn({ courseId: ctx.courseId }), XAPIVerbs.completed, ctx.timestamp),
115
+ lesson_started: (event, ctx) => {
116
+ const lessonId = event.name === "lesson_started" ? event.lessonId : void 0;
117
+ if (!lessonId) return null;
118
+ return statementFor(
119
+ buildLessonkitUrn({ courseId: ctx.courseId, lessonId }),
120
+ XAPIVerbs.initialized,
121
+ ctx.timestamp
122
+ );
123
+ },
124
+ lesson_completed: (event, ctx) => {
125
+ if (event.name !== "lesson_completed") return null;
126
+ const lessonId = event.lessonId;
127
+ const data = event.data;
128
+ const result = {};
129
+ if (typeof data?.durationMs === "number") {
130
+ result.duration = formatDurationMs(data.durationMs);
131
+ }
132
+ if (typeof data?.success === "boolean") result.success = data.success;
133
+ const score = buildXapiScoreResult({ score: data?.score, maxScore: data?.maxScore });
134
+ if (score) result.score = score;
135
+ return statementFor(buildLessonkitUrn({ courseId: ctx.courseId, lessonId }), XAPIVerbs.completed, ctx.timestamp, {
136
+ result: Object.keys(result).length ? result : void 0
137
+ });
138
+ },
139
+ lesson_time_on_task: () => null,
140
+ quiz_answered: (event, ctx) => {
141
+ if (event.name !== "quiz_answered") return null;
142
+ const result = {};
143
+ if (typeof event.data.correct === "boolean") result.success = event.data.correct;
144
+ return statementFor(
145
+ buildLessonkitUrn({ courseId: ctx.courseId, lessonId: event.lessonId, checkId: event.data.checkId }),
146
+ XAPIVerbs.answered,
147
+ ctx.timestamp,
148
+ { result: Object.keys(result).length ? result : void 0 }
149
+ );
150
+ },
151
+ quiz_completed: (event, ctx) => {
152
+ if (event.name !== "quiz_completed") return null;
153
+ const score = buildXapiScoreResult({ score: event.data.score, maxScore: event.data.maxScore });
154
+ return statementFor(
155
+ buildLessonkitUrn({ courseId: ctx.courseId, lessonId: event.lessonId, checkId: event.data.checkId }),
156
+ XAPIVerbs.completed,
157
+ ctx.timestamp,
158
+ { result: score ? { score } : void 0 }
159
+ );
160
+ },
161
+ assessment_answered: (event, ctx) => {
162
+ if (event.name !== "assessment_answered") return null;
163
+ const result = {};
164
+ if (typeof event.data.correct === "boolean") result.success = event.data.correct;
165
+ return statementFor(
166
+ buildLessonkitUrn({ courseId: ctx.courseId, lessonId: event.lessonId, checkId: event.data.checkId }),
167
+ XAPIVerbs.answered,
168
+ ctx.timestamp,
169
+ { result: Object.keys(result).length ? result : void 0 }
170
+ );
171
+ },
172
+ assessment_completed: (event, ctx) => {
173
+ if (event.name !== "assessment_completed") return null;
174
+ const score = buildXapiScoreResult({ score: event.data.score, maxScore: event.data.maxScore });
175
+ return statementFor(
176
+ buildLessonkitUrn({ courseId: ctx.courseId, lessonId: event.lessonId, checkId: event.data.checkId }),
177
+ XAPIVerbs.completed,
178
+ ctx.timestamp,
179
+ { result: score ? { score } : void 0 }
180
+ );
181
+ },
182
+ interaction: experiencedBlockMapper,
183
+ book_page_viewed: experiencedBlockMapper,
184
+ compound_page_viewed: experiencedBlockMapper,
185
+ hotspot_opened: experiencedBlockMapper,
186
+ accordion_section_toggled: experiencedBlockMapper,
187
+ flashcard_flipped: experiencedBlockMapper,
188
+ image_slider_changed: experiencedBlockMapper
189
+ };
190
+ function telemetryEventToXAPIStatement(event) {
191
+ const mapper = TELEMETRY_XAPI_MAPPERS[event.name];
192
+ if (!mapper) {
193
+ throw new Error(`Unhandled telemetry event: ${event.name}`);
194
+ }
195
+ return mapper(event, {
196
+ courseId: event.courseId,
197
+ timestamp: event.timestamp
198
+ });
199
+ }
197
200
 
198
201
  // src/client.ts
199
202
  function isDevEnvironment() {
@@ -203,7 +206,11 @@ function isDevEnvironment() {
203
206
  function createXAPIClient(opts) {
204
207
  const transport = opts?.transport;
205
208
  const courseId = opts?.courseId;
206
- const queue = opts?.queue ?? createInMemoryXAPIQueue();
209
+ const queue = opts?.queue ?? createInMemoryXAPIQueue({
210
+ maxSize: opts?.maxQueueSize,
211
+ onDepth: opts?.onQueueDepth,
212
+ onCap: opts?.onQueueCap
213
+ });
207
214
  let warnedNoTransport = false;
208
215
  let warnedTransportFailure = false;
209
216
  const inflightById = /* @__PURE__ */ new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/xapi",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "private": false,
5
5
  "description": "xAPI statement generation primitives for LessonKit.",
6
6
  "license": "Apache-2.0",
@@ -9,7 +9,7 @@
9
9
  "url": "git+https://github.com/eddiethedean/lessonkit.git",
10
10
  "directory": "packages/xapi"
11
11
  },
12
- "homepage": "https://github.com/eddiethedean/lessonkit",
12
+ "homepage": "https://lessonkit.readthedocs.io/en/latest/reference/xapi.html",
13
13
  "bugs": {
14
14
  "url": "https://github.com/eddiethedean/lessonkit/issues"
15
15
  },
@@ -48,7 +48,7 @@
48
48
  "lint": "echo \"(no lint configured yet)\""
49
49
  },
50
50
  "dependencies": {
51
- "@lessonkit/core": "1.1.0"
51
+ "@lessonkit/core": "1.2.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "tsup": "^8.5.0",