@lessonkit/xapi 0.9.2 → 1.0.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 +25 -19
- package/dist/index.cjs +33 -11
- package/dist/index.js +33 -11
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,41 +1,47 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lessonkit/xapi
|
|
2
2
|
|
|
3
|
-
[](https://github.com/eddiethedean/lessonkit/actions/workflows/ci.yml)
|
|
4
|
-
[](https://lessonkit.readthedocs.io/en/latest/)
|
|
5
3
|
[](https://www.npmjs.com/package/@lessonkit/xapi)
|
|
4
|
+
[](https://lessonkit.readthedocs.io/en/latest/reference/xapi.html)
|
|
6
5
|
[](https://github.com/eddiethedean/lessonkit/blob/main/LICENSE)
|
|
7
6
|
|
|
8
|
-
xAPI statement generation
|
|
7
|
+
xAPI statement generation, in-memory queueing, and telemetry-to-xAPI mapping.
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
Requires Node.js **18+**.
|
|
11
10
|
|
|
12
11
|
## Install
|
|
13
12
|
|
|
14
13
|
```bash
|
|
15
|
-
npm install @lessonkit/xapi
|
|
14
|
+
npm install @lessonkit/xapi @lessonkit/core
|
|
16
15
|
```
|
|
17
16
|
|
|
18
|
-
##
|
|
17
|
+
## Usage
|
|
19
18
|
|
|
20
|
-
```
|
|
21
|
-
import { createXAPIClient } from "@lessonkit/xapi";
|
|
19
|
+
```typescript
|
|
20
|
+
import { createXAPIClient, telemetryEventToXAPIStatement } from "@lessonkit/xapi";
|
|
22
21
|
|
|
23
22
|
const xapi = createXAPIClient({
|
|
24
|
-
courseId: "
|
|
25
|
-
transport: (statement) => {
|
|
26
|
-
|
|
23
|
+
courseId: "my-course",
|
|
24
|
+
transport: async (statement) => {
|
|
25
|
+
await fetch("/xapi/statements", { method: "POST", body: JSON.stringify(statement) });
|
|
27
26
|
},
|
|
28
27
|
});
|
|
29
28
|
|
|
30
|
-
xapi.completeLesson({ lessonId: "
|
|
29
|
+
xapi.completeLesson({ lessonId: "lesson-1", durationMs: 1200, success: true });
|
|
30
|
+
await xapi.flush();
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Map from telemetry events: `telemetryEventToXAPIStatement(event)` — uses canonical LessonKit URNs.
|
|
34
34
|
|
|
35
|
-
##
|
|
35
|
+
## Behavior
|
|
36
36
|
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
37
|
+
- No transport → statements queue in memory (dev warns once).
|
|
38
|
+
- Transport failure → re-queue; call `flush()` to retry.
|
|
39
|
+
- Concurrent `flush()` calls are coalesced.
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
## Docs
|
|
42
|
+
|
|
43
|
+
[xAPI reference](https://lessonkit.readthedocs.io/en/latest/reference/xapi.html) · [Telemetry & xAPI guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/telemetry-and-xapi.html)
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
Apache-2.0
|
package/dist/index.cjs
CHANGED
|
@@ -29,21 +29,31 @@ module.exports = __toCommonJS(index_exports);
|
|
|
29
29
|
// src/queue.ts
|
|
30
30
|
function createInMemoryXAPIQueue() {
|
|
31
31
|
const buffer = [];
|
|
32
|
+
let flushInFlight = null;
|
|
33
|
+
const runFlush = async (transport) => {
|
|
34
|
+
while (buffer.length) {
|
|
35
|
+
const statement = buffer[0];
|
|
36
|
+
try {
|
|
37
|
+
await transport(statement);
|
|
38
|
+
buffer.shift();
|
|
39
|
+
} catch {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
32
44
|
return {
|
|
33
45
|
enqueue: (statement) => {
|
|
46
|
+
if (statement.id && buffer.some((s) => s.id === statement.id)) return;
|
|
34
47
|
buffer.push(statement);
|
|
35
48
|
},
|
|
36
49
|
size: () => buffer.length,
|
|
37
50
|
flush: async (transport) => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
51
|
+
if (flushInFlight) return flushInFlight;
|
|
52
|
+
if (!buffer.length) return;
|
|
53
|
+
flushInFlight = runFlush(transport).finally(() => {
|
|
54
|
+
flushInFlight = null;
|
|
55
|
+
});
|
|
56
|
+
return flushInFlight;
|
|
47
57
|
}
|
|
48
58
|
};
|
|
49
59
|
}
|
|
@@ -118,10 +128,15 @@ function telemetryEventToXAPIStatement(event) {
|
|
|
118
128
|
case "quiz_answered": {
|
|
119
129
|
const lessonId = event.lessonId;
|
|
120
130
|
const checkId = event.data.checkId;
|
|
131
|
+
const result = {};
|
|
132
|
+
if (typeof event.data.correct === "boolean") {
|
|
133
|
+
result.success = event.data.correct;
|
|
134
|
+
}
|
|
121
135
|
return statementFor(
|
|
122
136
|
(0, import_core.buildLessonkitUrn)({ courseId, lessonId, checkId }),
|
|
123
137
|
XAPIVerbs.answered,
|
|
124
|
-
event.timestamp
|
|
138
|
+
event.timestamp,
|
|
139
|
+
{ result: Object.keys(result).length ? result : void 0 }
|
|
125
140
|
);
|
|
126
141
|
}
|
|
127
142
|
case "quiz_completed": {
|
|
@@ -182,6 +197,7 @@ function createXAPIClient(opts) {
|
|
|
182
197
|
const queue = opts?.queue ?? createInMemoryXAPIQueue();
|
|
183
198
|
let warnedNoTransport = false;
|
|
184
199
|
let warnedTransportFailure = false;
|
|
200
|
+
const inflightById = /* @__PURE__ */ new Map();
|
|
185
201
|
const sendOrQueue = (statement) => {
|
|
186
202
|
if (!transport) {
|
|
187
203
|
queue.enqueue(statement);
|
|
@@ -193,7 +209,9 @@ function createXAPIClient(opts) {
|
|
|
193
209
|
}
|
|
194
210
|
return;
|
|
195
211
|
}
|
|
196
|
-
|
|
212
|
+
const existing = inflightById.get(statement.id);
|
|
213
|
+
if (existing) return;
|
|
214
|
+
const flight = Promise.resolve().then(() => transport(statement)).catch(() => {
|
|
197
215
|
queue.enqueue(statement);
|
|
198
216
|
if (isDevEnvironment() && !warnedTransportFailure) {
|
|
199
217
|
warnedTransportFailure = true;
|
|
@@ -201,7 +219,11 @@ function createXAPIClient(opts) {
|
|
|
201
219
|
"[lessonkit] xAPI transport failed; statement re-queued. Check your LRS endpoint or transport implementation."
|
|
202
220
|
);
|
|
203
221
|
}
|
|
222
|
+
}).finally(() => {
|
|
223
|
+
inflightById.delete(statement.id);
|
|
204
224
|
});
|
|
225
|
+
inflightById.set(statement.id, flight);
|
|
226
|
+
void flight;
|
|
205
227
|
};
|
|
206
228
|
const emit = (event) => {
|
|
207
229
|
const statement = telemetryEventToXAPIStatement(event);
|
package/dist/index.js
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
// src/queue.ts
|
|
2
2
|
function createInMemoryXAPIQueue() {
|
|
3
3
|
const buffer = [];
|
|
4
|
+
let flushInFlight = null;
|
|
5
|
+
const runFlush = async (transport) => {
|
|
6
|
+
while (buffer.length) {
|
|
7
|
+
const statement = buffer[0];
|
|
8
|
+
try {
|
|
9
|
+
await transport(statement);
|
|
10
|
+
buffer.shift();
|
|
11
|
+
} catch {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
4
16
|
return {
|
|
5
17
|
enqueue: (statement) => {
|
|
18
|
+
if (statement.id && buffer.some((s) => s.id === statement.id)) return;
|
|
6
19
|
buffer.push(statement);
|
|
7
20
|
},
|
|
8
21
|
size: () => buffer.length,
|
|
9
22
|
flush: async (transport) => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
23
|
+
if (flushInFlight) return flushInFlight;
|
|
24
|
+
if (!buffer.length) return;
|
|
25
|
+
flushInFlight = runFlush(transport).finally(() => {
|
|
26
|
+
flushInFlight = null;
|
|
27
|
+
});
|
|
28
|
+
return flushInFlight;
|
|
19
29
|
}
|
|
20
30
|
};
|
|
21
31
|
}
|
|
@@ -90,10 +100,15 @@ function telemetryEventToXAPIStatement(event) {
|
|
|
90
100
|
case "quiz_answered": {
|
|
91
101
|
const lessonId = event.lessonId;
|
|
92
102
|
const checkId = event.data.checkId;
|
|
103
|
+
const result = {};
|
|
104
|
+
if (typeof event.data.correct === "boolean") {
|
|
105
|
+
result.success = event.data.correct;
|
|
106
|
+
}
|
|
93
107
|
return statementFor(
|
|
94
108
|
buildLessonkitUrn({ courseId, lessonId, checkId }),
|
|
95
109
|
XAPIVerbs.answered,
|
|
96
|
-
event.timestamp
|
|
110
|
+
event.timestamp,
|
|
111
|
+
{ result: Object.keys(result).length ? result : void 0 }
|
|
97
112
|
);
|
|
98
113
|
}
|
|
99
114
|
case "quiz_completed": {
|
|
@@ -154,6 +169,7 @@ function createXAPIClient(opts) {
|
|
|
154
169
|
const queue = opts?.queue ?? createInMemoryXAPIQueue();
|
|
155
170
|
let warnedNoTransport = false;
|
|
156
171
|
let warnedTransportFailure = false;
|
|
172
|
+
const inflightById = /* @__PURE__ */ new Map();
|
|
157
173
|
const sendOrQueue = (statement) => {
|
|
158
174
|
if (!transport) {
|
|
159
175
|
queue.enqueue(statement);
|
|
@@ -165,7 +181,9 @@ function createXAPIClient(opts) {
|
|
|
165
181
|
}
|
|
166
182
|
return;
|
|
167
183
|
}
|
|
168
|
-
|
|
184
|
+
const existing = inflightById.get(statement.id);
|
|
185
|
+
if (existing) return;
|
|
186
|
+
const flight = Promise.resolve().then(() => transport(statement)).catch(() => {
|
|
169
187
|
queue.enqueue(statement);
|
|
170
188
|
if (isDevEnvironment() && !warnedTransportFailure) {
|
|
171
189
|
warnedTransportFailure = true;
|
|
@@ -173,7 +191,11 @@ function createXAPIClient(opts) {
|
|
|
173
191
|
"[lessonkit] xAPI transport failed; statement re-queued. Check your LRS endpoint or transport implementation."
|
|
174
192
|
);
|
|
175
193
|
}
|
|
194
|
+
}).finally(() => {
|
|
195
|
+
inflightById.delete(statement.id);
|
|
176
196
|
});
|
|
197
|
+
inflightById.set(statement.id, flight);
|
|
198
|
+
void flight;
|
|
177
199
|
};
|
|
178
200
|
const emit = (event) => {
|
|
179
201
|
const statement = telemetryEventToXAPIStatement(event);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/xapi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "xAPI statement generation primitives for LessonKit.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
"training",
|
|
22
22
|
"lrs"
|
|
23
23
|
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
24
27
|
"type": "module",
|
|
25
28
|
"main": "./dist/index.cjs",
|
|
26
29
|
"module": "./dist/index.js",
|
|
@@ -45,7 +48,7 @@
|
|
|
45
48
|
"lint": "echo \"(no lint configured yet)\""
|
|
46
49
|
},
|
|
47
50
|
"dependencies": {
|
|
48
|
-
"@lessonkit/core": "0.
|
|
51
|
+
"@lessonkit/core": "1.0.0"
|
|
49
52
|
},
|
|
50
53
|
"devDependencies": {
|
|
51
54
|
"tsup": "^8.5.0",
|