@lessonkit/react 1.0.0 → 1.0.2
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/block-catalog.v1.json +6 -0
- package/dist/index.cjs +242 -99
- package/dist/index.d.cts +245 -37
- package/dist/index.d.ts +245 -37
- package/dist/index.js +242 -97
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "react";
|
|
17
17
|
import { createLessonkitRuntime, createTrackingClient as createTrackingClient2, assertValidId } from "@lessonkit/core";
|
|
18
18
|
import { createInMemoryXAPIQueue } from "@lessonkit/xapi";
|
|
19
|
-
import { telemetryEventToXAPIStatement as
|
|
19
|
+
import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement3 } from "@lessonkit/xapi";
|
|
20
20
|
|
|
21
21
|
// src/runtime/emitTelemetry.ts
|
|
22
22
|
import { buildTelemetryEvent, tryBuildTelemetryEvent } from "@lessonkit/core";
|
|
@@ -135,9 +135,65 @@ import {
|
|
|
135
135
|
markCourseStarted,
|
|
136
136
|
hasCourseStartedEmittedToTracking,
|
|
137
137
|
markCourseStartedEmittedToTracking,
|
|
138
|
+
hasCourseStartedPipelineDelivered,
|
|
139
|
+
markCourseStartedPipelineDelivered,
|
|
138
140
|
migrateCourseStartedMark
|
|
139
141
|
} from "@lessonkit/core";
|
|
140
142
|
|
|
143
|
+
// src/runtime/courseStartedPipeline.ts
|
|
144
|
+
import { telemetryEventToXAPIStatement as telemetryEventToXAPIStatement2 } from "@lessonkit/xapi";
|
|
145
|
+
function isDevEnvironment3() {
|
|
146
|
+
const g = globalThis;
|
|
147
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
148
|
+
}
|
|
149
|
+
function warnExtraSinkFailure(sinkId, err) {
|
|
150
|
+
if (isDevEnvironment3()) {
|
|
151
|
+
console.warn(
|
|
152
|
+
`[lessonkit] course_started extra sink "${sinkId}" failed:`,
|
|
153
|
+
err instanceof Error ? err.message : err
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function emitExtraSinks(sinks, event, emitCtx) {
|
|
158
|
+
await Promise.all(
|
|
159
|
+
sinks.map(async (sink) => {
|
|
160
|
+
let result;
|
|
161
|
+
try {
|
|
162
|
+
result = sink.emit(event, emitCtx);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
warnExtraSinkFailure(sink.id, err);
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
if (result != null && typeof result.then === "function") {
|
|
168
|
+
try {
|
|
169
|
+
await result;
|
|
170
|
+
} catch (err) {
|
|
171
|
+
warnExtraSinkFailure(sink.id, err);
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
async function emitCourseStartedNonTrackingPipeline(opts) {
|
|
179
|
+
let xapiStatementSent = false;
|
|
180
|
+
if (!opts.skipXapi && opts.xapi) {
|
|
181
|
+
const statement = telemetryEventToXAPIStatement2(opts.event);
|
|
182
|
+
if (statement) {
|
|
183
|
+
opts.xapi.send(statement);
|
|
184
|
+
xapiStatementSent = true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
forwardTelemetryToLxpack(opts.event, opts.lxpackBridge);
|
|
188
|
+
const emitCtx = {
|
|
189
|
+
courseId: opts.event.courseId,
|
|
190
|
+
sessionId: opts.event.sessionId,
|
|
191
|
+
attemptId: opts.event.attemptId
|
|
192
|
+
};
|
|
193
|
+
await emitExtraSinks(opts.extraSinks ?? [], opts.event, emitCtx);
|
|
194
|
+
return { xapiStatementSent };
|
|
195
|
+
}
|
|
196
|
+
|
|
141
197
|
// src/runtime/plugins.ts
|
|
142
198
|
import { createPluginRegistry } from "@lessonkit/core";
|
|
143
199
|
function createReactPluginHost(plugins) {
|
|
@@ -186,11 +242,13 @@ async function disposeTrackingClient(client) {
|
|
|
186
242
|
// src/provider/useLessonkitProviderRuntime.ts
|
|
187
243
|
var useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
188
244
|
var defaultStorage = createSessionStoragePort();
|
|
245
|
+
var courseStartedTrackingFlightKey = null;
|
|
189
246
|
function isTrackingActive(tracking) {
|
|
190
247
|
return tracking?.enabled !== false;
|
|
191
248
|
}
|
|
192
|
-
|
|
193
|
-
|
|
249
|
+
function isCourseStartedSinkSettled(result) {
|
|
250
|
+
return result === "emitted";
|
|
251
|
+
}
|
|
194
252
|
function buildCourseStartedEvent(opts) {
|
|
195
253
|
const pluginCtx = buildPluginContext({
|
|
196
254
|
courseId: opts.courseId,
|
|
@@ -207,85 +265,113 @@ function buildCourseStartedEvent(opts) {
|
|
|
207
265
|
});
|
|
208
266
|
return opts.pluginHost ? opts.pluginHost.runTelemetry(built, pluginCtx) : built;
|
|
209
267
|
}
|
|
210
|
-
function
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
268
|
+
async function emitCourseStartedToTracking(tracking, storage, sessionId, courseId, event, shouldCommit) {
|
|
269
|
+
const flightKey = `${sessionId}:${courseId}`;
|
|
270
|
+
if (hasCourseStartedEmittedToTracking(storage, sessionId, courseId)) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
if (courseStartedTrackingFlightKey === flightKey) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
courseStartedTrackingFlightKey = flightKey;
|
|
217
277
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
278
|
+
if (shouldCommit && !shouldCommit()) return false;
|
|
279
|
+
tracking.track(event);
|
|
280
|
+
await tracking.flush?.();
|
|
281
|
+
if (shouldCommit && !shouldCommit()) return false;
|
|
282
|
+
markCourseStartedEmittedToTracking(storage, sessionId, courseId);
|
|
283
|
+
return true;
|
|
284
|
+
} catch {
|
|
285
|
+
return false;
|
|
286
|
+
} finally {
|
|
287
|
+
if (courseStartedTrackingFlightKey === flightKey) {
|
|
288
|
+
courseStartedTrackingFlightKey = null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function emitCourseStartedPipelineOnly(opts) {
|
|
293
|
+
try {
|
|
294
|
+
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
295
|
+
const { xapiStatementSent } = await emitCourseStartedNonTrackingPipeline({
|
|
222
296
|
event: opts.event,
|
|
223
|
-
|
|
297
|
+
xapi: opts.xapi,
|
|
224
298
|
lxpackBridge: opts.lxpackBridge,
|
|
225
|
-
extraSinks: opts.extraSinks
|
|
299
|
+
extraSinks: opts.extraSinks,
|
|
300
|
+
skipXapi: opts.skipXapi
|
|
226
301
|
});
|
|
302
|
+
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
227
303
|
markCourseStarted(opts.storage, opts.sessionId, opts.courseId);
|
|
228
|
-
|
|
304
|
+
markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId);
|
|
305
|
+
if (xapiStatementSent) {
|
|
306
|
+
opts.onXapiStatementSent?.();
|
|
307
|
+
}
|
|
308
|
+
return "emitted";
|
|
229
309
|
} catch {
|
|
230
|
-
return
|
|
310
|
+
return "failed";
|
|
231
311
|
}
|
|
232
312
|
}
|
|
233
|
-
function emitCourseStarted(opts) {
|
|
313
|
+
async function emitCourseStarted(opts) {
|
|
234
314
|
const event = buildCourseStartedEvent(opts);
|
|
235
|
-
if (event === null) return
|
|
315
|
+
if (event === null) return "filtered";
|
|
236
316
|
const trackingAlreadyEmitted = hasCourseStartedEmittedToTracking(
|
|
237
317
|
opts.storage,
|
|
238
318
|
opts.sessionId,
|
|
239
319
|
opts.courseId
|
|
240
320
|
);
|
|
241
321
|
if (!trackingAlreadyEmitted) {
|
|
242
|
-
|
|
243
|
-
opts.tracking
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
322
|
+
const tracked = await emitCourseStartedToTracking(
|
|
323
|
+
opts.tracking,
|
|
324
|
+
opts.storage,
|
|
325
|
+
opts.sessionId,
|
|
326
|
+
opts.courseId,
|
|
327
|
+
event,
|
|
328
|
+
opts.shouldCommit
|
|
329
|
+
);
|
|
330
|
+
if (!tracked) return "failed";
|
|
248
331
|
}
|
|
249
|
-
return emitCourseStartedPipelineOnly({
|
|
332
|
+
return emitCourseStartedPipelineOnly({
|
|
333
|
+
...opts,
|
|
334
|
+
event,
|
|
335
|
+
skipXapi: opts.skipXapi,
|
|
336
|
+
onXapiStatementSent: opts.onXapiStatementSent,
|
|
337
|
+
shouldCommit: opts.shouldCommit
|
|
338
|
+
});
|
|
250
339
|
}
|
|
251
|
-
function emitCourseStartedToTrackingOnly(opts) {
|
|
340
|
+
async function emitCourseStartedToTrackingOnly(opts) {
|
|
252
341
|
const event = buildCourseStartedEvent(opts);
|
|
253
|
-
if (event === null) return
|
|
342
|
+
if (event === null) return "filtered";
|
|
254
343
|
const trackingAlreadyEmitted = hasCourseStartedEmittedToTracking(
|
|
255
344
|
opts.storage,
|
|
256
345
|
opts.sessionId,
|
|
257
346
|
opts.courseId
|
|
258
347
|
);
|
|
259
348
|
if (!trackingAlreadyEmitted) {
|
|
260
|
-
|
|
261
|
-
opts.tracking
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
349
|
+
const tracked = await emitCourseStartedToTracking(
|
|
350
|
+
opts.tracking,
|
|
351
|
+
opts.storage,
|
|
352
|
+
opts.sessionId,
|
|
353
|
+
opts.courseId,
|
|
354
|
+
event,
|
|
355
|
+
opts.shouldCommit
|
|
356
|
+
);
|
|
357
|
+
if (!tracked) return "failed";
|
|
266
358
|
}
|
|
267
|
-
const pluginCtx = buildPluginContext({
|
|
268
|
-
courseId: opts.courseId,
|
|
269
|
-
sessionId: opts.sessionId,
|
|
270
|
-
attemptId: opts.attemptId,
|
|
271
|
-
user: opts.user
|
|
272
|
-
});
|
|
273
359
|
try {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
tracking: noopTrackingClient,
|
|
277
|
-
xapi: null,
|
|
360
|
+
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
361
|
+
await emitCourseStartedNonTrackingPipeline({
|
|
278
362
|
event,
|
|
279
|
-
|
|
363
|
+
xapi: null,
|
|
280
364
|
lxpackBridge: opts.lxpackBridge,
|
|
281
|
-
extraSinks: opts.extraSinks
|
|
365
|
+
extraSinks: opts.extraSinks,
|
|
366
|
+
skipXapi: true
|
|
282
367
|
});
|
|
283
|
-
|
|
368
|
+
markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId);
|
|
369
|
+
return "emitted";
|
|
284
370
|
} catch {
|
|
285
|
-
return
|
|
371
|
+
return "failed";
|
|
286
372
|
}
|
|
287
373
|
}
|
|
288
|
-
function emitPendingCourseStarted(opts) {
|
|
374
|
+
async function emitPendingCourseStarted(opts) {
|
|
289
375
|
const trackingEmitted = hasCourseStartedEmittedToTracking(
|
|
290
376
|
opts.storage,
|
|
291
377
|
opts.sessionId,
|
|
@@ -297,13 +383,28 @@ function emitPendingCourseStarted(opts) {
|
|
|
297
383
|
}
|
|
298
384
|
if (trackingEmitted && !sessionStarted) {
|
|
299
385
|
const event = buildCourseStartedEvent(opts);
|
|
300
|
-
if (event === null) return
|
|
386
|
+
if (event === null) return "filtered";
|
|
301
387
|
return emitCourseStartedPipelineOnly({ ...opts, event });
|
|
302
388
|
}
|
|
303
389
|
if (!trackingEmitted && !sessionStarted) {
|
|
304
390
|
return emitCourseStarted(opts);
|
|
305
391
|
}
|
|
306
|
-
|
|
392
|
+
const pipelineDelivered = hasCourseStartedPipelineDelivered(
|
|
393
|
+
opts.storage,
|
|
394
|
+
opts.sessionId,
|
|
395
|
+
opts.courseId
|
|
396
|
+
);
|
|
397
|
+
if (sessionStarted && trackingEmitted && !pipelineDelivered) {
|
|
398
|
+
const event = buildCourseStartedEvent(opts);
|
|
399
|
+
if (event === null) return "filtered";
|
|
400
|
+
return emitCourseStartedPipelineOnly({
|
|
401
|
+
...opts,
|
|
402
|
+
event,
|
|
403
|
+
skipXapi: opts.skipXapi,
|
|
404
|
+
onXapiStatementSent: opts.onXapiStatementSent
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return "emitted";
|
|
307
408
|
}
|
|
308
409
|
function assertTrackingSinkConfig(tracking) {
|
|
309
410
|
if (!tracking?.sink || !tracking?.batchSink) return;
|
|
@@ -344,6 +445,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
344
445
|
pluginHostRef.current = pluginHost;
|
|
345
446
|
const progressRef = useRef(createProgressController());
|
|
346
447
|
const courseStartedEmittedToSinkRef = useRef(false);
|
|
448
|
+
const courseStartedEmitGenerationRef = useRef(0);
|
|
347
449
|
const prevCourseIdForProgressRef = useRef(normalizedCourseId);
|
|
348
450
|
const pendingCourseIdResetRef = useRef(false);
|
|
349
451
|
const prevUseV2RuntimeRef = useRef(useV2Runtime);
|
|
@@ -363,6 +465,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
363
465
|
}
|
|
364
466
|
pendingCourseIdResetRef.current = true;
|
|
365
467
|
courseStartedEmittedToSinkRef.current = false;
|
|
468
|
+
courseStartedEmitGenerationRef.current += 1;
|
|
366
469
|
} else if (useV2Runtime && !headlessRef.current) {
|
|
367
470
|
headlessRef.current = createLessonkitRuntime({
|
|
368
471
|
courseId: normalizedCourseId,
|
|
@@ -380,6 +483,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
380
483
|
}
|
|
381
484
|
pendingCourseIdResetRef.current = true;
|
|
382
485
|
courseStartedEmittedToSinkRef.current = false;
|
|
486
|
+
courseStartedEmitGenerationRef.current += 1;
|
|
383
487
|
}
|
|
384
488
|
if (useV2Runtime && headlessRef.current) {
|
|
385
489
|
progressRef.current = headlessRef.current.progress;
|
|
@@ -429,21 +533,24 @@ function useLessonkitProviderRuntime(config) {
|
|
|
429
533
|
const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!alreadyStarted || clientChanged);
|
|
430
534
|
if (needsBootstrap) {
|
|
431
535
|
try {
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
)
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (
|
|
444
|
-
|
|
536
|
+
const event = buildCourseStartedEvent({
|
|
537
|
+
pluginHost: pluginHostRef.current,
|
|
538
|
+
courseId: cid,
|
|
539
|
+
sessionId,
|
|
540
|
+
attemptId: attemptIdRef.current,
|
|
541
|
+
user: userRef.current,
|
|
542
|
+
lxpackBridge: lxpackBridgeModeRef.current
|
|
543
|
+
});
|
|
544
|
+
if (event === null) {
|
|
545
|
+
} else {
|
|
546
|
+
const statement = telemetryEventToXAPIStatement3(event);
|
|
547
|
+
if (statement) {
|
|
548
|
+
next.send(statement);
|
|
549
|
+
if (!alreadyStarted) {
|
|
550
|
+
markCourseStarted(defaultStorage, sessionId, cid);
|
|
551
|
+
}
|
|
552
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
445
553
|
}
|
|
446
|
-
xapiCourseStartedSentOnClientRef.current = true;
|
|
447
554
|
}
|
|
448
555
|
} catch {
|
|
449
556
|
}
|
|
@@ -514,29 +621,39 @@ function useLessonkitProviderRuntime(config) {
|
|
|
514
621
|
const sessionId = sessionIdRef.current;
|
|
515
622
|
const cid = courseIdRef.current;
|
|
516
623
|
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
624
|
+
const courseStartedFullySettled = hasCourseStartedEmittedToTracking(defaultStorage, sessionId, cid) && hasCourseStarted(defaultStorage, sessionId, cid) && hasCourseStartedPipelineDelivered(defaultStorage, sessionId, cid);
|
|
517
625
|
if (!trackingActive) {
|
|
518
626
|
courseStartedEmittedToSinkRef.current = false;
|
|
519
|
-
} else if (
|
|
520
|
-
const emitted = emitPendingCourseStarted({
|
|
521
|
-
pluginHost: pluginHostRef.current,
|
|
522
|
-
tracking: next,
|
|
523
|
-
xapi: xapiRef.current,
|
|
524
|
-
storage: defaultStorage,
|
|
525
|
-
sessionId,
|
|
526
|
-
courseId: cid,
|
|
527
|
-
attemptId: attemptIdRef.current,
|
|
528
|
-
user: userRef.current,
|
|
529
|
-
lxpackBridge: lxpackBridgeModeRef.current,
|
|
530
|
-
extraSinks: extraSinksRef.current
|
|
531
|
-
});
|
|
532
|
-
if (emitted) {
|
|
533
|
-
markCourseStartedEmittedToTracking(defaultStorage, sessionId, cid);
|
|
534
|
-
}
|
|
535
|
-
courseStartedEmittedToSinkRef.current = emitted;
|
|
536
|
-
} else if (trackingActive) {
|
|
627
|
+
} else if (courseStartedFullySettled) {
|
|
537
628
|
courseStartedEmittedToSinkRef.current = true;
|
|
629
|
+
} else if (!courseStartedEmittedToSinkRef.current) {
|
|
630
|
+
const generation = ++courseStartedEmitGenerationRef.current;
|
|
631
|
+
const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
|
|
632
|
+
void (async () => {
|
|
633
|
+
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
634
|
+
const result = await emitPendingCourseStarted({
|
|
635
|
+
pluginHost: pluginHostRef.current,
|
|
636
|
+
tracking: next,
|
|
637
|
+
xapi: xapiRef.current,
|
|
638
|
+
storage: defaultStorage,
|
|
639
|
+
sessionId,
|
|
640
|
+
courseId: cid,
|
|
641
|
+
attemptId: attemptIdRef.current,
|
|
642
|
+
user: userRef.current,
|
|
643
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
644
|
+
extraSinks: extraSinksRef.current,
|
|
645
|
+
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
646
|
+
onXapiStatementSent: () => {
|
|
647
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
648
|
+
},
|
|
649
|
+
shouldCommit
|
|
650
|
+
});
|
|
651
|
+
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
652
|
+
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
653
|
+
})();
|
|
538
654
|
}
|
|
539
655
|
return () => {
|
|
656
|
+
courseStartedEmitGenerationRef.current += 1;
|
|
540
657
|
if (prev !== trackingRef.current) {
|
|
541
658
|
void disposeTrackingClient(prev);
|
|
542
659
|
}
|
|
@@ -613,7 +730,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
613
730
|
} catch {
|
|
614
731
|
}
|
|
615
732
|
if (!courseStartedEmittedToSinkRef.current) {
|
|
616
|
-
const
|
|
733
|
+
const result = await emitPendingCourseStarted({
|
|
617
734
|
pluginHost: pluginHostRef.current,
|
|
618
735
|
tracking: trackingRef.current,
|
|
619
736
|
xapi: xapiRef.current,
|
|
@@ -625,7 +742,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
625
742
|
lxpackBridge: lxpackBridgeModeRef.current,
|
|
626
743
|
extraSinks: extraSinksRef.current
|
|
627
744
|
});
|
|
628
|
-
courseStartedEmittedToSinkRef.current =
|
|
745
|
+
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
629
746
|
}
|
|
630
747
|
})();
|
|
631
748
|
}, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
|
|
@@ -639,7 +756,10 @@ function useLessonkitProviderRuntime(config) {
|
|
|
639
756
|
[track]
|
|
640
757
|
);
|
|
641
758
|
const completeLesson = useCallback(
|
|
642
|
-
(lessonId) => {
|
|
759
|
+
(lessonId, opts) => {
|
|
760
|
+
if (opts?.courseId !== void 0 && opts.courseId !== courseIdRef.current) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
643
763
|
if (useV2Runtime && headlessRef.current) {
|
|
644
764
|
headlessRef.current.completeLesson(lessonId, emitLifecycleEvent);
|
|
645
765
|
syncProgress();
|
|
@@ -857,11 +977,15 @@ function useEnclosingLessonId() {
|
|
|
857
977
|
|
|
858
978
|
// src/runtime/validateComponentId.ts
|
|
859
979
|
import { assertValidId as assertValidId2 } from "@lessonkit/core";
|
|
860
|
-
function
|
|
980
|
+
function isDevEnvironment4() {
|
|
861
981
|
const g = globalThis;
|
|
862
982
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
863
983
|
}
|
|
864
984
|
function normalizeComponentId(id, path) {
|
|
985
|
+
if (path === "courseId") return assertValidId2(id, "courseId");
|
|
986
|
+
if (path === "lessonId") return assertValidId2(id, "lessonId");
|
|
987
|
+
if (path === "checkId") return assertValidId2(id, "checkId");
|
|
988
|
+
if (path === "blockId") return assertValidId2(id, "blockId");
|
|
865
989
|
return assertValidId2(id, path);
|
|
866
990
|
}
|
|
867
991
|
|
|
@@ -869,7 +993,7 @@ function normalizeComponentId(id, path) {
|
|
|
869
993
|
var mountCounts = /* @__PURE__ */ new Map();
|
|
870
994
|
var warnedConcurrentLessons = false;
|
|
871
995
|
function registerLessonMount(lessonId) {
|
|
872
|
-
if (
|
|
996
|
+
if (isDevEnvironment4() && mountCounts.size > 0 && !mountCounts.has(lessonId) && !warnedConcurrentLessons) {
|
|
873
997
|
warnedConcurrentLessons = true;
|
|
874
998
|
console.warn(
|
|
875
999
|
"[lessonkit] Multiple <Lesson> components are mounted; only one should be active at a time. Set autoCompleteOnUnmount={false} on routed lessons or unmount the previous lesson before showing the next."
|
|
@@ -912,9 +1036,18 @@ function Lesson(props) {
|
|
|
912
1036
|
const { setActiveLesson, config } = useLessonkit();
|
|
913
1037
|
const { completeLesson } = useCompletion();
|
|
914
1038
|
const lessonMountGenerationRef = useRef2(0);
|
|
1039
|
+
const liveCourseIdRef = useRef2(config.courseId);
|
|
1040
|
+
liveCourseIdRef.current = config.courseId;
|
|
915
1041
|
useEffect2(() => {
|
|
916
1042
|
const unregister = registerLessonMount(lessonId);
|
|
917
1043
|
const generation = ++lessonMountGenerationRef.current;
|
|
1044
|
+
const mountedCourseId = config.courseId;
|
|
1045
|
+
let effectSurvivedTick = false;
|
|
1046
|
+
queueMicrotask(() => {
|
|
1047
|
+
queueMicrotask(() => {
|
|
1048
|
+
effectSurvivedTick = true;
|
|
1049
|
+
});
|
|
1050
|
+
});
|
|
918
1051
|
setActiveLesson(lessonId);
|
|
919
1052
|
return () => {
|
|
920
1053
|
unregister();
|
|
@@ -923,8 +1056,10 @@ function Lesson(props) {
|
|
|
923
1056
|
}
|
|
924
1057
|
if (!autoComplete) return;
|
|
925
1058
|
queueMicrotask(() => {
|
|
1059
|
+
if (!effectSurvivedTick) return;
|
|
926
1060
|
if (lessonMountGenerationRef.current !== generation) return;
|
|
927
|
-
|
|
1061
|
+
if (liveCourseIdRef.current !== mountedCourseId) return;
|
|
1062
|
+
completeLesson(lessonId, { courseId: mountedCourseId });
|
|
928
1063
|
});
|
|
929
1064
|
};
|
|
930
1065
|
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
@@ -983,11 +1118,10 @@ function KnowledgeCheck(props) {
|
|
|
983
1118
|
);
|
|
984
1119
|
}
|
|
985
1120
|
function Quiz(props) {
|
|
986
|
-
const checkId = useMemo3(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
987
1121
|
const enclosingLessonId = useEnclosingLessonId();
|
|
988
1122
|
const missingLesson = enclosingLessonId === void 0;
|
|
989
1123
|
useEffect2(() => {
|
|
990
|
-
if (!missingLesson ||
|
|
1124
|
+
if (!missingLesson || isDevEnvironment4()) return;
|
|
991
1125
|
if (!warnedQuizOutsideLesson) {
|
|
992
1126
|
warnedQuizOutsideLesson = true;
|
|
993
1127
|
console.error(
|
|
@@ -995,9 +1129,17 @@ function Quiz(props) {
|
|
|
995
1129
|
);
|
|
996
1130
|
}
|
|
997
1131
|
}, [missingLesson]);
|
|
998
|
-
if (missingLesson &&
|
|
1132
|
+
if (missingLesson && isDevEnvironment4()) {
|
|
999
1133
|
throw new Error("[lessonkit] <Quiz> must be wrapped in <Lesson>");
|
|
1000
1134
|
}
|
|
1135
|
+
if (missingLesson) {
|
|
1136
|
+
return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": props.checkId, children: /* @__PURE__ */ jsx2("p", { children: "Quiz must be placed inside a Lesson." }) });
|
|
1137
|
+
}
|
|
1138
|
+
return /* @__PURE__ */ jsx2(QuizInner, { ...props, enclosingLessonId });
|
|
1139
|
+
}
|
|
1140
|
+
function QuizInner(props) {
|
|
1141
|
+
const { enclosingLessonId } = props;
|
|
1142
|
+
const checkId = useMemo3(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1001
1143
|
const quiz = useQuizState(enclosingLessonId);
|
|
1002
1144
|
const { plugins, config, session } = useLessonkit();
|
|
1003
1145
|
const [selected, setSelected] = useState2(null);
|
|
@@ -1020,9 +1162,6 @@ function Quiz(props) {
|
|
|
1020
1162
|
}
|
|
1021
1163
|
return choice === props.answer;
|
|
1022
1164
|
};
|
|
1023
|
-
if (missingLesson) {
|
|
1024
|
-
return /* @__PURE__ */ jsx2("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": checkId, children: /* @__PURE__ */ jsx2("p", { children: "Quiz must be placed inside a Lesson." }) });
|
|
1025
|
-
}
|
|
1026
1165
|
const passed = quizPassed;
|
|
1027
1166
|
return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
|
|
1028
1167
|
/* @__PURE__ */ jsx2("p", { id: questionId, children: props.question }),
|
|
@@ -1378,7 +1517,13 @@ var BLOCK_CATALOG = [
|
|
|
1378
1517
|
{ name: "checkId", type: "CheckId", required: true, description: "Stable check identifier for telemetry and LXPack assessments." },
|
|
1379
1518
|
{ name: "question", type: "string", required: true, description: "Question text shown above choices." },
|
|
1380
1519
|
{ name: "choices", type: "string[]", required: true, description: "Radio button choice labels." },
|
|
1381
|
-
{ name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." }
|
|
1520
|
+
{ name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." },
|
|
1521
|
+
{
|
|
1522
|
+
name: "passingScore",
|
|
1523
|
+
type: "number",
|
|
1524
|
+
required: false,
|
|
1525
|
+
description: "Minimum score required to pass (defaults to maxScore when omitted)."
|
|
1526
|
+
}
|
|
1382
1527
|
],
|
|
1383
1528
|
requiredIds: ["checkId"],
|
|
1384
1529
|
parentConstraints: ["Lesson"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lessonkit/react",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "React components and hooks for building learning experiences with LessonKit.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -56,11 +56,11 @@
|
|
|
56
56
|
"react-dom": ">=18"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@lessonkit/accessibility": "1.0.
|
|
60
|
-
"@lessonkit/core": "1.0.
|
|
61
|
-
"@lessonkit/lxpack": "1.0.
|
|
62
|
-
"@lessonkit/themes": "1.0.
|
|
63
|
-
"@lessonkit/xapi": "1.0.
|
|
59
|
+
"@lessonkit/accessibility": "1.0.2",
|
|
60
|
+
"@lessonkit/core": "1.0.2",
|
|
61
|
+
"@lessonkit/lxpack": "1.0.2",
|
|
62
|
+
"@lessonkit/themes": "1.0.2",
|
|
63
|
+
"@lessonkit/xapi": "1.0.2"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@storybook/addon-essentials": "8.6.18",
|
|
@@ -80,6 +80,6 @@
|
|
|
80
80
|
"tsup": "^8.5.0",
|
|
81
81
|
"typescript": "^5.8.3",
|
|
82
82
|
"vite": "^6.3.5",
|
|
83
|
-
"vitest": "^
|
|
83
|
+
"vitest": "^4.1.8"
|
|
84
84
|
}
|
|
85
85
|
}
|