@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/cli.js CHANGED
@@ -3729,11 +3729,11 @@ function generateLrsBridgeCode(options) {
3729
3729
  // Send a course completion statement \u2014 called by skin or auto-detected via SCORM
3730
3730
  LRS.sendCompletionStatement = function(data) {
3731
3731
  data = data || {};
3732
- if (scormCompletionSent) {
3733
- log('Completion statement already sent (SCORM auto-detected). Skipping duplicate.');
3734
- return;
3732
+ // Cancel any pending SCORM auto-detection to avoid double-fire for same attempt
3733
+ if (scormCompletionDebounce) {
3734
+ clearTimeout(scormCompletionDebounce);
3735
+ scormCompletionDebounce = null;
3735
3736
  }
3736
- scormCompletionSent = true;
3737
3737
 
3738
3738
  var status = data.status || 'completed';
3739
3739
  var courseTitle = data.courseTitle || (LRS.courseInfo && LRS.courseInfo.title ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course');
@@ -3763,11 +3763,58 @@ function generateLrsBridgeCode(options) {
3763
3763
  };
3764
3764
  if (data.employeeName) activityDetails.employeeName = data.employeeName;
3765
3765
 
3766
- log('Sending completion statement:', verbKey, 'score:', result.score, 'title:', courseTitle);
3767
- var statement = buildStatement(verbKey, ACTIVITY_TYPES.course, activityDetails, result, null);
3766
+ // 1. Always send "completed" statement
3767
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
3768
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
3769
+ sendStatement(completedStatement);
3770
+
3771
+ // 2. Send "passed" or "failed" statement (assessment outcome)
3772
+ if (status === 'passed' || status === 'failed') {
3773
+ log('Sending', status, 'statement');
3774
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
3775
+ sendStatement(outcomeStatement);
3776
+ }
3777
+ };
3778
+
3779
+ // Send a "terminated" statement \u2014 called by skin close button or auto on page unload
3780
+ var exitStatementSent = false;
3781
+ LRS.sendExitStatement = function(data) {
3782
+ if (exitStatementSent) return;
3783
+ exitStatementSent = true;
3784
+ data = data || {};
3785
+
3786
+ var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
3787
+ var duration = null;
3788
+ if (LRS.launchTime) {
3789
+ var ms = new Date().getTime() - new Date(LRS.launchTime).getTime();
3790
+ var secs = Math.floor(ms / 1000);
3791
+ var mins = Math.floor(secs / 60);
3792
+ var hrs = Math.floor(mins / 60);
3793
+ duration = 'PT' + (hrs > 0 ? hrs + 'H' : '') + (mins % 60) + 'M' + (secs % 60) + 'S';
3794
+ }
3795
+
3796
+ var activityDetails = {
3797
+ courseTitle: courseTitle,
3798
+ exitSource: data.source || 'page-unload',
3799
+ employeeEmail: LRS.actor && LRS.actor.mbox ? LRS.actor.mbox.replace('mailto:', '') : undefined,
3800
+ employeeId: LRS.employeeId || undefined,
3801
+ sessionDuration: duration,
3802
+ statementsSent: LRS.stats.statementsSent
3803
+ };
3804
+
3805
+ var result = duration ? { duration: duration } : null;
3806
+ log('Sending exit statement, duration:', duration);
3807
+ var statement = buildStatement('terminated', ACTIVITY_TYPES.course, activityDetails, result, null);
3768
3808
  sendStatement(statement);
3769
3809
  };
3770
3810
 
3811
+ // Auto-send exit statement on page unload
3812
+ window.addEventListener('beforeunload', function() {
3813
+ if (!exitStatementSent && LRS.actor) {
3814
+ LRS.sendExitStatement({ source: 'page-unload' });
3815
+ }
3816
+ });
3817
+
3771
3818
  /**
3772
3819
  * Re-extract actor from SCORM after LMSInitialize has been called.
3773
3820
  * Call this from the SCORM wrapper after scormInit() succeeds,
@@ -3874,6 +3921,23 @@ function generateLrsBridgeCode(options) {
3874
3921
  LRS.interacted({ type: 'choice-branch', id: data.id, name: 'Choice Branch Discarded' });
3875
3922
  };
3876
3923
 
3924
+ // ========================================================================
3925
+ // PUBLIC API \u2014 expose core internals for skin scripts
3926
+ // Usage: window.pa_patcher.lrs.api.sendStatement(stmt)
3927
+ // ========================================================================
3928
+ LRS.api = {
3929
+ sendStatement: sendStatement,
3930
+ buildStatement: buildStatement,
3931
+ buildCourseActivityObject: buildCourseActivityObject,
3932
+ buildXylemeContext: buildXylemeContext,
3933
+ generateUUID: generateUUID,
3934
+ decodeEntities: decodeEntities,
3935
+ extractActor: extractActor,
3936
+ getCachedLessonInfo: getCachedLessonInfo,
3937
+ VERBS: VERBS,
3938
+ ACTIVITY_TYPES: ACTIVITY_TYPES
3939
+ };
3940
+
3877
3941
  // ========================================================================
3878
3942
  // 8. RISE EVENT INTERCEPTORS
3879
3943
  // ========================================================================
@@ -4439,7 +4503,7 @@ function generateLrsBridgeCode(options) {
4439
4503
  var scormInteractionsSent = {}; // Track which interactions were already sent
4440
4504
  var scormTrackerActive = false; // Set true when SCORM tracker wraps SetValue \u2014 KC handler defers
4441
4505
  var scormCourseData = {}; // Accumulates course-level SCORM data (score, status)
4442
- var scormCompletionSent = false; // Prevent duplicate completion statements
4506
+ var scormCompletionDebounce = null; // Debounce timer \u2014 prevents duplicate fires within same completion event
4443
4507
 
4444
4508
  function interceptScormSetValue(key, value) {
4445
4509
  if (typeof key !== 'string') return;
@@ -4459,12 +4523,15 @@ function generateLrsBridgeCode(options) {
4459
4523
  if (key === 'cmi.success_status') { scormCourseData.successStatus = String(value).toLowerCase(); }
4460
4524
 
4461
4525
  // Fire course completion statement when status indicates pass/fail/complete
4462
- if ((key === 'cmi.core.lesson_status' || key === 'cmi.success_status' || key === 'cmi.completion_status') && !scormCompletionSent) {
4526
+ // Uses debounce (not one-time flag) so retries/new attempts are captured
4527
+ if (key === 'cmi.core.lesson_status' || key === 'cmi.success_status' || key === 'cmi.completion_status') {
4463
4528
  var status = String(value).toLowerCase();
4464
4529
  if (status === 'passed' || status === 'failed' || status === 'completed') {
4465
- scormCompletionSent = true;
4466
- // Slight delay to let score values arrive (Rise often sets score just before status)
4467
- setTimeout(function() { sendCourseCompletionStatement(status); }, 200);
4530
+ if (scormCompletionDebounce) clearTimeout(scormCompletionDebounce);
4531
+ scormCompletionDebounce = setTimeout(function() {
4532
+ scormCompletionDebounce = null;
4533
+ sendCourseCompletionStatement(status);
4534
+ }, 500);
4468
4535
  }
4469
4536
  }
4470
4537
 
@@ -4575,11 +4642,6 @@ function generateLrsBridgeCode(options) {
4575
4642
 
4576
4643
  var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
4577
4644
 
4578
- // Determine verb
4579
- var verbKey = 'completed';
4580
- if (status === 'passed') verbKey = 'passed';
4581
- if (status === 'failed') verbKey = 'failed';
4582
-
4583
4645
  // Build result with score
4584
4646
  var result = { completion: true };
4585
4647
 
@@ -4605,10 +4667,17 @@ function generateLrsBridgeCode(options) {
4605
4667
  employeeId: LRS.employeeId || undefined
4606
4668
  };
4607
4669
 
4608
- log('Sending course completion:', verbKey, 'score:', result.score, 'title:', courseTitle);
4670
+ // 1. Send "completed" statement (course was finished)
4671
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
4672
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
4673
+ sendStatement(completedStatement);
4609
4674
 
4610
- var statement = buildStatement(verbKey, ACTIVITY_TYPES.course, activityDetails, result, null);
4611
- sendStatement(statement);
4675
+ // 2. Send "passed" or "failed" statement (assessment outcome)
4676
+ if (status === 'passed' || status === 'failed') {
4677
+ log('Sending', status, 'statement');
4678
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
4679
+ sendStatement(outcomeStatement);
4680
+ }
4612
4681
 
4613
4682
  logGroupEnd();
4614
4683
  }