@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.js CHANGED
@@ -3397,11 +3397,11 @@ function generateLrsBridgeCode(options) {
3397
3397
  // Send a course completion statement \u2014 called by skin or auto-detected via SCORM
3398
3398
  LRS.sendCompletionStatement = function(data) {
3399
3399
  data = data || {};
3400
- if (scormCompletionSent) {
3401
- log('Completion statement already sent (SCORM auto-detected). Skipping duplicate.');
3402
- return;
3400
+ // Cancel any pending SCORM auto-detection to avoid double-fire for same attempt
3401
+ if (scormCompletionDebounce) {
3402
+ clearTimeout(scormCompletionDebounce);
3403
+ scormCompletionDebounce = null;
3403
3404
  }
3404
- scormCompletionSent = true;
3405
3405
 
3406
3406
  var status = data.status || 'completed';
3407
3407
  var courseTitle = data.courseTitle || (LRS.courseInfo && LRS.courseInfo.title ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course');
@@ -3431,11 +3431,58 @@ function generateLrsBridgeCode(options) {
3431
3431
  };
3432
3432
  if (data.employeeName) activityDetails.employeeName = data.employeeName;
3433
3433
 
3434
- log('Sending completion statement:', verbKey, 'score:', result.score, 'title:', courseTitle);
3435
- var statement = buildStatement(verbKey, ACTIVITY_TYPES.course, activityDetails, result, null);
3434
+ // 1. Always send "completed" statement
3435
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
3436
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
3437
+ sendStatement(completedStatement);
3438
+
3439
+ // 2. Send "passed" or "failed" statement (assessment outcome)
3440
+ if (status === 'passed' || status === 'failed') {
3441
+ log('Sending', status, 'statement');
3442
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
3443
+ sendStatement(outcomeStatement);
3444
+ }
3445
+ };
3446
+
3447
+ // Send a "terminated" statement \u2014 called by skin close button or auto on page unload
3448
+ var exitStatementSent = false;
3449
+ LRS.sendExitStatement = function(data) {
3450
+ if (exitStatementSent) return;
3451
+ exitStatementSent = true;
3452
+ data = data || {};
3453
+
3454
+ var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
3455
+ var duration = null;
3456
+ if (LRS.launchTime) {
3457
+ var ms = new Date().getTime() - new Date(LRS.launchTime).getTime();
3458
+ var secs = Math.floor(ms / 1000);
3459
+ var mins = Math.floor(secs / 60);
3460
+ var hrs = Math.floor(mins / 60);
3461
+ duration = 'PT' + (hrs > 0 ? hrs + 'H' : '') + (mins % 60) + 'M' + (secs % 60) + 'S';
3462
+ }
3463
+
3464
+ var activityDetails = {
3465
+ courseTitle: courseTitle,
3466
+ exitSource: data.source || 'page-unload',
3467
+ employeeEmail: LRS.actor && LRS.actor.mbox ? LRS.actor.mbox.replace('mailto:', '') : undefined,
3468
+ employeeId: LRS.employeeId || undefined,
3469
+ sessionDuration: duration,
3470
+ statementsSent: LRS.stats.statementsSent
3471
+ };
3472
+
3473
+ var result = duration ? { duration: duration } : null;
3474
+ log('Sending exit statement, duration:', duration);
3475
+ var statement = buildStatement('terminated', ACTIVITY_TYPES.course, activityDetails, result, null);
3436
3476
  sendStatement(statement);
3437
3477
  };
3438
3478
 
3479
+ // Auto-send exit statement on page unload
3480
+ window.addEventListener('beforeunload', function() {
3481
+ if (!exitStatementSent && LRS.actor) {
3482
+ LRS.sendExitStatement({ source: 'page-unload' });
3483
+ }
3484
+ });
3485
+
3439
3486
  /**
3440
3487
  * Re-extract actor from SCORM after LMSInitialize has been called.
3441
3488
  * Call this from the SCORM wrapper after scormInit() succeeds,
@@ -3542,6 +3589,23 @@ function generateLrsBridgeCode(options) {
3542
3589
  LRS.interacted({ type: 'choice-branch', id: data.id, name: 'Choice Branch Discarded' });
3543
3590
  };
3544
3591
 
3592
+ // ========================================================================
3593
+ // PUBLIC API \u2014 expose core internals for skin scripts
3594
+ // Usage: window.pa_patcher.lrs.api.sendStatement(stmt)
3595
+ // ========================================================================
3596
+ LRS.api = {
3597
+ sendStatement: sendStatement,
3598
+ buildStatement: buildStatement,
3599
+ buildCourseActivityObject: buildCourseActivityObject,
3600
+ buildXylemeContext: buildXylemeContext,
3601
+ generateUUID: generateUUID,
3602
+ decodeEntities: decodeEntities,
3603
+ extractActor: extractActor,
3604
+ getCachedLessonInfo: getCachedLessonInfo,
3605
+ VERBS: VERBS,
3606
+ ACTIVITY_TYPES: ACTIVITY_TYPES
3607
+ };
3608
+
3545
3609
  // ========================================================================
3546
3610
  // 8. RISE EVENT INTERCEPTORS
3547
3611
  // ========================================================================
@@ -4107,7 +4171,7 @@ function generateLrsBridgeCode(options) {
4107
4171
  var scormInteractionsSent = {}; // Track which interactions were already sent
4108
4172
  var scormTrackerActive = false; // Set true when SCORM tracker wraps SetValue \u2014 KC handler defers
4109
4173
  var scormCourseData = {}; // Accumulates course-level SCORM data (score, status)
4110
- var scormCompletionSent = false; // Prevent duplicate completion statements
4174
+ var scormCompletionDebounce = null; // Debounce timer \u2014 prevents duplicate fires within same completion event
4111
4175
 
4112
4176
  function interceptScormSetValue(key, value) {
4113
4177
  if (typeof key !== 'string') return;
@@ -4127,12 +4191,15 @@ function generateLrsBridgeCode(options) {
4127
4191
  if (key === 'cmi.success_status') { scormCourseData.successStatus = String(value).toLowerCase(); }
4128
4192
 
4129
4193
  // Fire course completion statement when status indicates pass/fail/complete
4130
- if ((key === 'cmi.core.lesson_status' || key === 'cmi.success_status' || key === 'cmi.completion_status') && !scormCompletionSent) {
4194
+ // Uses debounce (not one-time flag) so retries/new attempts are captured
4195
+ if (key === 'cmi.core.lesson_status' || key === 'cmi.success_status' || key === 'cmi.completion_status') {
4131
4196
  var status = String(value).toLowerCase();
4132
4197
  if (status === 'passed' || status === 'failed' || status === 'completed') {
4133
- scormCompletionSent = true;
4134
- // Slight delay to let score values arrive (Rise often sets score just before status)
4135
- setTimeout(function() { sendCourseCompletionStatement(status); }, 200);
4198
+ if (scormCompletionDebounce) clearTimeout(scormCompletionDebounce);
4199
+ scormCompletionDebounce = setTimeout(function() {
4200
+ scormCompletionDebounce = null;
4201
+ sendCourseCompletionStatement(status);
4202
+ }, 500);
4136
4203
  }
4137
4204
  }
4138
4205
 
@@ -4243,11 +4310,6 @@ function generateLrsBridgeCode(options) {
4243
4310
 
4244
4311
  var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
4245
4312
 
4246
- // Determine verb
4247
- var verbKey = 'completed';
4248
- if (status === 'passed') verbKey = 'passed';
4249
- if (status === 'failed') verbKey = 'failed';
4250
-
4251
4313
  // Build result with score
4252
4314
  var result = { completion: true };
4253
4315
 
@@ -4273,10 +4335,17 @@ function generateLrsBridgeCode(options) {
4273
4335
  employeeId: LRS.employeeId || undefined
4274
4336
  };
4275
4337
 
4276
- log('Sending course completion:', verbKey, 'score:', result.score, 'title:', courseTitle);
4338
+ // 1. Send "completed" statement (course was finished)
4339
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
4340
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
4341
+ sendStatement(completedStatement);
4277
4342
 
4278
- var statement = buildStatement(verbKey, ACTIVITY_TYPES.course, activityDetails, result, null);
4279
- sendStatement(statement);
4343
+ // 2. Send "passed" or "failed" statement (assessment outcome)
4344
+ if (status === 'passed' || status === 'failed') {
4345
+ log('Sending', status, 'statement');
4346
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
4347
+ sendStatement(outcomeStatement);
4348
+ }
4280
4349
 
4281
4350
  logGroupEnd();
4282
4351
  }