@lessonkit/react 0.2.1 → 0.3.1
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 +2 -1
- package/dist/index.cjs +79 -34
- package/dist/index.js +74 -29
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ export default function App() {
|
|
|
53
53
|
}
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
## API (0.
|
|
56
|
+
## API (0.3.0)
|
|
57
57
|
|
|
58
58
|
### Components
|
|
59
59
|
|
|
@@ -83,4 +83,5 @@ export default function App() {
|
|
|
83
83
|
for that lesson. Use stable `lessonId` values so completion and time-on-task telemetry stay consistent.
|
|
84
84
|
- If you omit `session.sessionId`, the provider reuses a tab-scoped id via `sessionStorage` so React
|
|
85
85
|
Strict Mode remounts do not split analytics sessions in development.
|
|
86
|
+
- Accessibility guidance lives in [`docs/ACCESSIBILITY.md`](../../docs/ACCESSIBILITY.md).
|
|
86
87
|
|
package/dist/index.cjs
CHANGED
|
@@ -42,43 +42,74 @@ var import_accessibility = require("@lessonkit/accessibility");
|
|
|
42
42
|
|
|
43
43
|
// src/context.tsx
|
|
44
44
|
var import_react = require("react");
|
|
45
|
-
var
|
|
45
|
+
var import_core2 = require("@lessonkit/core");
|
|
46
46
|
var import_xapi = require("@lessonkit/xapi");
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
|
|
48
|
+
// src/runtime/ports.ts
|
|
49
|
+
function createNoopStorage() {
|
|
50
|
+
return {
|
|
51
|
+
getItem: () => null,
|
|
52
|
+
setItem: () => {
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function createSessionStoragePort() {
|
|
57
|
+
if (typeof sessionStorage === "undefined") return createNoopStorage();
|
|
58
|
+
return {
|
|
59
|
+
getItem: (key) => {
|
|
60
|
+
try {
|
|
61
|
+
return sessionStorage.getItem(key);
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
setItem: (key, value) => {
|
|
67
|
+
try {
|
|
68
|
+
sessionStorage.setItem(key, value);
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/runtime/session.ts
|
|
76
|
+
var import_core = require("@lessonkit/core");
|
|
49
77
|
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
50
78
|
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
51
|
-
function
|
|
52
|
-
client?.flush?.();
|
|
53
|
-
client?.dispose?.();
|
|
54
|
-
}
|
|
55
|
-
function resolveSessionId(provided) {
|
|
79
|
+
function resolveSessionId(storage, provided) {
|
|
56
80
|
if (provided) return provided;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return id;
|
|
63
|
-
}
|
|
64
|
-
return (0, import_core.createSessionId)();
|
|
81
|
+
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
82
|
+
if (existing) return existing;
|
|
83
|
+
const id = (0, import_core.createSessionId)();
|
|
84
|
+
storage.setItem(SESSION_STORAGE_KEY, id);
|
|
85
|
+
return id;
|
|
65
86
|
}
|
|
66
87
|
function courseStartedStorageKey(sessionId, courseId) {
|
|
67
88
|
return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
68
89
|
}
|
|
69
|
-
function hasCourseStarted(sessionId, courseId) {
|
|
70
|
-
if (
|
|
71
|
-
return
|
|
90
|
+
function hasCourseStarted(storage, sessionId, courseId) {
|
|
91
|
+
if (!courseId) return false;
|
|
92
|
+
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
93
|
+
}
|
|
94
|
+
function markCourseStarted(storage, sessionId, courseId) {
|
|
95
|
+
if (!courseId) return;
|
|
96
|
+
storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
72
97
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
98
|
+
|
|
99
|
+
// src/context.tsx
|
|
100
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
101
|
+
var LessonkitContext = (0, import_react.createContext)(null);
|
|
102
|
+
var useIsoLayoutEffect = typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect;
|
|
103
|
+
function disposeTrackingClient(client) {
|
|
104
|
+
client?.flush?.();
|
|
105
|
+
client?.dispose?.();
|
|
76
106
|
}
|
|
107
|
+
var defaultStorage = createSessionStoragePort();
|
|
77
108
|
function createTrackingClientFromConfig(config) {
|
|
78
109
|
if (config.tracking?.enabled === false) {
|
|
79
|
-
return (0,
|
|
110
|
+
return (0, import_core2.createTrackingClient)();
|
|
80
111
|
}
|
|
81
|
-
return (0,
|
|
112
|
+
return (0, import_core2.createTrackingClient)({
|
|
82
113
|
sink: config.tracking?.sink,
|
|
83
114
|
batchSink: config.tracking?.batchSink,
|
|
84
115
|
batch: config.tracking?.batch
|
|
@@ -92,7 +123,7 @@ function createXapiClientFromConfig(config, queue) {
|
|
|
92
123
|
}
|
|
93
124
|
function LessonkitProvider(props) {
|
|
94
125
|
const config = props.config ?? {};
|
|
95
|
-
const sessionIdRef = (0, import_react.useRef)(resolveSessionId(config.session?.sessionId));
|
|
126
|
+
const sessionIdRef = (0, import_react.useRef)(resolveSessionId(defaultStorage, config.session?.sessionId));
|
|
96
127
|
if (config.session?.sessionId) sessionIdRef.current = config.session.sessionId;
|
|
97
128
|
const attemptIdRef = (0, import_react.useRef)(config.session?.attemptId);
|
|
98
129
|
const userRef = (0, import_react.useRef)(config.session?.user);
|
|
@@ -100,26 +131,32 @@ function LessonkitProvider(props) {
|
|
|
100
131
|
userRef.current = config.session?.user;
|
|
101
132
|
const courseIdRef = (0, import_react.useRef)(config.courseId);
|
|
102
133
|
courseIdRef.current = config.courseId;
|
|
103
|
-
const trackingRef = (0, import_react.useRef)((0,
|
|
134
|
+
const trackingRef = (0, import_react.useRef)((0, import_core2.createTrackingClient)());
|
|
104
135
|
const [tracking, setTracking] = (0, import_react.useState)(() => trackingRef.current);
|
|
136
|
+
const courseStartedInProviderRef = (0, import_react.useRef)(false);
|
|
105
137
|
const trackingEnabled = config.tracking?.enabled;
|
|
106
138
|
const trackingSink = config.tracking?.sink;
|
|
107
139
|
const trackingBatchSink = config.tracking?.batchSink;
|
|
108
140
|
const batchEnabled = config.tracking?.batch?.enabled;
|
|
109
141
|
const batchFlushIntervalMs = config.tracking?.batch?.flushIntervalMs;
|
|
110
142
|
const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
|
|
111
|
-
(
|
|
143
|
+
useIsoLayoutEffect(() => {
|
|
112
144
|
const prev = trackingRef.current;
|
|
113
145
|
const next = createTrackingClientFromConfig(config);
|
|
114
146
|
trackingRef.current = next;
|
|
115
147
|
setTracking(next);
|
|
116
148
|
const sessionId = sessionIdRef.current;
|
|
117
149
|
const cid = courseIdRef.current;
|
|
118
|
-
|
|
119
|
-
|
|
150
|
+
const shouldEmitCourseStarted = cid ? !hasCourseStarted(defaultStorage, sessionId, cid) : !courseStartedInProviderRef.current;
|
|
151
|
+
if (shouldEmitCourseStarted) {
|
|
152
|
+
if (cid) {
|
|
153
|
+
markCourseStarted(defaultStorage, sessionId, cid);
|
|
154
|
+
} else {
|
|
155
|
+
courseStartedInProviderRef.current = true;
|
|
156
|
+
}
|
|
120
157
|
next.track({
|
|
121
158
|
name: "course_started",
|
|
122
|
-
timestamp: (0,
|
|
159
|
+
timestamp: (0, import_core2.nowIso)(),
|
|
123
160
|
courseId: cid,
|
|
124
161
|
sessionId,
|
|
125
162
|
attemptId: attemptIdRef.current,
|
|
@@ -144,14 +181,22 @@ function LessonkitProvider(props) {
|
|
|
144
181
|
const xapiClient = config.xapi?.client;
|
|
145
182
|
const xapiTransport = config.xapi?.transport;
|
|
146
183
|
const courseId = config.courseId;
|
|
147
|
-
(
|
|
184
|
+
useIsoLayoutEffect(() => {
|
|
148
185
|
const prev = xapiRef.current;
|
|
149
186
|
const next = createXapiClientFromConfig(config, xapiQueueRef.current);
|
|
150
187
|
xapiRef.current = next;
|
|
151
188
|
setXapi(next);
|
|
152
189
|
void (async () => {
|
|
153
|
-
if (prev)
|
|
154
|
-
|
|
190
|
+
if (prev) {
|
|
191
|
+
try {
|
|
192
|
+
await prev.flush();
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
await next?.flush();
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
155
200
|
})();
|
|
156
201
|
return () => {
|
|
157
202
|
void prev?.flush();
|
|
@@ -171,7 +216,7 @@ function LessonkitProvider(props) {
|
|
|
171
216
|
(name, data, opts) => {
|
|
172
217
|
trackingRef.current?.track({
|
|
173
218
|
name,
|
|
174
|
-
timestamp: (0,
|
|
219
|
+
timestamp: (0, import_core2.nowIso)(),
|
|
175
220
|
courseId: courseIdRef.current,
|
|
176
221
|
lessonId: opts?.lessonId ?? activeLessonIdRef.current,
|
|
177
222
|
sessionId: sessionIdRef.current,
|
package/dist/index.js
CHANGED
|
@@ -12,38 +12,69 @@ import {
|
|
|
12
12
|
useRef,
|
|
13
13
|
useState
|
|
14
14
|
} from "react";
|
|
15
|
-
import {
|
|
15
|
+
import { createTrackingClient, nowIso } from "@lessonkit/core";
|
|
16
16
|
import { createInMemoryXAPIQueue, createXAPIClient } from "@lessonkit/xapi";
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
|
|
18
|
+
// src/runtime/ports.ts
|
|
19
|
+
function createNoopStorage() {
|
|
20
|
+
return {
|
|
21
|
+
getItem: () => null,
|
|
22
|
+
setItem: () => {
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function createSessionStoragePort() {
|
|
27
|
+
if (typeof sessionStorage === "undefined") return createNoopStorage();
|
|
28
|
+
return {
|
|
29
|
+
getItem: (key) => {
|
|
30
|
+
try {
|
|
31
|
+
return sessionStorage.getItem(key);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
setItem: (key, value) => {
|
|
37
|
+
try {
|
|
38
|
+
sessionStorage.setItem(key, value);
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/runtime/session.ts
|
|
46
|
+
import { createSessionId } from "@lessonkit/core";
|
|
19
47
|
var SESSION_STORAGE_KEY = "lessonkit:sessionId";
|
|
20
48
|
var COURSE_STARTED_PREFIX = "lessonkit:course_started:";
|
|
21
|
-
function
|
|
22
|
-
client?.flush?.();
|
|
23
|
-
client?.dispose?.();
|
|
24
|
-
}
|
|
25
|
-
function resolveSessionId(provided) {
|
|
49
|
+
function resolveSessionId(storage, provided) {
|
|
26
50
|
if (provided) return provided;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return id;
|
|
33
|
-
}
|
|
34
|
-
return createSessionId();
|
|
51
|
+
const existing = storage.getItem(SESSION_STORAGE_KEY);
|
|
52
|
+
if (existing) return existing;
|
|
53
|
+
const id = createSessionId();
|
|
54
|
+
storage.setItem(SESSION_STORAGE_KEY, id);
|
|
55
|
+
return id;
|
|
35
56
|
}
|
|
36
57
|
function courseStartedStorageKey(sessionId, courseId) {
|
|
37
58
|
return `${COURSE_STARTED_PREFIX}${sessionId}:${courseId ?? ""}`;
|
|
38
59
|
}
|
|
39
|
-
function hasCourseStarted(sessionId, courseId) {
|
|
40
|
-
if (
|
|
41
|
-
return
|
|
60
|
+
function hasCourseStarted(storage, sessionId, courseId) {
|
|
61
|
+
if (!courseId) return false;
|
|
62
|
+
return storage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
|
|
42
63
|
}
|
|
43
|
-
function markCourseStarted(sessionId, courseId) {
|
|
44
|
-
if (
|
|
45
|
-
|
|
64
|
+
function markCourseStarted(storage, sessionId, courseId) {
|
|
65
|
+
if (!courseId) return;
|
|
66
|
+
storage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
|
|
46
67
|
}
|
|
68
|
+
|
|
69
|
+
// src/context.tsx
|
|
70
|
+
import { jsx } from "react/jsx-runtime";
|
|
71
|
+
var LessonkitContext = createContext(null);
|
|
72
|
+
var useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
73
|
+
function disposeTrackingClient(client) {
|
|
74
|
+
client?.flush?.();
|
|
75
|
+
client?.dispose?.();
|
|
76
|
+
}
|
|
77
|
+
var defaultStorage = createSessionStoragePort();
|
|
47
78
|
function createTrackingClientFromConfig(config) {
|
|
48
79
|
if (config.tracking?.enabled === false) {
|
|
49
80
|
return createTrackingClient();
|
|
@@ -62,7 +93,7 @@ function createXapiClientFromConfig(config, queue) {
|
|
|
62
93
|
}
|
|
63
94
|
function LessonkitProvider(props) {
|
|
64
95
|
const config = props.config ?? {};
|
|
65
|
-
const sessionIdRef = useRef(resolveSessionId(config.session?.sessionId));
|
|
96
|
+
const sessionIdRef = useRef(resolveSessionId(defaultStorage, config.session?.sessionId));
|
|
66
97
|
if (config.session?.sessionId) sessionIdRef.current = config.session.sessionId;
|
|
67
98
|
const attemptIdRef = useRef(config.session?.attemptId);
|
|
68
99
|
const userRef = useRef(config.session?.user);
|
|
@@ -72,21 +103,27 @@ function LessonkitProvider(props) {
|
|
|
72
103
|
courseIdRef.current = config.courseId;
|
|
73
104
|
const trackingRef = useRef(createTrackingClient());
|
|
74
105
|
const [tracking, setTracking] = useState(() => trackingRef.current);
|
|
106
|
+
const courseStartedInProviderRef = useRef(false);
|
|
75
107
|
const trackingEnabled = config.tracking?.enabled;
|
|
76
108
|
const trackingSink = config.tracking?.sink;
|
|
77
109
|
const trackingBatchSink = config.tracking?.batchSink;
|
|
78
110
|
const batchEnabled = config.tracking?.batch?.enabled;
|
|
79
111
|
const batchFlushIntervalMs = config.tracking?.batch?.flushIntervalMs;
|
|
80
112
|
const batchMaxBatchSize = config.tracking?.batch?.maxBatchSize;
|
|
81
|
-
|
|
113
|
+
useIsoLayoutEffect(() => {
|
|
82
114
|
const prev = trackingRef.current;
|
|
83
115
|
const next = createTrackingClientFromConfig(config);
|
|
84
116
|
trackingRef.current = next;
|
|
85
117
|
setTracking(next);
|
|
86
118
|
const sessionId = sessionIdRef.current;
|
|
87
119
|
const cid = courseIdRef.current;
|
|
88
|
-
|
|
89
|
-
|
|
120
|
+
const shouldEmitCourseStarted = cid ? !hasCourseStarted(defaultStorage, sessionId, cid) : !courseStartedInProviderRef.current;
|
|
121
|
+
if (shouldEmitCourseStarted) {
|
|
122
|
+
if (cid) {
|
|
123
|
+
markCourseStarted(defaultStorage, sessionId, cid);
|
|
124
|
+
} else {
|
|
125
|
+
courseStartedInProviderRef.current = true;
|
|
126
|
+
}
|
|
90
127
|
next.track({
|
|
91
128
|
name: "course_started",
|
|
92
129
|
timestamp: nowIso(),
|
|
@@ -114,14 +151,22 @@ function LessonkitProvider(props) {
|
|
|
114
151
|
const xapiClient = config.xapi?.client;
|
|
115
152
|
const xapiTransport = config.xapi?.transport;
|
|
116
153
|
const courseId = config.courseId;
|
|
117
|
-
|
|
154
|
+
useIsoLayoutEffect(() => {
|
|
118
155
|
const prev = xapiRef.current;
|
|
119
156
|
const next = createXapiClientFromConfig(config, xapiQueueRef.current);
|
|
120
157
|
xapiRef.current = next;
|
|
121
158
|
setXapi(next);
|
|
122
159
|
void (async () => {
|
|
123
|
-
if (prev)
|
|
124
|
-
|
|
160
|
+
if (prev) {
|
|
161
|
+
try {
|
|
162
|
+
await prev.flush();
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
await next?.flush();
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
125
170
|
})();
|
|
126
171
|
return () => {
|
|
127
172
|
void prev?.flush();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "React components and hooks for building learning experiences with LessonKit.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"react-dom": ">=18"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@lessonkit/accessibility": "0.
|
|
54
|
-
"@lessonkit/core": "0.
|
|
55
|
-
"@lessonkit/xapi": "0.
|
|
53
|
+
"@lessonkit/accessibility": "0.3.1",
|
|
54
|
+
"@lessonkit/core": "0.3.1",
|
|
55
|
+
"@lessonkit/xapi": "0.3.1"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@testing-library/react": "^16.3.0",
|