@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/block-catalog.v1.json
CHANGED
|
@@ -247,6 +247,12 @@
|
|
|
247
247
|
"type": "string",
|
|
248
248
|
"required": true,
|
|
249
249
|
"description": "Correct choice value (must match one choice)."
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"name": "passingScore",
|
|
253
|
+
"type": "number",
|
|
254
|
+
"required": false,
|
|
255
|
+
"description": "Minimum score required to pass (defaults to maxScore when omitted)."
|
|
250
256
|
}
|
|
251
257
|
],
|
|
252
258
|
"requiredIds": [
|
package/dist/index.cjs
CHANGED
|
@@ -70,8 +70,8 @@ var import_react2 = require("react");
|
|
|
70
70
|
// src/provider/useLessonkitProviderRuntime.ts
|
|
71
71
|
var import_react = require("react");
|
|
72
72
|
var import_core8 = require("@lessonkit/core");
|
|
73
|
-
var import_xapi3 = require("@lessonkit/xapi");
|
|
74
73
|
var import_xapi4 = require("@lessonkit/xapi");
|
|
74
|
+
var import_xapi5 = require("@lessonkit/xapi");
|
|
75
75
|
|
|
76
76
|
// src/runtime/emitTelemetry.ts
|
|
77
77
|
var import_core2 = require("@lessonkit/core");
|
|
@@ -169,6 +169,60 @@ function createXapiClientFromConfig(config, queue) {
|
|
|
169
169
|
// src/runtime/session.ts
|
|
170
170
|
var import_core5 = require("@lessonkit/core");
|
|
171
171
|
|
|
172
|
+
// src/runtime/courseStartedPipeline.ts
|
|
173
|
+
var import_xapi3 = require("@lessonkit/xapi");
|
|
174
|
+
function isDevEnvironment3() {
|
|
175
|
+
const g = globalThis;
|
|
176
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
177
|
+
}
|
|
178
|
+
function warnExtraSinkFailure(sinkId, err) {
|
|
179
|
+
if (isDevEnvironment3()) {
|
|
180
|
+
console.warn(
|
|
181
|
+
`[lessonkit] course_started extra sink "${sinkId}" failed:`,
|
|
182
|
+
err instanceof Error ? err.message : err
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function emitExtraSinks(sinks, event, emitCtx) {
|
|
187
|
+
await Promise.all(
|
|
188
|
+
sinks.map(async (sink) => {
|
|
189
|
+
let result;
|
|
190
|
+
try {
|
|
191
|
+
result = sink.emit(event, emitCtx);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
warnExtraSinkFailure(sink.id, err);
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
if (result != null && typeof result.then === "function") {
|
|
197
|
+
try {
|
|
198
|
+
await result;
|
|
199
|
+
} catch (err) {
|
|
200
|
+
warnExtraSinkFailure(sink.id, err);
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
async function emitCourseStartedNonTrackingPipeline(opts) {
|
|
208
|
+
let xapiStatementSent = false;
|
|
209
|
+
if (!opts.skipXapi && opts.xapi) {
|
|
210
|
+
const statement = (0, import_xapi3.telemetryEventToXAPIStatement)(opts.event);
|
|
211
|
+
if (statement) {
|
|
212
|
+
opts.xapi.send(statement);
|
|
213
|
+
xapiStatementSent = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
forwardTelemetryToLxpack(opts.event, opts.lxpackBridge);
|
|
217
|
+
const emitCtx = {
|
|
218
|
+
courseId: opts.event.courseId,
|
|
219
|
+
sessionId: opts.event.sessionId,
|
|
220
|
+
attemptId: opts.event.attemptId
|
|
221
|
+
};
|
|
222
|
+
await emitExtraSinks(opts.extraSinks ?? [], opts.event, emitCtx);
|
|
223
|
+
return { xapiStatementSent };
|
|
224
|
+
}
|
|
225
|
+
|
|
172
226
|
// src/runtime/plugins.ts
|
|
173
227
|
var import_core6 = require("@lessonkit/core");
|
|
174
228
|
function createReactPluginHost(plugins) {
|
|
@@ -217,11 +271,13 @@ async function disposeTrackingClient(client) {
|
|
|
217
271
|
// src/provider/useLessonkitProviderRuntime.ts
|
|
218
272
|
var useIsoLayoutEffect = typeof window !== "undefined" ? import_react.useLayoutEffect : import_react.useEffect;
|
|
219
273
|
var defaultStorage = (0, import_core3.createSessionStoragePort)();
|
|
274
|
+
var courseStartedTrackingFlightKey = null;
|
|
220
275
|
function isTrackingActive(tracking) {
|
|
221
276
|
return tracking?.enabled !== false;
|
|
222
277
|
}
|
|
223
|
-
|
|
224
|
-
|
|
278
|
+
function isCourseStartedSinkSettled(result) {
|
|
279
|
+
return result === "emitted";
|
|
280
|
+
}
|
|
225
281
|
function buildCourseStartedEvent(opts) {
|
|
226
282
|
const pluginCtx = buildPluginContext({
|
|
227
283
|
courseId: opts.courseId,
|
|
@@ -238,85 +294,113 @@ function buildCourseStartedEvent(opts) {
|
|
|
238
294
|
});
|
|
239
295
|
return opts.pluginHost ? opts.pluginHost.runTelemetry(built, pluginCtx) : built;
|
|
240
296
|
}
|
|
241
|
-
function
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
297
|
+
async function emitCourseStartedToTracking(tracking, storage, sessionId, courseId, event, shouldCommit) {
|
|
298
|
+
const flightKey = `${sessionId}:${courseId}`;
|
|
299
|
+
if ((0, import_core5.hasCourseStartedEmittedToTracking)(storage, sessionId, courseId)) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
if (courseStartedTrackingFlightKey === flightKey) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
courseStartedTrackingFlightKey = flightKey;
|
|
248
306
|
try {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
307
|
+
if (shouldCommit && !shouldCommit()) return false;
|
|
308
|
+
tracking.track(event);
|
|
309
|
+
await tracking.flush?.();
|
|
310
|
+
if (shouldCommit && !shouldCommit()) return false;
|
|
311
|
+
(0, import_core5.markCourseStartedEmittedToTracking)(storage, sessionId, courseId);
|
|
312
|
+
return true;
|
|
313
|
+
} catch {
|
|
314
|
+
return false;
|
|
315
|
+
} finally {
|
|
316
|
+
if (courseStartedTrackingFlightKey === flightKey) {
|
|
317
|
+
courseStartedTrackingFlightKey = null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function emitCourseStartedPipelineOnly(opts) {
|
|
322
|
+
try {
|
|
323
|
+
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
324
|
+
const { xapiStatementSent } = await emitCourseStartedNonTrackingPipeline({
|
|
253
325
|
event: opts.event,
|
|
254
|
-
|
|
326
|
+
xapi: opts.xapi,
|
|
255
327
|
lxpackBridge: opts.lxpackBridge,
|
|
256
|
-
extraSinks: opts.extraSinks
|
|
328
|
+
extraSinks: opts.extraSinks,
|
|
329
|
+
skipXapi: opts.skipXapi
|
|
257
330
|
});
|
|
331
|
+
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
258
332
|
(0, import_core5.markCourseStarted)(opts.storage, opts.sessionId, opts.courseId);
|
|
259
|
-
|
|
333
|
+
(0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId);
|
|
334
|
+
if (xapiStatementSent) {
|
|
335
|
+
opts.onXapiStatementSent?.();
|
|
336
|
+
}
|
|
337
|
+
return "emitted";
|
|
260
338
|
} catch {
|
|
261
|
-
return
|
|
339
|
+
return "failed";
|
|
262
340
|
}
|
|
263
341
|
}
|
|
264
|
-
function emitCourseStarted(opts) {
|
|
342
|
+
async function emitCourseStarted(opts) {
|
|
265
343
|
const event = buildCourseStartedEvent(opts);
|
|
266
|
-
if (event === null) return
|
|
344
|
+
if (event === null) return "filtered";
|
|
267
345
|
const trackingAlreadyEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
|
|
268
346
|
opts.storage,
|
|
269
347
|
opts.sessionId,
|
|
270
348
|
opts.courseId
|
|
271
349
|
);
|
|
272
350
|
if (!trackingAlreadyEmitted) {
|
|
273
|
-
|
|
274
|
-
opts.tracking
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
351
|
+
const tracked = await emitCourseStartedToTracking(
|
|
352
|
+
opts.tracking,
|
|
353
|
+
opts.storage,
|
|
354
|
+
opts.sessionId,
|
|
355
|
+
opts.courseId,
|
|
356
|
+
event,
|
|
357
|
+
opts.shouldCommit
|
|
358
|
+
);
|
|
359
|
+
if (!tracked) return "failed";
|
|
279
360
|
}
|
|
280
|
-
return emitCourseStartedPipelineOnly({
|
|
361
|
+
return emitCourseStartedPipelineOnly({
|
|
362
|
+
...opts,
|
|
363
|
+
event,
|
|
364
|
+
skipXapi: opts.skipXapi,
|
|
365
|
+
onXapiStatementSent: opts.onXapiStatementSent,
|
|
366
|
+
shouldCommit: opts.shouldCommit
|
|
367
|
+
});
|
|
281
368
|
}
|
|
282
|
-
function emitCourseStartedToTrackingOnly(opts) {
|
|
369
|
+
async function emitCourseStartedToTrackingOnly(opts) {
|
|
283
370
|
const event = buildCourseStartedEvent(opts);
|
|
284
|
-
if (event === null) return
|
|
371
|
+
if (event === null) return "filtered";
|
|
285
372
|
const trackingAlreadyEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
|
|
286
373
|
opts.storage,
|
|
287
374
|
opts.sessionId,
|
|
288
375
|
opts.courseId
|
|
289
376
|
);
|
|
290
377
|
if (!trackingAlreadyEmitted) {
|
|
291
|
-
|
|
292
|
-
opts.tracking
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
378
|
+
const tracked = await emitCourseStartedToTracking(
|
|
379
|
+
opts.tracking,
|
|
380
|
+
opts.storage,
|
|
381
|
+
opts.sessionId,
|
|
382
|
+
opts.courseId,
|
|
383
|
+
event,
|
|
384
|
+
opts.shouldCommit
|
|
385
|
+
);
|
|
386
|
+
if (!tracked) return "failed";
|
|
297
387
|
}
|
|
298
|
-
const pluginCtx = buildPluginContext({
|
|
299
|
-
courseId: opts.courseId,
|
|
300
|
-
sessionId: opts.sessionId,
|
|
301
|
-
attemptId: opts.attemptId,
|
|
302
|
-
user: opts.user
|
|
303
|
-
});
|
|
304
388
|
try {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
tracking: noopTrackingClient,
|
|
308
|
-
xapi: null,
|
|
389
|
+
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
390
|
+
await emitCourseStartedNonTrackingPipeline({
|
|
309
391
|
event,
|
|
310
|
-
|
|
392
|
+
xapi: null,
|
|
311
393
|
lxpackBridge: opts.lxpackBridge,
|
|
312
|
-
extraSinks: opts.extraSinks
|
|
394
|
+
extraSinks: opts.extraSinks,
|
|
395
|
+
skipXapi: true
|
|
313
396
|
});
|
|
314
|
-
|
|
397
|
+
(0, import_core5.markCourseStartedPipelineDelivered)(opts.storage, opts.sessionId, opts.courseId);
|
|
398
|
+
return "emitted";
|
|
315
399
|
} catch {
|
|
316
|
-
return
|
|
400
|
+
return "failed";
|
|
317
401
|
}
|
|
318
402
|
}
|
|
319
|
-
function emitPendingCourseStarted(opts) {
|
|
403
|
+
async function emitPendingCourseStarted(opts) {
|
|
320
404
|
const trackingEmitted = (0, import_core5.hasCourseStartedEmittedToTracking)(
|
|
321
405
|
opts.storage,
|
|
322
406
|
opts.sessionId,
|
|
@@ -328,13 +412,28 @@ function emitPendingCourseStarted(opts) {
|
|
|
328
412
|
}
|
|
329
413
|
if (trackingEmitted && !sessionStarted) {
|
|
330
414
|
const event = buildCourseStartedEvent(opts);
|
|
331
|
-
if (event === null) return
|
|
415
|
+
if (event === null) return "filtered";
|
|
332
416
|
return emitCourseStartedPipelineOnly({ ...opts, event });
|
|
333
417
|
}
|
|
334
418
|
if (!trackingEmitted && !sessionStarted) {
|
|
335
419
|
return emitCourseStarted(opts);
|
|
336
420
|
}
|
|
337
|
-
|
|
421
|
+
const pipelineDelivered = (0, import_core5.hasCourseStartedPipelineDelivered)(
|
|
422
|
+
opts.storage,
|
|
423
|
+
opts.sessionId,
|
|
424
|
+
opts.courseId
|
|
425
|
+
);
|
|
426
|
+
if (sessionStarted && trackingEmitted && !pipelineDelivered) {
|
|
427
|
+
const event = buildCourseStartedEvent(opts);
|
|
428
|
+
if (event === null) return "filtered";
|
|
429
|
+
return emitCourseStartedPipelineOnly({
|
|
430
|
+
...opts,
|
|
431
|
+
event,
|
|
432
|
+
skipXapi: opts.skipXapi,
|
|
433
|
+
onXapiStatementSent: opts.onXapiStatementSent
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return "emitted";
|
|
338
437
|
}
|
|
339
438
|
function assertTrackingSinkConfig(tracking) {
|
|
340
439
|
if (!tracking?.sink || !tracking?.batchSink) return;
|
|
@@ -375,6 +474,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
375
474
|
pluginHostRef.current = pluginHost;
|
|
376
475
|
const progressRef = (0, import_react.useRef)((0, import_core4.createProgressController)());
|
|
377
476
|
const courseStartedEmittedToSinkRef = (0, import_react.useRef)(false);
|
|
477
|
+
const courseStartedEmitGenerationRef = (0, import_react.useRef)(0);
|
|
378
478
|
const prevCourseIdForProgressRef = (0, import_react.useRef)(normalizedCourseId);
|
|
379
479
|
const pendingCourseIdResetRef = (0, import_react.useRef)(false);
|
|
380
480
|
const prevUseV2RuntimeRef = (0, import_react.useRef)(useV2Runtime);
|
|
@@ -394,6 +494,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
394
494
|
}
|
|
395
495
|
pendingCourseIdResetRef.current = true;
|
|
396
496
|
courseStartedEmittedToSinkRef.current = false;
|
|
497
|
+
courseStartedEmitGenerationRef.current += 1;
|
|
397
498
|
} else if (useV2Runtime && !headlessRef.current) {
|
|
398
499
|
headlessRef.current = (0, import_core8.createLessonkitRuntime)({
|
|
399
500
|
courseId: normalizedCourseId,
|
|
@@ -411,6 +512,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
411
512
|
}
|
|
412
513
|
pendingCourseIdResetRef.current = true;
|
|
413
514
|
courseStartedEmittedToSinkRef.current = false;
|
|
515
|
+
courseStartedEmitGenerationRef.current += 1;
|
|
414
516
|
}
|
|
415
517
|
if (useV2Runtime && headlessRef.current) {
|
|
416
518
|
progressRef.current = headlessRef.current.progress;
|
|
@@ -421,7 +523,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
421
523
|
}, []);
|
|
422
524
|
const activeLessonIdRef = (0, import_react.useRef)(progress.activeLessonId);
|
|
423
525
|
activeLessonIdRef.current = progress.activeLessonId;
|
|
424
|
-
const xapiQueueRef = (0, import_react.useRef)((0,
|
|
526
|
+
const xapiQueueRef = (0, import_react.useRef)((0, import_xapi4.createInMemoryXAPIQueue)());
|
|
425
527
|
const xapiRef = (0, import_react.useRef)(null);
|
|
426
528
|
const [xapi, setXapi] = (0, import_react.useState)(null);
|
|
427
529
|
const prevXapiCourseIdRef = (0, import_react.useRef)(normalizedCourseId);
|
|
@@ -442,7 +544,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
442
544
|
}
|
|
443
545
|
void xapiRef.current?.flush();
|
|
444
546
|
}
|
|
445
|
-
xapiQueueRef.current = (0,
|
|
547
|
+
xapiQueueRef.current = (0, import_xapi4.createInMemoryXAPIQueue)();
|
|
446
548
|
prevXapiCourseIdRef.current = courseId;
|
|
447
549
|
xapiCourseStartedSentOnClientRef.current = false;
|
|
448
550
|
}
|
|
@@ -460,21 +562,24 @@ function useLessonkitProviderRuntime(config) {
|
|
|
460
562
|
const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && (!alreadyStarted || clientChanged);
|
|
461
563
|
if (needsBootstrap) {
|
|
462
564
|
try {
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
)
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
if (
|
|
475
|
-
|
|
565
|
+
const event = buildCourseStartedEvent({
|
|
566
|
+
pluginHost: pluginHostRef.current,
|
|
567
|
+
courseId: cid,
|
|
568
|
+
sessionId,
|
|
569
|
+
attemptId: attemptIdRef.current,
|
|
570
|
+
user: userRef.current,
|
|
571
|
+
lxpackBridge: lxpackBridgeModeRef.current
|
|
572
|
+
});
|
|
573
|
+
if (event === null) {
|
|
574
|
+
} else {
|
|
575
|
+
const statement = (0, import_xapi5.telemetryEventToXAPIStatement)(event);
|
|
576
|
+
if (statement) {
|
|
577
|
+
next.send(statement);
|
|
578
|
+
if (!alreadyStarted) {
|
|
579
|
+
(0, import_core5.markCourseStarted)(defaultStorage, sessionId, cid);
|
|
580
|
+
}
|
|
581
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
476
582
|
}
|
|
477
|
-
xapiCourseStartedSentOnClientRef.current = true;
|
|
478
583
|
}
|
|
479
584
|
} catch {
|
|
480
585
|
}
|
|
@@ -545,29 +650,39 @@ function useLessonkitProviderRuntime(config) {
|
|
|
545
650
|
const sessionId = sessionIdRef.current;
|
|
546
651
|
const cid = courseIdRef.current;
|
|
547
652
|
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
653
|
+
const courseStartedFullySettled = (0, import_core5.hasCourseStartedEmittedToTracking)(defaultStorage, sessionId, cid) && (0, import_core5.hasCourseStarted)(defaultStorage, sessionId, cid) && (0, import_core5.hasCourseStartedPipelineDelivered)(defaultStorage, sessionId, cid);
|
|
548
654
|
if (!trackingActive) {
|
|
549
655
|
courseStartedEmittedToSinkRef.current = false;
|
|
550
|
-
} else if (
|
|
551
|
-
const emitted = emitPendingCourseStarted({
|
|
552
|
-
pluginHost: pluginHostRef.current,
|
|
553
|
-
tracking: next,
|
|
554
|
-
xapi: xapiRef.current,
|
|
555
|
-
storage: defaultStorage,
|
|
556
|
-
sessionId,
|
|
557
|
-
courseId: cid,
|
|
558
|
-
attemptId: attemptIdRef.current,
|
|
559
|
-
user: userRef.current,
|
|
560
|
-
lxpackBridge: lxpackBridgeModeRef.current,
|
|
561
|
-
extraSinks: extraSinksRef.current
|
|
562
|
-
});
|
|
563
|
-
if (emitted) {
|
|
564
|
-
(0, import_core5.markCourseStartedEmittedToTracking)(defaultStorage, sessionId, cid);
|
|
565
|
-
}
|
|
566
|
-
courseStartedEmittedToSinkRef.current = emitted;
|
|
567
|
-
} else if (trackingActive) {
|
|
656
|
+
} else if (courseStartedFullySettled) {
|
|
568
657
|
courseStartedEmittedToSinkRef.current = true;
|
|
658
|
+
} else if (!courseStartedEmittedToSinkRef.current) {
|
|
659
|
+
const generation = ++courseStartedEmitGenerationRef.current;
|
|
660
|
+
const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
|
|
661
|
+
void (async () => {
|
|
662
|
+
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
663
|
+
const result = await emitPendingCourseStarted({
|
|
664
|
+
pluginHost: pluginHostRef.current,
|
|
665
|
+
tracking: next,
|
|
666
|
+
xapi: xapiRef.current,
|
|
667
|
+
storage: defaultStorage,
|
|
668
|
+
sessionId,
|
|
669
|
+
courseId: cid,
|
|
670
|
+
attemptId: attemptIdRef.current,
|
|
671
|
+
user: userRef.current,
|
|
672
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
673
|
+
extraSinks: extraSinksRef.current,
|
|
674
|
+
skipXapi: xapiCourseStartedSentOnClientRef.current,
|
|
675
|
+
onXapiStatementSent: () => {
|
|
676
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
677
|
+
},
|
|
678
|
+
shouldCommit
|
|
679
|
+
});
|
|
680
|
+
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
681
|
+
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
682
|
+
})();
|
|
569
683
|
}
|
|
570
684
|
return () => {
|
|
685
|
+
courseStartedEmitGenerationRef.current += 1;
|
|
571
686
|
if (prev !== trackingRef.current) {
|
|
572
687
|
void disposeTrackingClient(prev);
|
|
573
688
|
}
|
|
@@ -644,7 +759,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
644
759
|
} catch {
|
|
645
760
|
}
|
|
646
761
|
if (!courseStartedEmittedToSinkRef.current) {
|
|
647
|
-
const
|
|
762
|
+
const result = await emitPendingCourseStarted({
|
|
648
763
|
pluginHost: pluginHostRef.current,
|
|
649
764
|
tracking: trackingRef.current,
|
|
650
765
|
xapi: xapiRef.current,
|
|
@@ -656,7 +771,7 @@ function useLessonkitProviderRuntime(config) {
|
|
|
656
771
|
lxpackBridge: lxpackBridgeModeRef.current,
|
|
657
772
|
extraSinks: extraSinksRef.current
|
|
658
773
|
});
|
|
659
|
-
courseStartedEmittedToSinkRef.current =
|
|
774
|
+
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
660
775
|
}
|
|
661
776
|
})();
|
|
662
777
|
}, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress]);
|
|
@@ -670,7 +785,10 @@ function useLessonkitProviderRuntime(config) {
|
|
|
670
785
|
[track]
|
|
671
786
|
);
|
|
672
787
|
const completeLesson = (0, import_react.useCallback)(
|
|
673
|
-
(lessonId) => {
|
|
788
|
+
(lessonId, opts) => {
|
|
789
|
+
if (opts?.courseId !== void 0 && opts.courseId !== courseIdRef.current) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
674
792
|
if (useV2Runtime && headlessRef.current) {
|
|
675
793
|
headlessRef.current.completeLesson(lessonId, emitLifecycleEvent);
|
|
676
794
|
syncProgress();
|
|
@@ -888,11 +1006,15 @@ function useEnclosingLessonId() {
|
|
|
888
1006
|
|
|
889
1007
|
// src/runtime/validateComponentId.ts
|
|
890
1008
|
var import_core9 = require("@lessonkit/core");
|
|
891
|
-
function
|
|
1009
|
+
function isDevEnvironment4() {
|
|
892
1010
|
const g = globalThis;
|
|
893
1011
|
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV !== "production";
|
|
894
1012
|
}
|
|
895
1013
|
function normalizeComponentId(id, path) {
|
|
1014
|
+
if (path === "courseId") return (0, import_core9.assertValidId)(id, "courseId");
|
|
1015
|
+
if (path === "lessonId") return (0, import_core9.assertValidId)(id, "lessonId");
|
|
1016
|
+
if (path === "checkId") return (0, import_core9.assertValidId)(id, "checkId");
|
|
1017
|
+
if (path === "blockId") return (0, import_core9.assertValidId)(id, "blockId");
|
|
896
1018
|
return (0, import_core9.assertValidId)(id, path);
|
|
897
1019
|
}
|
|
898
1020
|
|
|
@@ -900,7 +1022,7 @@ function normalizeComponentId(id, path) {
|
|
|
900
1022
|
var mountCounts = /* @__PURE__ */ new Map();
|
|
901
1023
|
var warnedConcurrentLessons = false;
|
|
902
1024
|
function registerLessonMount(lessonId) {
|
|
903
|
-
if (
|
|
1025
|
+
if (isDevEnvironment4() && mountCounts.size > 0 && !mountCounts.has(lessonId) && !warnedConcurrentLessons) {
|
|
904
1026
|
warnedConcurrentLessons = true;
|
|
905
1027
|
console.warn(
|
|
906
1028
|
"[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."
|
|
@@ -943,9 +1065,18 @@ function Lesson(props) {
|
|
|
943
1065
|
const { setActiveLesson, config } = useLessonkit();
|
|
944
1066
|
const { completeLesson } = useCompletion();
|
|
945
1067
|
const lessonMountGenerationRef = (0, import_react5.useRef)(0);
|
|
1068
|
+
const liveCourseIdRef = (0, import_react5.useRef)(config.courseId);
|
|
1069
|
+
liveCourseIdRef.current = config.courseId;
|
|
946
1070
|
(0, import_react5.useEffect)(() => {
|
|
947
1071
|
const unregister = registerLessonMount(lessonId);
|
|
948
1072
|
const generation = ++lessonMountGenerationRef.current;
|
|
1073
|
+
const mountedCourseId = config.courseId;
|
|
1074
|
+
let effectSurvivedTick = false;
|
|
1075
|
+
queueMicrotask(() => {
|
|
1076
|
+
queueMicrotask(() => {
|
|
1077
|
+
effectSurvivedTick = true;
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
949
1080
|
setActiveLesson(lessonId);
|
|
950
1081
|
return () => {
|
|
951
1082
|
unregister();
|
|
@@ -954,8 +1085,10 @@ function Lesson(props) {
|
|
|
954
1085
|
}
|
|
955
1086
|
if (!autoComplete) return;
|
|
956
1087
|
queueMicrotask(() => {
|
|
1088
|
+
if (!effectSurvivedTick) return;
|
|
957
1089
|
if (lessonMountGenerationRef.current !== generation) return;
|
|
958
|
-
|
|
1090
|
+
if (liveCourseIdRef.current !== mountedCourseId) return;
|
|
1091
|
+
completeLesson(lessonId, { courseId: mountedCourseId });
|
|
959
1092
|
});
|
|
960
1093
|
};
|
|
961
1094
|
}, [lessonId, config.courseId, setActiveLesson, completeLesson, autoComplete]);
|
|
@@ -1014,11 +1147,10 @@ function KnowledgeCheck(props) {
|
|
|
1014
1147
|
);
|
|
1015
1148
|
}
|
|
1016
1149
|
function Quiz(props) {
|
|
1017
|
-
const checkId = (0, import_react5.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1018
1150
|
const enclosingLessonId = useEnclosingLessonId();
|
|
1019
1151
|
const missingLesson = enclosingLessonId === void 0;
|
|
1020
1152
|
(0, import_react5.useEffect)(() => {
|
|
1021
|
-
if (!missingLesson ||
|
|
1153
|
+
if (!missingLesson || isDevEnvironment4()) return;
|
|
1022
1154
|
if (!warnedQuizOutsideLesson) {
|
|
1023
1155
|
warnedQuizOutsideLesson = true;
|
|
1024
1156
|
console.error(
|
|
@@ -1026,9 +1158,17 @@ function Quiz(props) {
|
|
|
1026
1158
|
);
|
|
1027
1159
|
}
|
|
1028
1160
|
}, [missingLesson]);
|
|
1029
|
-
if (missingLesson &&
|
|
1161
|
+
if (missingLesson && isDevEnvironment4()) {
|
|
1030
1162
|
throw new Error("[lessonkit] <Quiz> must be wrapped in <Lesson>");
|
|
1031
1163
|
}
|
|
1164
|
+
if (missingLesson) {
|
|
1165
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": props.checkId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: "Quiz must be placed inside a Lesson." }) });
|
|
1166
|
+
}
|
|
1167
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(QuizInner, { ...props, enclosingLessonId });
|
|
1168
|
+
}
|
|
1169
|
+
function QuizInner(props) {
|
|
1170
|
+
const { enclosingLessonId } = props;
|
|
1171
|
+
const checkId = (0, import_react5.useMemo)(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
|
|
1032
1172
|
const quiz = useQuizState(enclosingLessonId);
|
|
1033
1173
|
const { plugins, config, session } = useLessonkit();
|
|
1034
1174
|
const [selected, setSelected] = (0, import_react5.useState)(null);
|
|
@@ -1051,9 +1191,6 @@ function Quiz(props) {
|
|
|
1051
1191
|
}
|
|
1052
1192
|
return choice === props.answer;
|
|
1053
1193
|
};
|
|
1054
|
-
if (missingLesson) {
|
|
1055
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { role: "alert", "aria-label": "Quiz configuration error", "data-lk-check-id": checkId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: "Quiz must be placed inside a Lesson." }) });
|
|
1056
|
-
}
|
|
1057
1194
|
const passed = quizPassed;
|
|
1058
1195
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
|
|
1059
1196
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { id: questionId, children: props.question }),
|
|
@@ -1386,7 +1523,13 @@ var BLOCK_CATALOG = [
|
|
|
1386
1523
|
{ name: "checkId", type: "CheckId", required: true, description: "Stable check identifier for telemetry and LXPack assessments." },
|
|
1387
1524
|
{ name: "question", type: "string", required: true, description: "Question text shown above choices." },
|
|
1388
1525
|
{ name: "choices", type: "string[]", required: true, description: "Radio button choice labels." },
|
|
1389
|
-
{ name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." }
|
|
1526
|
+
{ name: "answer", type: "string", required: true, description: "Correct choice value (must match one choice)." },
|
|
1527
|
+
{
|
|
1528
|
+
name: "passingScore",
|
|
1529
|
+
type: "number",
|
|
1530
|
+
required: false,
|
|
1531
|
+
description: "Minimum score required to pass (defaults to maxScore when omitted)."
|
|
1532
|
+
}
|
|
1390
1533
|
],
|
|
1391
1534
|
requiredIds: ["checkId"],
|
|
1392
1535
|
parentConstraints: ["Lesson"],
|