@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 CHANGED
@@ -53,7 +53,7 @@ export default function App() {
53
53
  }
54
54
  ```
55
55
 
56
- ## API (0.2.1)
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 import_core = require("@lessonkit/core");
45
+ var import_core2 = require("@lessonkit/core");
46
46
  var import_xapi = require("@lessonkit/xapi");
47
- var import_jsx_runtime = require("react/jsx-runtime");
48
- var LessonkitContext = (0, import_react.createContext)(null);
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 disposeTrackingClient(client) {
52
- client?.flush?.();
53
- client?.dispose?.();
54
- }
55
- function resolveSessionId(provided) {
79
+ function resolveSessionId(storage, provided) {
56
80
  if (provided) return provided;
57
- if (typeof sessionStorage !== "undefined") {
58
- const existing = sessionStorage.getItem(SESSION_STORAGE_KEY);
59
- if (existing) return existing;
60
- const id = (0, import_core.createSessionId)();
61
- sessionStorage.setItem(SESSION_STORAGE_KEY, id);
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 (typeof sessionStorage === "undefined") return false;
71
- return sessionStorage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
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
- function markCourseStarted(sessionId, courseId) {
74
- if (typeof sessionStorage === "undefined") return;
75
- sessionStorage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
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, import_core.createTrackingClient)();
110
+ return (0, import_core2.createTrackingClient)();
80
111
  }
81
- return (0, import_core.createTrackingClient)({
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, import_core.createTrackingClient)());
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
- (0, import_react.useLayoutEffect)(() => {
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
- if (!hasCourseStarted(sessionId, cid)) {
119
- markCourseStarted(sessionId, cid);
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, import_core.nowIso)(),
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
- (0, import_react.useLayoutEffect)(() => {
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) await prev.flush();
154
- await next?.flush();
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, import_core.nowIso)(),
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 { createSessionId, createTrackingClient, nowIso } from "@lessonkit/core";
15
+ import { createTrackingClient, nowIso } from "@lessonkit/core";
16
16
  import { createInMemoryXAPIQueue, createXAPIClient } from "@lessonkit/xapi";
17
- import { jsx } from "react/jsx-runtime";
18
- var LessonkitContext = createContext(null);
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 disposeTrackingClient(client) {
22
- client?.flush?.();
23
- client?.dispose?.();
24
- }
25
- function resolveSessionId(provided) {
49
+ function resolveSessionId(storage, provided) {
26
50
  if (provided) return provided;
27
- if (typeof sessionStorage !== "undefined") {
28
- const existing = sessionStorage.getItem(SESSION_STORAGE_KEY);
29
- if (existing) return existing;
30
- const id = createSessionId();
31
- sessionStorage.setItem(SESSION_STORAGE_KEY, id);
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 (typeof sessionStorage === "undefined") return false;
41
- return sessionStorage.getItem(courseStartedStorageKey(sessionId, courseId)) === "1";
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 (typeof sessionStorage === "undefined") return;
45
- sessionStorage.setItem(courseStartedStorageKey(sessionId, courseId), "1");
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
- useLayoutEffect(() => {
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
- if (!hasCourseStarted(sessionId, cid)) {
89
- markCourseStarted(sessionId, cid);
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
- useLayoutEffect(() => {
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) await prev.flush();
124
- await next?.flush();
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.2.1",
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.2.1",
54
- "@lessonkit/core": "0.2.1",
55
- "@lessonkit/xapi": "0.2.1"
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",