@patch-adams/core 1.5.11 → 1.5.13

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/dist/index.cjs CHANGED
@@ -3406,11 +3406,11 @@ function generateLrsBridgeCode(options) {
3406
3406
  // Send a course completion statement \u2014 called by skin or auto-detected via SCORM
3407
3407
  LRS.sendCompletionStatement = function(data) {
3408
3408
  data = data || {};
3409
- if (scormCompletionSent) {
3410
- log('Completion statement already sent (SCORM auto-detected). Skipping duplicate.');
3411
- return;
3409
+ // Cancel any pending SCORM auto-detection to avoid double-fire for same attempt
3410
+ if (scormCompletionDebounce) {
3411
+ clearTimeout(scormCompletionDebounce);
3412
+ scormCompletionDebounce = null;
3412
3413
  }
3413
- scormCompletionSent = true;
3414
3414
 
3415
3415
  var status = data.status || 'completed';
3416
3416
  var courseTitle = data.courseTitle || (LRS.courseInfo && LRS.courseInfo.title ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course');
@@ -3440,11 +3440,58 @@ function generateLrsBridgeCode(options) {
3440
3440
  };
3441
3441
  if (data.employeeName) activityDetails.employeeName = data.employeeName;
3442
3442
 
3443
- log('Sending completion statement:', verbKey, 'score:', result.score, 'title:', courseTitle);
3444
- var statement = buildStatement(verbKey, ACTIVITY_TYPES.course, activityDetails, result, null);
3443
+ // 1. Always send "completed" statement
3444
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
3445
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
3446
+ sendStatement(completedStatement);
3447
+
3448
+ // 2. Send "passed" or "failed" statement (assessment outcome)
3449
+ if (status === 'passed' || status === 'failed') {
3450
+ log('Sending', status, 'statement');
3451
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
3452
+ sendStatement(outcomeStatement);
3453
+ }
3454
+ };
3455
+
3456
+ // Send a "terminated" statement \u2014 called by skin close button or auto on page unload
3457
+ var exitStatementSent = false;
3458
+ LRS.sendExitStatement = function(data) {
3459
+ if (exitStatementSent) return;
3460
+ exitStatementSent = true;
3461
+ data = data || {};
3462
+
3463
+ var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
3464
+ var duration = null;
3465
+ if (LRS.launchTime) {
3466
+ var ms = new Date().getTime() - new Date(LRS.launchTime).getTime();
3467
+ var secs = Math.floor(ms / 1000);
3468
+ var mins = Math.floor(secs / 60);
3469
+ var hrs = Math.floor(mins / 60);
3470
+ duration = 'PT' + (hrs > 0 ? hrs + 'H' : '') + (mins % 60) + 'M' + (secs % 60) + 'S';
3471
+ }
3472
+
3473
+ var activityDetails = {
3474
+ courseTitle: courseTitle,
3475
+ exitSource: data.source || 'page-unload',
3476
+ employeeEmail: LRS.actor && LRS.actor.mbox ? LRS.actor.mbox.replace('mailto:', '') : undefined,
3477
+ employeeId: LRS.employeeId || undefined,
3478
+ sessionDuration: duration,
3479
+ statementsSent: LRS.stats.statementsSent
3480
+ };
3481
+
3482
+ var result = duration ? { duration: duration } : null;
3483
+ log('Sending exit statement, duration:', duration);
3484
+ var statement = buildStatement('terminated', ACTIVITY_TYPES.course, activityDetails, result, null);
3445
3485
  sendStatement(statement);
3446
3486
  };
3447
3487
 
3488
+ // Auto-send exit statement on page unload
3489
+ window.addEventListener('beforeunload', function() {
3490
+ if (!exitStatementSent && LRS.actor) {
3491
+ LRS.sendExitStatement({ source: 'page-unload' });
3492
+ }
3493
+ });
3494
+
3448
3495
  /**
3449
3496
  * Re-extract actor from SCORM after LMSInitialize has been called.
3450
3497
  * Call this from the SCORM wrapper after scormInit() succeeds,
@@ -3551,6 +3598,23 @@ function generateLrsBridgeCode(options) {
3551
3598
  LRS.interacted({ type: 'choice-branch', id: data.id, name: 'Choice Branch Discarded' });
3552
3599
  };
3553
3600
 
3601
+ // ========================================================================
3602
+ // PUBLIC API \u2014 expose core internals for skin scripts
3603
+ // Usage: window.pa_patcher.lrs.api.sendStatement(stmt)
3604
+ // ========================================================================
3605
+ LRS.api = {
3606
+ sendStatement: sendStatement,
3607
+ buildStatement: buildStatement,
3608
+ buildCourseActivityObject: buildCourseActivityObject,
3609
+ buildXylemeContext: buildXylemeContext,
3610
+ generateUUID: generateUUID,
3611
+ decodeEntities: decodeEntities,
3612
+ extractActor: extractActor,
3613
+ getCachedLessonInfo: getCachedLessonInfo,
3614
+ VERBS: VERBS,
3615
+ ACTIVITY_TYPES: ACTIVITY_TYPES
3616
+ };
3617
+
3554
3618
  // ========================================================================
3555
3619
  // 8. RISE EVENT INTERCEPTORS
3556
3620
  // ========================================================================
@@ -4116,7 +4180,7 @@ function generateLrsBridgeCode(options) {
4116
4180
  var scormInteractionsSent = {}; // Track which interactions were already sent
4117
4181
  var scormTrackerActive = false; // Set true when SCORM tracker wraps SetValue \u2014 KC handler defers
4118
4182
  var scormCourseData = {}; // Accumulates course-level SCORM data (score, status)
4119
- var scormCompletionSent = false; // Prevent duplicate completion statements
4183
+ var scormCompletionDebounce = null; // Debounce timer \u2014 prevents duplicate fires within same completion event
4120
4184
 
4121
4185
  function interceptScormSetValue(key, value) {
4122
4186
  if (typeof key !== 'string') return;
@@ -4136,12 +4200,15 @@ function generateLrsBridgeCode(options) {
4136
4200
  if (key === 'cmi.success_status') { scormCourseData.successStatus = String(value).toLowerCase(); }
4137
4201
 
4138
4202
  // Fire course completion statement when status indicates pass/fail/complete
4139
- if ((key === 'cmi.core.lesson_status' || key === 'cmi.success_status' || key === 'cmi.completion_status') && !scormCompletionSent) {
4203
+ // Uses debounce (not one-time flag) so retries/new attempts are captured
4204
+ if (key === 'cmi.core.lesson_status' || key === 'cmi.success_status' || key === 'cmi.completion_status') {
4140
4205
  var status = String(value).toLowerCase();
4141
4206
  if (status === 'passed' || status === 'failed' || status === 'completed') {
4142
- scormCompletionSent = true;
4143
- // Slight delay to let score values arrive (Rise often sets score just before status)
4144
- setTimeout(function() { sendCourseCompletionStatement(status); }, 200);
4207
+ if (scormCompletionDebounce) clearTimeout(scormCompletionDebounce);
4208
+ scormCompletionDebounce = setTimeout(function() {
4209
+ scormCompletionDebounce = null;
4210
+ sendCourseCompletionStatement(status);
4211
+ }, 500);
4145
4212
  }
4146
4213
  }
4147
4214
 
@@ -4252,11 +4319,6 @@ function generateLrsBridgeCode(options) {
4252
4319
 
4253
4320
  var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
4254
4321
 
4255
- // Determine verb
4256
- var verbKey = 'completed';
4257
- if (status === 'passed') verbKey = 'passed';
4258
- if (status === 'failed') verbKey = 'failed';
4259
-
4260
4322
  // Build result with score
4261
4323
  var result = { completion: true };
4262
4324
 
@@ -4282,10 +4344,17 @@ function generateLrsBridgeCode(options) {
4282
4344
  employeeId: LRS.employeeId || undefined
4283
4345
  };
4284
4346
 
4285
- log('Sending course completion:', verbKey, 'score:', result.score, 'title:', courseTitle);
4347
+ // 1. Send "completed" statement (course was finished)
4348
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
4349
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
4350
+ sendStatement(completedStatement);
4286
4351
 
4287
- var statement = buildStatement(verbKey, ACTIVITY_TYPES.course, activityDetails, result, null);
4288
- sendStatement(statement);
4352
+ // 2. Send "passed" or "failed" statement (assessment outcome)
4353
+ if (status === 'passed' || status === 'failed') {
4354
+ log('Sending', status, 'statement');
4355
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
4356
+ sendStatement(outcomeStatement);
4357
+ }
4289
4358
 
4290
4359
  logGroupEnd();
4291
4360
  }