@lessonkit/react 1.4.0 → 1.5.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 +17 -9
- package/block-catalog.v3.json +445 -32
- package/dist/{AssessmentLessonGuard-D2Plzybb.d.cts → AssessmentLessonGuard-BzNPbjaV.d.cts} +1 -1
- package/dist/{AssessmentLessonGuard-D2Plzybb.d.ts → AssessmentLessonGuard-BzNPbjaV.d.ts} +1 -1
- package/dist/blocks-entry.cjs +1850 -328
- package/dist/blocks-entry.d.cts +61 -1
- package/dist/blocks-entry.d.ts +61 -1
- package/dist/blocks-entry.js +12 -2
- package/dist/{chunk-4LQ4TTEE.js → chunk-5P23C2W3.js} +1830 -313
- package/dist/{chunk-TDM3ARE7.js → chunk-7TJQJFYR.js} +483 -276
- package/dist/{chunk-UUTXECVW.js → chunk-ELGQ4XI3.js} +49 -30
- package/dist/index.cjs +2620 -774
- package/dist/index.d.cts +92 -5
- package/dist/index.d.ts +92 -5
- package/dist/index.js +125 -5
- package/dist/testing.cjs +86 -50
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +2 -2
- package/package.json +15 -18
|
@@ -52,103 +52,14 @@ function AssessmentLessonGuard(props) {
|
|
|
52
52
|
return /* @__PURE__ */ jsx(Fragment, { children: props.children(enclosingLessonId) });
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// src/runtime/productionGuard.ts
|
|
56
|
-
function isProductionEnvironment() {
|
|
57
|
-
try {
|
|
58
|
-
if (import.meta.env?.PROD === true) return true;
|
|
59
|
-
} catch {
|
|
60
|
-
}
|
|
61
|
-
const g = globalThis;
|
|
62
|
-
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production";
|
|
63
|
-
}
|
|
64
|
-
function shouldEnforceProductionGuard() {
|
|
65
|
-
try {
|
|
66
|
-
if (import.meta.env?.MODE === "test") return false;
|
|
67
|
-
} catch {
|
|
68
|
-
}
|
|
69
|
-
return isProductionEnvironment();
|
|
70
|
-
}
|
|
71
|
-
function looksLikeConsoleSink(fn) {
|
|
72
|
-
if (typeof fn !== "function") return false;
|
|
73
|
-
const src = Function.prototype.toString.call(fn);
|
|
74
|
-
return /console\.(log|debug|info)\s*\(/.test(src);
|
|
75
|
-
}
|
|
76
|
-
function isTrackingDeliveryConfigured(tracking) {
|
|
77
|
-
if (!tracking || tracking.enabled === false) return false;
|
|
78
|
-
return Boolean(tracking.sink || tracking.batchSink);
|
|
79
|
-
}
|
|
80
|
-
function isXapiDeliveryConfigured(xapi) {
|
|
81
|
-
if (!xapi || xapi.enabled === false) return false;
|
|
82
|
-
if (xapi.client) return true;
|
|
83
|
-
return typeof xapi.transport === "function";
|
|
84
|
-
}
|
|
85
|
-
function trackingUsesConsole(config) {
|
|
86
|
-
const tracking = config.tracking;
|
|
87
|
-
if (!tracking || tracking.enabled === false) return false;
|
|
88
|
-
if (tracking.batchSink && looksLikeConsoleSink(tracking.batchSink)) return true;
|
|
89
|
-
if (tracking.sink && looksLikeConsoleSink(tracking.sink)) return true;
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
function xapiUsesConsole(config) {
|
|
93
|
-
const xapi = config.xapi;
|
|
94
|
-
if (!xapi || xapi.enabled === false || xapi.client) return false;
|
|
95
|
-
return typeof xapi.transport === "function" && looksLikeConsoleSink(xapi.transport);
|
|
96
|
-
}
|
|
97
|
-
function observabilityIncomplete(observability, opts) {
|
|
98
|
-
if (!opts.trackingEnabled && !opts.xapiEnabled) return false;
|
|
99
|
-
const required = [observability?.onLxpackBridgeMiss];
|
|
100
|
-
if (opts.trackingEnabled) {
|
|
101
|
-
required.push(observability?.onTelemetrySinkError, observability?.onTelemetryBufferDrop);
|
|
102
|
-
}
|
|
103
|
-
if (opts.xapiEnabled) {
|
|
104
|
-
required.push(
|
|
105
|
-
observability?.onXapiQueueDepth,
|
|
106
|
-
observability?.onXapiQueueCap,
|
|
107
|
-
observability?.onXapiTransportError
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
return required.some((hook) => !hook);
|
|
111
|
-
}
|
|
112
|
-
function requiredObservabilityHookCount(opts) {
|
|
113
|
-
let count = 1;
|
|
114
|
-
if (opts.trackingEnabled) count += 2;
|
|
115
|
-
if (opts.xapiEnabled) count += 3;
|
|
116
|
-
return count;
|
|
117
|
-
}
|
|
118
|
-
function assertProductionCourseConfig(config) {
|
|
119
|
-
if (!isProductionEnvironment()) return;
|
|
120
|
-
if (config.tracking && config.tracking.enabled !== false && !isTrackingDeliveryConfigured(config.tracking)) {
|
|
121
|
-
throw new Error(
|
|
122
|
-
"[lessonkit] Production build has tracking enabled but no sink or batchSink configured."
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
const trackingEnabled = isTrackingDeliveryConfigured(config.tracking);
|
|
126
|
-
const xapiEnabled = isXapiDeliveryConfigured(config.xapi);
|
|
127
|
-
if (trackingUsesConsole(config)) {
|
|
128
|
-
throw new Error(
|
|
129
|
-
"[lessonkit] Production build uses console telemetry sinks. Wire createFetchBatchSink or a real sink. See production checklist."
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
if (xapiUsesConsole(config)) {
|
|
133
|
-
throw new Error(
|
|
134
|
-
"[lessonkit] Production build uses console xAPI transport. Wire createFetchTransport to your LRS proxy. See production checklist."
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
if (observabilityIncomplete(config.observability, { trackingEnabled, xapiEnabled })) {
|
|
138
|
-
const hookCount = requiredObservabilityHookCount({ trackingEnabled, xapiEnabled });
|
|
139
|
-
throw new Error(
|
|
140
|
-
`[lessonkit] Production build missing observability hooks. Wire all ${hookCount} config.observability callbacks before go-live.`
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
55
|
// src/runtime/emitTelemetry.ts
|
|
146
56
|
import { buildTelemetryEvent, tryBuildTelemetryEvent } from "@lessonkit/core";
|
|
147
57
|
|
|
148
58
|
// src/runtime/telemetryPipeline.ts
|
|
149
59
|
import {
|
|
150
60
|
createTelemetryPipeline,
|
|
151
|
-
createTrackingPipelineSink
|
|
61
|
+
createTrackingPipelineSink,
|
|
62
|
+
isLifecycleTelemetryEvent
|
|
152
63
|
} from "@lessonkit/core";
|
|
153
64
|
import { telemetryEventToXAPIStatement } from "@lessonkit/xapi";
|
|
154
65
|
|
|
@@ -161,16 +72,22 @@ import {
|
|
|
161
72
|
telemetryEventToLessonkit
|
|
162
73
|
} from "@lessonkit/lxpack/bridge";
|
|
163
74
|
var BRIDGE_MISS_EVENT_NAMES = /* @__PURE__ */ new Set([
|
|
75
|
+
"course_started",
|
|
164
76
|
"course_completed",
|
|
165
77
|
"lesson_completed",
|
|
78
|
+
"assessment_answered",
|
|
166
79
|
"assessment_completed",
|
|
167
80
|
"quiz_completed"
|
|
168
81
|
]);
|
|
169
82
|
function forwardTelemetryToLxpack(event, mode = "auto", opts) {
|
|
170
|
-
|
|
83
|
+
const bridgeOpts = { allowedParentOrigins: opts?.allowedParentOrigins, mode };
|
|
84
|
+
if (mode === "auto" && opts?.onBridgeMiss && BRIDGE_MISS_EVENT_NAMES.has(event.name) && !getLxpackBridge(void 0, bridgeOpts)) {
|
|
171
85
|
opts.onBridgeMiss(event);
|
|
172
86
|
}
|
|
173
|
-
forwardTelemetryToBridge(event, mode
|
|
87
|
+
forwardTelemetryToBridge(event, mode, void 0, {
|
|
88
|
+
allowedParentOrigins: opts?.allowedParentOrigins,
|
|
89
|
+
onBridgeError: opts?.onBridgeError
|
|
90
|
+
});
|
|
174
91
|
}
|
|
175
92
|
|
|
176
93
|
// src/runtime/telemetryPipeline.ts
|
|
@@ -183,17 +100,34 @@ function createLegacyPipeline(opts, extraSinks = []) {
|
|
|
183
100
|
createTrackingPipelineSink("tracking", (event) => opts.tracking.track(event)),
|
|
184
101
|
{
|
|
185
102
|
id: "xapi",
|
|
186
|
-
emit(event) {
|
|
103
|
+
async emit(event) {
|
|
104
|
+
let statement;
|
|
187
105
|
try {
|
|
188
|
-
|
|
189
|
-
if (statement) opts.xapi?.send(statement);
|
|
106
|
+
statement = telemetryEventToXAPIStatement(event);
|
|
190
107
|
} catch (err) {
|
|
108
|
+
opts.onXapiMappingError?.(err);
|
|
191
109
|
if (isDevEnvironment2()) {
|
|
192
110
|
console.warn(
|
|
193
111
|
"[lessonkit] xAPI mapping skipped:",
|
|
194
112
|
err instanceof Error ? err.message : err
|
|
195
113
|
);
|
|
196
114
|
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (!statement || !opts.xapi) return;
|
|
118
|
+
try {
|
|
119
|
+
opts.xapi.send(statement);
|
|
120
|
+
if (isLifecycleTelemetryEvent(event.name)) {
|
|
121
|
+
await opts.xapi.flush();
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
opts.onXapiTransportError?.(err);
|
|
125
|
+
if (isDevEnvironment2()) {
|
|
126
|
+
console.warn(
|
|
127
|
+
"[lessonkit] xAPI delivery failed:",
|
|
128
|
+
err instanceof Error ? err.message : err
|
|
129
|
+
);
|
|
130
|
+
}
|
|
197
131
|
}
|
|
198
132
|
}
|
|
199
133
|
},
|
|
@@ -201,7 +135,9 @@ function createLegacyPipeline(opts, extraSinks = []) {
|
|
|
201
135
|
id: "lxpack-bridge",
|
|
202
136
|
emit(event) {
|
|
203
137
|
forwardTelemetryToLxpack(event, opts.lxpackBridge, {
|
|
204
|
-
onBridgeMiss: opts.onLxpackBridgeMiss
|
|
138
|
+
onBridgeMiss: opts.onLxpackBridgeMiss,
|
|
139
|
+
onBridgeError: opts.onLxpackBridgeError,
|
|
140
|
+
allowedParentOrigins: opts.allowedParentOrigins
|
|
205
141
|
});
|
|
206
142
|
}
|
|
207
143
|
},
|
|
@@ -209,7 +145,7 @@ function createLegacyPipeline(opts, extraSinks = []) {
|
|
|
209
145
|
]);
|
|
210
146
|
}
|
|
211
147
|
function emitThroughPipeline(event, opts, extraSinks) {
|
|
212
|
-
createLegacyPipeline(opts, extraSinks).emit(event);
|
|
148
|
+
return createLegacyPipeline(opts, extraSinks).emit(event);
|
|
213
149
|
}
|
|
214
150
|
|
|
215
151
|
// src/runtime/emitTelemetry.ts
|
|
@@ -230,9 +166,13 @@ function emitTelemetry(tracking, xapi, event, opts) {
|
|
|
230
166
|
tracking,
|
|
231
167
|
xapi,
|
|
232
168
|
lxpackBridge: opts?.lxpackBridge ?? "auto",
|
|
233
|
-
|
|
169
|
+
allowedParentOrigins: opts?.allowedParentOrigins,
|
|
170
|
+
onLxpackBridgeMiss: opts?.onLxpackBridgeMiss,
|
|
171
|
+
onLxpackBridgeError: opts?.onLxpackBridgeError,
|
|
172
|
+
onXapiMappingError: opts?.onXapiMappingError,
|
|
173
|
+
onXapiTransportError: opts?.onXapiTransportError
|
|
234
174
|
};
|
|
235
|
-
emitThroughPipeline(event, legacy, opts?.extraSinks);
|
|
175
|
+
return emitThroughPipeline(event, legacy, opts?.extraSinks);
|
|
236
176
|
}
|
|
237
177
|
|
|
238
178
|
// src/runtime/courseStartedPipeline.ts
|
|
@@ -273,16 +213,34 @@ async function emitExtraSinks(sinks, event, emitCtx) {
|
|
|
273
213
|
async function emitCourseStartedNonTrackingPipeline(opts) {
|
|
274
214
|
let xapiStatementSent = false;
|
|
275
215
|
if (!opts.skipXapi && opts.xapi) {
|
|
276
|
-
|
|
216
|
+
let statement;
|
|
217
|
+
try {
|
|
218
|
+
statement = telemetryEventToXAPIStatement2(opts.event);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
opts.onXapiMappingError?.(err);
|
|
221
|
+
if (isDevEnvironment4()) {
|
|
222
|
+
console.warn(
|
|
223
|
+
"[lessonkit] course_started xAPI mapping skipped:",
|
|
224
|
+
err instanceof Error ? err.message : err
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
statement = null;
|
|
228
|
+
}
|
|
277
229
|
if (statement) {
|
|
278
230
|
opts.xapi.send(statement);
|
|
279
231
|
await opts.xapi.flush();
|
|
280
232
|
xapiStatementSent = true;
|
|
233
|
+
opts.onXapiDelivered?.();
|
|
281
234
|
}
|
|
282
235
|
}
|
|
283
236
|
forwardTelemetryToLxpack(opts.event, opts.lxpackBridge, {
|
|
284
|
-
onBridgeMiss: opts.onLxpackBridgeMiss
|
|
237
|
+
onBridgeMiss: opts.onLxpackBridgeMiss,
|
|
238
|
+
onBridgeError: opts.onLxpackBridgeError,
|
|
239
|
+
allowedParentOrigins: opts.allowedParentOrigins
|
|
285
240
|
});
|
|
241
|
+
if (opts.onBeforeExtraSinks) {
|
|
242
|
+
await opts.onBeforeExtraSinks();
|
|
243
|
+
}
|
|
286
244
|
const emitCtx = {
|
|
287
245
|
courseId: opts.event.courseId,
|
|
288
246
|
sessionId: opts.event.sessionId,
|
|
@@ -306,8 +264,12 @@ function emitTelemetryWithPlugins(opts) {
|
|
|
306
264
|
if (next === null) return;
|
|
307
265
|
emitTelemetry(opts.tracking, opts.xapi, next, {
|
|
308
266
|
lxpackBridge: opts.lxpackBridge ?? "auto",
|
|
267
|
+
allowedParentOrigins: opts.allowedParentOrigins,
|
|
309
268
|
extraSinks: opts.extraSinks,
|
|
310
|
-
onLxpackBridgeMiss: opts.onLxpackBridgeMiss
|
|
269
|
+
onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
|
|
270
|
+
onLxpackBridgeError: opts.onLxpackBridgeError,
|
|
271
|
+
onXapiMappingError: opts.onXapiMappingError,
|
|
272
|
+
onXapiTransportError: opts.onXapiTransportError
|
|
311
273
|
});
|
|
312
274
|
}
|
|
313
275
|
|
|
@@ -322,34 +284,48 @@ import {
|
|
|
322
284
|
markCourseStartedEmittedToTracking,
|
|
323
285
|
hasCourseStartedPipelineDelivered,
|
|
324
286
|
markCourseStartedPipelineDelivered,
|
|
287
|
+
hasCourseStartedXapiSent,
|
|
288
|
+
markCourseStartedXapiSent,
|
|
325
289
|
migrateCourseStartedMark
|
|
326
290
|
} from "@lessonkit/core";
|
|
327
291
|
|
|
328
292
|
// src/provider/courseStarted/emit.ts
|
|
293
|
+
function createCourseStartedFlightScope() {
|
|
294
|
+
return {
|
|
295
|
+
trackingFlights: /* @__PURE__ */ new Map(),
|
|
296
|
+
emitFlights: /* @__PURE__ */ new Map()
|
|
297
|
+
};
|
|
298
|
+
}
|
|
329
299
|
function resolveTrackingClient(source) {
|
|
330
300
|
return typeof source === "function" ? source() : source;
|
|
331
301
|
}
|
|
332
|
-
var
|
|
333
|
-
var courseStartedEmitFlights = /* @__PURE__ */ new Map();
|
|
302
|
+
var defaultFlightScope = createCourseStartedFlightScope();
|
|
334
303
|
function resetCourseStartedTrackingFlightForTests() {
|
|
335
|
-
|
|
336
|
-
|
|
304
|
+
defaultFlightScope.trackingFlights.clear();
|
|
305
|
+
defaultFlightScope.emitFlights.clear();
|
|
306
|
+
}
|
|
307
|
+
function resetCourseStartedTrackingFlights(scope) {
|
|
308
|
+
const target = scope ?? defaultFlightScope;
|
|
309
|
+
target.trackingFlights.clear();
|
|
310
|
+
target.emitFlights.clear();
|
|
311
|
+
}
|
|
312
|
+
function resolveFlightScope(scope) {
|
|
313
|
+
return scope ?? defaultFlightScope;
|
|
337
314
|
}
|
|
338
315
|
function isTrackingActive(tracking) {
|
|
339
316
|
return tracking?.enabled !== false;
|
|
340
317
|
}
|
|
341
318
|
function isCourseStartedSinkSettled(result) {
|
|
342
|
-
return result === "emitted"
|
|
319
|
+
return result === "emitted";
|
|
343
320
|
}
|
|
344
321
|
async function deliverToTracking(client, event) {
|
|
345
322
|
if (client.deliver) {
|
|
346
323
|
return client.deliver(event);
|
|
347
324
|
}
|
|
348
325
|
client.track(event);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
return false;
|
|
326
|
+
if (!client.flush) return true;
|
|
327
|
+
const flushed = await client.flush();
|
|
328
|
+
return flushed !== false;
|
|
353
329
|
}
|
|
354
330
|
function buildCourseStartedEvent(opts) {
|
|
355
331
|
const pluginCtx = buildPluginContext({
|
|
@@ -365,14 +341,19 @@ function buildCourseStartedEvent(opts) {
|
|
|
365
341
|
attemptId: opts.attemptId,
|
|
366
342
|
user: opts.user
|
|
367
343
|
});
|
|
368
|
-
|
|
344
|
+
const withId = {
|
|
345
|
+
...built,
|
|
346
|
+
id: `${opts.sessionId}:${opts.courseId}:course_started`
|
|
347
|
+
};
|
|
348
|
+
return opts.pluginHost ? opts.pluginHost.runTelemetry(withId, pluginCtx) : withId;
|
|
369
349
|
}
|
|
370
|
-
async function emitCourseStartedToTracking(tracking, storage, sessionId, courseId, event, shouldCommit) {
|
|
350
|
+
async function emitCourseStartedToTracking(tracking, storage, sessionId, courseId, event, shouldCommit, flightScope) {
|
|
351
|
+
const scope = resolveFlightScope(flightScope);
|
|
371
352
|
const flightKey = `${sessionId}:${courseId}`;
|
|
372
353
|
if (hasCourseStartedEmittedToTracking(storage, sessionId, courseId)) {
|
|
373
354
|
return true;
|
|
374
355
|
}
|
|
375
|
-
const existing =
|
|
356
|
+
const existing = scope.trackingFlights.get(flightKey);
|
|
376
357
|
if (existing) {
|
|
377
358
|
return existing;
|
|
378
359
|
}
|
|
@@ -380,7 +361,7 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
|
|
|
380
361
|
const flight = new Promise((resolve) => {
|
|
381
362
|
resolveFlight = resolve;
|
|
382
363
|
});
|
|
383
|
-
|
|
364
|
+
scope.trackingFlights.set(flightKey, flight);
|
|
384
365
|
void (async () => {
|
|
385
366
|
try {
|
|
386
367
|
if (shouldCommit && !shouldCommit()) {
|
|
@@ -398,6 +379,10 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
|
|
|
398
379
|
return;
|
|
399
380
|
}
|
|
400
381
|
if (markCourseStartedEmittedToTracking(storage, sessionId, courseId) === false) {
|
|
382
|
+
if (hasCourseStartedEmittedToTracking(storage, sessionId, courseId)) {
|
|
383
|
+
resolveFlight(true);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
401
386
|
resolveFlight(false);
|
|
402
387
|
return;
|
|
403
388
|
}
|
|
@@ -405,30 +390,50 @@ async function emitCourseStartedToTracking(tracking, storage, sessionId, courseI
|
|
|
405
390
|
} catch {
|
|
406
391
|
resolveFlight(false);
|
|
407
392
|
} finally {
|
|
408
|
-
if (
|
|
409
|
-
|
|
393
|
+
if (scope.trackingFlights.get(flightKey) === flight) {
|
|
394
|
+
scope.trackingFlights.delete(flightKey);
|
|
410
395
|
}
|
|
411
396
|
}
|
|
412
397
|
})();
|
|
413
398
|
return flight;
|
|
414
399
|
}
|
|
400
|
+
function resolveSkipXapi(storage, sessionId, courseId, skipXapi) {
|
|
401
|
+
return Boolean(skipXapi || hasCourseStartedXapiSent(storage, sessionId, courseId));
|
|
402
|
+
}
|
|
415
403
|
async function emitCourseStartedPipelineOnly(opts) {
|
|
416
404
|
try {
|
|
417
405
|
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
406
|
+
const skipXapi = resolveSkipXapi(opts.storage, opts.sessionId, opts.courseId, opts.skipXapi);
|
|
418
407
|
const { xapiStatementSent } = await emitCourseStartedNonTrackingPipeline({
|
|
419
408
|
event: opts.event,
|
|
420
409
|
xapi: opts.xapi,
|
|
421
410
|
lxpackBridge: opts.lxpackBridge,
|
|
411
|
+
allowedParentOrigins: opts.allowedParentOrigins,
|
|
422
412
|
onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
|
|
413
|
+
onLxpackBridgeError: opts.onLxpackBridgeError,
|
|
414
|
+
onXapiMappingError: opts.onXapiMappingError,
|
|
423
415
|
extraSinks: opts.extraSinks,
|
|
424
|
-
skipXapi
|
|
416
|
+
skipXapi,
|
|
417
|
+
onXapiDelivered: () => {
|
|
418
|
+
markCourseStartedXapiSent(opts.storage, opts.sessionId, opts.courseId);
|
|
419
|
+
opts.onXapiStatementSent?.();
|
|
420
|
+
},
|
|
421
|
+
onBeforeExtraSinks: async () => {
|
|
422
|
+
if (opts.shouldCommit && !opts.shouldCommit()) throw new Error("course_started commit aborted");
|
|
423
|
+
if (markCourseStarted(opts.storage, opts.sessionId, opts.courseId) === false && !hasCourseStarted(opts.storage, opts.sessionId, opts.courseId)) {
|
|
424
|
+
throw new Error("course_started mark failed");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
425
427
|
});
|
|
426
428
|
if (opts.shouldCommit && !opts.shouldCommit()) return "failed";
|
|
427
|
-
if (markCourseStarted(opts.storage, opts.sessionId, opts.courseId) === false)
|
|
428
|
-
|
|
429
|
+
if (markCourseStarted(opts.storage, opts.sessionId, opts.courseId) === false && !hasCourseStarted(opts.storage, opts.sessionId, opts.courseId)) {
|
|
430
|
+
return "failed";
|
|
431
|
+
}
|
|
432
|
+
if (markCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId) === false && !hasCourseStartedPipelineDelivered(opts.storage, opts.sessionId, opts.courseId)) {
|
|
429
433
|
return "failed";
|
|
430
434
|
}
|
|
431
|
-
if (xapiStatementSent) {
|
|
435
|
+
if (xapiStatementSent && !hasCourseStartedXapiSent(opts.storage, opts.sessionId, opts.courseId)) {
|
|
436
|
+
markCourseStartedXapiSent(opts.storage, opts.sessionId, opts.courseId);
|
|
432
437
|
opts.onXapiStatementSent?.();
|
|
433
438
|
}
|
|
434
439
|
return "emitted";
|
|
@@ -445,7 +450,8 @@ async function emitCourseStarted(opts) {
|
|
|
445
450
|
opts.sessionId,
|
|
446
451
|
opts.courseId,
|
|
447
452
|
event,
|
|
448
|
-
opts.shouldCommit
|
|
453
|
+
opts.shouldCommit,
|
|
454
|
+
opts.flightScope
|
|
449
455
|
);
|
|
450
456
|
if (!tracked) return "failed";
|
|
451
457
|
return emitCourseStartedPipelineOnly({
|
|
@@ -465,7 +471,8 @@ async function emitCourseStartedToTrackingOnly(opts) {
|
|
|
465
471
|
opts.sessionId,
|
|
466
472
|
opts.courseId,
|
|
467
473
|
event,
|
|
468
|
-
opts.shouldCommit
|
|
474
|
+
opts.shouldCommit,
|
|
475
|
+
opts.flightScope
|
|
469
476
|
);
|
|
470
477
|
if (!tracked) return "failed";
|
|
471
478
|
try {
|
|
@@ -474,7 +481,10 @@ async function emitCourseStartedToTrackingOnly(opts) {
|
|
|
474
481
|
event,
|
|
475
482
|
xapi: null,
|
|
476
483
|
lxpackBridge: opts.lxpackBridge,
|
|
484
|
+
allowedParentOrigins: opts.allowedParentOrigins,
|
|
477
485
|
onLxpackBridgeMiss: opts.onLxpackBridgeMiss,
|
|
486
|
+
onLxpackBridgeError: opts.onLxpackBridgeError,
|
|
487
|
+
onXapiMappingError: opts.onXapiMappingError,
|
|
478
488
|
extraSinks: opts.extraSinks,
|
|
479
489
|
skipXapi: true
|
|
480
490
|
});
|
|
@@ -487,10 +497,11 @@ async function emitCourseStartedToTrackingOnly(opts) {
|
|
|
487
497
|
}
|
|
488
498
|
}
|
|
489
499
|
async function emitPendingCourseStarted(opts) {
|
|
500
|
+
const scope = resolveFlightScope(opts.flightScope);
|
|
490
501
|
const flightKey = `${opts.sessionId}:${opts.courseId}`;
|
|
491
502
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
492
|
-
const existing =
|
|
493
|
-
const flight = existing ?? startPendingCourseStartedFlight(opts, flightKey);
|
|
503
|
+
const existing = scope.emitFlights.get(flightKey);
|
|
504
|
+
const flight = existing ?? startPendingCourseStartedFlight(opts, flightKey, scope);
|
|
494
505
|
const result = await flight;
|
|
495
506
|
if (result !== "failed") return result;
|
|
496
507
|
const sessionStarted = hasCourseStarted(opts.storage, opts.sessionId, opts.courseId);
|
|
@@ -511,12 +522,12 @@ async function emitPendingCourseStarted(opts) {
|
|
|
511
522
|
}
|
|
512
523
|
return "failed";
|
|
513
524
|
}
|
|
514
|
-
function startPendingCourseStartedFlight(opts, flightKey) {
|
|
525
|
+
function startPendingCourseStartedFlight(opts, flightKey, scope) {
|
|
515
526
|
const flight = emitPendingCourseStartedInner(opts);
|
|
516
|
-
|
|
527
|
+
scope.emitFlights.set(flightKey, flight);
|
|
517
528
|
void flight.finally(() => {
|
|
518
|
-
if (
|
|
519
|
-
|
|
529
|
+
if (scope.emitFlights.get(flightKey) === flight) {
|
|
530
|
+
scope.emitFlights.delete(flightKey);
|
|
520
531
|
}
|
|
521
532
|
});
|
|
522
533
|
return flight;
|
|
@@ -536,16 +547,17 @@ async function emitPendingCourseStartedInner(opts) {
|
|
|
536
547
|
if (sessionStarted && trackingEmitted && pipelineDelivered) {
|
|
537
548
|
return "emitted";
|
|
538
549
|
}
|
|
550
|
+
const skipXapi = resolveSkipXapi(opts.storage, opts.sessionId, opts.courseId, opts.skipXapi);
|
|
539
551
|
if (sessionStarted && !trackingEmitted) {
|
|
540
552
|
return emitCourseStartedToTrackingOnly(opts);
|
|
541
553
|
}
|
|
542
554
|
if (trackingEmitted && !sessionStarted) {
|
|
543
555
|
const event = buildCourseStartedEvent(opts);
|
|
544
556
|
if (event === null) return "filtered";
|
|
545
|
-
return emitCourseStartedPipelineOnly({ ...opts, event });
|
|
557
|
+
return emitCourseStartedPipelineOnly({ ...opts, event, skipXapi });
|
|
546
558
|
}
|
|
547
559
|
if (!trackingEmitted && !sessionStarted) {
|
|
548
|
-
return emitCourseStarted(opts);
|
|
560
|
+
return emitCourseStarted({ ...opts, skipXapi });
|
|
549
561
|
}
|
|
550
562
|
if (sessionStarted && trackingEmitted && !pipelineDelivered) {
|
|
551
563
|
const event = buildCourseStartedEvent(opts);
|
|
@@ -553,7 +565,7 @@ async function emitPendingCourseStartedInner(opts) {
|
|
|
553
565
|
return emitCourseStartedPipelineOnly({
|
|
554
566
|
...opts,
|
|
555
567
|
event,
|
|
556
|
-
skipXapi
|
|
568
|
+
skipXapi,
|
|
557
569
|
onXapiStatementSent: opts.onXapiStatementSent
|
|
558
570
|
});
|
|
559
571
|
}
|
|
@@ -566,6 +578,124 @@ function assertTrackingSinkConfig(tracking) {
|
|
|
566
578
|
);
|
|
567
579
|
}
|
|
568
580
|
|
|
581
|
+
// src/runtime/productionGuard.ts
|
|
582
|
+
function isProductionEnvironment() {
|
|
583
|
+
try {
|
|
584
|
+
if (import.meta.env?.PROD === true) return true;
|
|
585
|
+
} catch {
|
|
586
|
+
}
|
|
587
|
+
const g = globalThis;
|
|
588
|
+
return typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production";
|
|
589
|
+
}
|
|
590
|
+
function shouldEnforceProductionGuard() {
|
|
591
|
+
try {
|
|
592
|
+
if (import.meta.env?.MODE === "test") return false;
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
return isProductionEnvironment();
|
|
596
|
+
}
|
|
597
|
+
function looksLikeConsoleSink(fn) {
|
|
598
|
+
if (typeof fn !== "function") return false;
|
|
599
|
+
const src = Function.prototype.toString.call(fn);
|
|
600
|
+
return /console\.(log|debug|info)\s*\(/.test(src);
|
|
601
|
+
}
|
|
602
|
+
function isTrackingDeliveryConfigured(tracking) {
|
|
603
|
+
if (!tracking || tracking.enabled === false) return false;
|
|
604
|
+
return Boolean(tracking.sink || tracking.batchSink || tracking.createClient);
|
|
605
|
+
}
|
|
606
|
+
function isXapiDeliveryConfigured(xapi) {
|
|
607
|
+
if (!xapi || xapi.enabled === false) return false;
|
|
608
|
+
if (xapi.client) return true;
|
|
609
|
+
return typeof xapi.transport === "function";
|
|
610
|
+
}
|
|
611
|
+
function trackingUsesConsole(config) {
|
|
612
|
+
const tracking = config.tracking;
|
|
613
|
+
if (!tracking || tracking.enabled === false) return false;
|
|
614
|
+
if (tracking.consoleSink === true) return true;
|
|
615
|
+
if (tracking.batchSink && looksLikeConsoleSink(tracking.batchSink)) return true;
|
|
616
|
+
if (tracking.sink && looksLikeConsoleSink(tracking.sink)) return true;
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
function xapiUsesConsole(config) {
|
|
620
|
+
const xapi = config.xapi;
|
|
621
|
+
if (!xapi || xapi.enabled === false || xapi.client) return false;
|
|
622
|
+
if (xapi.consoleTransport === true) return true;
|
|
623
|
+
return typeof xapi.transport === "function" && looksLikeConsoleSink(xapi.transport);
|
|
624
|
+
}
|
|
625
|
+
function observabilityIncomplete(observability, opts) {
|
|
626
|
+
if (!opts.trackingEnabled && !opts.xapiEnabled) return false;
|
|
627
|
+
const required = [observability?.onLxpackBridgeMiss];
|
|
628
|
+
if (opts.trackingEnabled) {
|
|
629
|
+
required.push(observability?.onTelemetrySinkError, observability?.onTelemetryBufferDrop);
|
|
630
|
+
}
|
|
631
|
+
if (opts.xapiEnabled) {
|
|
632
|
+
required.push(
|
|
633
|
+
observability?.onXapiQueueDepth,
|
|
634
|
+
observability?.onXapiQueueCap,
|
|
635
|
+
observability?.onXapiTransportError,
|
|
636
|
+
observability?.onXapiMappingError
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
return required.some((hook) => !hook);
|
|
640
|
+
}
|
|
641
|
+
function requiredObservabilityHookCount(opts) {
|
|
642
|
+
let count = 1;
|
|
643
|
+
if (opts.trackingEnabled) count += 2;
|
|
644
|
+
if (opts.xapiEnabled) count += 4;
|
|
645
|
+
return count;
|
|
646
|
+
}
|
|
647
|
+
function warnConsoleSinkHeuristic(config) {
|
|
648
|
+
if (!isDevEnvironment()) return;
|
|
649
|
+
if (config.preview?.allowConsoleTelemetry) return;
|
|
650
|
+
if (looksLikeConsoleSink(config.tracking?.sink) || looksLikeConsoleSink(config.tracking?.batchSink)) {
|
|
651
|
+
console.warn(
|
|
652
|
+
"[lessonkit] Telemetry sink looks like console.log; use preview.allowConsoleTelemetry for docs or wire a real sink."
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
if (looksLikeConsoleSink(config.xapi?.transport)) {
|
|
656
|
+
console.warn(
|
|
657
|
+
"[lessonkit] xAPI transport looks like console.log; use preview.allowConsoleTelemetry for docs or wire createFetchTransport."
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function assertProductionCourseConfig(config) {
|
|
662
|
+
if (!isProductionEnvironment()) {
|
|
663
|
+
warnConsoleSinkHeuristic(config);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
assertTrackingSinkConfig(config.tracking);
|
|
667
|
+
const trackingImplicitlyEnabled = config.tracking === void 0 || config.tracking.enabled !== false;
|
|
668
|
+
if (trackingImplicitlyEnabled && !isTrackingDeliveryConfigured(config.tracking)) {
|
|
669
|
+
throw new Error(
|
|
670
|
+
"[lessonkit] Production build has tracking enabled but no sink or batchSink configured."
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
if (config.xapi?.enabled === true && !isXapiDeliveryConfigured(config.xapi)) {
|
|
674
|
+
throw new Error(
|
|
675
|
+
"[lessonkit] Production build has xAPI enabled but no transport or client configured."
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
const allowConsole = config.preview?.allowConsoleTelemetry === true;
|
|
679
|
+
const trackingEnabled = isTrackingDeliveryConfigured(config.tracking);
|
|
680
|
+
const xapiEnabled = isXapiDeliveryConfigured(config.xapi);
|
|
681
|
+
if (!allowConsole && trackingUsesConsole(config)) {
|
|
682
|
+
throw new Error(
|
|
683
|
+
"[lessonkit] Production build uses console telemetry sinks. Wire createFetchBatchSink or a real sink. See production checklist."
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (!allowConsole && xapiUsesConsole(config)) {
|
|
687
|
+
throw new Error(
|
|
688
|
+
"[lessonkit] Production build uses console xAPI transport. Wire createFetchTransport to your LRS proxy. See production checklist."
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
if (observabilityIncomplete(config.observability, { trackingEnabled, xapiEnabled })) {
|
|
692
|
+
const hookCount = requiredObservabilityHookCount({ trackingEnabled, xapiEnabled });
|
|
693
|
+
throw new Error(
|
|
694
|
+
`[lessonkit] Production build missing observability hooks. Wire all ${hookCount} config.observability callbacks before go-live.`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
569
699
|
// src/provider/useLessonkitProviderRuntime.ts
|
|
570
700
|
import {
|
|
571
701
|
useCallback,
|
|
@@ -598,6 +728,39 @@ function wrapBatchSink(batchSink, observability) {
|
|
|
598
728
|
}
|
|
599
729
|
};
|
|
600
730
|
}
|
|
731
|
+
function warnMissingProductionObservability(observability, opts) {
|
|
732
|
+
let isProduction = false;
|
|
733
|
+
try {
|
|
734
|
+
const env = import.meta.env;
|
|
735
|
+
if (env?.MODE === "test") return;
|
|
736
|
+
isProduction = env?.PROD === true;
|
|
737
|
+
} catch {
|
|
738
|
+
}
|
|
739
|
+
if (!isProduction) {
|
|
740
|
+
const g = globalThis;
|
|
741
|
+
isProduction = typeof g.process !== "undefined" && g.process.env?.NODE_ENV === "production";
|
|
742
|
+
}
|
|
743
|
+
if (!isProduction) return;
|
|
744
|
+
if (!opts.trackingEnabled && !opts.xapiEnabled) return;
|
|
745
|
+
const required = [observability?.onLxpackBridgeMiss];
|
|
746
|
+
if (opts.trackingEnabled) {
|
|
747
|
+
required.push(observability?.onTelemetrySinkError, observability?.onTelemetryBufferDrop);
|
|
748
|
+
}
|
|
749
|
+
if (opts.xapiEnabled) {
|
|
750
|
+
required.push(
|
|
751
|
+
observability?.onXapiQueueDepth,
|
|
752
|
+
observability?.onXapiQueueCap,
|
|
753
|
+
observability?.onXapiTransportError,
|
|
754
|
+
observability?.onXapiMappingError
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
if (!required.some((hook) => !hook)) return;
|
|
758
|
+
if (typeof console !== "undefined") {
|
|
759
|
+
console.warn(
|
|
760
|
+
"[lessonkit] Production deployment without observability hooks \u2014 telemetry/xAPI failures and buffer drops will be silent. See https://lessonkit.readthedocs.io/en/latest/guides/react-developers/production-checklist.html"
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
601
764
|
function wrapTrackingSink(sink, observability) {
|
|
602
765
|
if (!sink || !observability?.onTelemetrySinkError) return sink;
|
|
603
766
|
const onError = observability.onTelemetrySinkError;
|
|
@@ -650,7 +813,8 @@ function createXapiClientFromConfig(config, queue, observability) {
|
|
|
650
813
|
exitTransport: config.xapi?.exitTransport,
|
|
651
814
|
abortInFlight: config.xapi?.abortInFlight,
|
|
652
815
|
queue,
|
|
653
|
-
onTransportError: observability?.onXapiTransportError
|
|
816
|
+
onTransportError: observability?.onXapiTransportError,
|
|
817
|
+
onMappingError: observability?.onXapiMappingError
|
|
654
818
|
});
|
|
655
819
|
}
|
|
656
820
|
|
|
@@ -683,9 +847,12 @@ var useIsoLayoutEffect = (
|
|
|
683
847
|
/* v8 ignore next -- SSR uses useEffect when window is unavailable */
|
|
684
848
|
typeof window !== "undefined" ? useLayoutEffect : useEffect2
|
|
685
849
|
);
|
|
686
|
-
var
|
|
850
|
+
var providerStoragesForTests = /* @__PURE__ */ new Set();
|
|
687
851
|
function resetLessonkitProviderStorageForTests() {
|
|
688
|
-
|
|
852
|
+
for (const storage of providerStoragesForTests) {
|
|
853
|
+
resetStoragePortForTests(storage);
|
|
854
|
+
}
|
|
855
|
+
providerStoragesForTests.clear();
|
|
689
856
|
resetSharedVolatileSessionIdForTests();
|
|
690
857
|
}
|
|
691
858
|
function useLessonkitProviderRuntime(config) {
|
|
@@ -699,6 +866,11 @@ function useLessonkitProviderRuntime(config) {
|
|
|
699
866
|
);
|
|
700
867
|
if (shouldEnforceProductionGuard()) {
|
|
701
868
|
assertProductionCourseConfig(normalizedConfig);
|
|
869
|
+
} else {
|
|
870
|
+
warnMissingProductionObservability(normalizedConfig.observability, {
|
|
871
|
+
trackingEnabled: isTrackingDeliveryConfigured(normalizedConfig.tracking),
|
|
872
|
+
xapiEnabled: isXapiDeliveryConfigured(normalizedConfig.xapi)
|
|
873
|
+
});
|
|
702
874
|
}
|
|
703
875
|
const useV2Runtime = normalizedConfig.runtimeVersion !== "v1";
|
|
704
876
|
useEffect2(() => {
|
|
@@ -712,13 +884,17 @@ function useLessonkitProviderRuntime(config) {
|
|
|
712
884
|
const extraSinksRef = useRef(normalizedConfig.sinks);
|
|
713
885
|
extraSinksRef.current = normalizedConfig.sinks;
|
|
714
886
|
const headlessRef = useRef(null);
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
} else if (prevConfiguredSessionIdRef.current) {
|
|
720
|
-
sessionIdRef.current = resolveSessionId(defaultStorage, void 0);
|
|
887
|
+
const providerStorageRef = useRef(null);
|
|
888
|
+
if (!providerStorageRef.current) {
|
|
889
|
+
providerStorageRef.current = normalizedConfig.storage ?? createSessionStoragePort();
|
|
890
|
+
providerStoragesForTests.add(providerStorageRef.current);
|
|
721
891
|
}
|
|
892
|
+
const providerStorage = providerStorageRef.current;
|
|
893
|
+
const sessionIdRef = useRef(
|
|
894
|
+
resolveSessionId(providerStorage, normalizedConfig.session?.sessionId)
|
|
895
|
+
);
|
|
896
|
+
const [sessionId, setSessionId] = useState(() => sessionIdRef.current);
|
|
897
|
+
const prevConfiguredSessionIdRef = useRef(normalizedConfig.session?.sessionId);
|
|
722
898
|
const attemptIdRef = useRef(normalizedConfig.session?.attemptId);
|
|
723
899
|
const userRef = useRef(normalizedConfig.session?.user);
|
|
724
900
|
attemptIdRef.current = normalizedConfig.session?.attemptId;
|
|
@@ -727,11 +903,16 @@ function useLessonkitProviderRuntime(config) {
|
|
|
727
903
|
courseIdRef.current = normalizedCourseId;
|
|
728
904
|
const lxpackBridgeModeRef = useRef(normalizedConfig.lxpack?.bridge ?? "auto");
|
|
729
905
|
lxpackBridgeModeRef.current = normalizedConfig.lxpack?.bridge ?? "auto";
|
|
906
|
+
const allowedParentOriginsRef = useRef(normalizedConfig.lxpack?.allowedParentOrigins);
|
|
907
|
+
allowedParentOriginsRef.current = normalizedConfig.lxpack?.allowedParentOrigins;
|
|
730
908
|
const observabilityRef = useRef(normalizedConfig.observability);
|
|
731
909
|
observabilityRef.current = normalizedConfig.observability;
|
|
732
910
|
const onLxpackBridgeMiss = useCallback((event) => {
|
|
733
911
|
observabilityRef.current?.onLxpackBridgeMiss?.(event);
|
|
734
912
|
}, []);
|
|
913
|
+
const onLxpackBridgeError = useCallback((err) => {
|
|
914
|
+
observabilityRef.current?.onLxpackBridgeError?.(err);
|
|
915
|
+
}, []);
|
|
735
916
|
const pluginsFingerprint = normalizedConfig.plugins?.map((p) => `${p.id}\0${p.version}`).join("|") ?? "";
|
|
736
917
|
const pluginHost = useMemo(
|
|
737
918
|
() => createReactPluginHost(normalizedConfig.plugins),
|
|
@@ -742,23 +923,38 @@ function useLessonkitProviderRuntime(config) {
|
|
|
742
923
|
const progressRef = useRef(createProgressController());
|
|
743
924
|
const courseStartedEmittedToSinkRef = useRef(false);
|
|
744
925
|
const courseStartedEmitGenerationRef = useRef(0);
|
|
926
|
+
const courseStartedFlightScopeRef = useRef(createCourseStartedFlightScope());
|
|
927
|
+
const pendingSessionReEmitRef = useRef(false);
|
|
928
|
+
const prevPluginsFingerprintRef = useRef(pluginsFingerprint);
|
|
929
|
+
if (prevPluginsFingerprintRef.current !== pluginsFingerprint) {
|
|
930
|
+
prevPluginsFingerprintRef.current = pluginsFingerprint;
|
|
931
|
+
courseStartedEmitGenerationRef.current += 1;
|
|
932
|
+
courseStartedEmittedToSinkRef.current = false;
|
|
933
|
+
resetCourseStartedTrackingFlights(courseStartedFlightScopeRef.current);
|
|
934
|
+
}
|
|
745
935
|
const prevCourseIdForProgressRef = useRef(normalizedCourseId);
|
|
746
936
|
const pendingCourseIdResetRef = useRef(false);
|
|
747
937
|
const prevUseV2RuntimeRef = useRef(useV2Runtime);
|
|
748
938
|
const xapiCourseStartedSentOnClientRef = useRef(false);
|
|
749
939
|
const xapiBootstrapSendRef = useRef(false);
|
|
940
|
+
const xapiBootstrapQueuedRef = useRef(false);
|
|
941
|
+
const xapiBootstrapInFlightRef = useRef(false);
|
|
750
942
|
if (prevUseV2RuntimeRef.current !== useV2Runtime) {
|
|
751
943
|
prevUseV2RuntimeRef.current = useV2Runtime;
|
|
752
944
|
if (useV2Runtime) {
|
|
753
|
-
headlessRef.current = createLessonkitRuntime(
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
945
|
+
headlessRef.current = createLessonkitRuntime(
|
|
946
|
+
{
|
|
947
|
+
courseId: normalizedCourseId,
|
|
948
|
+
runtimeVersion: "v2",
|
|
949
|
+
session: normalizedConfig.session,
|
|
950
|
+
plugins: pluginHostRef.current ?? normalizedConfig.plugins,
|
|
951
|
+
deferPluginSetup: true
|
|
952
|
+
},
|
|
953
|
+
{ storage: providerStorage }
|
|
954
|
+
);
|
|
760
955
|
progressRef.current = headlessRef.current.progress;
|
|
761
956
|
} else {
|
|
957
|
+
headlessRef.current?.dispose();
|
|
762
958
|
headlessRef.current = null;
|
|
763
959
|
progressRef.current = createProgressController();
|
|
764
960
|
}
|
|
@@ -766,13 +962,16 @@ function useLessonkitProviderRuntime(config) {
|
|
|
766
962
|
courseStartedEmittedToSinkRef.current = false;
|
|
767
963
|
courseStartedEmitGenerationRef.current += 1;
|
|
768
964
|
} else if (useV2Runtime && !headlessRef.current) {
|
|
769
|
-
headlessRef.current = createLessonkitRuntime(
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
965
|
+
headlessRef.current = createLessonkitRuntime(
|
|
966
|
+
{
|
|
967
|
+
courseId: normalizedCourseId,
|
|
968
|
+
runtimeVersion: "v2",
|
|
969
|
+
session: normalizedConfig.session,
|
|
970
|
+
plugins: pluginHostRef.current ?? normalizedConfig.plugins,
|
|
971
|
+
deferPluginSetup: true
|
|
972
|
+
},
|
|
973
|
+
{ storage: providerStorage }
|
|
974
|
+
);
|
|
776
975
|
}
|
|
777
976
|
if (prevCourseIdForProgressRef.current !== normalizedCourseId) {
|
|
778
977
|
prevCourseIdForProgressRef.current = normalizedCourseId;
|
|
@@ -820,6 +1019,8 @@ function useLessonkitProviderRuntime(config) {
|
|
|
820
1019
|
prevXapiCourseIdRef.current = courseId;
|
|
821
1020
|
xapiCourseStartedSentOnClientRef.current = false;
|
|
822
1021
|
xapiBootstrapSendRef.current = false;
|
|
1022
|
+
xapiBootstrapQueuedRef.current = false;
|
|
1023
|
+
xapiBootstrapInFlightRef.current = false;
|
|
823
1024
|
}
|
|
824
1025
|
const prev = xapiRef.current;
|
|
825
1026
|
const next = createXapiClientFromConfig(
|
|
@@ -832,32 +1033,36 @@ function useLessonkitProviderRuntime(config) {
|
|
|
832
1033
|
let bootstrapSent = false;
|
|
833
1034
|
let bootstrapAlreadyStarted = false;
|
|
834
1035
|
if (next) {
|
|
835
|
-
const
|
|
1036
|
+
const sessionId2 = sessionIdRef.current;
|
|
836
1037
|
const cid = courseIdRef.current;
|
|
837
1038
|
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
838
|
-
bootstrapAlreadyStarted = hasCourseStarted(
|
|
1039
|
+
bootstrapAlreadyStarted = hasCourseStarted(providerStorage, sessionId2, cid);
|
|
839
1040
|
const clientChanged = !prev || prev !== next;
|
|
840
1041
|
const skipBootstrap = trackingActive && !bootstrapAlreadyStarted;
|
|
841
|
-
const
|
|
1042
|
+
const xapiAlreadySentForSession = hasCourseStartedXapiSent(providerStorage, sessionId2, cid);
|
|
1043
|
+
const needsBootstrap = !skipBootstrap && !xapiCourseStartedSentOnClientRef.current && !xapiBootstrapQueuedRef.current && !xapiAlreadySentForSession && (!bootstrapAlreadyStarted || clientChanged);
|
|
842
1044
|
if (needsBootstrap) {
|
|
843
1045
|
try {
|
|
844
1046
|
const event = buildCourseStartedEvent({
|
|
845
1047
|
pluginHost: pluginHostRef.current,
|
|
846
1048
|
courseId: cid,
|
|
847
|
-
sessionId,
|
|
1049
|
+
sessionId: sessionId2,
|
|
848
1050
|
attemptId: attemptIdRef.current,
|
|
849
1051
|
user: userRef.current,
|
|
850
|
-
lxpackBridge: lxpackBridgeModeRef.current
|
|
1052
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
1053
|
+
allowedParentOrigins: allowedParentOriginsRef.current
|
|
851
1054
|
});
|
|
852
1055
|
if (event !== null) {
|
|
853
1056
|
const statement = telemetryEventToXAPIStatement3(event);
|
|
854
1057
|
if (statement) {
|
|
855
1058
|
next.send(statement);
|
|
856
|
-
|
|
1059
|
+
xapiBootstrapQueuedRef.current = true;
|
|
1060
|
+
xapiBootstrapInFlightRef.current = true;
|
|
857
1061
|
bootstrapSent = true;
|
|
858
1062
|
}
|
|
859
1063
|
}
|
|
860
|
-
} catch {
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
observabilityRef.current?.onXapiMappingError?.(err);
|
|
861
1066
|
}
|
|
862
1067
|
}
|
|
863
1068
|
}
|
|
@@ -873,12 +1078,19 @@ function useLessonkitProviderRuntime(config) {
|
|
|
873
1078
|
try {
|
|
874
1079
|
await next?.flush();
|
|
875
1080
|
if (bootstrapSent && !cancelled) {
|
|
1081
|
+
xapiBootstrapSendRef.current = true;
|
|
1082
|
+
xapiBootstrapInFlightRef.current = false;
|
|
876
1083
|
if (!bootstrapAlreadyStarted) {
|
|
877
|
-
markCourseStarted(
|
|
1084
|
+
markCourseStarted(providerStorage, sessionIdRef.current, courseIdRef.current);
|
|
878
1085
|
}
|
|
1086
|
+
markCourseStartedXapiSent(providerStorage, sessionIdRef.current, courseIdRef.current);
|
|
879
1087
|
xapiCourseStartedSentOnClientRef.current = true;
|
|
880
1088
|
}
|
|
881
1089
|
} catch {
|
|
1090
|
+
if (bootstrapSent && !cancelled) {
|
|
1091
|
+
xapiBootstrapQueuedRef.current = false;
|
|
1092
|
+
xapiBootstrapInFlightRef.current = false;
|
|
1093
|
+
}
|
|
882
1094
|
}
|
|
883
1095
|
})();
|
|
884
1096
|
return () => {
|
|
@@ -908,6 +1120,39 @@ function useLessonkitProviderRuntime(config) {
|
|
|
908
1120
|
}),
|
|
909
1121
|
[]
|
|
910
1122
|
);
|
|
1123
|
+
const emitCourseStartedOnce = useCallback(
|
|
1124
|
+
async (sid, cid) => {
|
|
1125
|
+
if (courseStartedEmittedToSinkRef.current) return;
|
|
1126
|
+
const generation = courseStartedEmitGenerationRef.current;
|
|
1127
|
+
const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
|
|
1128
|
+
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
1129
|
+
const result = await emitPendingCourseStarted({
|
|
1130
|
+
pluginHost: pluginHostRef.current,
|
|
1131
|
+
tracking: () => trackingRef.current,
|
|
1132
|
+
xapi: xapiRef.current,
|
|
1133
|
+
storage: providerStorage,
|
|
1134
|
+
sessionId: sid,
|
|
1135
|
+
courseId: cid,
|
|
1136
|
+
attemptId: attemptIdRef.current,
|
|
1137
|
+
user: userRef.current,
|
|
1138
|
+
lxpackBridge: lxpackBridgeModeRef.current,
|
|
1139
|
+
allowedParentOrigins: allowedParentOriginsRef.current,
|
|
1140
|
+
onLxpackBridgeMiss,
|
|
1141
|
+
onLxpackBridgeError,
|
|
1142
|
+
onXapiMappingError: observabilityRef.current?.onXapiMappingError,
|
|
1143
|
+
extraSinks: extraSinksRef.current,
|
|
1144
|
+
skipXapi: xapiCourseStartedSentOnClientRef.current || xapiBootstrapSendRef.current || xapiBootstrapInFlightRef.current,
|
|
1145
|
+
onXapiStatementSent: () => {
|
|
1146
|
+
xapiCourseStartedSentOnClientRef.current = true;
|
|
1147
|
+
},
|
|
1148
|
+
shouldCommit,
|
|
1149
|
+
flightScope: courseStartedFlightScopeRef.current
|
|
1150
|
+
});
|
|
1151
|
+
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
1152
|
+
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
1153
|
+
},
|
|
1154
|
+
[onLxpackBridgeMiss, onLxpackBridgeError]
|
|
1155
|
+
);
|
|
911
1156
|
useIsoLayoutEffect(() => {
|
|
912
1157
|
const prev = trackingRef.current;
|
|
913
1158
|
const baseSink = wrapTrackingSink(normalizedConfig.tracking?.sink, observabilityRef.current);
|
|
@@ -943,45 +1188,19 @@ function useLessonkitProviderRuntime(config) {
|
|
|
943
1188
|
trackingRef.current = next;
|
|
944
1189
|
trackingClientForUnmountRef.current = next;
|
|
945
1190
|
setTracking(next);
|
|
946
|
-
const
|
|
1191
|
+
const sessionId2 = sessionIdRef.current;
|
|
947
1192
|
const cid = courseIdRef.current;
|
|
948
1193
|
const trackingActive = isTrackingActive(normalizedConfig.tracking);
|
|
949
|
-
const courseStartedFullySettled = hasCourseStartedEmittedToTracking(
|
|
1194
|
+
const courseStartedFullySettled = hasCourseStartedEmittedToTracking(providerStorage, sessionId2, cid) && hasCourseStarted(providerStorage, sessionId2, cid) && hasCourseStartedPipelineDelivered(providerStorage, sessionId2, cid);
|
|
950
1195
|
if (!trackingActive) {
|
|
951
1196
|
courseStartedEmittedToSinkRef.current = false;
|
|
952
1197
|
} else if (courseStartedFullySettled) {
|
|
953
1198
|
courseStartedEmittedToSinkRef.current = true;
|
|
954
|
-
} else
|
|
955
|
-
|
|
956
|
-
const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
|
|
957
|
-
void (async () => {
|
|
958
|
-
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
959
|
-
const result = await emitPendingCourseStarted({
|
|
960
|
-
pluginHost: pluginHostRef.current,
|
|
961
|
-
tracking: () => trackingRef.current,
|
|
962
|
-
xapi: xapiRef.current,
|
|
963
|
-
storage: defaultStorage,
|
|
964
|
-
sessionId,
|
|
965
|
-
courseId: cid,
|
|
966
|
-
attemptId: attemptIdRef.current,
|
|
967
|
-
user: userRef.current,
|
|
968
|
-
lxpackBridge: lxpackBridgeModeRef.current,
|
|
969
|
-
onLxpackBridgeMiss,
|
|
970
|
-
extraSinks: extraSinksRef.current,
|
|
971
|
-
skipXapi: xapiCourseStartedSentOnClientRef.current || xapiBootstrapSendRef.current,
|
|
972
|
-
onXapiStatementSent: () => {
|
|
973
|
-
xapiCourseStartedSentOnClientRef.current = true;
|
|
974
|
-
},
|
|
975
|
-
shouldCommit
|
|
976
|
-
});
|
|
977
|
-
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
978
|
-
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
979
|
-
})();
|
|
1199
|
+
} else {
|
|
1200
|
+
void emitCourseStartedOnce(sessionId2, cid);
|
|
980
1201
|
}
|
|
981
1202
|
return () => {
|
|
982
|
-
|
|
983
|
-
void disposeTrackingClient(prev);
|
|
984
|
-
}
|
|
1203
|
+
void disposeTrackingClient(prev);
|
|
985
1204
|
};
|
|
986
1205
|
}, [
|
|
987
1206
|
trackingEnabled,
|
|
@@ -990,9 +1209,10 @@ function useLessonkitProviderRuntime(config) {
|
|
|
990
1209
|
batchEnabled,
|
|
991
1210
|
batchFlushIntervalMs,
|
|
992
1211
|
batchMaxBatchSize,
|
|
993
|
-
|
|
1212
|
+
pluginsFingerprint,
|
|
994
1213
|
normalizedCourseId,
|
|
995
|
-
buildCurrentPluginCtx
|
|
1214
|
+
buildCurrentPluginCtx,
|
|
1215
|
+
emitCourseStartedOnce
|
|
996
1216
|
]);
|
|
997
1217
|
const emitWithBridge = useCallback((trackingClient, event) => {
|
|
998
1218
|
emitTelemetryWithPlugins({
|
|
@@ -1007,22 +1227,16 @@ function useLessonkitProviderRuntime(config) {
|
|
|
1007
1227
|
user: userRef.current
|
|
1008
1228
|
}),
|
|
1009
1229
|
lxpackBridge: lxpackBridgeModeRef.current,
|
|
1230
|
+
allowedParentOrigins: allowedParentOriginsRef.current,
|
|
1010
1231
|
onLxpackBridgeMiss,
|
|
1011
|
-
|
|
1232
|
+
onLxpackBridgeError,
|
|
1233
|
+
extraSinks: extraSinksRef.current,
|
|
1234
|
+
onXapiMappingError: observabilityRef.current?.onXapiMappingError,
|
|
1235
|
+
onXapiTransportError: observabilityRef.current?.onXapiTransportError
|
|
1012
1236
|
});
|
|
1013
|
-
}, [onLxpackBridgeMiss]);
|
|
1237
|
+
}, [onLxpackBridgeMiss, onLxpackBridgeError]);
|
|
1014
1238
|
const emitLifecycleEvent = useCallback(
|
|
1015
|
-
(
|
|
1016
|
-
const event = tryBuildTelemetryEvent({
|
|
1017
|
-
name,
|
|
1018
|
-
courseId: courseIdRef.current,
|
|
1019
|
-
lessonId: lessonId ?? activeLessonIdRef.current,
|
|
1020
|
-
sessionId: sessionIdRef.current,
|
|
1021
|
-
attemptId: attemptIdRef.current,
|
|
1022
|
-
user: userRef.current,
|
|
1023
|
-
data
|
|
1024
|
-
});
|
|
1025
|
-
if (!event) return;
|
|
1239
|
+
(event) => {
|
|
1026
1240
|
emitWithBridge(trackingRef.current, event);
|
|
1027
1241
|
},
|
|
1028
1242
|
[emitWithBridge]
|
|
@@ -1048,39 +1262,16 @@ function useLessonkitProviderRuntime(config) {
|
|
|
1048
1262
|
pendingCourseIdResetRef.current = false;
|
|
1049
1263
|
syncProgress();
|
|
1050
1264
|
if (!isTrackingActive(normalizedConfig.tracking)) return;
|
|
1051
|
-
const
|
|
1265
|
+
const sessionId2 = sessionIdRef.current;
|
|
1052
1266
|
const cid = courseIdRef.current;
|
|
1053
1267
|
void (async () => {
|
|
1054
1268
|
try {
|
|
1055
1269
|
await trackingRef.current?.flush?.();
|
|
1056
1270
|
} catch {
|
|
1057
1271
|
}
|
|
1058
|
-
|
|
1059
|
-
const generation = courseStartedEmitGenerationRef.current;
|
|
1060
|
-
const shouldCommit = () => generation === courseStartedEmitGenerationRef.current;
|
|
1061
|
-
const result = await emitPendingCourseStarted({
|
|
1062
|
-
pluginHost: pluginHostRef.current,
|
|
1063
|
-
tracking: () => trackingRef.current,
|
|
1064
|
-
xapi: xapiRef.current,
|
|
1065
|
-
storage: defaultStorage,
|
|
1066
|
-
sessionId,
|
|
1067
|
-
courseId: cid,
|
|
1068
|
-
attemptId: attemptIdRef.current,
|
|
1069
|
-
user: userRef.current,
|
|
1070
|
-
lxpackBridge: lxpackBridgeModeRef.current,
|
|
1071
|
-
onLxpackBridgeMiss,
|
|
1072
|
-
extraSinks: extraSinksRef.current,
|
|
1073
|
-
skipXapi: xapiCourseStartedSentOnClientRef.current || xapiBootstrapSendRef.current,
|
|
1074
|
-
onXapiStatementSent: () => {
|
|
1075
|
-
xapiCourseStartedSentOnClientRef.current = true;
|
|
1076
|
-
},
|
|
1077
|
-
shouldCommit
|
|
1078
|
-
});
|
|
1079
|
-
if (generation !== courseStartedEmitGenerationRef.current) return;
|
|
1080
|
-
courseStartedEmittedToSinkRef.current = isCourseStartedSinkSettled(result);
|
|
1081
|
-
}
|
|
1272
|
+
await emitCourseStartedOnce(sessionId2, cid);
|
|
1082
1273
|
})();
|
|
1083
|
-
}, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress,
|
|
1274
|
+
}, [normalizedCourseId, normalizedConfig.tracking?.enabled, syncProgress, emitCourseStartedOnce]);
|
|
1084
1275
|
const emitLessonCompleted = useCallback(
|
|
1085
1276
|
(lessonId, durationMs) => {
|
|
1086
1277
|
track("lesson_completed", { lessonId, durationMs }, { lessonId });
|
|
@@ -1130,23 +1321,13 @@ function useLessonkitProviderRuntime(config) {
|
|
|
1130
1321
|
};
|
|
1131
1322
|
}, []);
|
|
1132
1323
|
useEffect2(() => {
|
|
1133
|
-
if (typeof
|
|
1324
|
+
if (typeof window === "undefined") return;
|
|
1134
1325
|
const flushOnPageExit = () => {
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
trackingRef.current?.flushOnExit?.();
|
|
1138
|
-
} finally {
|
|
1139
|
-
void xapiRef.current?.flush();
|
|
1140
|
-
void trackingRef.current?.flush?.();
|
|
1141
|
-
}
|
|
1142
|
-
};
|
|
1143
|
-
const onVisibilityChange = () => {
|
|
1144
|
-
if (document.visibilityState === "hidden") flushOnPageExit();
|
|
1326
|
+
xapiRef.current?.flushOnExit?.();
|
|
1327
|
+
trackingRef.current?.flushOnExit?.();
|
|
1145
1328
|
};
|
|
1146
|
-
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
1147
1329
|
window.addEventListener("pagehide", flushOnPageExit);
|
|
1148
1330
|
return () => {
|
|
1149
|
-
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
1150
1331
|
window.removeEventListener("pagehide", flushOnPageExit);
|
|
1151
1332
|
};
|
|
1152
1333
|
}, []);
|
|
@@ -1241,36 +1422,56 @@ function useLessonkitProviderRuntime(config) {
|
|
|
1241
1422
|
host.disposeAll();
|
|
1242
1423
|
};
|
|
1243
1424
|
}, [pluginHost, useV2Runtime, normalizedCourseId, sessionAttemptId, sessionConfiguredId, sessionUserKey]);
|
|
1244
|
-
|
|
1425
|
+
useIsoLayoutEffect(() => {
|
|
1245
1426
|
const nextConfigured = normalizedConfig.session?.sessionId;
|
|
1246
1427
|
const prevConfigured = prevConfiguredSessionIdRef.current;
|
|
1247
1428
|
if (nextConfigured === prevConfigured) return;
|
|
1248
1429
|
prevConfiguredSessionIdRef.current = nextConfigured;
|
|
1249
1430
|
const cid = courseIdRef.current;
|
|
1250
|
-
if (nextConfigured) {
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
const
|
|
1254
|
-
if (
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1431
|
+
if (nextConfigured !== void 0) {
|
|
1432
|
+
const resolved = resolveSessionId(providerStorage, nextConfigured);
|
|
1433
|
+
const tabId = getTabSessionId(providerStorage);
|
|
1434
|
+
const isExplicitLearnerSwap = prevConfigured !== void 0 && prevConfigured !== nextConfigured;
|
|
1435
|
+
if (isExplicitLearnerSwap) {
|
|
1436
|
+
courseStartedEmittedToSinkRef.current = false;
|
|
1437
|
+
courseStartedEmitGenerationRef.current += 1;
|
|
1438
|
+
pendingSessionReEmitRef.current = true;
|
|
1439
|
+
} else if (tabId && tabId !== resolved) {
|
|
1440
|
+
migrateCourseStartedMark(providerStorage, tabId, resolved, cid);
|
|
1259
1441
|
}
|
|
1260
|
-
sessionIdRef.current =
|
|
1442
|
+
sessionIdRef.current = resolved;
|
|
1443
|
+
setSessionId(resolved);
|
|
1261
1444
|
} else if (prevConfigured) {
|
|
1262
|
-
const nextAuto = resolveSessionId(
|
|
1263
|
-
migrateCourseStartedMark(
|
|
1445
|
+
const nextAuto = resolveSessionId(providerStorage, void 0);
|
|
1446
|
+
migrateCourseStartedMark(providerStorage, prevConfigured, nextAuto, cid);
|
|
1264
1447
|
sessionIdRef.current = nextAuto;
|
|
1448
|
+
setSessionId(nextAuto);
|
|
1265
1449
|
}
|
|
1266
1450
|
}, [sessionConfiguredId, normalizedCourseId]);
|
|
1451
|
+
useEffect2(() => {
|
|
1452
|
+
if (!pendingSessionReEmitRef.current) return;
|
|
1453
|
+
pendingSessionReEmitRef.current = false;
|
|
1454
|
+
if (!isTrackingActive(normalizedConfig.tracking)) return;
|
|
1455
|
+
void emitCourseStartedOnce(sessionIdRef.current, courseIdRef.current);
|
|
1456
|
+
}, [sessionConfiguredId, emitCourseStartedOnce, normalizedConfig.tracking]);
|
|
1457
|
+
useLayoutEffect(() => {
|
|
1458
|
+
if (sessionIdRef.current !== sessionId) {
|
|
1459
|
+
setSessionId(sessionIdRef.current);
|
|
1460
|
+
}
|
|
1461
|
+
}, [sessionConfiguredId, sessionId]);
|
|
1462
|
+
useEffect2(() => {
|
|
1463
|
+
return () => {
|
|
1464
|
+
headlessRef.current?.dispose();
|
|
1465
|
+
headlessRef.current = null;
|
|
1466
|
+
};
|
|
1467
|
+
}, []);
|
|
1267
1468
|
const runtime = useMemo(
|
|
1268
1469
|
() => ({
|
|
1269
1470
|
config: normalizedConfig,
|
|
1270
1471
|
tracking,
|
|
1271
1472
|
xapi,
|
|
1272
|
-
storage:
|
|
1273
|
-
session: { sessionId
|
|
1473
|
+
storage: providerStorage,
|
|
1474
|
+
session: { sessionId, attemptId: attemptIdRef.current, user: userRef.current },
|
|
1274
1475
|
progress,
|
|
1275
1476
|
setActiveLesson,
|
|
1276
1477
|
completeLesson,
|
|
@@ -1290,7 +1491,8 @@ function useLessonkitProviderRuntime(config) {
|
|
|
1290
1491
|
pluginHost,
|
|
1291
1492
|
sessionUser,
|
|
1292
1493
|
sessionAttemptId,
|
|
1293
|
-
sessionConfiguredId
|
|
1494
|
+
sessionConfiguredId,
|
|
1495
|
+
sessionId
|
|
1294
1496
|
]
|
|
1295
1497
|
);
|
|
1296
1498
|
return runtime;
|
|
@@ -1394,7 +1596,9 @@ var COMPOUND_CONTAINER_TYPES = /* @__PURE__ */ new Set([
|
|
|
1394
1596
|
"SlideDeck",
|
|
1395
1597
|
"TimedCue",
|
|
1396
1598
|
"InteractiveVideo",
|
|
1397
|
-
"AssessmentSequence"
|
|
1599
|
+
"AssessmentSequence",
|
|
1600
|
+
"BranchingScenario",
|
|
1601
|
+
"BranchNode"
|
|
1398
1602
|
]);
|
|
1399
1603
|
function warnOrThrow(msg, strict) {
|
|
1400
1604
|
if (strict) throw new Error(msg);
|
|
@@ -1466,14 +1670,14 @@ function validateSubtreeForForbidden(node, forbidden, strict) {
|
|
|
1466
1670
|
});
|
|
1467
1671
|
}
|
|
1468
1672
|
function validateAccordionSections(sections, strict) {
|
|
1469
|
-
|
|
1673
|
+
const enforceStrict = strict ?? !isDevEnvironment();
|
|
1470
1674
|
for (const section of sections) {
|
|
1471
|
-
validateSubtreeForForbidden(section.content, ACCORDION_FORBIDDEN_CHILD_TYPES,
|
|
1675
|
+
validateSubtreeForForbidden(section.content, ACCORDION_FORBIDDEN_CHILD_TYPES, enforceStrict);
|
|
1472
1676
|
}
|
|
1473
1677
|
}
|
|
1474
1678
|
function validateCompoundChildren(parent, children, strict) {
|
|
1475
|
-
|
|
1476
|
-
validateNode(parent, children, 0,
|
|
1679
|
+
const enforceStrict = strict ?? !isDevEnvironment();
|
|
1680
|
+
validateNode(parent, children, 0, enforceStrict);
|
|
1477
1681
|
}
|
|
1478
1682
|
function resetCompoundValidationWarningsForTests() {
|
|
1479
1683
|
warnedPairs.clear();
|
|
@@ -1688,14 +1892,15 @@ function meetsPassingThreshold(score, maxScore, passingScore) {
|
|
|
1688
1892
|
}
|
|
1689
1893
|
function scoreFromCustom(custom, fallbackCorrect, fallbackMax = 1, passingScore) {
|
|
1690
1894
|
const maxScore = custom?.maxScore ?? fallbackMax;
|
|
1895
|
+
const hasNumericScore = custom?.score != null && Number.isFinite(custom.score);
|
|
1896
|
+
if (hasNumericScore) {
|
|
1897
|
+
const passed2 = custom.passed !== void 0 ? custom.passed : meetsPassingThreshold(custom.score, maxScore, passingScore);
|
|
1898
|
+
return { score: custom.score, maxScore, passed: passed2 };
|
|
1899
|
+
}
|
|
1691
1900
|
if (custom?.passed !== void 0) {
|
|
1692
|
-
const score2 = custom.passed ?
|
|
1901
|
+
const score2 = custom.passed ? maxScore : 0;
|
|
1693
1902
|
return { score: score2, maxScore, passed: custom.passed };
|
|
1694
1903
|
}
|
|
1695
|
-
if (custom?.maxScore != null && custom.maxScore > 0 && custom.score != null) {
|
|
1696
|
-
const passed2 = meetsPassingThreshold(custom.score, custom.maxScore, passingScore);
|
|
1697
|
-
return { score: custom.score, maxScore: custom.maxScore, passed: passed2 };
|
|
1698
|
-
}
|
|
1699
1904
|
const score = fallbackCorrect ? maxScore : 0;
|
|
1700
1905
|
const passed = meetsPassingThreshold(score, maxScore, passingScore);
|
|
1701
1906
|
return { score, maxScore, passed };
|
|
@@ -1747,6 +1952,7 @@ export {
|
|
|
1747
1952
|
readStringField,
|
|
1748
1953
|
readNumberField,
|
|
1749
1954
|
readBooleanStateField,
|
|
1955
|
+
aggregateAssessmentScores,
|
|
1750
1956
|
useCompoundHydrationBridgeRef,
|
|
1751
1957
|
CompoundPageIndexProvider,
|
|
1752
1958
|
CompoundProvider,
|
|
@@ -1754,9 +1960,9 @@ export {
|
|
|
1754
1960
|
useCompoundHandlesVersion,
|
|
1755
1961
|
useCompoundHandleRef,
|
|
1756
1962
|
useAssessmentHandleRegistration,
|
|
1963
|
+
resetCourseStartedTrackingFlightForTests,
|
|
1757
1964
|
shouldEnforceProductionGuard,
|
|
1758
1965
|
assertProductionCourseConfig,
|
|
1759
|
-
resetCourseStartedTrackingFlightForTests,
|
|
1760
1966
|
resetLessonkitProviderStorageForTests,
|
|
1761
1967
|
LessonkitContext,
|
|
1762
1968
|
LessonkitProvider,
|
|
@@ -1769,6 +1975,7 @@ export {
|
|
|
1769
1975
|
meetsPassingThreshold,
|
|
1770
1976
|
usePluginScoring,
|
|
1771
1977
|
setLessonkitBlockType,
|
|
1978
|
+
getLessonkitBlockType,
|
|
1772
1979
|
validateAccordionSections,
|
|
1773
1980
|
validateCompoundChildren,
|
|
1774
1981
|
resetCompoundValidationWarningsForTests
|