@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.cjs CHANGED
@@ -3739,11 +3739,11 @@ function generateLrsBridgeCode(options) {
3739
3739
  // Send a course completion statement \u2014 called by skin or auto-detected via SCORM
3740
3740
  LRS.sendCompletionStatement = function(data) {
3741
3741
  data = data || {};
3742
- if (scormCompletionSent) {
3743
- log('Completion statement already sent (SCORM auto-detected). Skipping duplicate.');
3744
- return;
3742
+ // Cancel any pending SCORM auto-detection to avoid double-fire for same attempt
3743
+ if (scormCompletionDebounce) {
3744
+ clearTimeout(scormCompletionDebounce);
3745
+ scormCompletionDebounce = null;
3745
3746
  }
3746
- scormCompletionSent = true;
3747
3747
 
3748
3748
  var status = data.status || 'completed';
3749
3749
  var courseTitle = data.courseTitle || (LRS.courseInfo && LRS.courseInfo.title ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course');
@@ -3773,11 +3773,58 @@ function generateLrsBridgeCode(options) {
3773
3773
  };
3774
3774
  if (data.employeeName) activityDetails.employeeName = data.employeeName;
3775
3775
 
3776
- log('Sending completion statement:', verbKey, 'score:', result.score, 'title:', courseTitle);
3777
- var statement = buildStatement(verbKey, ACTIVITY_TYPES.course, activityDetails, result, null);
3776
+ // 1. Always send "completed" statement
3777
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
3778
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
3779
+ sendStatement(completedStatement);
3780
+
3781
+ // 2. Send "passed" or "failed" statement (assessment outcome)
3782
+ if (status === 'passed' || status === 'failed') {
3783
+ log('Sending', status, 'statement');
3784
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
3785
+ sendStatement(outcomeStatement);
3786
+ }
3787
+ };
3788
+
3789
+ // Send a "terminated" statement \u2014 called by skin close button or auto on page unload
3790
+ var exitStatementSent = false;
3791
+ LRS.sendExitStatement = function(data) {
3792
+ if (exitStatementSent) return;
3793
+ exitStatementSent = true;
3794
+ data = data || {};
3795
+
3796
+ var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
3797
+ var duration = null;
3798
+ if (LRS.launchTime) {
3799
+ var ms = new Date().getTime() - new Date(LRS.launchTime).getTime();
3800
+ var secs = Math.floor(ms / 1000);
3801
+ var mins = Math.floor(secs / 60);
3802
+ var hrs = Math.floor(mins / 60);
3803
+ duration = 'PT' + (hrs > 0 ? hrs + 'H' : '') + (mins % 60) + 'M' + (secs % 60) + 'S';
3804
+ }
3805
+
3806
+ var activityDetails = {
3807
+ courseTitle: courseTitle,
3808
+ exitSource: data.source || 'page-unload',
3809
+ employeeEmail: LRS.actor && LRS.actor.mbox ? LRS.actor.mbox.replace('mailto:', '') : undefined,
3810
+ employeeId: LRS.employeeId || undefined,
3811
+ sessionDuration: duration,
3812
+ statementsSent: LRS.stats.statementsSent
3813
+ };
3814
+
3815
+ var result = duration ? { duration: duration } : null;
3816
+ log('Sending exit statement, duration:', duration);
3817
+ var statement = buildStatement('terminated', ACTIVITY_TYPES.course, activityDetails, result, null);
3778
3818
  sendStatement(statement);
3779
3819
  };
3780
3820
 
3821
+ // Auto-send exit statement on page unload
3822
+ window.addEventListener('beforeunload', function() {
3823
+ if (!exitStatementSent && LRS.actor) {
3824
+ LRS.sendExitStatement({ source: 'page-unload' });
3825
+ }
3826
+ });
3827
+
3781
3828
  /**
3782
3829
  * Re-extract actor from SCORM after LMSInitialize has been called.
3783
3830
  * Call this from the SCORM wrapper after scormInit() succeeds,
@@ -3884,6 +3931,23 @@ function generateLrsBridgeCode(options) {
3884
3931
  LRS.interacted({ type: 'choice-branch', id: data.id, name: 'Choice Branch Discarded' });
3885
3932
  };
3886
3933
 
3934
+ // ========================================================================
3935
+ // PUBLIC API \u2014 expose core internals for skin scripts
3936
+ // Usage: window.pa_patcher.lrs.api.sendStatement(stmt)
3937
+ // ========================================================================
3938
+ LRS.api = {
3939
+ sendStatement: sendStatement,
3940
+ buildStatement: buildStatement,
3941
+ buildCourseActivityObject: buildCourseActivityObject,
3942
+ buildXylemeContext: buildXylemeContext,
3943
+ generateUUID: generateUUID,
3944
+ decodeEntities: decodeEntities,
3945
+ extractActor: extractActor,
3946
+ getCachedLessonInfo: getCachedLessonInfo,
3947
+ VERBS: VERBS,
3948
+ ACTIVITY_TYPES: ACTIVITY_TYPES
3949
+ };
3950
+
3887
3951
  // ========================================================================
3888
3952
  // 8. RISE EVENT INTERCEPTORS
3889
3953
  // ========================================================================
@@ -4449,7 +4513,7 @@ function generateLrsBridgeCode(options) {
4449
4513
  var scormInteractionsSent = {}; // Track which interactions were already sent
4450
4514
  var scormTrackerActive = false; // Set true when SCORM tracker wraps SetValue \u2014 KC handler defers
4451
4515
  var scormCourseData = {}; // Accumulates course-level SCORM data (score, status)
4452
- var scormCompletionSent = false; // Prevent duplicate completion statements
4516
+ var scormCompletionDebounce = null; // Debounce timer \u2014 prevents duplicate fires within same completion event
4453
4517
 
4454
4518
  function interceptScormSetValue(key, value) {
4455
4519
  if (typeof key !== 'string') return;
@@ -4469,12 +4533,15 @@ function generateLrsBridgeCode(options) {
4469
4533
  if (key === 'cmi.success_status') { scormCourseData.successStatus = String(value).toLowerCase(); }
4470
4534
 
4471
4535
  // Fire course completion statement when status indicates pass/fail/complete
4472
- if ((key === 'cmi.core.lesson_status' || key === 'cmi.success_status' || key === 'cmi.completion_status') && !scormCompletionSent) {
4536
+ // Uses debounce (not one-time flag) so retries/new attempts are captured
4537
+ if (key === 'cmi.core.lesson_status' || key === 'cmi.success_status' || key === 'cmi.completion_status') {
4473
4538
  var status = String(value).toLowerCase();
4474
4539
  if (status === 'passed' || status === 'failed' || status === 'completed') {
4475
- scormCompletionSent = true;
4476
- // Slight delay to let score values arrive (Rise often sets score just before status)
4477
- setTimeout(function() { sendCourseCompletionStatement(status); }, 200);
4540
+ if (scormCompletionDebounce) clearTimeout(scormCompletionDebounce);
4541
+ scormCompletionDebounce = setTimeout(function() {
4542
+ scormCompletionDebounce = null;
4543
+ sendCourseCompletionStatement(status);
4544
+ }, 500);
4478
4545
  }
4479
4546
  }
4480
4547
 
@@ -4585,11 +4652,6 @@ function generateLrsBridgeCode(options) {
4585
4652
 
4586
4653
  var courseTitle = (LRS.courseInfo && LRS.courseInfo.title) ? decodeEntities(LRS.courseInfo.title) : decodeEntities(document.title) || 'Course';
4587
4654
 
4588
- // Determine verb
4589
- var verbKey = 'completed';
4590
- if (status === 'passed') verbKey = 'passed';
4591
- if (status === 'failed') verbKey = 'failed';
4592
-
4593
4655
  // Build result with score
4594
4656
  var result = { completion: true };
4595
4657
 
@@ -4615,10 +4677,17 @@ function generateLrsBridgeCode(options) {
4615
4677
  employeeId: LRS.employeeId || undefined
4616
4678
  };
4617
4679
 
4618
- log('Sending course completion:', verbKey, 'score:', result.score, 'title:', courseTitle);
4680
+ // 1. Send "completed" statement (course was finished)
4681
+ log('Sending completed statement, score:', result.score, 'title:', courseTitle);
4682
+ var completedStatement = buildStatement('completed', ACTIVITY_TYPES.course, activityDetails, result, null);
4683
+ sendStatement(completedStatement);
4619
4684
 
4620
- var statement = buildStatement(verbKey, ACTIVITY_TYPES.course, activityDetails, result, null);
4621
- sendStatement(statement);
4685
+ // 2. Send "passed" or "failed" statement (assessment outcome)
4686
+ if (status === 'passed' || status === 'failed') {
4687
+ log('Sending', status, 'statement');
4688
+ var outcomeStatement = buildStatement(status, ACTIVITY_TYPES.course, activityDetails, result, null);
4689
+ sendStatement(outcomeStatement);
4690
+ }
4622
4691
 
4623
4692
  logGroupEnd();
4624
4693
  }